Skip to content

Commit 33e94ea

Browse files
authored
feat: signed int and or support (#202)
* feat: added unsigned integer and or support for composability * chore: code formats * feat: added test coverage and new mee version 2.2.2 * chore: code format * feat: updated the new contracts for mee version 2.2.2 * feat: refactored some test cases * feat: added additional test coverage for new composibility flow
1 parent 49e2536 commit 33e94ea

8 files changed

Lines changed: 788 additions & 48 deletions

File tree

scripts/fund:nexus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ async function main() {
9292
accountIndex: ACCOUNT_INDEX,
9393
version: getMEEVersion(MEEVersion.V2_2_1)
9494
})
95+
await processChain(chainId, account, {
96+
accountIndex: ACCOUNT_INDEX_ONE,
97+
version: getMEEVersion(MEEVersion.V2_2_2)
98+
})
9599
await processChain(chainId, account, {
96100
accountIndex: ACCOUNT_INDEX,
97101
version: getMEEVersion(MEEVersion.V1_1_0)

src/sdk/account/decorators/instructions/buildComposable.test.ts

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ import {
5151
createCondition,
5252
getMEEVersion,
5353
greaterThanOrEqualTo,
54+
greaterThanOrEqualToSigned,
55+
lessThanOrEqualTo,
56+
lessThanOrEqualToSigned,
57+
orConstraint,
5458
runtimeERC20BalanceOf,
5559
runtimeNativeBalanceOf
5660
} from "../../../modules"
@@ -78,8 +82,10 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => {
7882

7983
let mcNexus: MultichainSmartAccount
8084
let mcNexus_compos_v1_1_0: MultichainSmartAccount
85+
let mcNexus_compos_v1_1_1: MultichainSmartAccount
8186
let meeClient: MeeClient
8287
let meeClient_compos_v1_1_0: MeeClient
88+
let meeClient_compos_v1_1_1: MeeClient
8389
let accountConfigs: {
8490
name: string
8591
mcNexus: MultichainSmartAccount
@@ -126,6 +132,18 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => {
126132
]
127133
})
128134

135+
mcNexus_compos_v1_1_1 = await toMultichainNexusAccount({
136+
signer: eoaAccount,
137+
index: 1n,
138+
chainConfigurations: [
139+
{
140+
chain: chain,
141+
transport: http(network.rpcUrl),
142+
version: getMEEVersion(MEEVersion.V2_2_2)
143+
}
144+
]
145+
})
146+
129147
meeClient = await createMeeClient({
130148
account: mcNexus
131149
})
@@ -134,6 +152,10 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => {
134152
account: mcNexus_compos_v1_1_0
135153
})
136154

155+
meeClient_compos_v1_1_1 = await createMeeClient({
156+
account: mcNexus_compos_v1_1_1
157+
})
158+
137159
accountConfigs = [
138160
{
139161
name: "Composability v1.0.0",
@@ -146,6 +168,12 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => {
146168
mcNexus: mcNexus_compos_v1_1_0,
147169
meeClient: meeClient_compos_v1_1_0,
148170
eoaAccount
171+
},
172+
{
173+
name: "Composability v1.1.1",
174+
mcNexus: mcNexus_compos_v1_1_1,
175+
meeClient: meeClient_compos_v1_1_1,
176+
eoaAccount
149177
}
150178
]
151179

@@ -1808,6 +1836,295 @@ describe.runIf(runLifecycleTests)("mee.buildComposable", () => {
18081836
})
18091837
})
18101838

