From 6c8637ff5f429ec214e1aed4ba403e0f5090f189 Mon Sep 17 00:00:00 2001 From: PedroAraoz Date: Wed, 22 Jul 2026 18:11:45 -0300 Subject: [PATCH 01/14] lib: partial add swap and split --- packages/lib-ts/src/helpers/index.ts | 1 + packages/lib-ts/src/helpers/swapAndSplit.ts | 73 +++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/lib-ts/src/helpers/swapAndSplit.ts diff --git a/packages/lib-ts/src/helpers/index.ts b/packages/lib-ts/src/helpers/index.ts index ef9389bd..5a995687 100644 --- a/packages/lib-ts/src/helpers/index.ts +++ b/packages/lib-ts/src/helpers/index.ts @@ -5,4 +5,5 @@ export * from './BorshDeserializer' export * from './constants' export * from './serialize' export * from './strings' +export * from './swapAndSplit' export { Consensus, Math } diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts new file mode 100644 index 00000000..4fe02636 --- /dev/null +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -0,0 +1,73 @@ +import { environment } from '../environment' +import { EvmDynamicArg, EvmDynamicCallBuilder, IntentBuilder, SwapBuilder } from '../intents' +import { TokenAmount } from '../tokens' +import { Address, Bytes, ChainId, EvmEncodeParam } from '../types' + +import { MIMIC_HELPER_ADDRESS } from './constants' + +const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) + +export function buildSwapAndSplit( + smartAccount: Address, + chainId: ChainId, + tokenIn: TokenAmount, + tokenOut: TokenAmount, + recipients: Address[], + pcts: u8[], + swapUser: Address = smartAccount +): IntentBuilder { + if (recipients.length != pcts.length + 1) throw new Error('recipients must have one more element than pcts') + if (recipients.length <= 1) throw new Error('More than 1 recipient needed') + const builder = new IntentBuilder() + const settler = environment.getContext().findSettler(chainId) + const swap = SwapBuilder.forChain(chainId) + .addUser(swapUser) + .addTokenInFromTokenAmount(tokenIn) + .addTokenOutFromTokenAmount(tokenOut, settler) + + builder.addOperationBuilder(swap) + + const SWAP_OUTPUT = EvmDynamicArg.variable(0, 0, false) + const pctSelector = Bytes.fromHexString('0x73d9f5d0') + const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) + + for (let i = 0; i < pcts.length; i++) { + const pct = pcts[i] + dynamicCall1.addCall(MIMIC_HELPER, pctSelector, [ + SWAP_OUTPUT, // amount + EvmDynamicArg.literal([new EvmEncodeParam('uint8', pct.toString())], false), // percent + ]) + } + // last % with remainder + const pctRemainderSelector = Bytes.fromHexString('0x05702f4f') + dynamicCall1.addCall(MIMIC_HELPER, pctRemainderSelector, [ + SWAP_OUTPUT, // amount + EvmDynamicArg.literal( + [ + EvmEncodeParam.fromValues( + 'uint8[]', + pcts.map((pct: u8) => new EvmEncodeParam('uint8', pct.toString())) + ), + ], + true + ), //pct[] + ]) + + builder.addOperationBuilder(dynamicCall1) + + const transferSelector = Bytes.fromHexString('0xa9059cbb') + const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) + + for (let i = 0; i < recipients.length; i++) { + const target = tokenOut.token.address + const recipient = recipients[i] + dynamicCall2.addCall(target, transferSelector, [ + EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to + EvmDynamicArg.variable(1, i, false), // value ( dynamicCall1 sub 'i' result ) + ]) + } + + builder.addOperationBuilder(dynamicCall2) + + return builder +} From 5c9732dc055a1ed776639031f852d068de286e65 Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 27 Jul 2026 14:22:02 -0400 Subject: [PATCH 02/14] lib: use public smart account --- packages/lib-ts/src/helpers/constants.ts | 1 + packages/lib-ts/src/helpers/swapAndSplit.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/lib-ts/src/helpers/constants.ts b/packages/lib-ts/src/helpers/constants.ts index e6b8460f..373553f7 100644 --- a/packages/lib-ts/src/helpers/constants.ts +++ b/packages/lib-ts/src/helpers/constants.ts @@ -13,3 +13,4 @@ export enum ListType { } export const MIMIC_HELPER_ADDRESS = '0x5cf82cbed1110fc2f75b3413d53abac492931804' +export const MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS = '' diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index 4fe02636..0a292e22 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -3,33 +3,34 @@ import { EvmDynamicArg, EvmDynamicCallBuilder, IntentBuilder, SwapBuilder } from import { TokenAmount } from '../tokens' import { Address, Bytes, ChainId, EvmEncodeParam } from '../types' -import { MIMIC_HELPER_ADDRESS } from './constants' +import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './constants' const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) +const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) export function buildSwapAndSplit( - smartAccount: Address, chainId: ChainId, tokenIn: TokenAmount, tokenOut: TokenAmount, recipients: Address[], pcts: u8[], - swapUser: Address = smartAccount + user: Address | null = null ): IntentBuilder { - if (recipients.length != pcts.length + 1) throw new Error('recipients must have one more element than pcts') + if (recipients.length != pcts.length + 1) throw new Error('Recipients must have one more element than pcts') if (recipients.length <= 1) throw new Error('More than 1 recipient needed') + const builder = new IntentBuilder() - const settler = environment.getContext().findSettler(chainId) + const swap = SwapBuilder.forChain(chainId) - .addUser(swapUser) + .addUser(user || environment.getContext().user) .addTokenInFromTokenAmount(tokenIn) - .addTokenOutFromTokenAmount(tokenOut, settler) + .addTokenOutFromTokenAmount(tokenOut, MIMIC_PUBLIC_SMART_ACCOUNT) builder.addOperationBuilder(swap) const SWAP_OUTPUT = EvmDynamicArg.variable(0, 0, false) const pctSelector = Bytes.fromHexString('0x73d9f5d0') - const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) + const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < pcts.length; i++) { const pct = pcts[i] @@ -56,7 +57,7 @@ export function buildSwapAndSplit( builder.addOperationBuilder(dynamicCall1) const transferSelector = Bytes.fromHexString('0xa9059cbb') - const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) + const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < recipients.length; i++) { const target = tokenOut.token.address From 0084244ce7727fb1ccf4ee7b837d245c11bd7fec Mon Sep 17 00:00:00 2001 From: lgalende Date: Tue, 28 Jul 2026 12:29:42 -0400 Subject: [PATCH 03/14] lib: use balanceOf instead of pctRemainder --- packages/lib-ts/src/helpers/constants.ts | 4 +- packages/lib-ts/src/helpers/swapAndSplit.ts | 69 ++++++++++++--------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/lib-ts/src/helpers/constants.ts b/packages/lib-ts/src/helpers/constants.ts index 373553f7..a9130b42 100644 --- a/packages/lib-ts/src/helpers/constants.ts +++ b/packages/lib-ts/src/helpers/constants.ts @@ -12,5 +12,5 @@ export enum ListType { DenyList = 1, } -export const MIMIC_HELPER_ADDRESS = '0x5cf82cbed1110fc2f75b3413d53abac492931804' -export const MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS = '' +export const MIMIC_HELPER_ADDRESS = '0x2401b81ffc7015f06ac289c1eebc42822b29a0ef' +export const MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS = '0xf8425597f287347d049c61f07b5e8652ef09212f' diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index 0a292e22..772df36d 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -8,67 +8,80 @@ import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './cons const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) +export class Allocation { + constructor( + public recipient: Address, + public pct: u8 + ) {} +} + export function buildSwapAndSplit( chainId: ChainId, - tokenIn: TokenAmount, - tokenOut: TokenAmount, - recipients: Address[], - pcts: u8[], + amountIn: TokenAmount, + minAmountOut: TokenAmount, + allocations: Allocation[], user: Address | null = null ): IntentBuilder { - if (recipients.length != pcts.length + 1) throw new Error('Recipients must have one more element than pcts') - if (recipients.length <= 1) throw new Error('More than 1 recipient needed') + const totalPct = allocations.reduce((total, allocation) => total + allocation.pct, 0) + if (totalPct !== 100) throw new Error('Total allocation percentage must be 100') const builder = new IntentBuilder() const swap = SwapBuilder.forChain(chainId) .addUser(user || environment.getContext().user) - .addTokenInFromTokenAmount(tokenIn) - .addTokenOutFromTokenAmount(tokenOut, MIMIC_PUBLIC_SMART_ACCOUNT) + .addTokenInFromTokenAmount(amountIn) + .addTokenOutFromTokenAmount(minAmountOut, MIMIC_PUBLIC_SMART_ACCOUNT) builder.addOperationBuilder(swap) + // Calculate the corresponding amount for each allocation, except the last one const SWAP_OUTPUT = EvmDynamicArg.variable(0, 0, false) const pctSelector = Bytes.fromHexString('0x73d9f5d0') const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - for (let i = 0; i < pcts.length; i++) { - const pct = pcts[i] + for (let i = 0; i < allocations.length - 1; i++) { + const pct = allocations[i].pct dynamicCall1.addCall(MIMIC_HELPER, pctSelector, [ SWAP_OUTPUT, // amount EvmDynamicArg.literal([new EvmEncodeParam('uint8', pct.toString())], false), // percent ]) } - // last % with remainder - const pctRemainderSelector = Bytes.fromHexString('0x05702f4f') - dynamicCall1.addCall(MIMIC_HELPER, pctRemainderSelector, [ - SWAP_OUTPUT, // amount - EvmDynamicArg.literal( - [ - EvmEncodeParam.fromValues( - 'uint8[]', - pcts.map((pct: u8) => new EvmEncodeParam('uint8', pct.toString())) - ), - ], - true - ), //pct[] - ]) builder.addOperationBuilder(dynamicCall1) + // Transfer the corresponding amounts to each recipient, except the last one const transferSelector = Bytes.fromHexString('0xa9059cbb') const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - for (let i = 0; i < recipients.length; i++) { - const target = tokenOut.token.address - const recipient = recipients[i] + for (let i = 0; i < allocations.length - 1; i++) { + const target = minAmountOut.token.address + const recipient = allocations[i].recipient dynamicCall2.addCall(target, transferSelector, [ EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to - EvmDynamicArg.variable(1, i, false), // value ( dynamicCall1 sub 'i' result ) + EvmDynamicArg.variable(1, i, false), // value (dynamicCall1 sub 'i' result) ]) } builder.addOperationBuilder(dynamicCall2) + // Get the remaining balance + const balanceOfSelector = Bytes.fromHexString('0x70a08231') + const dynamicCall3 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + dynamicCall3.addCall(minAmountOut.token.address, balanceOfSelector, [ + EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account + ]) + + builder.addOperationBuilder(dynamicCall3) + + // Transfer the remaining balance to the last recipient + const lastRecipient = allocations[allocations.length - 1].recipient + const dynamicCall4 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + dynamicCall4.addCall(minAmountOut.token.address, transferSelector, [ + EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to + EvmDynamicArg.variable(3, 0, false), // value (dynamicCall3 result) + ]) + + builder.addOperationBuilder(dynamicCall4) + return builder } From 4594e715edeccefb9f52e9c8c71ac9eb2890b81f Mon Sep 17 00:00:00 2001 From: lgalende Date: Tue, 28 Jul 2026 12:33:18 -0400 Subject: [PATCH 04/14] chore: add inline docs and extract constants --- packages/lib-ts/src/helpers/swapAndSplit.ts | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index 772df36d..6310fbb7 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -8,6 +8,10 @@ import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './cons const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) +const PCT_SELECTOR = Bytes.fromHexString('0x73d9f5d0') +const TRANSFER_SELECTOR = Bytes.fromHexString('0xa9059cbb') +const BALANCE_OF_SELECTOR = Bytes.fromHexString('0x70a08231') + export class Allocation { constructor( public recipient: Address, @@ -15,6 +19,17 @@ export class Allocation { ) {} } +/** + * Creates an IntentBuilder containing operations to swap tokens and split the output to multiple recipients. + * The last recipient percentage is ignored, and will receive the remaining balance after the other allocations. + * @param chainId The chain ID of the swap. + * @param amountIn The amount of tokens to swap. + * @param minAmountOut The minimum amount of tokens to receive from the swap. + * @param allocations An array containing a recipient address and a percentage (0-100). + * It represents how the output of the swap will be split among the recipients. + * @param user The user address for the swap (optional). If not provided, the context user will be used. + * @returns An IntentBuilder object that can be used to build and send the intent. + */ export function buildSwapAndSplit( chainId: ChainId, amountIn: TokenAmount, @@ -36,12 +51,11 @@ export function buildSwapAndSplit( // Calculate the corresponding amount for each allocation, except the last one const SWAP_OUTPUT = EvmDynamicArg.variable(0, 0, false) - const pctSelector = Bytes.fromHexString('0x73d9f5d0') const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { const pct = allocations[i].pct - dynamicCall1.addCall(MIMIC_HELPER, pctSelector, [ + dynamicCall1.addCall(MIMIC_HELPER, PCT_SELECTOR, [ SWAP_OUTPUT, // amount EvmDynamicArg.literal([new EvmEncodeParam('uint8', pct.toString())], false), // percent ]) @@ -50,13 +64,12 @@ export function buildSwapAndSplit( builder.addOperationBuilder(dynamicCall1) // Transfer the corresponding amounts to each recipient, except the last one - const transferSelector = Bytes.fromHexString('0xa9059cbb') const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { const target = minAmountOut.token.address const recipient = allocations[i].recipient - dynamicCall2.addCall(target, transferSelector, [ + dynamicCall2.addCall(target, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to EvmDynamicArg.variable(1, i, false), // value (dynamicCall1 sub 'i' result) ]) @@ -65,9 +78,8 @@ export function buildSwapAndSplit( builder.addOperationBuilder(dynamicCall2) // Get the remaining balance - const balanceOfSelector = Bytes.fromHexString('0x70a08231') const dynamicCall3 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall3.addCall(minAmountOut.token.address, balanceOfSelector, [ + dynamicCall3.addCall(minAmountOut.token.address, BALANCE_OF_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account ]) @@ -76,7 +88,7 @@ export function buildSwapAndSplit( // Transfer the remaining balance to the last recipient const lastRecipient = allocations[allocations.length - 1].recipient const dynamicCall4 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall4.addCall(minAmountOut.token.address, transferSelector, [ + dynamicCall4.addCall(minAmountOut.token.address, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to EvmDynamicArg.variable(3, 0, false), // value (dynamicCall3 result) ]) From 2bda7c896a28221892e17a6aa64fe776d4f6e73b Mon Sep 17 00:00:00 2001 From: lgalende Date: Tue, 28 Jul 2026 12:37:20 -0400 Subject: [PATCH 05/14] chore: add changeset --- .changeset/fiery-frogs-exist.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fiery-frogs-exist.md diff --git a/.changeset/fiery-frogs-exist.md b/.changeset/fiery-frogs-exist.md new file mode 100644 index 00000000..e7db2921 --- /dev/null +++ b/.changeset/fiery-frogs-exist.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +"@mimicprotocol/test-ts": patch +--- + +Add swapAndSplit function From 6aabb4a2ed9d9bcef29fe8313840d4ab4307ce4d Mon Sep 17 00:00:00 2001 From: lgalende Date: Tue, 28 Jul 2026 15:19:56 -0400 Subject: [PATCH 06/14] lib: use bps for allocation percentage --- packages/lib-ts/src/helpers/constants.ts | 2 +- packages/lib-ts/src/helpers/swapAndSplit.ts | 23 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/lib-ts/src/helpers/constants.ts b/packages/lib-ts/src/helpers/constants.ts index a9130b42..69d3a01d 100644 --- a/packages/lib-ts/src/helpers/constants.ts +++ b/packages/lib-ts/src/helpers/constants.ts @@ -12,5 +12,5 @@ export enum ListType { DenyList = 1, } -export const MIMIC_HELPER_ADDRESS = '0x2401b81ffc7015f06ac289c1eebc42822b29a0ef' +export const MIMIC_HELPER_ADDRESS = '0x3b4f927bb967ab873725b8b765043b6cfacba38a' export const MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS = '0xf8425597f287347d049c61f07b5e8652ef09212f' diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index 6310fbb7..fb727e4a 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -8,25 +8,27 @@ import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './cons const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) -const PCT_SELECTOR = Bytes.fromHexString('0x73d9f5d0') +const PCT_SELECTOR = Bytes.fromHexString('0xe7032021') const TRANSFER_SELECTOR = Bytes.fromHexString('0xa9059cbb') const BALANCE_OF_SELECTOR = Bytes.fromHexString('0x70a08231') +const MAX_PCT_BPS: u16 = 10_000 + export class Allocation { constructor( public recipient: Address, - public pct: u8 + public pctBps: u16 ) {} } /** - * Creates an IntentBuilder containing operations to swap tokens and split the output to multiple recipients. + * @dev Creates an IntentBuilder containing operations to swap tokens and transfer the output to multiple recipients. * The last recipient percentage is ignored, and will receive the remaining balance after the other allocations. - * @param chainId The chain ID of the swap. + * @param chainId The chain ID of the swap and the transfers. * @param amountIn The amount of tokens to swap. * @param minAmountOut The minimum amount of tokens to receive from the swap. - * @param allocations An array containing a recipient address and a percentage (0-100). - * It represents how the output of the swap will be split among the recipients. + * @param allocations An array containing a recipient address and a percentage in basis points (e.g., 50 = 0.5%, 10_000 = 100%). + * It represents how the output of the swap will be split among the recipients. The total allocation must add up to 10_000. * @param user The user address for the swap (optional). If not provided, the context user will be used. * @returns An IntentBuilder object that can be used to build and send the intent. */ @@ -37,8 +39,9 @@ export function buildSwapAndSplit( allocations: Allocation[], user: Address | null = null ): IntentBuilder { - const totalPct = allocations.reduce((total, allocation) => total + allocation.pct, 0) - if (totalPct !== 100) throw new Error('Total allocation percentage must be 100') + let totalPctBps: u32 = 0 + for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps + if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must be 10_000 bps') const builder = new IntentBuilder() @@ -54,10 +57,10 @@ export function buildSwapAndSplit( const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { - const pct = allocations[i].pct + const pctBps = allocations[i].pctBps dynamicCall1.addCall(MIMIC_HELPER, PCT_SELECTOR, [ SWAP_OUTPUT, // amount - EvmDynamicArg.literal([new EvmEncodeParam('uint8', pct.toString())], false), // percent + EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps ]) } From 1d37232ddbee47b0aab9e08ee2f349a1f2b73e51 Mon Sep 17 00:00:00 2001 From: lgalende Date: Tue, 28 Jul 2026 17:43:14 -0400 Subject: [PATCH 07/14] lib: add native token check --- packages/lib-ts/src/helpers/swapAndSplit.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index fb727e4a..e93475c9 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -25,8 +25,8 @@ export class Allocation { * @dev Creates an IntentBuilder containing operations to swap tokens and transfer the output to multiple recipients. * The last recipient percentage is ignored, and will receive the remaining balance after the other allocations. * @param chainId The chain ID of the swap and the transfers. - * @param amountIn The amount of tokens to swap. - * @param minAmountOut The minimum amount of tokens to receive from the swap. + * @param amountIn The amount of tokens to swap. If the token is native, the `user` must be a smart account. + * @param minAmountOut The minimum amount of tokens to receive from the swap. ERC20 tokens only. * @param allocations An array containing a recipient address and a percentage in basis points (e.g., 50 = 0.5%, 10_000 = 100%). * It represents how the output of the swap will be split among the recipients. The total allocation must add up to 10_000. * @param user The user address for the swap (optional). If not provided, the context user will be used. @@ -39,10 +39,15 @@ export function buildSwapAndSplit( allocations: Allocation[], user: Address | null = null ): IntentBuilder { + if (allocations.length <= 1) throw new Error('More than 1 allocation is needed') + let totalPctBps: u32 = 0 for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must be 10_000 bps') + const tokenOut = minAmountOut.token.address + if (tokenOut.isNative()) throw new Error('Output token cannot be native') + const builder = new IntentBuilder() const swap = SwapBuilder.forChain(chainId) @@ -70,9 +75,8 @@ export function buildSwapAndSplit( const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { - const target = minAmountOut.token.address const recipient = allocations[i].recipient - dynamicCall2.addCall(target, TRANSFER_SELECTOR, [ + dynamicCall2.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to EvmDynamicArg.variable(1, i, false), // value (dynamicCall1 sub 'i' result) ]) @@ -82,7 +86,7 @@ export function buildSwapAndSplit( // Get the remaining balance const dynamicCall3 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall3.addCall(minAmountOut.token.address, BALANCE_OF_SELECTOR, [ + dynamicCall3.addCall(tokenOut, BALANCE_OF_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account ]) @@ -91,7 +95,7 @@ export function buildSwapAndSplit( // Transfer the remaining balance to the last recipient const lastRecipient = allocations[allocations.length - 1].recipient const dynamicCall4 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall4.addCall(minAmountOut.token.address, TRANSFER_SELECTOR, [ + dynamicCall4.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to EvmDynamicArg.variable(3, 0, false), // value (dynamicCall3 result) ]) From 107f9770319f8a8d8a7573e8c4d2d43e15a284aa Mon Sep 17 00:00:00 2001 From: lgalende Date: Wed, 29 Jul 2026 12:23:00 -0400 Subject: [PATCH 08/14] chore: test swapAndSplit --- packages/lib-ts/src/helpers/swapAndSplit.ts | 3 +- .../lib-ts/tests/helpers/swapAndSplit.spec.ts | 137 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/lib-ts/tests/helpers/swapAndSplit.spec.ts diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index e93475c9..baefe54b 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -58,13 +58,12 @@ export function buildSwapAndSplit( builder.addOperationBuilder(swap) // Calculate the corresponding amount for each allocation, except the last one - const SWAP_OUTPUT = EvmDynamicArg.variable(0, 0, false) const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { const pctBps = allocations[i].pctBps dynamicCall1.addCall(MIMIC_HELPER, PCT_SELECTOR, [ - SWAP_OUTPUT, // amount + EvmDynamicArg.variable(0, 0, false), // amount (swap output) EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps ]) } diff --git a/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts b/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts new file mode 100644 index 00000000..94232133 --- /dev/null +++ b/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts @@ -0,0 +1,137 @@ +import { buildSwapAndSplit } from '../../src/helpers' +import { MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from '../../src/helpers/constants' +import { Allocation } from '../../src/helpers/swapAndSplit' +import { EvmDynamicCall, OperationType, Swap } from '../../src/intents' +import { ERC20Token, TokenAmount } from '../../src/tokens' +import { Address } from '../../src/types' +import { randomSettler, setContext, setEvmEncode } from '../helpers' + +const chainId = 1 +const user = Address.fromString('0x0000000000000000000000000000000000000001') +const recipient1 = Address.fromString('0x0000000000000000000000000000000000000002') +const recipient2 = Address.fromString('0x0000000000000000000000000000000000000003') +const recipient3 = Address.fromString('0x0000000000000000000000000000000000000004') +const tokenIn = ERC20Token.fromAddress( + Address.fromString('0x0000000000000000000000000000000000000010'), + chainId, + 6, + 'USDC' +) +const tokenOut = ERC20Token.fromAddress( + Address.fromString('0x0000000000000000000000000000000000000020'), + chainId, + 18, + 'DAI' +) +const amountIn = TokenAmount.fromStringDecimal(tokenIn, '10') +const minAmountOut = TokenAmount.fromStringDecimal(tokenOut, '100') + +describe('buildSwapAndSplit', () => { + beforeEach(() => { + setContext(1, 1, user.toString(), [randomSettler(chainId)], 'trigger-123') + + setEvmEncode('uint256', '0', '0x1000') // Dynamic variable 0 + setEvmEncode('uint256', '1', '0x1001') // Dynamic variable 1 + setEvmEncode('uint256', '3', '0x1003') // Dynamic variable 3 + + setEvmEncode('uint16', '9050', '0x9050') + setEvmEncode('uint16', '125', '0x0125') + + setEvmEncode('address', MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS, '0x2000') + setEvmEncode('address', recipient1.toString(), '0x2001') + setEvmEncode('address', recipient2.toString(), '0x2002') + setEvmEncode('address', recipient3.toString(), '0x2003') + }) + + it('creates the intent properly', () => { + const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, [ + new Allocation(recipient1, 9050), + new Allocation(recipient2, 125), + new Allocation(recipient3, 825), + ]).build() + + expect(intent.operations.length).toBe(5) + expect(intent.operations[0].opType).toBe(OperationType.Swap) + expect(intent.operations[1].opType).toBe(OperationType.EvmDynamicCall) + expect(intent.operations[2].opType).toBe(OperationType.EvmDynamicCall) + expect(intent.operations[3].opType).toBe(OperationType.EvmDynamicCall) + expect(intent.operations[4].opType).toBe(OperationType.EvmDynamicCall) + + const swap = changetype(intent.operations[0]) + expect(swap.sourceChain).toBe(chainId) + expect(swap.destinationChain).toBe(chainId) + expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) + expect(swap.tokensIn[0].amount).toBe(amountIn.amount.toString()) + expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) + expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.amount.toString()) + expect(swap.user).toBe(user.toString()) + + const pctCall = changetype(intent.operations[1]) + expect(pctCall.calls.length).toBe(2) + + expect(pctCall.calls[0].selector).toBe('0xe7032021') + expect(pctCall.calls[0].arguments[0].data).toBe('0x1000') + expect(pctCall.calls[0].arguments[1].data).toBe('0x9050') + + expect(pctCall.calls[1].selector).toBe('0xe7032021') + expect(pctCall.calls[1].arguments[0].data).toBe('0x1000') + expect(pctCall.calls[1].arguments[1].data).toBe('0x0125') + + const transferCall = changetype(intent.operations[2]) + expect(transferCall.calls.length).toBe(2) + + expect(transferCall.calls[0].selector).toBe('0xa9059cbb') + expect(transferCall.calls[0].arguments[0].data).toBe('0x2001') + expect(transferCall.calls[0].arguments[1].data).toBe('0x1001') + + expect(transferCall.calls[1].selector).toBe('0xa9059cbb') + expect(transferCall.calls[1].arguments[0].data).toBe('0x2002') + expect(transferCall.calls[1].arguments[1].data).toBe('0x1001') + + const balanceOfCall = changetype(intent.operations[3]) + expect(balanceOfCall.calls.length).toBe(1) + + expect(balanceOfCall.calls[0].selector).toBe('0x70a08231') + expect(balanceOfCall.calls[0].arguments[0].data).toBe('0x2000') + + const finalTransferCall = changetype(intent.operations[4]) + expect(finalTransferCall.calls.length).toBe(1) + + expect(finalTransferCall.calls[0].selector).toBe('0xa9059cbb') + expect(finalTransferCall.calls[0].arguments[0].data).toBe('0x2003') + expect(finalTransferCall.calls[0].arguments[1].data).toBe('0x1003') + }) + + it('throws when there is less than two allocations', () => { + expect(() => { + buildSwapAndSplit(chainId, amountIn, minAmountOut, []) + }).toThrow('More than 1 allocation is needed') + + expect(() => { + buildSwapAndSplit(chainId, amountIn, minAmountOut, [new Allocation(recipient1, 10_000)]) + }).toThrow('More than 1 allocation is needed') + }) + + it('throws when allocations do not add up to 100%', () => { + expect(() => { + const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 0)] + + buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) + }).toThrow('Total allocation percentage must be 10_000 bps') + + expect(() => { + const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 2)] + + buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) + }).toThrow('Total allocation percentage must be 10_000 bps') + }) + + it('throws when output token is native', () => { + expect(() => { + const nativeAmountOut = TokenAmount.fromStringDecimal(ERC20Token.native(chainId), '100') + const allocations = [new Allocation(recipient1, 50), new Allocation(recipient2, 9950)] + + buildSwapAndSplit(chainId, amountIn, nativeAmountOut, allocations) + }).toThrow('Output token cannot be native') + }) +}) From 5927ff5fa9a1462a50504c85e142222355c21f3c Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 17:32:21 -0400 Subject: [PATCH 09/14] chore: improve error message and comments --- packages/lib-ts/src/helpers/swapAndSplit.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index baefe54b..bf43b4cc 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -23,7 +23,8 @@ export class Allocation { /** * @dev Creates an IntentBuilder containing operations to swap tokens and transfer the output to multiple recipients. - * The last recipient percentage is ignored, and will receive the remaining balance after the other allocations. + * Each recipient receives the percentage of the output token specified in the allocations array. + * The last recipient receives its specified percentage plus any remaining balance caused by rounding. * @param chainId The chain ID of the swap and the transfers. * @param amountIn The amount of tokens to swap. If the token is native, the `user` must be a smart account. * @param minAmountOut The minimum amount of tokens to receive from the swap. ERC20 tokens only. @@ -43,7 +44,7 @@ export function buildSwapAndSplit( let totalPctBps: u32 = 0 for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps - if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must be 10_000 bps') + if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must add up to 10_000 bps') const tokenOut = minAmountOut.token.address if (tokenOut.isNative()) throw new Error('Output token cannot be native') @@ -57,7 +58,7 @@ export function buildSwapAndSplit( builder.addOperationBuilder(swap) - // Calculate the corresponding amount for each allocation, except the last one + // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { @@ -70,7 +71,7 @@ export function buildSwapAndSplit( builder.addOperationBuilder(dynamicCall1) - // Transfer the corresponding amounts to each recipient, except the last one + // Transfer the corresponding amounts to each recipient, except the last one, which will receive the remaining balance const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { From 3705d718efecd6e92b3f366199bcdf515b02eedc Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 17:38:53 -0400 Subject: [PATCH 10/14] chore: rename dynamic call builders --- packages/lib-ts/src/helpers/swapAndSplit.ts | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index bf43b4cc..fc689df9 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -59,48 +59,48 @@ export function buildSwapAndSplit( builder.addOperationBuilder(swap) // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance - const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const pctDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { const pctBps = allocations[i].pctBps - dynamicCall1.addCall(MIMIC_HELPER, PCT_SELECTOR, [ + pctDynamicCall.addCall(MIMIC_HELPER, PCT_SELECTOR, [ EvmDynamicArg.variable(0, 0, false), // amount (swap output) EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps ]) } - builder.addOperationBuilder(dynamicCall1) + builder.addOperationBuilder(pctDynamicCall) // Transfer the corresponding amounts to each recipient, except the last one, which will receive the remaining balance - const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const transferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) for (let i = 0; i < allocations.length - 1; i++) { const recipient = allocations[i].recipient - dynamicCall2.addCall(tokenOut, TRANSFER_SELECTOR, [ + transferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to - EvmDynamicArg.variable(1, i, false), // value (dynamicCall1 sub 'i' result) + EvmDynamicArg.variable(1, i, false), // value (pctDynamicCall sub 'i' result) ]) } - builder.addOperationBuilder(dynamicCall2) + builder.addOperationBuilder(transferDynamicCall) // Get the remaining balance - const dynamicCall3 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall3.addCall(tokenOut, BALANCE_OF_SELECTOR, [ + const balanceOfDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + balanceOfDynamicCall.addCall(tokenOut, BALANCE_OF_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account ]) - builder.addOperationBuilder(dynamicCall3) + builder.addOperationBuilder(balanceOfDynamicCall) // Transfer the remaining balance to the last recipient const lastRecipient = allocations[allocations.length - 1].recipient - const dynamicCall4 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) - dynamicCall4.addCall(tokenOut, TRANSFER_SELECTOR, [ + const lastTransferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + lastTransferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to - EvmDynamicArg.variable(3, 0, false), // value (dynamicCall3 result) + EvmDynamicArg.variable(3, 0, false), // value (balanceOfDynamicCall result) ]) - builder.addOperationBuilder(dynamicCall4) + builder.addOperationBuilder(lastTransferDynamicCall) return builder } From 219aca4bcfab76511b7a1f485a1da46b29bba681 Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 18:09:42 -0400 Subject: [PATCH 11/14] chore: improve dynamic args readability --- packages/lib-ts/src/helpers/swapAndSplit.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index fc689df9..de110465 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -14,6 +14,12 @@ const BALANCE_OF_SELECTOR = Bytes.fromHexString('0x70a08231') const MAX_PCT_BPS: u16 = 10_000 +const SWAP_OP_INDEX: u32 = 0 +const SWAP_OP_SUB_INDEX: u32 = 0 +const PCT_OP_INDEX: u32 = 1 +const BALANCE_OF_OP_INDEX: u32 = 3 +const BALANCE_OF_OP_SUB_INDEX: u32 = 0 + export class Allocation { constructor( public recipient: Address, @@ -60,11 +66,12 @@ export function buildSwapAndSplit( // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance const pctDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const swapOutput = EvmDynamicArg.variable(SWAP_OP_INDEX, SWAP_OP_SUB_INDEX, false) for (let i = 0; i < allocations.length - 1; i++) { const pctBps = allocations[i].pctBps pctDynamicCall.addCall(MIMIC_HELPER, PCT_SELECTOR, [ - EvmDynamicArg.variable(0, 0, false), // amount (swap output) + swapOutput, // amount EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps ]) } @@ -76,9 +83,10 @@ export function buildSwapAndSplit( for (let i = 0; i < allocations.length - 1; i++) { const recipient = allocations[i].recipient + const pctOutput = EvmDynamicArg.variable(PCT_OP_INDEX, i, false) transferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to - EvmDynamicArg.variable(1, i, false), // value (pctDynamicCall sub 'i' result) + pctOutput, // value ]) } @@ -93,11 +101,13 @@ export function buildSwapAndSplit( builder.addOperationBuilder(balanceOfDynamicCall) // Transfer the remaining balance to the last recipient - const lastRecipient = allocations[allocations.length - 1].recipient const lastTransferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const lastRecipient = allocations[allocations.length - 1].recipient + const balanceOfOutput = EvmDynamicArg.variable(BALANCE_OF_OP_INDEX, BALANCE_OF_OP_SUB_INDEX, false) + lastTransferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to - EvmDynamicArg.variable(3, 0, false), // value (balanceOfDynamicCall result) + balanceOfOutput, // value ]) builder.addOperationBuilder(lastTransferDynamicCall) From 4df8cfdd879e53b7f9276fc6b7a6254a2441d800 Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 18:12:24 -0400 Subject: [PATCH 12/14] lib: avoid hardcoding default user --- packages/lib-ts/src/helpers/swapAndSplit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index de110465..e60a8f59 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -58,10 +58,11 @@ export function buildSwapAndSplit( const builder = new IntentBuilder() const swap = SwapBuilder.forChain(chainId) - .addUser(user || environment.getContext().user) .addTokenInFromTokenAmount(amountIn) .addTokenOutFromTokenAmount(minAmountOut, MIMIC_PUBLIC_SMART_ACCOUNT) + if (user) swap.addUser(user) + builder.addOperationBuilder(swap) // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance From 99b52ab70f3213fb7a2153da94d912f9951c141b Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 18:32:27 -0400 Subject: [PATCH 13/14] chore: move MAX_PCT_BPS to constants and rename --- packages/lib-ts/src/helpers/constants.ts | 2 ++ packages/lib-ts/src/helpers/swapAndSplit.ts | 11 ++++++----- packages/lib-ts/src/tokens/TokenAmount.ts | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/lib-ts/src/helpers/constants.ts b/packages/lib-ts/src/helpers/constants.ts index 69d3a01d..4c64030f 100644 --- a/packages/lib-ts/src/helpers/constants.ts +++ b/packages/lib-ts/src/helpers/constants.ts @@ -7,6 +7,8 @@ export const MAX_UINT256_HEX = '0xffffffffffffffffffffffffffffffffffffffffffffff export const STANDARD_DECIMALS: u8 = 18 +export const ONE_HUNDRED_PCT_BPS: u16 = 10_000 + export enum ListType { AllowList = 0, DenyList = 1, diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index e60a8f59..694c1d7f 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -1,9 +1,8 @@ -import { environment } from '../environment' import { EvmDynamicArg, EvmDynamicCallBuilder, IntentBuilder, SwapBuilder } from '../intents' import { TokenAmount } from '../tokens' import { Address, Bytes, ChainId, EvmEncodeParam } from '../types' -import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './constants' +import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS, ONE_HUNDRED_PCT_BPS } from './constants' const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) @@ -12,7 +11,7 @@ const PCT_SELECTOR = Bytes.fromHexString('0xe7032021') const TRANSFER_SELECTOR = Bytes.fromHexString('0xa9059cbb') const BALANCE_OF_SELECTOR = Bytes.fromHexString('0x70a08231') -const MAX_PCT_BPS: u16 = 10_000 +const MIN_ALLOCATIONS: i32 = 2 const SWAP_OP_INDEX: u32 = 0 const SWAP_OP_SUB_INDEX: u32 = 0 @@ -46,11 +45,13 @@ export function buildSwapAndSplit( allocations: Allocation[], user: Address | null = null ): IntentBuilder { - if (allocations.length <= 1) throw new Error('More than 1 allocation is needed') + if (allocations.length < MIN_ALLOCATIONS) throw new Error(`At least ${MIN_ALLOCATIONS} allocations are required`) let totalPctBps: u32 = 0 for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps - if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must add up to 10_000 bps') + if (totalPctBps !== ONE_HUNDRED_PCT_BPS) { + throw new Error(`Total allocation percentage must add up to ${ONE_HUNDRED_PCT_BPS} bps`) + } const tokenOut = minAmountOut.token.address if (tokenOut.isNative()) throw new Error('Output token cannot be native') diff --git a/packages/lib-ts/src/tokens/TokenAmount.ts b/packages/lib-ts/src/tokens/TokenAmount.ts index d292c745..f4a7f01c 100644 --- a/packages/lib-ts/src/tokens/TokenAmount.ts +++ b/packages/lib-ts/src/tokens/TokenAmount.ts @@ -1,11 +1,12 @@ import { environment } from '../environment' +import { ONE_HUNDRED_PCT_BPS } from '../helpers/constants' import { BigInt, JSON, Result } from '../types' import { BlockchainToken } from './BlockchainToken' import { SerializableToken, Token } from './Token' import { USD } from './USD' -const BPS_SCALE = BigInt.fromI32(10_000) +const BPS_SCALE = BigInt.fromI32(ONE_HUNDRED_PCT_BPS) /** * Represents an amount of a specific token, combining the token metadata with a quantity. From 1c8170e2b524900b446b0a5d4438d905b03b134a Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Aug 2026 22:09:45 -0400 Subject: [PATCH 14/14] lib: add smart account optional param --- packages/lib-ts/src/helpers/swapAndSplit.ts | 17 ++++--- .../lib-ts/tests/helpers/swapAndSplit.spec.ts | 45 ++++++++++++++++--- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/lib-ts/src/helpers/swapAndSplit.ts b/packages/lib-ts/src/helpers/swapAndSplit.ts index 694c1d7f..55da6d19 100644 --- a/packages/lib-ts/src/helpers/swapAndSplit.ts +++ b/packages/lib-ts/src/helpers/swapAndSplit.ts @@ -36,6 +36,8 @@ export class Allocation { * @param allocations An array containing a recipient address and a percentage in basis points (e.g., 50 = 0.5%, 10_000 = 100%). * It represents how the output of the swap will be split among the recipients. The total allocation must add up to 10_000. * @param user The user address for the swap (optional). If not provided, the context user will be used. + * @param smartAccount The smart account that receives the swap output and executes the transfers (optional). + * If not provided, the Mimic public smart account will be used. * @returns An IntentBuilder object that can be used to build and send the intent. */ export function buildSwapAndSplit( @@ -43,7 +45,8 @@ export function buildSwapAndSplit( amountIn: TokenAmount, minAmountOut: TokenAmount, allocations: Allocation[], - user: Address | null = null + user: Address | null = null, + smartAccount: Address = MIMIC_PUBLIC_SMART_ACCOUNT ): IntentBuilder { if (allocations.length < MIN_ALLOCATIONS) throw new Error(`At least ${MIN_ALLOCATIONS} allocations are required`) @@ -60,14 +63,14 @@ export function buildSwapAndSplit( const swap = SwapBuilder.forChain(chainId) .addTokenInFromTokenAmount(amountIn) - .addTokenOutFromTokenAmount(minAmountOut, MIMIC_PUBLIC_SMART_ACCOUNT) + .addTokenOutFromTokenAmount(minAmountOut, smartAccount) if (user) swap.addUser(user) builder.addOperationBuilder(swap) // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance - const pctDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const pctDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) const swapOutput = EvmDynamicArg.variable(SWAP_OP_INDEX, SWAP_OP_SUB_INDEX, false) for (let i = 0; i < allocations.length - 1; i++) { @@ -81,7 +84,7 @@ export function buildSwapAndSplit( builder.addOperationBuilder(pctDynamicCall) // Transfer the corresponding amounts to each recipient, except the last one, which will receive the remaining balance - const transferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const transferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) for (let i = 0; i < allocations.length - 1; i++) { const recipient = allocations[i].recipient @@ -95,15 +98,15 @@ export function buildSwapAndSplit( builder.addOperationBuilder(transferDynamicCall) // Get the remaining balance - const balanceOfDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const balanceOfDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) balanceOfDynamicCall.addCall(tokenOut, BALANCE_OF_SELECTOR, [ - EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account + EvmDynamicArg.literal([new EvmEncodeParam('address', smartAccount.toString())], false), // account ]) builder.addOperationBuilder(balanceOfDynamicCall) // Transfer the remaining balance to the last recipient - const lastTransferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) + const lastTransferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) const lastRecipient = allocations[allocations.length - 1].recipient const balanceOfOutput = EvmDynamicArg.variable(BALANCE_OF_OP_INDEX, BALANCE_OF_OP_SUB_INDEX, false) diff --git a/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts b/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts index 94232133..d5070fe9 100644 --- a/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts +++ b/packages/lib-ts/tests/helpers/swapAndSplit.spec.ts @@ -11,6 +11,8 @@ const user = Address.fromString('0x0000000000000000000000000000000000000001') const recipient1 = Address.fromString('0x0000000000000000000000000000000000000002') const recipient2 = Address.fromString('0x0000000000000000000000000000000000000003') const recipient3 = Address.fromString('0x0000000000000000000000000000000000000004') +const anotherUser = Address.fromString('0x0000000000000000000000000000000000000005') +const anotherSmartAccount = Address.fromString('0x0000000000000000000000000000000000000006') const tokenIn = ERC20Token.fromAddress( Address.fromString('0x0000000000000000000000000000000000000010'), chainId, @@ -41,6 +43,7 @@ describe('buildSwapAndSplit', () => { setEvmEncode('address', recipient1.toString(), '0x2001') setEvmEncode('address', recipient2.toString(), '0x2002') setEvmEncode('address', recipient3.toString(), '0x2003') + setEvmEncode('address', anotherSmartAccount.toString(), '0x2006') }) it('creates the intent properly', () => { @@ -60,11 +63,12 @@ describe('buildSwapAndSplit', () => { const swap = changetype(intent.operations[0]) expect(swap.sourceChain).toBe(chainId) expect(swap.destinationChain).toBe(chainId) + expect(swap.user).toBe(user.toString()) expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) expect(swap.tokensIn[0].amount).toBe(amountIn.amount.toString()) expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.amount.toString()) - expect(swap.user).toBe(user.toString()) + expect(swap.tokensOut[0].recipient).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS.toString()) const pctCall = changetype(intent.operations[1]) expect(pctCall.calls.length).toBe(2) @@ -102,14 +106,43 @@ describe('buildSwapAndSplit', () => { expect(finalTransferCall.calls[0].arguments[1].data).toBe('0x1003') }) + it('uses the given user when defined', () => { + const allocations = [new Allocation(recipient1, 9050), new Allocation(recipient2, 950)] + const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations, anotherUser).build() + + const swap = changetype(intent.operations[0]) + expect(swap.user).toBe(anotherUser.toString()) + expect(swap.tokensOut[0].recipient).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) + + for (let i = 1; i < intent.operations.length; i++) { + expect(intent.operations[i].user).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) + } + }) + + it('uses the given smart account when defined', () => { + const allocations = [new Allocation(recipient1, 9050), new Allocation(recipient2, 950)] + const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations, null, anotherSmartAccount).build() + + const swap = changetype(intent.operations[0]) + expect(swap.tokensOut[0].recipient).toBe(anotherSmartAccount.toString()) + expect(swap.user).toBe(user.toString()) + + for (let i = 1; i < intent.operations.length; i++) { + expect(intent.operations[i].user).toBe(anotherSmartAccount.toString()) + } + + const balanceOfCall = changetype(intent.operations[3]) + expect(balanceOfCall.calls[0].arguments[0].data).toBe('0x2006') + }) + it('throws when there is less than two allocations', () => { expect(() => { buildSwapAndSplit(chainId, amountIn, minAmountOut, []) - }).toThrow('More than 1 allocation is needed') + }).toThrow() expect(() => { buildSwapAndSplit(chainId, amountIn, minAmountOut, [new Allocation(recipient1, 10_000)]) - }).toThrow('More than 1 allocation is needed') + }).toThrow() }) it('throws when allocations do not add up to 100%', () => { @@ -117,13 +150,13 @@ describe('buildSwapAndSplit', () => { const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 0)] buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) - }).toThrow('Total allocation percentage must be 10_000 bps') + }).toThrow() expect(() => { const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 2)] buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) - }).toThrow('Total allocation percentage must be 10_000 bps') + }).toThrow() }) it('throws when output token is native', () => { @@ -132,6 +165,6 @@ describe('buildSwapAndSplit', () => { const allocations = [new Allocation(recipient1, 50), new Allocation(recipient2, 9950)] buildSwapAndSplit(chainId, amountIn, nativeAmountOut, allocations) - }).toThrow('Output token cannot be native') + }).toThrow() }) })