From d531b86666975238d8d7f0572743d9b0be320e74 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 9 Jun 2026 09:46:08 +0530 Subject: [PATCH 1/3] feat: added innconstraint --- src/sdk/modules/utils/composabilityCalls.ts | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/sdk/modules/utils/composabilityCalls.ts b/src/sdk/modules/utils/composabilityCalls.ts index 91ff5dc7..e0a826fc 100644 --- a/src/sdk/modules/utils/composabilityCalls.ts +++ b/src/sdk/modules/utils/composabilityCalls.ts @@ -129,6 +129,7 @@ export type ChildConstraint = | { gte: ConstraintValue } | { lte: ConstraintValue } | { eq: ConstraintValue } + | { in: { lower: ConstraintValue; upper: ConstraintValue } } | { gteSigned: bigint } | { lteSigned: bigint } @@ -233,6 +234,13 @@ export const lessThanOrEqualToSigned = (value: AnyData): ConstraintField => { return { type: ConstraintType.LTE_SIGNED, value } } +export const inRange = ( + lower: ConstraintValue, + upper: ConstraintValue +): ConstraintField => { + return { type: ConstraintType.IN, value: { lower, upper } } +} + export const orConstraint = ( subConstraints: ConstraintField[] ): ConstraintField => { @@ -243,6 +251,7 @@ const CHILD_CONSTRAINT_TYPES = new Set([ ConstraintType.EQ, ConstraintType.GTE, ConstraintType.LTE, + ConstraintType.IN, ConstraintType.GTE_SIGNED, ConstraintType.LTE_SIGNED ]) @@ -280,6 +289,51 @@ const validateAndProcessChildConstraint = ( return { constraintType: constraint.type, referenceData } } + // IN path — range check: lower <= value <= upper (unsigned bytes32 comparison) + if (constraint.type === ConstraintType.IN) { + const { lower, upper } = constraint.value as { + lower: ConstraintValue + upper: ConstraintValue + } + + for (const [label, v] of [ + ["lower", lower], + ["upper", upper] + ] as const) { + if ( + typeof v !== "bigint" && + typeof v !== "boolean" && + !isHex(v) && + !isAddress(v) + ) { + throw new Error( + `Invalid IN constraint: ${label} must be bigint, boolean, hex, or address` + ) + } + if (typeof v === "bigint" && v < 0n) { + throw new Error( + `Invalid IN constraint: ${label} must be non-negative (use GTE_SIGNED/LTE_SIGNED for signed ranges)` + ) + } + } + + if ( + typeof lower === "bigint" && + typeof upper === "bigint" && + lower > upper + ) { + throw new Error("Invalid IN constraint: lower must be <= upper") + } + + const lowerHex = toBytes32(lower) + const upperHex = toBytes32(upper) + const referenceData = encodeAbiParameters( + [{ type: "bytes32" }, { type: "bytes32" }], + [lowerHex as Hex, upperHex as Hex] + ) + return prepareConstraint(ConstraintType.IN, referenceData) + } + // Unsigned path (EQ, GTE, LTE) — existing logic unchanged if ( typeof constraint.value !== "bigint" && From 94795ff17964bd1092732acf7d62ffdb02e52f9e Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 9 Jun 2026 09:46:24 +0530 Subject: [PATCH 2/3] feat: added in constraints unit test cases --- src/sdk/modules/utils/conditions.test.ts | 78 +++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/sdk/modules/utils/conditions.test.ts b/src/sdk/modules/utils/conditions.test.ts index 56c22ba2..c08fc62a 100644 --- a/src/sdk/modules/utils/conditions.test.ts +++ b/src/sdk/modules/utils/conditions.test.ts @@ -6,9 +6,9 @@ import { CONSTRAINT_TUPLE_ABI, ConstraintType, InputParamFetcherType, - equalTo, greaterThanOrEqualTo, greaterThanOrEqualToSigned, + inRange, lessThanOrEqualTo, lessThanOrEqualToSigned, orConstraint, @@ -182,6 +182,82 @@ describe("Conditional Execution - Unit Tests", () => { }) }) + describe("validateAndProcessConstraints — IN range constraint", () => { + test("inRange encodes lower and upper bounds as two bytes32 values", () => { + const result = validateAndProcessConstraints([inRange(10n, 100n)]) + expect(result).toHaveLength(1) + expect(result[0].constraintType).toBe(ConstraintType.IN) + // referenceData must encode two bytes32 values (64 bytes = 128 hex chars + 0x prefix) + expect(result[0].referenceData.length).toBe(2 + 128) + }) + + test("inRange with equal lower and upper (exact value)", () => { + const result = validateAndProcessConstraints([inRange(42n, 42n)]) + expect(result).toHaveLength(1) + expect(result[0].constraintType).toBe(ConstraintType.IN) + }) + + test("inRange with zero lower bound", () => { + const result = validateAndProcessConstraints([inRange(0n, 1000n)]) + expect(result).toHaveLength(1) + expect(result[0].constraintType).toBe(ConstraintType.IN) + }) + + test("inRange referenceData decodes to correct bounds", () => { + const lower = 50n + const upper = 200n + const result = validateAndProcessConstraints([inRange(lower, upper)]) + const [decodedLower, decodedUpper] = decodeAbiParameters( + [{ type: "bytes32" }, { type: "bytes32" }], + result[0].referenceData as `0x${string}` + ) + expect(BigInt(decodedLower)).toBe(lower) + expect(BigInt(decodedUpper)).toBe(upper) + }) + + test("inRange throws for negative lower bound", () => { + expect(() => validateAndProcessConstraints([inRange(-1n, 100n)])).toThrow( + "lower must be non-negative" + ) + }) + + test("inRange throws for negative upper bound", () => { + expect(() => validateAndProcessConstraints([inRange(0n, -1n)])).toThrow( + "upper must be non-negative" + ) + }) + + test("inRange throws when lower > upper", () => { + expect(() => validateAndProcessConstraints([inRange(100n, 50n)])).toThrow( + "lower must be <= upper" + ) + }) + + test("inRange throws for invalid lower bound type", () => { + expect(() => + validateAndProcessConstraints([ + inRange("not-hex" as unknown as bigint, 100n) + ]) + ).toThrow("lower must be bigint, boolean, hex, or address") + }) + + test("inRange can be used inside OR constraint", () => { + const result = validateAndProcessConstraints([ + orConstraint([inRange(1n, 50n), greaterThanOrEqualTo(100n)]) + ]) + expect(result).toHaveLength(1) + expect(result[0].constraintType).toBe(ConstraintType.OR) + + const [subs] = decodeAbiParameters( + [CONSTRAINT_TUPLE_ABI], + result[0].referenceData as `0x${string}` + ) + expect(subs).toHaveLength(2) + expect(subs[0].constraintType).toBe(ConstraintType.IN) + expect(subs[1].constraintType).toBe(ConstraintType.GTE) + }) + }) + describe("validateAndProcessConstraints — signed integer and OR constraints", () => { test("greaterThanOrEqualToSigned encodes a positive bigint correctly", () => { const result = validateAndProcessConstraints([ From 495267c754635aa0a037a214d06b3b412d509c10 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 9 Jun 2026 09:46:32 +0530 Subject: [PATCH 3/3] feat: added in constraints execution test cases --- .../instructions/buildComposable.test.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/sdk/account/decorators/instructions/buildComposable.test.ts b/src/sdk/account/decorators/instructions/buildComposable.test.ts index 52f52eaa..82acf260 100644 --- a/src/sdk/account/decorators/instructions/buildComposable.test.ts +++ b/src/sdk/account/decorators/instructions/buildComposable.test.ts @@ -52,6 +52,7 @@ import { getMEEVersion, greaterThanOrEqualTo, greaterThanOrEqualToSigned, + inRange, lessThanOrEqualTo, lessThanOrEqualToSigned, orConstraint, @@ -2125,6 +2126,128 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => { ) }) + it("should execute composable sweep with inRange constraint", async () => { + const amountToSupply = parseUnits("0.1", 6) + const amountToTransfer = parseUnits("0.05", 6) + // Range: 1 wei .. 100 USDC — the transferred balance will always fall inside + const lowerBound = 1n + const upperBound = parseUnits("100", 6) + + for (const { + name, + mcNexus: testMcNexus, + meeClient: testMeeClient + } of accountConfigs) { + const staticTransferInstruction = await testMcNexus.buildComposable({ + type: "transfer", + data: { + recipient: runtimeTransferAddress as Address, + tokenAddress, + amount: amountToTransfer, + chainId: chain.id + } + }) + + const sweepInstruction = await testMcNexus.buildComposable({ + type: "default", + data: { + to: runtimeTransferAddress, + abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi, + functionName: "transferFunds", + args: [ + tokenAddress, + eoaAccount.address, + runtimeERC20BalanceOf({ + targetAddress: runtimeTransferAddress, + tokenAddress, + constraints: [inRange(lowerBound, upperBound)] + }) + ], + chainId: chain.id + } + }) + + const { hash } = await testMeeClient.executeFusionQuote({ + fusionQuote: await testMeeClient.getFusionQuote({ + trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply }, + instructions: [...staticTransferInstruction, ...sweepInstruction], + feeToken: { chainId: chain.id, address: tokenAddress } + }) + }) + + const { transactionStatus, explorerLinks } = + await testMeeClient.waitForSupertransactionReceipt({ + hash, + confirmations: TEST_BLOCK_CONFIRMATIONS + }) + expect(transactionStatus).to.be.eq("MINED_SUCCESS") + console.log(`[${name}] inRange constraint sweep:`, { + explorerLinks, + hash + }) + } + }) + + it("should execute composable sweep with OR containing inRange sub-constraint on composability v1.1.1", async () => { + const amountToSupply = parseUnits("0.1", 6) + const amountToTransfer = parseUnits("0.05", 6) + + const staticTransferInstruction = + await mcNexus_compos_v1_1_1.buildComposable({ + type: "transfer", + data: { + recipient: runtimeTransferAddress as Address, + tokenAddress, + amount: amountToTransfer, + chainId: chain.id + } + }) + + // OR(inRange(1, 0.05 USDC), GTE(0.1 USDC)) — first branch matches the transferred amount + const sweepInstruction = await mcNexus_compos_v1_1_1.buildComposable({ + type: "default", + data: { + to: runtimeTransferAddress, + abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi, + functionName: "transferFunds", + args: [ + tokenAddress, + eoaAccount.address, + runtimeERC20BalanceOf({ + targetAddress: runtimeTransferAddress, + tokenAddress, + constraints: [ + orConstraint([ + inRange(1n, parseUnits("0.05", 6)), + greaterThanOrEqualTo(parseUnits("0.1", 6)) + ]) + ] + }) + ], + chainId: chain.id + } + }) + + const { hash } = await meeClient_compos_v1_1_1.executeFusionQuote({ + fusionQuote: await meeClient_compos_v1_1_1.getFusionQuote({ + trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply }, + instructions: [...staticTransferInstruction, ...sweepInstruction], + feeToken: { chainId: chain.id, address: tokenAddress } + }) + }) + + const { transactionStatus, explorerLinks } = + await meeClient_compos_v1_1_1.waitForSupertransactionReceipt({ + hash, + confirmations: TEST_BLOCK_CONFIRMATIONS + }) + expect(transactionStatus).to.be.eq("MINED_SUCCESS") + console.log("[v1.1.1] OR(inRange, GTE) constraint sweep:", { + explorerLinks, + hash + }) + }) + // test the new 'runtimeParamViaCustomStaticCall' helper function and the injectable target at the same time it("should execute composable transaction using runtimeParamViaCustomStaticCall (for composability v1.1.0+ only)", async () => { const amount = 101n // 101 wei