1839+
// ========== Composability v1.1.1 — OR and signed-integer constraint tests ==========
1840+
1841+
it("should reject OR constraint when composability version < 1.1.1", async () => {
1842+
// mcNexus is on v1.0.0 — OR constraints must be rejected at build time
1843+
await expect(
1844+
mcNexus.buildComposable({
1845+
type: "default",
1846+
data: {
1847+
to: tokenAddress,
1848+
abi: erc20Abi,
1849+
functionName: "transferFrom",
1850+
args: [
1851+
eoaAccount.address,
1852+
mcNexus.addressOn(chain.id, true),
1853+
runtimeERC20BalanceOf({
1854+
targetAddress: eoaAccount.address,
1855+
tokenAddress,
1856+
constraints: [
1857+
orConstraint([greaterThanOrEqualTo(parseUnits("0.01", 6))])
1858+
]
1859+
})
1860+
],
1861+
chainId: chain.id
1862+
}
1863+
})
1864+
).rejects.toThrow("OR constraints require Composability v1.1.1")
1865+
})
1866+
1867+
it("should reject GTE_SIGNED constraint when composability version < 1.1.1", async () => {
1868+
// mcNexus_compos_v1_1_0 is on v1.1.0 — signed constraints must still be rejected
1869+
await expect(
1870+
mcNexus_compos_v1_1_0.buildComposable({
1871+
type: "default",
1872+
data: {
1873+
to: tokenAddress,
1874+
abi: erc20Abi,
1875+
functionName: "transferFrom",
1876+
args: [
1877+
eoaAccount.address,
1878+
mcNexus_compos_v1_1_0.addressOn(chain.id, true),
1879+
runtimeERC20BalanceOf({
1880+
targetAddress: eoaAccount.address,
1881+
tokenAddress,
1882+
constraints: [greaterThanOrEqualToSigned(0n)]
1883+
})
1884+
],
1885+
chainId: chain.id
1886+
}
1887+
})
1888+
).rejects.toThrow("signed integer")
1889+
})
1890+
1891+
it("should execute composable sweep with OR constraint on composability v1.1.1", async () => {
1892+
const amountToSupply = parseUnits("0.1", 6)
1893+
const amountToTransfer = parseUnits("0.05", 6)
1894+
1895+
// Static transfer: Nexus → runtimeTransferAddress (fixed amount, funded by trigger)
1896+
const staticTransferInstruction =
1897+
await mcNexus_compos_v1_1_1.buildComposable({
1898+
type: "transfer",
1899+
data: {
1900+
recipient: runtimeTransferAddress as Address,
1901+
tokenAddress,
1902+
amount: amountToTransfer,
1903+
chainId: chain.id
1904+
}
1905+
})
1906+
1907+
// Runtime sweep: runtimeTransferAddress → EOA, only if balance is within OR constraint range
1908+
const sweepInstruction = await mcNexus_compos_v1_1_1.buildComposable({
1909+
type: "default",
1910+
data: {
1911+
to: runtimeTransferAddress,
1912+
abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi,
1913+
functionName: "transferFunds",
1914+
args: [
1915+
tokenAddress,
1916+
eoaAccount.address,
1917+
runtimeERC20BalanceOf({
1918+
targetAddress: runtimeTransferAddress,
1919+
tokenAddress,
1920+
constraints: [
1921+
orConstraint([
1922+
greaterThanOrEqualTo(1n),
1923+
lessThanOrEqualTo(parseUnits("100", 6))
1924+
])
1925+
]
1926+
})
1927+
],
1928+
chainId: chain.id
1929+
}
1930+
})
1931+
1932+
const { hash } = await meeClient_compos_v1_1_1.executeFusionQuote({
1933+
fusionQuote: await meeClient_compos_v1_1_1.getFusionQuote({
1934+
trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply },
1935+
instructions: [...staticTransferInstruction, ...sweepInstruction],
1936+
feeToken: { chainId: chain.id, address: tokenAddress }
1937+
})
1938+
})
1939+
1940+
const { transactionStatus, explorerLinks } =
1941+
await meeClient_compos_v1_1_1.waitForSupertransactionReceipt({
1942+
hash,
1943+
confirmations: TEST_BLOCK_CONFIRMATIONS
1944+
})
1945+
expect(transactionStatus).to.be.eq("MINED_SUCCESS")
1946+
console.log("[v1.1.1] OR constraint composable sweep test:", {
1947+
explorerLinks,
1948+
hash
1949+
})
1950+
})
1951+
1952+
it("should execute composable sweep with GTE_SIGNED constraint on composability v1.1.1", async () => {
1953+
const amountToSupply = parseUnits("0.1", 6)
1954+
const amountToTransfer = parseUnits("0.05", 6)
1955+
1956+
// Static transfer: Nexus → runtimeTransferAddress (funded by trigger)
1957+
const staticTransferInstruction =
1958+
await mcNexus_compos_v1_1_1.buildComposable({
1959+
type: "transfer",
1960+
data: {
1961+
recipient: runtimeTransferAddress as Address,
1962+
tokenAddress,
1963+
amount: amountToTransfer,
1964+
chainId: chain.id
1965+
}
1966+
})
1967+
1968+
// Runtime sweep using GTE_SIGNED: runtime balance must be >= 1 (int256 comparison)
1969+
const sweepInstruction = await mcNexus_compos_v1_1_1.buildComposable({
1970+
type: "default",
1971+
data: {
1972+
to: runtimeTransferAddress,
1973+
abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi,
1974+
functionName: "transferFunds",
1975+
args: [
1976+
tokenAddress,
1977+
eoaAccount.address,
1978+
runtimeERC20BalanceOf({
1979+
targetAddress: runtimeTransferAddress,
1980+
tokenAddress,
1981+
constraints: [greaterThanOrEqualToSigned(1n)]
1982+
})
1983+
],
1984+
chainId: chain.id
1985+
}
1986+
})
1987+
1988+
const { hash } = await meeClient_compos_v1_1_1.executeFusionQuote({
1989+
fusionQuote: await meeClient_compos_v1_1_1.getFusionQuote({
1990+
trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply },
1991+
instructions: [...staticTransferInstruction, ...sweepInstruction],
1992+
feeToken: { chainId: chain.id, address: tokenAddress }
1993+
})
1994+
})
1995+
1996+
const { transactionStatus, explorerLinks } =
1997+
await meeClient_compos_v1_1_1.waitForSupertransactionReceipt({
1998+
hash,
1999+
confirmations: TEST_BLOCK_CONFIRMATIONS
2000+
})
2001+
expect(transactionStatus).to.be.eq("MINED_SUCCESS")
2002+
console.log("[v1.1.1] GTE_SIGNED constraint composable sweep test:", {
2003+
explorerLinks,
2004+
hash
2005+
})
2006+
})
2007+
2008+
it("should execute composable sweep with LTE_SIGNED constraint on composability v1.1.1", async () => {
2009+
const amountToSupply = parseUnits("0.1", 6)
2010+
const amountToTransfer = parseUnits("0.05", 6)
2011+
const amountCap = parseUnits("100", 6) // cap at 100 USDC (signed)
2012+
2013+
// Static transfer: Nexus → runtimeTransferAddress (funded by trigger)
2014+
const staticTransferInstruction =
2015+
await mcNexus_compos_v1_1_1.buildComposable({
2016+
type: "transfer",
2017+
data: {
2018+
recipient: runtimeTransferAddress as Address,
2019+
tokenAddress,
2020+
amount: amountToTransfer,
2021+
chainId: chain.id
2022+
}
2023+
})
2024+
2025+
// Runtime sweep using LTE_SIGNED: runtime balance must be <= 100 USDC (int256 comparison)
2026+
const sweepInstruction = await mcNexus_compos_v1_1_1.buildComposable({
2027+
type: "default",
2028+
data: {
2029+
to: runtimeTransferAddress,
2030+
abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi,
2031+
functionName: "transferFunds",
2032+
args: [
2033+
tokenAddress,
2034+
eoaAccount.address,
2035+
runtimeERC20BalanceOf({
2036+
targetAddress: runtimeTransferAddress,
2037+
tokenAddress,
2038+
constraints: [lessThanOrEqualToSigned(amountCap)]
2039+
})
2040+
],
2041+
chainId: chain.id
2042+
}
2043+
})
2044+
2045+
const { hash } = await meeClient_compos_v1_1_1.executeFusionQuote({
2046+
fusionQuote: await meeClient_compos_v1_1_1.getFusionQuote({
2047+
trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply },
2048+
instructions: [...staticTransferInstruction, ...sweepInstruction],
2049+
feeToken: { chainId: chain.id, address: tokenAddress }
2050+
})
2051+
})
2052+
2053+
const { transactionStatus, explorerLinks } =
2054+
await meeClient_compos_v1_1_1.waitForSupertransactionReceipt({
2055+
hash,
2056+
confirmations: TEST_BLOCK_CONFIRMATIONS
2057+
})
2058+
expect(transactionStatus).to.be.eq("MINED_SUCCESS")
2059+
console.log("[v1.1.1] LTE_SIGNED constraint composable sweep test:", {
2060+
explorerLinks,
2061+
hash
2062+
})
2063+
})
2064+
2065+
it("should execute composable sweep with OR constraint mixing signed and unsigned sub-constraints on composability v1.1.1", async () => {
2066+
const amountToSupply = parseUnits("0.1", 6)
2067+
const amountToTransfer = parseUnits("0.05", 6)
2068+
const amountCap = parseUnits("100", 6)
2069+
2070+
// Static transfer: Nexus → runtimeTransferAddress (funded by trigger)
2071+
const staticTransferInstruction =
2072+
await mcNexus_compos_v1_1_1.buildComposable({
2073+
type: "transfer",
2074+
data: {
2075+
recipient: runtimeTransferAddress as Address,
2076+
tokenAddress,
2077+
amount: amountToTransfer,
2078+
chainId: chain.id
2079+
}
2080+
})
2081+
2082+
// Runtime sweep: transfer only if balance satisfies OR(GTE_SIGNED(-1), LTE(100 USDC))
2083+
// This mixes a signed sub-constraint with an unsigned one inside a single OR.
2084+
const sweepInstruction = await mcNexus_compos_v1_1_1.buildComposable({
2085+
type: "default",
2086+
data: {
2087+
to: runtimeTransferAddress,
2088+
abi: COMPOSABILITY_RUNTIME_TRANSFER_ABI as Abi,
2089+
functionName: "transferFunds",
2090+
args: [
2091+
tokenAddress,
2092+
eoaAccount.address,
2093+
runtimeERC20BalanceOf({
2094+
targetAddress: runtimeTransferAddress,
2095+
tokenAddress,
2096+
constraints: [
2097+
orConstraint([
2098+
greaterThanOrEqualToSigned(-1n),
2099+
lessThanOrEqualTo(amountCap)
2100+
])
2101+
]
2102+
})
2103+
],
2104+
chainId: chain.id
2105+
}
2106+
})
2107+
2108+
const { hash } = await meeClient_compos_v1_1_1.executeFusionQuote({
2109+
fusionQuote: await meeClient_compos_v1_1_1.getFusionQuote({
2110+
trigger: { chainId: chain.id, tokenAddress, amount: amountToSupply },
2111+
instructions: [...staticTransferInstruction, ...sweepInstruction],
2112+
feeToken: { chainId: chain.id, address: tokenAddress }
2113+
})
2114+
})
2115+
2116+
const { transactionStatus, explorerLinks } =
2117+
await meeClient_compos_v1_1_1.waitForSupertransactionReceipt({
2118+
hash,
2119+
confirmations: TEST_BLOCK_CONFIRMATIONS
2120+
})
2121+
expect(transactionStatus).to.be.eq("MINED_SUCCESS")
2122+
console.log(
2123+
"[v1.1.1] OR constraint (signed + unsigned mix) composable sweep test:",
2124+
{ explorerLinks, hash }
2125+
)
2126+
})
2127+
18112128
// test the new 'runtimeParamViaCustomStaticCall' helper function and the injectable target at the same time
18122129
it("should execute composable transaction using runtimeParamViaCustomStaticCall (for composability v1.1.0+ only)", async () => {
18132130
const amount = 101n // 101 wei

0 commit comments

Comments
 (0)