diff --git a/.changeset/late-cobras-turn.md b/.changeset/late-cobras-turn.md new file mode 100644 index 00000000..5ed0a1b3 --- /dev/null +++ b/.changeset/late-cobras-turn.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/test-ts": patch +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +--- + +Refactor intents to have operations diff --git a/packages/cli/tests/fixtures/functions/function.ts b/packages/cli/tests/fixtures/functions/function.ts index 9095fd2a..44c9bfd4 100644 --- a/packages/cli/tests/fixtures/functions/function.ts +++ b/packages/cli/tests/fixtures/functions/function.ts @@ -1,4 +1,13 @@ -import { Address, BigInt, Bytes, ERC20Token, EvmCallBuilder, NULL_ADDRESS, TokenAmount } from '@mimicprotocol/lib-ts' +import { + Address, + BigInt, + Bytes, + ERC20Token, + EvmCallBuilder, + IntentBuilder, + NULL_ADDRESS, + TokenAmount, +} from '@mimicprotocol/lib-ts' /* eslint-disable @typescript-eslint/no-namespace */ declare namespace input { @@ -16,5 +25,9 @@ export default function main(): void { const maxFeeAmount = BigInt.fromI32(input.firstStaticNumber).times(BigInt.fromI32(input.secondStaticNumber)) const maxFee = TokenAmount.fromBigInt(maxFeeToken, maxFeeAmount) - EvmCallBuilder.forChain(chainId).addCall(target, data).addSettler(settler).addMaxFee(maxFee).build().send() + new IntentBuilder() + .addSettler(settler) + .addMaxFee(maxFee) + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(target, data)) + .send() } diff --git a/packages/cli/tests/fixtures/functions/invalid-function.ts b/packages/cli/tests/fixtures/functions/invalid-function.ts index cb75e020..fe05ac30 100644 --- a/packages/cli/tests/fixtures/functions/invalid-function.ts +++ b/packages/cli/tests/fixtures/functions/invalid-function.ts @@ -4,6 +4,7 @@ import { BlockchainToken, Bytes, EvmCallBuilder, + IntentBuilder, NULL_ADDRESS, TokenAmount, } from '@mimicprotocol/lib-ts' @@ -17,5 +18,9 @@ export default function main(): void { const maxFeeAmount = BigInt.zero().plus(BigInt.fromI32(undeclaredVariable)) const maxFee = TokenAmount.fromBigInt(maxFeeToken, maxFeeAmount) - EvmCallBuilder.forChain(chainId).addCall(target, data).addSettler(settler).addMaxFee(maxFee).build().send() + new IntentBuilder() + .addSettler(settler) + .addMaxFee(maxFee) + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(target, data)) + .send() } diff --git a/packages/integration/tests/001-init-intent/expected.log b/packages/integration/tests/001-init-intent/expected.log index 01f6f4a1..69e3663c 100644 --- a/packages/integration/tests/001-init-intent/expected.log +++ b/packages/integration/tests/001-init-intent/expected.log @@ -1 +1 @@ -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"10"}],"events":[],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"5"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"10"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"5"}]}]} diff --git a/packages/integration/tests/001-init-intent/src/function.ts b/packages/integration/tests/001-init-intent/src/function.ts index 4cd3d870..148884ab 100644 --- a/packages/integration/tests/001-init-intent/src/function.ts +++ b/packages/integration/tests/001-init-intent/src/function.ts @@ -4,7 +4,7 @@ import { Bytes, ChainId, ERC20Token, - EvmCallBuilder, + IntentBuilder, NULL_ADDRESS, TokenAmount, } from '@mimicprotocol/lib-ts' @@ -16,5 +16,5 @@ export default function main(): void { const value = BigInt.fromI32(5) const fee = TokenAmount.fromI32(ERC20Token.fromString(NULL_ADDRESS, chainId), 10) - EvmCallBuilder.forChain(chainId).addCall(target, data, value).addMaxFee(fee).build().send() + new IntentBuilder().addMaxFee(fee).addEvmCallOperation(chainId, target, data, value).send() } diff --git a/packages/integration/tests/002-evm-call-intent/expected.log b/packages/integration/tests/002-evm-call-intent/expected.log index 1e04836a..5bd2c522 100644 --- a/packages/integration/tests/002-evm-call-intent/expected.log +++ b/packages/integration/tests/002-evm-call-intent/expected.log @@ -1 +1 @@ -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0xa000000000000000000000000000000000000001","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa000000000000000000000000000000000000003","amount":"10"}],"events":[],"chainId":1,"calls":[{"target":"0xa000000000000000000000000000000000000002","data":"0xabcdef0123456789","value":"1000000000000000000"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa000000000000000000000000000000000000003","amount":"10"}],"operations":[{"opType":2,"chainId":1,"user":"0xa000000000000000000000000000000000000001","events":[],"calls":[{"target":"0xa000000000000000000000000000000000000002","data":"0xabcdef0123456789","value":"1000000000000000000"}]}]} diff --git a/packages/integration/tests/002-evm-call-intent/src/function.ts b/packages/integration/tests/002-evm-call-intent/src/function.ts index 4803367f..c42fde7f 100644 --- a/packages/integration/tests/002-evm-call-intent/src/function.ts +++ b/packages/integration/tests/002-evm-call-intent/src/function.ts @@ -9,7 +9,5 @@ export default function main(): void { EvmCallBuilder.forChain(inputs.chainId) .addCall(inputs.target, inputs.data, inputs.value) .addUser(inputs.user) - .addMaxFee(maxFee) - .build() - .send() + .send(maxFee) } diff --git a/packages/integration/tests/003-multiple-evm-call-intents/expected.log b/packages/integration/tests/003-multiple-evm-call-intents/expected.log index 0afb7dd2..8efb65c1 100644 --- a/packages/integration/tests/003-multiple-evm-call-intents/expected.log +++ b/packages/integration/tests/003-multiple-evm-call-intents/expected.log @@ -1,3 +1,3 @@ -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"10"}],"events":[],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]} -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"9"}],"events":[],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]} -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"8"}],"events":[],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"10"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]}]} +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"9"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]}]} +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000000","amount":"8"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0x0000000000000000000000000000000000000000","data":"0x","value":"0"}]}]} diff --git a/packages/integration/tests/003-multiple-evm-call-intents/src/function.ts b/packages/integration/tests/003-multiple-evm-call-intents/src/function.ts index a10e6cbc..09297974 100644 --- a/packages/integration/tests/003-multiple-evm-call-intents/src/function.ts +++ b/packages/integration/tests/003-multiple-evm-call-intents/src/function.ts @@ -8,11 +8,11 @@ export default function main(): void { const data = Bytes.empty() const fee1 = TokenAmount.fromI32(token, 10) - EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).addMaxFee(fee1).build().send() + EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).send(fee1) const fee2 = fee1.minus(TokenAmount.fromI32(token, 1)) - EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).addMaxFee(fee2).build().send() + EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).send(fee2) const fee3 = fee1.minus(TokenAmount.fromI32(token, 2)) - EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).addMaxFee(fee3).build().send() + EvmCallBuilder.forChain(inputs.chainId).addCall(target, data).send(fee3) } diff --git a/packages/integration/tests/004-three-intents/expected.log b/packages/integration/tests/004-three-intents/expected.log index 0c78abf8..293967ca 100644 --- a/packages/integration/tests/004-three-intents/expected.log +++ b/packages/integration/tests/004-three-intents/expected.log @@ -1,3 +1,3 @@ -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"10000000"}],"events":[],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000001","data":"0x","value":"0"},{"target":"0x0000000000000000000000000000000000000001","data":"0x7b000000","value":"0"}]} -_swap: {"op":0,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[],"events":[],"sourceChain":1,"tokensIn":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"100000000"}],"tokensOut":[{"token":"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","minAmount":"9500000000","recipient":"0x0000000000000000000000000000000000000001"}],"destinationChain":1} -_transfer: {"op":1,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"10000000"}],"events":[],"chainId":1,"transfers":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"100000000","recipient":"0x0000000000000000000000000000000000000001"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"10000000"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0x0000000000000000000000000000000000000001","data":"0x","value":"0"},{"target":"0x0000000000000000000000000000000000000001","data":"0x7b000000","value":"0"}]}]} +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[],"operations":[{"opType":0,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"sourceChain":1,"tokensIn":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"100000000"}],"tokensOut":[{"token":"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599","minAmount":"9500000000","recipient":"0x0000000000000000000000000000000000000001"}],"destinationChain":1}]} +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"10000000"}],"operations":[{"opType":1,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"transfers":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"100000000","recipient":"0x0000000000000000000000000000000000000001"}]}]} diff --git a/packages/integration/tests/004-three-intents/src/function.ts b/packages/integration/tests/004-three-intents/src/function.ts index 6bd5205d..f875f92c 100644 --- a/packages/integration/tests/004-three-intents/src/function.ts +++ b/packages/integration/tests/004-three-intents/src/function.ts @@ -22,7 +22,7 @@ export default function main(): void { const bytes = Bytes.fromI32(123) const callFee = TokenAmount.fromI32(USDC, 10) - EvmCallBuilder.forChain(chainId).addCall(target).addCall(target, bytes).addMaxFee(callFee).build().send() + EvmCallBuilder.forChain(chainId).addCall(target).addCall(target, bytes).send(callFee) // Normal swap const minAmountOut = BigInt.fromI32(inputs.amount).times(BigInt.fromI32(inputs.slippage)).div(BigInt.fromI32(100)) @@ -32,16 +32,11 @@ export default function main(): void { SwapBuilder.forChains(chainId, chainId) .addTokenInFromTokenAmount(tokenIn) .addTokenOutFromTokenAmount(tokenOut, target) - .build() .send() // Normal Transfer const tokenAmount = TokenAmount.fromI32(USDC, inputs.amount) const transferFee = TokenAmount.fromI32(USDC, 10) - TransferBuilder.forChain(chainId) - .addTransferFromTokenAmount(tokenAmount, target) - .addMaxFee(transferFee) - .build() - .send() + TransferBuilder.forChain(chainId).addTransferFromTokenAmount(tokenAmount, target).send(transferFee) } diff --git a/packages/integration/tests/008-write-contract-calls-tests/expected.log b/packages/integration/tests/008-write-contract-calls-tests/expected.log index ba04b370..6ea4a15f 100644 --- a/packages/integration/tests/008-write-contract-calls-tests/expected.log +++ b/packages/integration/tests/008-write-contract-calls-tests/expected.log @@ -1,3 +1,3 @@ _encode: [{"abiType":"address","value":"0x7f5c764cbc14f9669b88837ca1490cca17c31607"},{"abiType":"uint256","value":"100000000"},{"abiType":"address","value":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0"}] -_evmCall: {"op":2,"settler":"0xdcf1d9d12a0488dfb70a8696f44d6d3bc303963d","user":"0x047be3bb46f9416732fe39a05134f20235c19334","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","amount":"100000"}],"events":[],"chainId":10,"calls":[{"target":"0x794a61358d6845594f94dc1db02a252b5b4814ad","data":"0x69328dec00","value":"0"}]} -_evmCall: {"op":2,"settler":"0xdcf1d9d12a0488dfb70a8696f44d6d3bc303963d","user":"0x047be3bb46f9416732fe39a05134f20235c19334","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","amount":"100000"}],"events":[],"chainId":10,"calls":[{"target":"0x4200000000000000000000000000000000000006","data":"0xd0e30db0","value":"10"}]} \ No newline at end of file +_sendIntent: {"settler":"0xdcf1d9d12a0488dfb70a8696f44d6d3bc303963d","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","amount":"100000"}],"operations":[{"opType":2,"chainId":10,"user":"0x047be3bb46f9416732fe39a05134f20235c19334","events":[],"calls":[{"target":"0x794a61358d6845594f94dc1db02a252b5b4814ad","data":"0x69328dec00","value":"0"}]}]} +_sendIntent: {"settler":"0xdcf1d9d12a0488dfb70a8696f44d6d3bc303963d","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58","amount":"100000"}],"operations":[{"opType":2,"chainId":10,"user":"0x047be3bb46f9416732fe39a05134f20235c19334","events":[],"calls":[{"target":"0x4200000000000000000000000000000000000006","data":"0xd0e30db0","value":"10"}]}]} diff --git a/packages/integration/tests/008-write-contract-calls-tests/src/function.ts b/packages/integration/tests/008-write-contract-calls-tests/src/function.ts index 56b7bc6b..188a1797 100644 --- a/packages/integration/tests/008-write-contract-calls-tests/src/function.ts +++ b/packages/integration/tests/008-write-contract-calls-tests/src/function.ts @@ -22,12 +22,7 @@ export default function main(): void { const aaveContract = new AAVE(Address.fromString('0x794a61358d6845594f94dc1db02a252b5b4814ad'), chainId) - aaveContract - .withdraw(USDCe.address, userTokens[0].amount, context.user) - .addMaxFee(feeUsdt) - .addUser(inputs.smartAccount) - .build() - .send() - - weth.deposit(BigInt.fromI32(10)).addMaxFee(feeUsdt).addUser(inputs.smartAccount).build().send() + aaveContract.withdraw(USDCe.address, userTokens[0].amount, context.user).addUser(inputs.smartAccount).send(feeUsdt) + + weth.deposit(BigInt.fromI32(10)).addUser(inputs.smartAccount).send(feeUsdt) } diff --git a/packages/integration/tests/009-solana-intents/expected.log b/packages/integration/tests/009-solana-intents/expected.log index 28b4a9d3..e70ee4eb 100644 --- a/packages/integration/tests/009-solana-intents/expected.log +++ b/packages/integration/tests/009-solana-intents/expected.log @@ -1,3 +1,3 @@ -_transfer: {"op":1,"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"So11111111111111111111111111111111111111112","amount":"1"}],"events":[],"chainId":507424,"transfers":[{"token":"So11111111111111111111111111111111111111112","amount":"4000000000000","recipient":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K"}]} -_swap: {"op":0,"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","deadline":"1438223473","nonce":"0xabcd","maxFees":[],"events":[],"sourceChain":507424,"tokensIn":[{"token":"So11111111111111111111111111111111111111112","amount":"1000000000000"}],"tokensOut":[{"token":"So11111111111111111111111111111111111111112","minAmount":"2000","recipient":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K"}],"destinationChain":507424} -_svmCall: {"op":3,"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"So11111111111111111111111111111111111111112","amount":"1"}],"events":[],"chainId":507424,"instructions":[{"programId":"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb","accountsMeta":[{"pubkey":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","isWritable":false,"isSigner":true},{"pubkey":"So11111111111111111111111111111111111111112","isWritable":true,"isSigner":false}],"data":"0xabcd"}]} \ No newline at end of file +_sendIntent: {"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"So11111111111111111111111111111111111111112","amount":"1"}],"operations":[{"opType":1,"chainId":507424,"user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","events":[],"transfers":[{"token":"So11111111111111111111111111111111111111112","amount":"4000000000000","recipient":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K"}]}]} +_sendIntent: {"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[],"operations":[{"opType":0,"chainId":507424,"user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","events":[],"sourceChain":507424,"tokensIn":[{"token":"So11111111111111111111111111111111111111112","amount":"1000000000000"}],"tokensOut":[{"token":"So11111111111111111111111111111111111111112","minAmount":"2000","recipient":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K"}],"destinationChain":507424}]} +_sendIntent: {"settler":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"So11111111111111111111111111111111111111112","amount":"1"}],"operations":[{"opType":3,"chainId":507424,"user":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","events":[],"instructions":[{"programId":"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb","accountsMeta":[{"pubkey":"HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K","isWritable":false,"isSigner":true},{"pubkey":"So11111111111111111111111111111111111111112","isWritable":true,"isSigner":false}],"data":"0xabcd"}]}]} diff --git a/packages/integration/tests/009-solana-intents/src/function.ts b/packages/integration/tests/009-solana-intents/src/function.ts index 1232d491..7806db86 100644 --- a/packages/integration/tests/009-solana-intents/src/function.ts +++ b/packages/integration/tests/009-solana-intents/src/function.ts @@ -23,9 +23,7 @@ export default function main(): void { TransferBuilder.forChain(ChainId.SOLANA_MAINNET) .addTransferFromI32(splToken, 4000, solanaUser) .addUser(solanaUser) - .addMaxFee(new TokenAmount(splToken, BigInt.fromI32(1))) - .build() - .send() + .send(new TokenAmount(splToken, BigInt.fromI32(1))) // Swap @@ -33,7 +31,6 @@ export default function main(): void { .addTokenInFromStringDecimal(splToken, '1000') .addTokenOutFromTokenAmount(new TokenAmount(splToken, BigInt.fromI32(2000)), solanaUser) .addUser(solanaUser) - .build() .send() // Call @@ -50,7 +47,5 @@ export default function main(): void { SvmCallBuilder.forChain() .addInstruction(ix) .addUser(solanaUser) - .addMaxFee(new TokenAmount(splToken, BigInt.fromI32(1))) - .build() - .send() + .send(new TokenAmount(splToken, BigInt.fromI32(1))) } diff --git a/packages/integration/tests/010-intents-with-events/expected.log b/packages/integration/tests/010-intents-with-events/expected.log index f01411dd..842ffd59 100644 --- a/packages/integration/tests/010-intents-with-events/expected.log +++ b/packages/integration/tests/010-intents-with-events/expected.log @@ -1 +1 @@ -_evmCall: {"op":2,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000348","amount":"1000000000000000000"}],"events":[{"topic":"0xabcd","data":"0x64617461"},{"topic":"0xabcd","data":"0x64617461"}],"chainId":1,"calls":[{"target":"0x0000000000000000000000000000000000000001","data":"0x","value":"0"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0000000000000000000000000000000000000348","amount":"1000000000000000000"}],"operations":[{"opType":2,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[{"topic":"0xabcd","data":"0x64617461"},{"topic":"0xabcd","data":"0x64617461"}],"calls":[{"target":"0x0000000000000000000000000000000000000001","data":"0x","value":"0"}]}]} diff --git a/packages/integration/tests/010-intents-with-events/src/function.ts b/packages/integration/tests/010-intents-with-events/src/function.ts index 4eca78c7..9bf43621 100644 --- a/packages/integration/tests/010-intents-with-events/src/function.ts +++ b/packages/integration/tests/010-intents-with-events/src/function.ts @@ -2,11 +2,9 @@ import { Address, Bytes, DenominationToken, environment, evm, EvmCallBuilder, To export default function main(): void { EvmCallBuilder.forChain(1) - .addMaxFee(TokenAmount.fromStringDecimal(DenominationToken.USD(), '1')) .addCall(Address.fromString('0x0000000000000000000000000000000000000001'), Bytes.empty()) .addUser(environment.getContext().user) .addEvent(Bytes.fromHexString(evm.keccak('event1')), Bytes.fromUTF8('data')) .addEvent(Bytes.fromHexString(evm.keccak('event2')), Bytes.fromUTF8('data')) - .build() - .send() + .send(TokenAmount.fromStringDecimal(DenominationToken.USD(), '1')) } diff --git a/packages/integration/tests/013-token-inputs/expected.log b/packages/integration/tests/013-token-inputs/expected.log index bffa69a4..546d9dd9 100644 --- a/packages/integration/tests/013-token-inputs/expected.log +++ b/packages/integration/tests/013-token-inputs/expected.log @@ -1 +1 @@ -_transfer: {"op":1,"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0b2c639c533813f4aa9d7837caf62653d097ff85","amount":"1000000"}],"events":[],"chainId":10,"transfers":[{"token":"0x0b2c639c533813f4aa9d7837caf62653d097ff85","amount":"100500000","recipient":"0xdd4c30aa2f3284e462df0b45c99a7e6e9ea9d186"}]} \ No newline at end of file +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0x0b2c639c533813f4aa9d7837caf62653d097ff85","amount":"1000000"}],"operations":[{"opType":1,"chainId":10,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"transfers":[{"token":"0x0b2c639c533813f4aa9d7837caf62653d097ff85","amount":"100500000","recipient":"0xdd4c30aa2f3284e462df0b45c99a7e6e9ea9d186"}]}]} diff --git a/packages/integration/tests/013-token-inputs/src/function.ts b/packages/integration/tests/013-token-inputs/src/function.ts index c9567bfa..fb933eea 100644 --- a/packages/integration/tests/013-token-inputs/src/function.ts +++ b/packages/integration/tests/013-token-inputs/src/function.ts @@ -9,7 +9,5 @@ export default function main(): void { TransferBuilder.forChain(Optimism.CHAIN_ID) .addTransferFromTokenAmount(amount, recipient) - .addMaxFee(TokenAmount.fromStringDecimal(Optimism.USDC, '1')) - .build() - .send() + .send(TokenAmount.fromStringDecimal(Optimism.USDC, '1')) } diff --git a/packages/lib-ts/src/environment.ts b/packages/lib-ts/src/environment.ts index d95a9240..12b93bd4 100644 --- a/packages/lib-ts/src/environment.ts +++ b/packages/lib-ts/src/environment.ts @@ -3,7 +3,7 @@ import { JSON } from 'json-as/assembly' import { Context, SerializableContext } from './context' import { evm } from './evm' import { Consensus, ListType, MIMIC_HELPER_ADDRESS } from './helpers' -import { EvmCall, SvmCall, Swap, Transfer } from './intents' +import { Intent } from './intents' import { EvmCallQuery, EvmCallQueryResponse, @@ -23,17 +23,8 @@ import { BlockchainToken, Token, TokenAmount, USD } from './tokens' import { Address, BigInt, Bytes, ChainId, EvmDecodeParam, EvmEncodeParam, Result } from './types' export namespace environment { - @external('environment', '_evmCall') - declare function _evmCall(params: string): void - - @external('environment', '_svmCall') - declare function _svmCall(params: string): void - - @external('environment', '_swap') - declare function _swap(params: string): void - - @external('environment', '_transfer') - declare function _transfer(params: string): void + @external('environment', '_sendIntent') + declare function _sendIntent(params: string): void @external('environment', '_tokenPriceQuery') declare function _tokenPriceQuery(params: string): string @@ -54,35 +45,11 @@ export namespace environment { declare function _getContext(): string /** - * Generates a EVM Call intent containing contract calls on the blockchain. - * @param call - The EvmCall intent to generate - */ - export function evmCall(call: EvmCall): void { - _evmCall(JSON.stringify(call)) - } - - /** - * Generates a SVM Call intent containing contract calls on the blockchain. - * @param call - The SvmCall intent to generate - */ - export function svmCall(call: SvmCall): void { - _svmCall(JSON.stringify(call)) - } - - /** - * Generates a Swap intent for token exchange operations. - * @param swap - The Swap intent to generate - */ - export function swap(swap: Swap): void { - _swap(JSON.stringify(swap)) - } - - /** - * Generates a Transfer intent for sending tokens to recipients. - * @param transfer - The Transfer intent to generate + * Sends an intent containing one or more operations to the execution environment. + * @param intent - The intent to send */ - export function transfer(transfer: Transfer): void { - _transfer(JSON.stringify(transfer)) + export function sendIntent(intent: Intent): void { + _sendIntent(JSON.stringify(intent)) } /** diff --git a/packages/lib-ts/src/intents/Call/EvmCall.ts b/packages/lib-ts/src/intents/Call/EvmCall.ts index 2e2260b7..2f156eef 100644 --- a/packages/lib-ts/src/intents/Call/EvmCall.ts +++ b/packages/lib-ts/src/intents/Call/EvmCall.ts @@ -1,18 +1,18 @@ import { environment } from '../../environment' import { TokenAmount } from '../../tokens' import { Address, BigInt, Bytes, ChainId } from '../../types' -import { Intent, IntentBuilder, IntentEvent, MaxFee, OperationType } from '../Intent' +import { IntentBuilder } from '../Intent' +import { Operation, OperationBuilder, OperationEvent, OperationType } from '../Operation' /** - * Builder for creating EVM Call intents with contract call operations. - * Allows chaining multiple contract calls and configuring fees and settlement parameters. + * Builder for creating EVM call operations. */ -export class EvmCallBuilder extends IntentBuilder { +export class EvmCallBuilder extends OperationBuilder { protected chainId: ChainId protected calls: EvmCallData[] = [] /** - * Creates a EvmCallBuilder for the specified EVM blockchain network. + * Creates an EvmCallBuilder for the specified EVM blockchain network. * @param chainId - The blockchain network identifier * @returns A new EvmCallBuilder instance */ @@ -30,10 +30,10 @@ export class EvmCallBuilder extends IntentBuilder { } /** - * Adds a contract call to the intent. + * Adds a contract call to the operation. * @param target - The contract address to call - * @param data - The call data (optional, defaults to empty bytes) - * @param value - The native token value to send (optional, defaults to zero) + * @param data - The call data + * @param value - The native token value to send * @returns This EvmCallBuilder instance for method chaining */ addCall(target: Address, data: Bytes = Bytes.empty(), value: BigInt = BigInt.zero()): EvmCallBuilder { @@ -42,7 +42,7 @@ export class EvmCallBuilder extends IntentBuilder { } /** - * Adds multiple contract calls to the intent. + * Adds multiple contract calls to the operation. * @param calls - The contract calls to add * @returns This EvmCallBuilder instance for method chaining */ @@ -84,34 +84,7 @@ export class EvmCallBuilder extends IntentBuilder { } /** - * Sets the settler address for this intent. - * @param settler - The settler address as an Address instance - * @returns This EvmCallBuilder instance for method chaining - */ - addSettler(settler: Address): EvmCallBuilder { - return changetype(super.addSettler(settler)) - } - - /** - * Sets the settler address from a string. - * @param settler - The settler address as a hex string - * @returns This EvmCallBuilder instance for method chaining - */ - addSettlerAsString(settler: string): EvmCallBuilder { - return changetype(super.addSettlerAsString(settler)) - } - - /** - * Sets the deadline for this intent. - * @param deadline - The deadline as a timestamp - * @returns This EvmCallBuilder instance for method chaining - */ - addDeadline(deadline: BigInt): EvmCallBuilder { - return changetype(super.addDeadline(deadline)) - } - - /** - * Sets the user address for this intent. + * Sets the user address for this operation. * @param user - The user address * @returns This EvmCallBuilder instance for method chaining */ @@ -129,27 +102,7 @@ export class EvmCallBuilder extends IntentBuilder { } /** - * Sets the nonce for this intent. - * @param nonce - A unique identifier to prevent replay attacks - * @returns This EvmCallBuilder instance for method chaining - */ - addNonce(nonce: string): EvmCallBuilder { - return changetype(super.addNonce(nonce)) - } - - /** - * Adds a max fee for this intent. - * @param fee - The max fee token amount (must be on same chain) - * @returns This EvmCallBuilder instance for method chaining - */ - addMaxFee(fee: TokenAmount): EvmCallBuilder { - if (!fee.token.hasChain(this.chainId)) throw new Error('Fee token must be on the same chain') - this.maxFees.push(fee) - return this - } - - /** - * Sets an event for the intent. + * Sets an event for the operation. * @param topic - The topic to be indexed in the event * @param data - The event data * @returns This EvmCallBuilder instance for method chaining @@ -159,34 +112,34 @@ export class EvmCallBuilder extends IntentBuilder { } /** - * Sets multiple events for the intent. + * Sets multiple events for the operation. * @param events - The list of events to be added * @returns This EvmCallBuilder instance for method chaining */ - addEvents(events: IntentEvent[]): EvmCallBuilder { + addEvents(events: OperationEvent[]): EvmCallBuilder { return changetype(super.addEvents(events)) } /** - * Builds and returns the final EvmCall intent. + * Builds and returns the final EvmCall operation. * @returns A new EvmCall instance with all configured parameters */ build(): EvmCall { - return new EvmCall( - this.chainId, - this.calls, - this.maxFees, - this.settler, - this.user, - this.deadline, - this.nonce, - this.events - ) + return new EvmCall(this.chainId, this.calls, this.user, this.events) + } + + /** + * Builds this operation and sends it inside an intent with the provided fee data. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) + */ + send(maxFee: TokenAmount, feePayer: Address | null = null): void { + this.build().send(maxFee, feePayer) } } /** - * Represents data for a single contract call within a Call intent. + * Represents data for a single contract call within an EVM call operation. * Contains the target address, call data, and value to send. */ @json @@ -198,8 +151,8 @@ export class EvmCallData { /** * Creates a new EvmCallData instance. * @param target - The contract address to call - * @param data - The call data (optional, defaults to empty bytes) - * @param value - The native token value to send (optional, defaults to zero) + * @param data - The call data + * @param value - The native token value to send */ constructor(target: Address, data: Bytes = Bytes.empty(), value: BigInt = BigInt.zero()) { this.target = target.toString() @@ -209,75 +162,38 @@ export class EvmCallData { } /** - * Represents a Call intent containing one or more contract calls to be executed. + * Represents an EVM call operation containing one or more contract calls to be executed. */ @json -export class EvmCall extends Intent { - public chainId: ChainId +export class EvmCall extends Operation { public calls: EvmCallData[] /** - * Creates a EvmCall intent with a single contract call. - * @param chainId - The blockchain network identifier - * @param target - The contract address to call - * @param data - The call data - * @param maxFee - The max fee to pay for the call intent - * @param value - The native token value to send (optional, defaults to zero) - * @param settler - The settler address (optional) - * @param user - The user address (optional) - * @param deadline - The deadline timestamp (optional) - * @param nonce - The nonce for replay protection (optional) - * @returns A new Call instance - */ - static create( - chainId: ChainId, - target: Address, - data: Bytes, - maxFee: TokenAmount, - value: BigInt = BigInt.zero(), - settler: Address | null = null, - user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null - ): EvmCall { - const callData = new EvmCallData(target, data, value) - return new EvmCall(chainId, [callData], [maxFee], settler, user, deadline, nonce, events) - } - - /** - * Creates a new EvmCall intent. + * Creates a new EvmCall operation. * @param chainId - The blockchain network identifier * @param calls - Array of contract calls to execute - * @param maxFees - The list of max fees to pay for the call intent - * @param settler - The settler address (optional) - * @param user - The user address (optional) - * @param deadline - The deadline timestamp (optional) - * @param nonce - The nonce for replay protection (optional) + * @param user - The user address + * @param events - The operation events to emit */ constructor( chainId: ChainId, calls: EvmCallData[], - maxFees: TokenAmount[], - settler: Address | null = null, user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null + events: OperationEvent[] | null = null ) { - const fees: MaxFee[] = maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) - super(OperationType.EvmCall, chainId, fees, settler, user, deadline, nonce, events) + super(OperationType.EvmCall, chainId, user, events) if (calls.length === 0) throw new Error('Call list cannot be empty') - if (maxFees.length == 0) throw new Error('At least a max fee must be specified') - this.calls = calls - this.chainId = chainId } /** - * Sends this EvmCall intent to the execution environment. + * Sends this EvmCall operation wrapped in an intent. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) */ - public send(): void { - environment.evmCall(this) + public send(maxFee: TokenAmount, feePayer: Address | null = null): void { + const intentBuilder = new IntentBuilder().addMaxFee(maxFee).addOperation(this) + if (feePayer) intentBuilder.addFeePayer(feePayer) + environment.sendIntent(intentBuilder.build()) } } diff --git a/packages/lib-ts/src/intents/Call/SvmCall.ts b/packages/lib-ts/src/intents/Call/SvmCall.ts index e415200a..a1a70dfa 100644 --- a/packages/lib-ts/src/intents/Call/SvmCall.ts +++ b/packages/lib-ts/src/intents/Call/SvmCall.ts @@ -1,14 +1,15 @@ import { environment } from '../../environment' import { TokenAmount } from '../../tokens' -import { Address, BigInt, Bytes, ChainId } from '../../types' +import { Address, Bytes, ChainId } from '../../types' import { SvmAccountMeta } from '../../types/svm/SvmAccountMeta' -import { Intent, IntentBuilder, IntentEvent, MaxFee, OperationType } from '../Intent' +import { IntentBuilder } from '../Intent' +import { Operation, OperationBuilder, OperationEvent, OperationType } from '../Operation' /** * Builder for creating SVM Call intents with program call operations. * Allows chaining multiple calls and configuring fees and settlement parameters. */ -export class SvmCallBuilder extends IntentBuilder { +export class SvmCallBuilder extends OperationBuilder { protected chainId: ChainId protected instructions: SvmInstruction[] = [] @@ -75,33 +76,6 @@ export class SvmCallBuilder extends IntentBuilder { return this.instructions.slice(0) } - /** - * Sets the settler address for this intent. - * @param settler - The settler address as an Address instance - * @returns This SvmCallBuilder instance for method chaining - */ - addSettler(settler: Address): SvmCallBuilder { - return changetype(super.addSettler(settler)) - } - - /** - * Sets the settler address from a string. - * @param settler - The settler address as a hex string - * @returns This SvmCallBuilder instance for method chaining - */ - addSettlerAsString(settler: string): SvmCallBuilder { - return changetype(super.addSettlerAsString(settler)) - } - - /** - * Sets the deadline for this intent. - * @param deadline - The deadline as a timestamp - * @returns This SvmCallBuilder instance for method chaining - */ - addDeadline(deadline: BigInt): SvmCallBuilder { - return changetype(super.addDeadline(deadline)) - } - /** * Sets the user address for this intent. * @param user - The user address @@ -120,26 +94,6 @@ export class SvmCallBuilder extends IntentBuilder { return changetype(super.addUserAsString(user)) } - /** - * Sets the nonce for this intent. - * @param nonce - A unique identifier to prevent replay attacks - * @returns This SvmCallBuilder instance for method chaining - */ - addNonce(nonce: string): SvmCallBuilder { - return changetype(super.addNonce(nonce)) - } - - /** - * Adds a max fee for this intent. - * @param fee - The max fee token amount (must be on same chain) - * @returns This SvmCallBuilder instance for method chaining - */ - addMaxFee(fee: TokenAmount): SvmCallBuilder { - if (!fee.token.hasChain(this.chainId)) throw new Error('Fee token must be on the same chain') - this.maxFees.push(fee) - return this - } - /** * Sets an event for the intent. * @param topic - The topic to be indexed in the event @@ -155,7 +109,7 @@ export class SvmCallBuilder extends IntentBuilder { * @param events - The list of events to be added * @returns This SvmCallBuilder instance for method chaining */ - addEvents(events: IntentEvent[]): SvmCallBuilder { + addEvents(events: OperationEvent[]): SvmCallBuilder { return changetype(super.addEvents(events)) } @@ -164,7 +118,16 @@ export class SvmCallBuilder extends IntentBuilder { * @returns A new SvmCall instance with all configured parameters */ build(): SvmCall { - return new SvmCall(this.instructions, this.maxFees, this.settler, this.user, this.deadline, this.nonce, this.events) + return new SvmCall(this.instructions, this.user, this.events) + } + + /** + * Builds this operation and sends it inside an intent with the provided fee data. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) + */ + send(maxFee: TokenAmount, feePayer: Address | null = null): void { + this.build().send(maxFee, feePayer) } } @@ -193,7 +156,7 @@ export class SvmInstructionBuilder { } instruction(): SvmInstruction { - return SvmInstruction.create(this.programId, this.accountsMeta, this.data) + return new SvmInstruction(this.programId.toBase58String(), this.accountsMeta, this.data.toHexString()) } } @@ -204,44 +167,15 @@ export class SvmInstruction { public accountsMeta: SvmAccountMeta[], public data: string ) {} - - static create(programId: Address, accountsMeta: SvmAccountMeta[], data: Bytes): SvmInstruction { - return new SvmInstruction(programId.toBase58String(), accountsMeta, data.toHexString()) - } } /** * Represents a SVM Call intent containing one or more program calls to be executed. */ @json -export class SvmCall extends Intent { - public chainId: ChainId +export class SvmCall extends Operation { public instructions: SvmInstruction[] - /** - * Creates a SvmCall intent with a single program call. - * @param maxFee - The max fee to pay for the call intent - * @param settler - The settler address (optional) - * @param user - The user address (optional) - * @param deadline - The deadline timestamp (optional) - * @param nonce - The nonce for replay protection (optional) - * @returns A new Call instance - */ - static create( - programId: Address, - accountsMeta: SvmAccountMeta[], - data: Bytes, - maxFee: TokenAmount, - settler: Address | null = null, - user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null - ): SvmCall { - const instruction = SvmInstruction.create(programId, accountsMeta, data) - return new SvmCall([instruction], [maxFee], settler, user, deadline, nonce, events) - } - /** * Creates a new SvmCall intent. * @param instructions - Array of instructions to execute @@ -251,28 +185,20 @@ export class SvmCall extends Intent { * @param deadline - The deadline timestamp (optional) * @param nonce - The nonce for replay protection (optional) */ - constructor( - instructions: SvmInstruction[], - maxFees: TokenAmount[], - settler: Address | null = null, - user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null - ) { - const fees: MaxFee[] = maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) - super(OperationType.SvmCall, ChainId.SOLANA_MAINNET, fees, settler, user, deadline, nonce, events) + constructor(instructions: SvmInstruction[], user: Address | null = null, events: OperationEvent[] | null = null) { + super(OperationType.SvmCall, ChainId.SOLANA_MAINNET, user, events) if (instructions.length === 0) throw new Error('Call list cannot be empty') - if (maxFees.length == 0) throw new Error('At least a max fee must be specified') - this.instructions = instructions - this.chainId = ChainId.SOLANA_MAINNET } /** * Sends this SvmCall intent to the execution environment. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) */ - public send(): void { - environment.svmCall(this) + public send(maxFee: TokenAmount, feePayer: Address | null = null): void { + const intentBuilder = new IntentBuilder().addMaxFee(maxFee).addOperation(this) + if (feePayer) intentBuilder.addFeePayer(feePayer) + environment.sendIntent(intentBuilder.build()) } } diff --git a/packages/lib-ts/src/intents/Intent.ts b/packages/lib-ts/src/intents/Intent.ts index 6433edff..1cea25fe 100644 --- a/packages/lib-ts/src/intents/Intent.ts +++ b/packages/lib-ts/src/intents/Intent.ts @@ -1,28 +1,159 @@ import { environment } from '../environment' import { evm } from '../evm' import { NULL_ADDRESS } from '../helpers' -import { Token, TokenAmount } from '../tokens' +import { BlockchainToken, Token, TokenAmount } from '../tokens' import { Address, BigInt, Bytes, ChainId } from '../types' +import { SvmAccountMeta } from '../types/svm/SvmAccountMeta' -export enum OperationType { - Swap, - Transfer, - EvmCall, - SvmCall, -} +import { EvmCall, EvmCallData } from './Call/EvmCall' +import { SvmCall, SvmInstruction } from './Call/SvmCall' +import { Operation, OperationBuilder, OperationEvent } from './Operation' +import { Swap, SwapTokenIn, SwapTokenOut } from './Swap' +import { Transfer, TransferData } from './Transfer' const DEFAULT_DEADLINE = 5 * 60 // 5 minutes in seconds /** - * Base builder for creating intents. + * Builder for creating intents with one or more operations. */ -export abstract class IntentBuilder { - protected user: Address | null = null +export class IntentBuilder { protected settler: Address | null = null + protected feePayer: Address | null = null protected deadline: BigInt | null = null protected nonce: string | null = null protected maxFees: TokenAmount[] = [] - protected events: IntentEvent[] = [] + protected operations: Operation[] = [] + + /** + * Adds an operation to this intent. + * @param operation - The operation to add + * @returns This IntentBuilder instance for method chaining + */ + addOperation(operation: Operation): IntentBuilder { + this.operations.push(operation) + return this + } + + /** + * Adds multiple operations to this intent. + * @param operations - The operations to add + * @returns This IntentBuilder instance for method chaining + */ + addOperations(operations: Operation[]): IntentBuilder { + for (let i = 0; i < operations.length; i++) this.addOperation(operations[i]) + return this + } + + /** + * Adds a built operation builder to this intent. + * @param operationBuilder - The operation builder to build and add + * @returns This IntentBuilder instance for method chaining + */ + addOperationBuilder(operationBuilder: OperationBuilder): IntentBuilder { + return this.addOperation(operationBuilder.build()) + } + + /** + * Adds multiple built operation builders to this intent. + * @param operationBuilders - The operation builders to build and add + * @returns This IntentBuilder instance for method chaining + */ + addOperationsBuilders(operationBuilders: OperationBuilder[]): IntentBuilder { + for (let i = 0; i < operationBuilders.length; i++) this.addOperationBuilder(operationBuilders[i]) + return this + } + + /** + * Adds a single EVM call operation to this intent from raw parameters. + * @param chainId - The blockchain network identifier + * @param target - The contract address to call + * @param data - The encoded call data + * @param value - The native token value to send + * @param user - The user that should execute the operation + * @param events - The operation events to emit + * @returns This IntentBuilder instance for method chaining + */ + addEvmCallOperation( + chainId: ChainId, + target: Address, + data: Bytes = Bytes.empty(), + value: BigInt = BigInt.zero(), + user: Address | null = null, + events: OperationEvent[] | null = null + ): IntentBuilder { + return this.addOperation(new EvmCall(chainId, [new EvmCallData(target, data, value)], user, events)) + } + + /** + * Adds a single swap operation to this intent from raw parameters. + * @param sourceChain - The source blockchain network identifier + * @param tokenIn - The token to swap from + * @param amountIn - The amount to swap from + * @param tokenOut - The token to receive + * @param minAmountOut - The minimum amount to receive + * @param recipient - The recipient of the output token + * @param destinationChain - The destination blockchain network identifier + * @param user - The user that should execute the operation + * @param events - The operation events to emit + * @returns This IntentBuilder instance for method chaining + */ + addSwapOperation( + sourceChain: ChainId, + tokenIn: Token, + amountIn: BigInt, + tokenOut: Token, + minAmountOut: BigInt, + recipient: Address, + destinationChain: ChainId = sourceChain, + user: Address | null = null, + events: OperationEvent[] | null = null + ): IntentBuilder { + const swapIn = SwapTokenIn.fromBigInt(tokenIn, amountIn) + const swapOut = SwapTokenOut.fromBigInt(tokenOut, minAmountOut, recipient) + return this.addOperation(new Swap(sourceChain, [swapIn], [swapOut], destinationChain, user, events)) + } + + /** + * Adds a single transfer operation to this intent from raw parameters. + * @param token - The token to transfer + * @param amount - The amount to transfer + * @param recipient - The recipient of the transfer + * @param user - The user that should execute the operation + * @param events - The operation events to emit + * @returns This IntentBuilder instance for method chaining + */ + addTransferOperation( + token: Token, + amount: BigInt, + recipient: Address, + user: Address | null = null, + events: OperationEvent[] | null = null + ): IntentBuilder { + if (!(token instanceof BlockchainToken)) throw new Error('Transfer token must be a blockchain token') + const transferAmount = TokenAmount.fromBigInt(token, amount) + const transferData = TransferData.fromTokenAmount(transferAmount, recipient) + const chainId = changetype(token).chainId + return this.addOperation(new Transfer(chainId, [transferData], user, events)) + } + + /** + * Adds a single SVM call operation to this intent from raw parameters. + * @param programId - The program address to call + * @param accountsMeta - The accounts metadata for the instruction + * @param data - The encoded instruction data + * @param user - The user that should execute the operation + * @param events - The operation events to emit + * @returns This IntentBuilder instance for method chaining + */ + addSvmCallOperation( + programId: Address, + accountsMeta: SvmAccountMeta[], + data: Bytes, + user: Address | null = null, + events: OperationEvent[] | null = null + ): IntentBuilder { + return this.addOperation(new SvmCall([SvmInstruction.create(programId, accountsMeta, data)], user, events)) + } /** * Sets the settler address for this intent. @@ -44,32 +175,32 @@ export abstract class IntentBuilder { } /** - * Sets the deadline for this intent. - * @param deadline - The deadline as a timestamp + * Sets the fee payer address for this intent. + * @param feePayer - The fee payer address as an Address instance * @returns This IntentBuilder instance for method chaining */ - addDeadline(deadline: BigInt): IntentBuilder { - this.deadline = deadline + addFeePayer(feePayer: Address): IntentBuilder { + this.feePayer = feePayer return this } /** - * Sets the user address for this intent. - * @param user - The user address + * Sets the fee payer address from a string. + * @param feePayer - The fee payer address as a hex string * @returns This IntentBuilder instance for method chaining */ - addUser(user: Address): IntentBuilder { - this.user = user - return this + addFeePayerAsString(feePayer: string): IntentBuilder { + return this.addFeePayer(Address.fromString(feePayer)) } /** - * Sets the user address from a string. - * @param user - The user address as a hex string + * Sets the deadline for this intent. + * @param deadline - The deadline as a timestamp * @returns This IntentBuilder instance for method chaining */ - addUserAsString(user: string): IntentBuilder { - return this.addUser(Address.fromString(user)) + addDeadline(deadline: BigInt): IntentBuilder { + this.deadline = deadline + return this } /** @@ -83,41 +214,29 @@ export abstract class IntentBuilder { } /** - * Sets an event for the intent. - * @param topic - The topic to be indexed in the event - * @param data - The event data + * Adds a max fee for this intent. + * @param fee - The max fee token amount * @returns This IntentBuilder instance for method chaining */ - addEvent(topic: Bytes, data: Bytes): IntentBuilder { - const event = new IntentEvent(topic, data) - this.events.push(event) + addMaxFee(fee: TokenAmount): IntentBuilder { + this.maxFees.push(fee) return this } /** - * Sets multiple events for the intent. - * @param events - The list of events to be added - * @returns This IntentBuilder instance for method chaining + * Builds and returns the final intent. + * @returns A new intent */ - addEvents(events: IntentEvent[]): IntentBuilder { - for (let i = 0; i < events.length; i++) { - this.events.push(events[i]) - } - return this + build(): Intent { + return new Intent(this.maxFees, this.settler, this.feePayer, this.deadline, this.nonce, this.operations) } /** - * Adds a max fee for this intent. - * @param fee - The max fee token amount (must be on same chain) - * @returns This IntentBuilder instance for method chaining + * Builds and sends the final intent. */ - abstract addMaxFee(fee: TokenAmount): IntentBuilder - - /** - * Builds and returns the final intent. - * @returns A new intent - */ - abstract build(): Intent + send(): void { + this.build().send() + } } /** @@ -179,74 +298,58 @@ export class MaxFee { } } +let INTENT_INDEX: u32 = 0 + /** - * Represents an intent event. - * Specifies the topic and data for the event. The topic is an indexed parameter for the EVM events. + * Represents a sendable intent containing one or more operations plus intent-level metadata. */ @json -export class IntentEvent { - topic: string - data: string - - /** - * Creates a new Intent Event instance. - * @param topic - the topic that is going to be index in the event - * @param data - The event data - */ - constructor(topic: Bytes, data: Bytes) { - this.topic = topic.toHexString() - this.data = data.toHexString() - } -} - -let INTENT_INDEX: u32 = 0 -@json -export abstract class Intent { - public op: OperationType - public settler: string - public user: string - public deadline: string - public nonce: string - public maxFees: MaxFee[] - public events: IntentEvent[] +export class Intent { + public settler: string = '' + public feePayer: string = '' + public deadline: string = '' + public nonce: string = '' + public maxFees: MaxFee[] = [] + public operations: Operation[] /** * Creates a new intent. - * @param op - The type of intent to be created - * @param chainId - The chain ID for fetch the settler - * @param maxFees - The list of max fees to pay for the intent (optional) - * @param settler - The settler address (optional) - * @param user - The user address (optional) - * @param deadline - The deadline timestamp (optional) - * @param nonce - The nonce for replay protection (optional) - */ - protected constructor( - op: OperationType, - chainId: ChainId, - maxFees: MaxFee[] | null, - settler: Address | null, - user: Address | null, - deadline: BigInt | null, - nonce: string | null, - events: IntentEvent[] | null + * @param maxFees - The list of max fees to pay for the intent + * @param settler - The settler address + * @param feePayer - The fee payer address + * @param deadline - The deadline timestamp + * @param nonce - The nonce for replay protection + * @param operations - The operations to execute + */ + constructor( + maxFees: TokenAmount[] | null = null, + settler: Address | null = null, + feePayer: Address | null = null, + deadline: BigInt | null = null, + nonce: string | null = null, + operations: Operation[] | null = null ) { const context = environment.getContext() - this.op = op - this.maxFees = maxFees || [] - this.settler = settler ? settler.toString() : context.findSettler(chainId).toString() + this.operations = operations || [] + if (this.operations.length === 0) throw new Error('Operation list cannot be empty') + + this.maxFees = maxFees ? maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) : [] + const defaultChainId = this.operations[0].chainId + this.settler = settler ? settler.toString() : context.findSettler(defaultChainId).toString() + this.feePayer = feePayer ? feePayer.toString() : context.user.toString() this.deadline = deadline ? deadline.toString() : (context.timestamp / 1000 + DEFAULT_DEADLINE).toString() - this.user = user ? user.toString() : context.user.toString() - this.events = events || [] this.nonce = nonce ? nonce : evm.keccak(`${context.triggerSig}${context.timestamp}${context.triggerPayload.data}${++INTENT_INDEX}`) - if (!this.user || this.user == NULL_ADDRESS) throw new Error('A user must be specified') if (!this.settler || this.settler == NULL_ADDRESS) throw new Error('A settler contract must be specified') + if (!this.feePayer || this.feePayer == NULL_ADDRESS) throw new Error('A fee payer must be specified') } /** * Sends this intent to the execution environment. */ - abstract send(): void + send(): void { + environment.sendIntent(this) + } } diff --git a/packages/lib-ts/src/intents/Operation.ts b/packages/lib-ts/src/intents/Operation.ts new file mode 100644 index 00000000..d6baa9ec --- /dev/null +++ b/packages/lib-ts/src/intents/Operation.ts @@ -0,0 +1,112 @@ +import { environment } from '../environment' +import { NULL_ADDRESS } from '../helpers' +import { Address, Bytes, ChainId } from '../types' + +export enum OperationType { + Swap, + Transfer, + EvmCall, + SvmCall, +} + +/** + * Represents an operation event. + * Specifies the topic and data for the event. The topic is an indexed parameter for the EVM events. + */ +@json +export class OperationEvent { + topic: string + data: string + + /** + * Creates a new Operation Event instance. + * @param topic - the topic that is going to be index in the event + * @param data - The event data + */ + constructor(topic: Bytes, data: Bytes) { + this.topic = topic.toHexString() + this.data = data.toHexString() + } +} + +/** + * Base builder for creating operations. + */ +export abstract class OperationBuilder { + protected user: Address | null = null + protected events: OperationEvent[] = [] + + abstract build(): Operation + + /** + * Sets the user address for this intent. + * @param user - The user address + * @returns This OperationBuilder instance for method chaining + */ + addUser(user: Address): OperationBuilder { + this.user = user + return this + } + + /** + * Sets the user address from a string. + * @param user - The user address as a hex string + * @returns This OperationBuilder instance for method chaining + */ + addUserAsString(user: string): OperationBuilder { + return this.addUser(Address.fromString(user)) + } + + /** + * Sets an event for the intent. + * @param topic - The topic to be indexed in the event + * @param data - The event data + * @returns This OperationBuilder instance for method chaining + */ + addEvent(topic: Bytes, data: Bytes): OperationBuilder { + const event = new OperationEvent(topic, data) + this.events.push(event) + return this + } + + /** + * Sets multiple events for the intent. + * @param events - The list of events to be added + * @returns This OperationBuilder instance for method chaining + */ + addEvents(events: OperationEvent[]): OperationBuilder { + for (let i = 0; i < events.length; i++) { + this.events.push(events[i]) + } + return this + } +} + +@json +export abstract class Operation { + public opType: OperationType + public chainId: ChainId + public user: string + public events: OperationEvent[] + + /** + * Creates a new operation. + * @param opType - The type of operation to be created + * @param chainId - The source chain id for the operation + * @param user - The user address (optional) + * @param events - The list of events for the operation (optional) + */ + protected constructor( + opType: OperationType, + chainId: ChainId, + user: Address | null, + events: OperationEvent[] | null + ) { + const context = environment.getContext() + this.opType = opType + this.chainId = chainId + this.user = user ? user.toString() : context.user.toString() + this.events = events || [] + if (!this.user || this.user == NULL_ADDRESS) throw new Error('A user must be specified') + } +} diff --git a/packages/lib-ts/src/intents/Swap.ts b/packages/lib-ts/src/intents/Swap.ts index 3f01997f..9a43adc3 100644 --- a/packages/lib-ts/src/intents/Swap.ts +++ b/packages/lib-ts/src/intents/Swap.ts @@ -2,13 +2,14 @@ import { environment } from '../environment' import { Token, TokenAmount } from '../tokens' import { Address, BigInt, Bytes, ChainId } from '../types' -import { Intent, IntentBuilder, IntentEvent, MaxFee, OperationType } from './Intent' +import { IntentBuilder } from './Intent' +import { Operation, OperationBuilder, OperationEvent, OperationType } from './Operation' /** * Builder for creating Swap intents with token exchange operations. * Supports both single-chain and cross-chain swaps with multiple input and output tokens. */ -export class SwapBuilder extends IntentBuilder { +export class SwapBuilder extends OperationBuilder { protected sourceChain: ChainId protected destinationChain: ChainId protected tokensIn: SwapTokenIn[] = [] @@ -204,33 +205,6 @@ export class SwapBuilder extends IntentBuilder { return this.addTokenOut(SwapTokenOut.fromStringDecimal(token, amount, recipient)) } - /** - * Sets the settler address for this intent. - * @param settler - The settler address as an Address instance - * @returns This SwapBuilder instance for method chaining - */ - addSettler(settler: Address): SwapBuilder { - return changetype(super.addSettler(settler)) - } - - /** - * Sets the settler address from a string. - * @param settler - The settler address as a hex string - * @returns This SwapBuilder instance for method chaining - */ - addSettlerAsString(settler: string): SwapBuilder { - return changetype(super.addSettlerAsString(settler)) - } - - /** - * Sets the deadline for this intent. - * @param deadline - The deadline as a timestamp - * @returns This SwapBuilder instance for method chaining - */ - addDeadline(deadline: BigInt): SwapBuilder { - return changetype(super.addDeadline(deadline)) - } - /** * Sets the user address for this intent. * @param user - The user address @@ -249,26 +223,6 @@ export class SwapBuilder extends IntentBuilder { return changetype(super.addUserAsString(user)) } - /** - * Sets the nonce for this intent. - * @param nonce - A unique identifier to prevent replay attacks - * @returns This SwapBuilder instance for method chaining - */ - addNonce(nonce: string): SwapBuilder { - return changetype(super.addNonce(nonce)) - } - - /** - * Adds a max fee for this intent. - * @param fee - The max fee token amount (must be on same chain) - * @returns This SwapBuilder instance for method chaining - */ - addMaxFee(fee: TokenAmount): SwapBuilder { - if (!fee.token.hasChain(this.destinationChain)) throw new Error('Fee token must be on the destination chain') - this.maxFees.push(fee) - return this - } - /** * Sets an event for the intent. * @param topic - The topic to be indexed in the event @@ -284,7 +238,7 @@ export class SwapBuilder extends IntentBuilder { * @param events - The list of events to be added * @returns This SwapBuilder instance for method chaining */ - addEvents(events: IntentEvent[]): SwapBuilder { + addEvents(events: OperationEvent[]): SwapBuilder { return changetype(super.addEvents(events)) } @@ -295,18 +249,16 @@ export class SwapBuilder extends IntentBuilder { build(): Swap { if (this.tokensIn.length === 0 || this.tokensOut.length === 0) throw new Error('Tokens in and out are required') - return new Swap( - this.sourceChain, - this.tokensIn, - this.tokensOut, - this.destinationChain, - this.settler, - this.user, - this.deadline, - this.nonce, - this.maxFees, - this.events - ) + return new Swap(this.sourceChain, this.tokensIn, this.tokensOut, this.destinationChain, this.user, this.events) + } + + /** + * Builds this operation and sends it inside an intent with the provided fee data. + * @param maxFee - The max fee to pay for the intent (optional for swaps) + * @param feePayer - The fee payer for the intent (optional) + */ + send(maxFee: TokenAmount | null = null, feePayer: Address | null = null): void { + this.build().send(maxFee, feePayer) } } @@ -439,7 +391,7 @@ export class SwapTokenOut { * Represents a Swap intent for exchanging tokens between blockchain networks. */ @json -export class Swap extends Intent { +export class Swap extends Operation { /** * Creates a simple single-chain swap intent. * @param chainId - The blockchain network identifier @@ -448,29 +400,11 @@ export class Swap extends Intent { * @param tokenOut - The output token * @param minAmountOut - The minimum amount to receive * @param settler - The settler address (optional) - * @param user - The user address (optional) + * @param recipient - The recipient address (optional) * @param deadline - The deadline timestamp (optional) * @param nonce - The nonce for replay protection (optional) * @returns A new Swap instance */ - static create( - chainId: ChainId, - tokenIn: Token, - amountIn: BigInt, - tokenOut: Token, - minAmountOut: BigInt, - settler: Address | null = null, - user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null - ): Swap { - const context = environment.getContext() - const recipient = user || context.user - const swapIn = SwapTokenIn.fromBigInt(tokenIn, amountIn) - const swapOut = SwapTokenOut.fromBigInt(tokenOut, minAmountOut, recipient) - return new Swap(chainId, [swapIn], [swapOut], chainId, settler, user, deadline, nonce, [], events) - } /** * Creates a new Swap intent. @@ -489,23 +423,23 @@ export class Swap extends Intent { public tokensIn: SwapTokenIn[], public tokensOut: SwapTokenOut[], public destinationChain: ChainId, - settler: Address | null = null, user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - maxFees: TokenAmount[] | null = null, - events: IntentEvent[] | null = null + events: OperationEvent[] | null = null ) { - const fees: MaxFee[] = maxFees ? maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) : [] - super(OperationType.Swap, sourceChain, fees, settler, user, deadline, nonce, events) + super(OperationType.Swap, sourceChain, user, events) if (tokensIn.length === 0) throw new Error('TokenIn list cannot be empty') if (tokensOut.length === 0) throw new Error('TokenOut list cannot be empty') } /** * Sends this Swap intent to the execution environment. - */ - send(): void { - environment.swap(this) + * @param maxFee - The max fee to pay for the intent (optional for swaps) + * @param feePayer - The fee payer for the intent (optional) + */ + send(maxFee: TokenAmount | null = null, feePayer: Address | null = null): void { + const intentBuilder = new IntentBuilder().addOperation(this) + if (maxFee) intentBuilder.addMaxFee(maxFee) + if (feePayer) intentBuilder.addFeePayer(feePayer) + environment.sendIntent(intentBuilder.build()) } } diff --git a/packages/lib-ts/src/intents/Transfer.ts b/packages/lib-ts/src/intents/Transfer.ts index 22929758..caa5c9b3 100644 --- a/packages/lib-ts/src/intents/Transfer.ts +++ b/packages/lib-ts/src/intents/Transfer.ts @@ -1,14 +1,15 @@ import { environment } from '../environment' -import { ERC20Token, Token, TokenAmount } from '../tokens' +import { Token, TokenAmount } from '../tokens' import { Address, BigInt, Bytes, ChainId } from '../types' -import { Intent, IntentBuilder, IntentEvent, MaxFee, OperationType } from './Intent' +import { IntentBuilder } from './Intent' +import { Operation, OperationBuilder, OperationEvent, OperationType } from './Operation' /** * Builder for creating Transfer intents with token transfer operations. * Supports multiple transfers within a single transaction on the same chain. */ -export class TransferBuilder extends IntentBuilder { +export class TransferBuilder extends OperationBuilder { protected chainId: ChainId protected transfers: TransferData[] = [] @@ -135,33 +136,6 @@ export class TransferBuilder extends IntentBuilder { return this } - /** - * Sets the settler address for this intent. - * @param settler - The settler address as an Address instance - * @returns This TransferBuilder instance for method chaining - */ - addSettler(settler: Address): TransferBuilder { - return changetype(super.addSettler(settler)) - } - - /** - * Sets the settler address from a string. - * @param settler - The settler address as a hex or base58 string accordingly - * @returns This TransferBuilder instance for method chaining - */ - addSettlerAsString(settler: string): TransferBuilder { - return changetype(super.addSettlerAsString(settler)) - } - - /** - * Sets the deadline for this intent. - * @param deadline - The deadline as a timestamp - * @returns This TransferBuilder instance for method chaining - */ - addDeadline(deadline: BigInt): TransferBuilder { - return changetype(super.addDeadline(deadline)) - } - /** * Sets the user address for this intent. * @param user - The user address @@ -180,26 +154,6 @@ export class TransferBuilder extends IntentBuilder { return changetype(super.addUserAsString(user)) } - /** - * Sets the nonce for this intent. - * @param nonce - A unique identifier to prevent replay attacks - * @returns This TransferBuilder instance for method chaining - */ - addNonce(nonce: string): TransferBuilder { - return changetype(super.addNonce(nonce)) - } - - /** - * Adds a max fee for this intent. - * @param fee - The max fee token amount (must be on same chain) - * @returns This TransferBuilder instance for method chaining - */ - addMaxFee(fee: TokenAmount): TransferBuilder { - if (!fee.token.hasChain(this.chainId)) throw new Error('Fee token must be on the same chain') - this.maxFees.push(fee) - return this - } - /** * Sets an event for the intent. * @param topic - The topic to be indexed in the event @@ -215,7 +169,7 @@ export class TransferBuilder extends IntentBuilder { * @param events - The list of events to be added * @returns This TransferBuilder instance for method chaining */ - addEvents(events: IntentEvent[]): TransferBuilder { + addEvents(events: OperationEvent[]): TransferBuilder { return changetype(super.addEvents(events)) } @@ -224,16 +178,16 @@ export class TransferBuilder extends IntentBuilder { * @returns A new Transfer instance with all configured parameters */ build(): Transfer { - return new Transfer( - this.chainId, - this.transfers, - this.maxFees, - this.settler, - this.user, - this.deadline, - this.nonce, - this.events - ) + return new Transfer(this.chainId, this.transfers, this.user, this.events) + } + + /** + * Builds this operation and sends it inside an intent with the provided fee data. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) + */ + send(maxFee: TokenAmount, feePayer: Address | null = null): void { + this.build().send(maxFee, feePayer) } } @@ -307,48 +261,9 @@ export class TransferData { * Represents a Transfer intent for sending tokens to recipients on a blockchain network. */ @json -export class Transfer extends Intent { - public chainId: ChainId +export class Transfer extends Operation { public transfers: TransferData[] - /** - * Creates a simple single-token transfer intent. - * @param token - The Token to transfer - * @param amount - The amount to transfer - * @param recipient - The address to receive the tokens - * @param maxFee - The max fee to pay for the transfer intent - * @param settler - The settler address (optional) - * @param user - The user address (optional) - * @param deadline - The deadline timestamp (optional) - * @param nonce - The nonce for replay protection (optional) - * @returns A new Transfer instance - */ - static create( - token: Token, - amount: BigInt, - recipient: Address, - maxFee: BigInt, - settler: Address | null = null, - user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null - ): Transfer { - const transferAmount = TokenAmount.fromBigInt(token, amount) - const transferData = TransferData.fromTokenAmount(transferAmount, recipient) - const maxFees = [TokenAmount.fromBigInt(token, maxFee)] - return new Transfer( - token instanceof ERC20Token ? (token as ERC20Token).chainId : ChainId.SOLANA_MAINNET, - [transferData], - maxFees, - settler, - user, - deadline, - nonce, - events - ) - } - /** * Creates a new Transfer intent. * @param chainId - The blockchain network identifier @@ -362,33 +277,22 @@ export class Transfer extends Intent { constructor( chainId: ChainId, transfers: TransferData[], - maxFees: TokenAmount[], - settler: Address | null = null, user: Address | null = null, - deadline: BigInt | null = null, - nonce: string | null = null, - events: IntentEvent[] | null = null + events: OperationEvent[] | null = null ) { - const fees: MaxFee[] = maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) - super(OperationType.Transfer, chainId, fees, settler, user, deadline, nonce, events) + super(OperationType.Transfer, chainId, user, events) if (transfers.length === 0) throw new Error('Transfer list cannot be empty') - if (maxFees.length == 0) throw new Error('At least a max fee must be specified') - this.transfers = transfers - this.chainId = chainId } /** * Sends this Transfer intent to the execution environment. + * @param maxFee - The max fee to pay for the intent + * @param feePayer - The fee payer for the intent (optional) */ - send(): void { - environment.transfer(this) - } - - /** - * Whether the chainId is Solana or not - */ - isSVM(): bool { - return this.chainId === ChainId.SOLANA_MAINNET + send(maxFee: TokenAmount, feePayer: Address | null = null): void { + const intentBuilder = new IntentBuilder().addMaxFee(maxFee).addOperation(this) + if (feePayer) intentBuilder.addFeePayer(feePayer) + environment.sendIntent(intentBuilder.build()) } } diff --git a/packages/lib-ts/src/intents/index.ts b/packages/lib-ts/src/intents/index.ts index 1658efab..6921ba27 100644 --- a/packages/lib-ts/src/intents/index.ts +++ b/packages/lib-ts/src/intents/index.ts @@ -1,4 +1,5 @@ export * from './Call' export * from './Intent' +export * from './Operation' export * from './Swap' export * from './Transfer' diff --git a/packages/lib-ts/src/storage/storage.ts b/packages/lib-ts/src/storage/storage.ts index 4c6e82d5..8c5b9a60 100644 --- a/packages/lib-ts/src/storage/storage.ts +++ b/packages/lib-ts/src/storage/storage.ts @@ -2,7 +2,6 @@ import { environment } from '../environment' import { evm } from '../evm' import { MIMIC_HELPER_ADDRESS } from '../helpers' import { EvmCall, EvmCallBuilder } from '../intents' -import { TokenAmount } from '../tokens' import { Address, Bytes, ChainId, EvmDecodeParam, EvmEncodeParam, Result } from '../types' const ADDRESS = Address.fromHexString(MIMIC_HELPER_ADDRESS) @@ -11,7 +10,6 @@ const DEFAULT_CHAIN_ID = ChainId.OPTIMISM export namespace storage { export function createSetDataCall( smartAccount: Address, - maxFee: TokenAmount, key: string, data: Bytes, chainId: ChainId = DEFAULT_CHAIN_ID @@ -20,11 +18,7 @@ export namespace storage { '0x1c1bbd37' + evm.encode([EvmEncodeParam.fromValue('string', Bytes.fromUTF8(key)), EvmEncodeParam.fromValue('bytes', data)]) ) - return EvmCallBuilder.forChain(chainId) - .addUser(smartAccount) - .addMaxFee(maxFee) - .addCall(ADDRESS, encodedData) - .build() + return EvmCallBuilder.forChain(chainId).addUser(smartAccount).addCall(ADDRESS, encodedData).build() } export function getData( diff --git a/packages/lib-ts/tests/intents/EvmCall.spec.ts b/packages/lib-ts/tests/intents/EvmCall.spec.ts index 43f644f7..18d53916 100644 --- a/packages/lib-ts/tests/intents/EvmCall.spec.ts +++ b/packages/lib-ts/tests/intents/EvmCall.spec.ts @@ -1,144 +1,82 @@ import { JSON } from 'json-as' -import { EvmCall, EvmCallBuilder, EvmCallData, IntentEvent, OperationType } from '../../src/intents' -import { TokenAmount } from '../../src/tokens' +import { EvmCall, EvmCallBuilder, EvmCallData, OperationEvent, OperationType } from '../../src/intents' import { Address, BigInt, Bytes } from '../../src/types' -import { randomBytes, randomERC20Token, randomEvmAddress, randomSettler, setContext } from '../helpers' +import { randomBytes, randomEvmAddress, randomSettler, setContext } from '../helpers' -describe('Call', () => { - it('creates a simple Call with default values and stringifies it', () => { +describe('EvmCall', () => { + it('creates a simple operation with default values and stringifies it', () => { const chainId = 1 const user = randomEvmAddress() const target = randomEvmAddress() const calldata = randomBytes(32) - const fee = TokenAmount.fromI32(randomERC20Token(chainId), 100) const settler = randomSettler(chainId) setContext(1, 1, user.toString(), [settler], 'trigger-123') - const call = EvmCall.create(chainId, target, calldata, fee) - expect(call.op).toBe(OperationType.EvmCall) + const call = new EvmCall(chainId, [new EvmCallData(target, calldata)]) + expect(call.opType).toBe(OperationType.EvmCall) expect(call.user).toBe(user.toString()) - expect(call.settler).toBe(settler.address.toString()) expect(call.chainId).toBe(chainId) - expect(call.deadline).toBe('300') - expect(call.nonce).toBe('0x') - + expect(call.events.length).toBe(0) expect(call.calls.length).toBe(1) expect(call.calls[0].target).toBe(target.toString()) expect(call.calls[0].data).toBe(calldata.toHexString()) expect(call.calls[0].value).toBe('0') - expect(call.maxFees.length).toBe(1) - expect(call.maxFees[0].token).toBe(fee.token.address.toString()) - expect(call.maxFees[0].amount).toBe(fee.amount.toString()) - - expect(call.events.length).toBe(0) - expect(JSON.stringify(call)).toBe( - `{"op":2,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[],"chainId":${chainId},"calls":[{"target":"${target}","data":"${calldata.toHexString()}","value":"0"}]}` + `{"opType":2,"chainId":${chainId},"user":"${user}","events":[],"calls":[{"target":"${target}","data":"${calldata.toHexString()}","value":"0"}]}` ) }) - it('creates a simple Call with valid parameters and stringifies it', () => { + it('creates an operation with explicit user and events', () => { const chainId = 1 const user = randomEvmAddress() const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(9999999) const target = randomEvmAddress() const calldata = randomBytes(32) const value = BigInt.fromI32(10) - const fee = TokenAmount.fromI32(randomERC20Token(chainId), 100) setContext(1, 1, user.toString(), [settler], 'trigger-123') - const call = EvmCall.create( - chainId, - target, - calldata, - fee, - value, - Address.fromString(settler.address), - user, - deadline, - null, - [new IntentEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data'))] - ) - expect(call.op).toBe(OperationType.EvmCall) + const call = new EvmCall(chainId, [new EvmCallData(target, calldata, value)], user, [ + new OperationEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data')), + ]) + expect(call.opType).toBe(OperationType.EvmCall) expect(call.user).toBe(user.toString()) - expect(call.settler).toBe(settler.address.toString()) expect(call.chainId).toBe(chainId) - expect(call.deadline).toBe(deadline.toString()) - expect(call.nonce).toBe('0x') - - expect(call.calls.length).toBe(1) - expect(call.calls[0].target).toBe(target.toString()) - expect(call.calls[0].data).toBe(calldata.toHexString()) expect(call.calls[0].value).toBe(value.toString()) - - expect(call.maxFees.length).toBe(1) - expect(call.maxFees[0].token).toBe(fee.token.address.toString()) - expect(call.maxFees[0].amount).toBe(fee.amount.toString()) - expect(call.events.length).toBe(1) expect(call.events[0].topic).toBe('0x746f706963') expect(call.events[0].data).toBe('0x64617461') - expect(JSON.stringify(call)).toBe( - `{"op":2,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[{"topic":"0x746f706963","data":"0x64617461"}],"chainId":${chainId},"calls":[{"target":"${target}","data":"${calldata.toHexString()}","value":"${value}"}]}` + `{"opType":2,"chainId":${chainId},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"calls":[{"target":"${target}","data":"${calldata.toHexString()}","value":"${value.toString()}"}]}` ) }) - it('creates a complex Call with valid parameters and stringifies it', () => { + it('creates a complex operation with multiple calls', () => { const chainId = 1 const user = randomEvmAddress() const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(9999999) - const fee = TokenAmount.fromI32(randomERC20Token(chainId), 100) const callData1 = new EvmCallData(randomEvmAddress(), randomBytes(32), BigInt.fromI32(1)) const callData2 = new EvmCallData(randomEvmAddress(), randomBytes(32), BigInt.fromI32(2)) - const callDatas = [callData1, callData2] - const call = new EvmCall(chainId, callDatas, [fee], Address.fromString(settler.address), user, deadline, '0x') - expect(call.op).toBe(OperationType.EvmCall) - expect(call.user).toBe(user.toString()) - expect(call.settler).toBe(settler.address.toString()) - expect(call.chainId).toBe(chainId) - expect(call.deadline).toBe(deadline.toString()) - expect(call.nonce).toBe('0x') + setContext(1, 1, user.toString(), [settler], 'trigger-123') + const call = new EvmCall(chainId, [callData1, callData2], user) expect(call.calls.length).toBe(2) expect(call.calls[0].target).toBe(callData1.target) - expect(call.calls[0].data).toBe(callData1.data) - expect(call.calls[0].value).toBe(callData1.value) - expect(call.calls[1].target).toBe(callData2.target) - expect(call.calls[1].data).toBe(callData2.data) - expect(call.calls[1].value).toBe(callData2.value) - - expect(call.maxFees.length).toBe(1) - expect(call.maxFees[0].token).toBe(fee.token.address.toString()) - expect(call.maxFees[0].amount).toBe(fee.amount.toString()) - - expect(call.events.length).toBe(0) - expect(JSON.stringify(call)).toBe( - `{"op":2,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[],"chainId":${chainId},"calls":[{"target":"${callData1.target}","data":"${callData1.data}","value":"${callData1.value}"},{"target":"${callData2.target}","data":"${callData2.data}","value":"${callData2.value}"}]}` + `{"opType":2,"chainId":${chainId},"user":"${user}","events":[],"calls":[{"target":"${callData1.target}","data":"${callData1.data}","value":"${callData1.value}"},{"target":"${callData2.target}","data":"${callData2.data}","value":"${callData2.value}"}]}` ) }) it('throws an error when there is not Call Data', () => { expect(() => { - new EvmCall(1, [], []) + new EvmCall(1, []) }).toThrow('Call list cannot be empty') }) - - it('throws an error when there is no max fee', () => { - expect(() => { - const callData = new EvmCallData(randomEvmAddress(), randomBytes(32), BigInt.fromI32(1)) - new EvmCall(1, [callData], []) - }).toThrow('At least a max fee must be specified') - }) }) describe('EvmCallBuilder', () => { @@ -146,14 +84,13 @@ describe('EvmCallBuilder', () => { const target1Str = '0x0000000000000000000000000000000000000001' const target2Str = '0x0000000000000000000000000000000000000002' - it('adds multiple calls and builds call', () => { + it('adds multiple calls and builds an operation', () => { const target1 = Address.fromString(target1Str) const target2 = Address.fromString(target2Str) const builder = EvmCallBuilder.forChain(chainId) builder.addCall(target1, randomBytes(2), BigInt.fromString('1')) builder.addCall(target2, randomBytes(2), BigInt.fromString('2')) - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 100)) const call = builder.build() expect(call.calls.length).toBe(2) @@ -165,18 +102,10 @@ describe('EvmCallBuilder', () => { const target = Address.fromString(target1Str) const builder = EvmCallBuilder.forChain(chainId) - builder.addCall(target) // default Bytes.empty and BigInt.zero - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 100)) + builder.addCall(target) const call = builder.build() expect(call.calls[0].data).toBe(Bytes.empty().toHexString()) expect(call.calls[0].value).toBe('0') }) - - it('throws if fee token chainId mismatches constructor chainId', () => { - expect(() => { - const fee = TokenAmount.fromI32(randomERC20Token(2), 9) - EvmCallBuilder.forChain(chainId).addMaxFee(fee) - }).toThrow('Fee token must be on the same chain as the one requested for the call') - }) }) diff --git a/packages/lib-ts/tests/intents/Intent.spec.ts b/packages/lib-ts/tests/intents/Intent.spec.ts index cc75084c..84c7905d 100644 --- a/packages/lib-ts/tests/intents/Intent.spec.ts +++ b/packages/lib-ts/tests/intents/Intent.spec.ts @@ -1,6 +1,8 @@ +import { JSON } from 'json-as' + import { SerializableSettler } from '../../src/context' import { NULL_ADDRESS } from '../../src/helpers' -import { EvmCallBuilder } from '../../src/intents' +import { EvmCallBuilder, IntentBuilder } from '../../src/intents' import { TokenAmount } from '../../src/tokens' import { Address, BigInt, Bytes } from '../../src/types' import { randomERC20Token, randomSettler, setContext } from '../helpers' @@ -9,7 +11,7 @@ describe('IntentBuilder', () => { const chainId = 1 const targetAddressStr = '0x0000000000000000000000000000000000000001' - describe('when the user is not zero', () => { + describe('when the fee payer is not zero', () => { const userAddressStr = '0x0000000000000000000000000000000000000002' describe('when the settler is not zero', () => { @@ -17,49 +19,68 @@ describe('IntentBuilder', () => { it('sets intent properties via builder methods', () => { const target = Address.fromString(targetAddressStr) - const user = Address.fromString(userAddressStr) const settler = Address.fromString(settlerAddressStr) + const feePayer = Address.fromString(userAddressStr) const fee = TokenAmount.fromI32(randomERC20Token(chainId), 9) const deadline = BigInt.fromString('123456789') const customNonce = '0xabcdef123456' - setContext(0, 1, '0x0000000000000000000000000000000000000000', [], '1') + setContext(0, 1, '0x0000000000000000000000000000000000000004', [], '1') - const builder = EvmCallBuilder.forChain(chainId) - .addCall(target, Bytes.fromHexString('0x1234'), BigInt.fromString('1')) - .addUser(user) + const intent = new IntentBuilder() + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(target, Bytes.fromHexString('0x1234'))) .addSettler(settler) + .addFeePayer(feePayer) .addDeadline(deadline) .addNonce(customNonce) .addMaxFee(fee) - - const call = builder.build() - expect(call.user).toBe(user.toString()) - expect(call.settler).toBe(settler.toString()) - expect(call.deadline).toBe('123456789') - expect(call.nonce).toBe(customNonce) - expect(call.maxFees.length).toBe(1) - expect(call.maxFees[0].token).toBe(fee.token.address.toString()) - expect(call.maxFees[0].amount).toBe(fee.amount.toString()) + .build() + + expect(intent.operations.length).toBe(1) + expect(intent.operations[0].user).toBe('0x0000000000000000000000000000000000000004') + expect(intent.settler).toBe(settler.toString()) + expect(intent.feePayer).toBe(feePayer.toString()) + expect(intent.deadline).toBe('123456789') + expect(intent.nonce).toBe(customNonce) + expect(intent.maxFees.length).toBe(1) + expect(intent.maxFees[0].token).toBe(fee.token.address.toString()) + expect(intent.maxFees[0].amount).toBe(fee.amount.toString()) }) - it('uses default user, deadline, nonce and settler if not explicitly set', () => { + it('uses default fee payer, deadline, nonce and settler if not explicitly set', () => { const target = Address.fromString(targetAddressStr) const settler = randomSettler(chainId) - const fee = TokenAmount.fromI32(randomERC20Token(chainId), 9) setContext(0, 1, userAddressStr, [settler], 'trigger-transfer') - const builder = EvmCallBuilder.forChain(chainId).addCall(target).addMaxFee(fee) + const intent = new IntentBuilder().addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(target)).build() - const call = builder.build() - expect(call.user).toBe(userAddressStr) - expect(call.settler).toBe(settler.address.toString()) - expect(call.deadline).toBe('300') - expect(call.nonce).toBe('0x') - expect(call.maxFees.length).toBe(1) - expect(call.maxFees[0].token).toBe(fee.token.address.toString()) - expect(call.maxFees[0].amount).toBe(fee.amount.toString()) + expect(intent.operations.length).toBe(1) + expect(intent.operations[0].user).toBe(userAddressStr) + expect(intent.settler).toBe(settler.address.toString()) + expect(intent.feePayer).toBe(userAddressStr) + expect(intent.deadline).toBe('300') + expect(intent.nonce).toBe('0x') + }) + + it('stringifies the built intent', () => { + const target = Address.fromString(targetAddressStr) + const settler = Address.fromString(settlerAddressStr) + const feePayer = Address.fromString(userAddressStr) + + setContext(0, 1, '0x0000000000000000000000000000000000000004', [], '1') + + const intent = new IntentBuilder() + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(target, Bytes.fromHexString('0x1234'))) + .addSettler(settler) + .addFeePayer(feePayer) + .addDeadline(BigInt.fromString('123456789')) + .addNonce('0xabcdef123456') + .build() + + expect(JSON.stringify(intent)).toBe( + `{"settler":"${settler}","feePayer":"${feePayer}","deadline":"123456789","nonce":"0xabcdef123456","maxFees":[],"operations":[{"opType":2,"chainId":${chainId},"user":"0x0000000000000000000000000000000000000004","events":[],"calls":[{"target":"${target}","data":"0x1234","value":"0"}]}]}` + ) }) }) @@ -69,14 +90,15 @@ describe('IntentBuilder', () => { setContext(0, 1, userAddressStr, [settler], 'trigger-call') expect(() => { - const builder = EvmCallBuilder.forChain(chainId) - builder.build() + new IntentBuilder() + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(Address.fromString(targetAddressStr))) + .build() }).toThrow('A settler contract must be specified') }) }) }) - describe('when the user is zero', () => { + describe('when the fee payer is zero', () => { const userAddressStr = NULL_ADDRESS it('throws an error', () => { @@ -84,9 +106,21 @@ describe('IntentBuilder', () => { setContext(0, 1, userAddressStr, [settler], 'trigger-call') expect(() => { - const builder = EvmCallBuilder.forChain(chainId) - builder.build() - }).toThrow('A user must be specified') + new IntentBuilder() + .addOperationBuilder(EvmCallBuilder.forChain(chainId).addCall(Address.fromString(targetAddressStr))) + .build() + }).toThrow('A fee payer must be specified') + }) + }) + + describe('when there are no operations', () => { + it('throws an error', () => { + const settler = randomSettler(chainId) + setContext(0, 1, '0x0000000000000000000000000000000000000002', [settler], 'trigger-call') + + expect(() => { + new IntentBuilder().build() + }).toThrow('Operation list cannot be empty') }) }) }) diff --git a/packages/lib-ts/tests/intents/SvmCall.spec.ts b/packages/lib-ts/tests/intents/SvmCall.spec.ts index 4376fae6..ac0b75a0 100644 --- a/packages/lib-ts/tests/intents/SvmCall.spec.ts +++ b/packages/lib-ts/tests/intents/SvmCall.spec.ts @@ -1,17 +1,16 @@ import { JSON } from 'json-as' import { - IntentEvent, + OperationEvent, OperationType, SvmCall, SvmCallBuilder, SvmInstruction, SvmInstructionBuilder, } from '../../src/intents' -import { TokenAmount } from '../../src/tokens' -import { Address, BigInt, Bytes, ChainId } from '../../src/types' +import { Address, Bytes, ChainId } from '../../src/types' import { SvmAccountMeta } from '../../src/types/svm/SvmAccountMeta' -import { randomBytes, randomSPLToken, randomSvmAddress, randomSvmSettler, setContext } from '../helpers' +import { randomBytes, randomSvmAddress, randomSvmSettler, setContext } from '../helpers' describe('SvmCall', () => { describe('create', () => { @@ -20,32 +19,24 @@ describe('SvmCall', () => { const programId = randomSvmAddress() const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data = randomBytes(32) - const fee = TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100) const settler = randomSvmSettler() setContext(1, 1, user.toString(), [settler], 'trigger-123') - const svmCall = SvmCall.create(programId, accountsMeta, data, fee) - expect(svmCall.op).toBe(OperationType.SvmCall) + const svmCall = new SvmCall([new SvmInstruction(programId.toBase58String(), accountsMeta, data.toHexString())]) + expect(svmCall.opType).toBe(OperationType.SvmCall) expect(svmCall.user).toBe(user.toString()) - expect(svmCall.settler).toBe(settler.address.toString()) expect(svmCall.chainId).toBe(ChainId.SOLANA_MAINNET) - expect(svmCall.deadline).toBe('300') - expect(svmCall.nonce).toBe('0x') expect(svmCall.instructions.length).toBe(1) expect(svmCall.instructions[0].programId).toBe(programId.toBase58String()) expect(svmCall.instructions[0].accountsMeta).toStrictEqual(accountsMeta) expect(svmCall.instructions[0].data).toBe(data.toHexString()) - expect(svmCall.maxFees.length).toBe(1) - expect(svmCall.maxFees[0].token).toBe(fee.token.address.toString()) - expect(svmCall.maxFees[0].amount).toBe(fee.amount.toString()) - expect(svmCall.events.length).toBe(0) expect(JSON.stringify(svmCall)).toBe( - `{"op":3,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[],"chainId":${ChainId.SOLANA_MAINNET},"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}` + `{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}` ) }) @@ -54,46 +45,31 @@ describe('SvmCall', () => { const programId = randomSvmAddress() const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data = randomBytes(32) - const fee = TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100) const settler = randomSvmSettler() - const deadline = BigInt.fromI32(9999999) setContext(1, 1, user.toString(), [settler], 'trigger-123') - const svmCall = SvmCall.create( - programId, - accountsMeta, - data, - fee, - Address.fromString(settler.address), + const svmCall = new SvmCall( + [new SvmInstruction(programId.toBase58String(), accountsMeta, data.toHexString())], user, - deadline, - null, - [new IntentEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data'))] + [new OperationEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data'))] ) - expect(svmCall.op).toBe(OperationType.SvmCall) + expect(svmCall.opType).toBe(OperationType.SvmCall) expect(svmCall.user).toBe(user.toString()) - expect(svmCall.settler).toBe(settler.address.toString()) expect(svmCall.chainId).toBe(ChainId.SOLANA_MAINNET) - expect(svmCall.deadline).toBe(deadline.toString()) - expect(svmCall.nonce).toBe('0x') expect(svmCall.instructions.length).toBe(1) expect(svmCall.instructions[0].programId).toBe(programId.toBase58String()) expect(svmCall.instructions[0].accountsMeta).toStrictEqual(accountsMeta) expect(svmCall.instructions[0].data).toBe(data.toHexString()) - expect(svmCall.maxFees.length).toBe(1) - expect(svmCall.maxFees[0].token).toBe(fee.token.address.toString()) - expect(svmCall.maxFees[0].amount).toBe(fee.amount.toString()) - expect(svmCall.events.length).toBe(1) expect(svmCall.events[0].topic).toBe('0x746f706963') expect(svmCall.events[0].data).toBe('0x64617461') expect(JSON.stringify(svmCall)).toBe( - `{"op":3,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[{"topic":"0x746f706963","data":"0x64617461"}],"chainId":${ChainId.SOLANA_MAINNET},"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}` + `{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}` ) }) }) @@ -107,29 +83,17 @@ describe('SvmCall', () => { const accountsMeta2 = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data1 = randomBytes(32) const data2 = randomBytes(32) - const instruction1 = SvmInstruction.create(programId1, accountsMeta1, data1) - const instruction2 = SvmInstruction.create(programId2, accountsMeta2, data2) - const fee = TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100) + const instruction1 = new SvmInstruction(programId1.toBase58String(), accountsMeta1, data1.toHexString()) + const instruction2 = new SvmInstruction(programId2.toBase58String(), accountsMeta2, data2.toHexString()) const settler = randomSvmSettler() - const deadline = BigInt.fromI32(9999999) setContext(1, 1, user.toString(), [settler], 'trigger-123') - const svmCall = new SvmCall( - [instruction1, instruction2], - [fee], - Address.fromString(settler.address), - user, - deadline, - '0x' - ) + const svmCall = new SvmCall([instruction1, instruction2], user) - expect(svmCall.op).toBe(OperationType.SvmCall) + expect(svmCall.opType).toBe(OperationType.SvmCall) expect(svmCall.user).toBe(user.toString()) - expect(svmCall.settler).toBe(settler.address.toString()) expect(svmCall.chainId).toBe(ChainId.SOLANA_MAINNET) - expect(svmCall.deadline).toBe(deadline.toString()) - expect(svmCall.nonce).toBe('0x') expect(svmCall.instructions.length).toBe(2) expect(svmCall.instructions[0].programId).toBe(programId1.toBase58String()) @@ -140,14 +104,10 @@ describe('SvmCall', () => { expect(svmCall.instructions[1].accountsMeta).toStrictEqual(accountsMeta2) expect(svmCall.instructions[1].data).toBe(data2.toHexString()) - expect(svmCall.maxFees.length).toBe(1) - expect(svmCall.maxFees[0].token).toBe(fee.token.address.toString()) - expect(svmCall.maxFees[0].amount).toBe(fee.amount.toString()) - expect(svmCall.events.length).toBe(0) expect(JSON.stringify(svmCall)).toBe( - `{"op":3,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[],"chainId":${ChainId.SOLANA_MAINNET},"instructions":[{"programId":"${programId1.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta1[0].pubkey}","isWritable":${accountsMeta1[0].isWritable},"isSigner":${accountsMeta1[0].isSigner}}],"data":"${data1.toHexString()}"},{"programId":"${programId2.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta2[0].pubkey}","isWritable":${accountsMeta2[0].isWritable},"isSigner":${accountsMeta2[0].isSigner}}],"data":"${data2.toHexString()}"}]}` + `{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId1.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta1[0].pubkey}","isWritable":${accountsMeta1[0].isWritable},"isSigner":${accountsMeta1[0].isSigner}}],"data":"${data1.toHexString()}"},{"programId":"${programId2.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta2[0].pubkey}","isWritable":${accountsMeta2[0].isWritable},"isSigner":${accountsMeta2[0].isSigner}}],"data":"${data2.toHexString()}"}]}` ) }) }) @@ -155,20 +115,9 @@ describe('SvmCall', () => { describe('validations', () => { it('throws an error when instructions list is empty', () => { expect(() => { - const fee = TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100) - new SvmCall([], [fee]) + new SvmCall([]) }).toThrow('Call list cannot be empty') }) - - it('throws an error when there is no max fee', () => { - expect(() => { - const programId = randomSvmAddress() - const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] - const data = randomBytes(32) - const instruction = SvmInstruction.create(programId, accountsMeta, data) - new SvmCall([instruction], []) - }).toThrow('At least a max fee must be specified') - }) }) }) @@ -180,13 +129,12 @@ describe('SvmCallBuilder', () => { const accountsMeta2 = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data1 = randomBytes(32) const data2 = randomBytes(32) - const instruction1 = SvmInstruction.create(programId1, accountsMeta1, data1) - const instruction2 = SvmInstruction.create(programId2, accountsMeta2, data2) + const instruction1 = new SvmInstruction(programId1.toBase58String(), accountsMeta1, data1.toHexString()) + const instruction2 = new SvmInstruction(programId2.toBase58String(), accountsMeta2, data2.toHexString()) const builder = SvmCallBuilder.forChain() builder.addInstruction(instruction1) builder.addInstruction(instruction2) - builder.addMaxFee(TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100)) const svmCall = builder.build() expect(svmCall.instructions.length).toBe(2) @@ -198,11 +146,10 @@ describe('SvmCallBuilder', () => { const programId = randomSvmAddress() const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data = randomBytes(32) - const instruction = SvmInstruction.create(programId, accountsMeta, data) + const instruction = new SvmInstruction(programId.toBase58String(), accountsMeta, data.toHexString()) const builder = SvmCallBuilder.forChain() builder.addInstruction(instruction) - builder.addMaxFee(TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100)) const svmCall = builder.build() expect(svmCall.instructions.length).toBe(1) @@ -210,41 +157,27 @@ describe('SvmCallBuilder', () => { expect(svmCall.instructions[0].data).toBe(data.toHexString()) }) - it('adds settler, user, deadline, nonce, and events', () => { + it('adds user and events', () => { const user = randomSvmAddress() - const settler = randomSvmAddress() - const deadline = BigInt.fromI32(9999999) - const nonce = '0x123456' + const settler = randomSvmSettler() const programId = randomSvmAddress() const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] const data = randomBytes(32) - const instruction = SvmInstruction.create(programId, accountsMeta, data) + const instruction = new SvmInstruction(programId.toBase58String(), accountsMeta, data.toHexString()) + + setContext(1, 1, randomSvmAddress().toString(), [settler], 'trigger-123') const builder = SvmCallBuilder.forChain() - builder.addSettler(settler) builder.addUser(user) - builder.addDeadline(deadline) - builder.addNonce(nonce) builder.addInstruction(instruction) - builder.addMaxFee(TokenAmount.fromI32(randomSPLToken(ChainId.SOLANA_MAINNET), 100)) builder.addEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data')) const svmCall = builder.build() expect(svmCall.user).toBe(user.toString()) - expect(svmCall.settler).toBe(settler.toString()) - expect(svmCall.deadline).toBe(deadline.toString()) - expect(svmCall.nonce).toBe(nonce) expect(svmCall.events.length).toBe(1) expect(svmCall.events[0].topic).toBe('0x746f706963') expect(svmCall.events[0].data).toBe('0x64617461') }) - - it('throws if fee token chainId mismatches Solana chainId', () => { - expect(() => { - const fee = TokenAmount.fromI32(randomSPLToken(ChainId.ETHEREUM), 9) - SvmCallBuilder.forChain().addMaxFee(fee) - }).toThrow('Fee token must be on the same chain') - }) }) describe('SvmInstructionBuilder', () => { @@ -270,25 +203,32 @@ describe('SvmInstructionBuilder', () => { it('builds instruction with hex data', () => { const programId = randomSvmAddress() const accountsMeta = [SvmAccountMeta.fromAddress(randomSvmAddress())] - const hexData = '0xdeadbeef' - const builder = new SvmInstructionBuilder() - builder.setProgram(programId) - builder.setAccounts(accountsMeta) - builder.setDataFromHex(hexData) + const instruction = new SvmInstructionBuilder() + .setProgram(programId) + .setAccounts(accountsMeta) + .setDataFromHex('0xabcd') + .instruction() - const instruction = builder.instruction() - expect(instruction.programId).toBe(programId.toBase58String()) - expect(instruction.accountsMeta).toStrictEqual(accountsMeta) - expect(instruction.data).toBe(hexData) + expect(instruction.data).toBe('0xabcd') }) - it('creates instruction with default values', () => { - const builder = new SvmInstructionBuilder() - const instruction = builder.instruction() + it('builds instruction with default empty data', () => { + const programId = randomSvmAddress() - expect(instruction.programId).toBe(Address.zero(32).toBase58String()) + const instruction = new SvmInstructionBuilder().setProgram(programId).instruction() + expect(instruction.programId).toBe(programId.toBase58String()) expect(instruction.accountsMeta.length).toBe(0) expect(instruction.data).toBe(Bytes.empty().toHexString()) }) + + it('uses provided address strings', () => { + const programId = Address.fromString('So11111111111111111111111111111111111111112') + const account = SvmAccountMeta.fromAddress(Address.fromString('So11111111111111111111111111111111111111113')) + + const instruction = new SvmInstructionBuilder().setProgram(programId).setAccounts([account]).instruction() + + expect(instruction.programId).toBe(programId.toBase58String()) + expect(instruction.accountsMeta[0].pubkey).toBe(account.pubkey) + }) }) diff --git a/packages/lib-ts/tests/intents/Swap.spec.ts b/packages/lib-ts/tests/intents/Swap.spec.ts index 6b72006c..92d819d9 100644 --- a/packages/lib-ts/tests/intents/Swap.spec.ts +++ b/packages/lib-ts/tests/intents/Swap.spec.ts @@ -2,7 +2,7 @@ import { JSON } from 'json-as' import { OperationType, Swap, SwapBuilder, SwapTokenIn, SwapTokenOut } from '../../src/intents' import { ERC20Token, SPLToken, TokenAmount } from '../../src/tokens' -import { Address, BigInt, ChainId } from '../../src/types' +import { Address, ChainId } from '../../src/types' import { randomERC20Token, randomEvmAddress, @@ -16,118 +16,51 @@ describe('Swap', () => { it('creates a simple Swap with default values', () => { const chainId = 1 const user = randomEvmAddress() - const tokenIn = ERC20Token.fromAddress(randomEvmAddress(), chainId) - const amountIn = BigInt.fromI32(10) - const tokenOut = ERC20Token.fromAddress(randomEvmAddress(), chainId) - const minAmountOut = BigInt.fromI32(100) + const tokenIn = SwapTokenIn.fromI32(randomERC20Token(chainId), 10) + const tokenOut = SwapTokenOut.fromI32(randomERC20Token(chainId), 100, randomEvmAddress()) const settler = randomSettler(chainId) setContext(1, 1, user.toString(), [settler], 'trigger-456') - const swap = Swap.create(chainId, tokenIn, amountIn, tokenOut, minAmountOut) - expect(swap.op).toBe(OperationType.Swap) + const swap = new Swap(chainId, [tokenIn], [tokenOut], chainId) + expect(swap.opType).toBe(OperationType.Swap) expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe('300') - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) expect(swap.sourceChain).toBe(chainId) expect(swap.tokensIn.length).toBe(1) - expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) - expect(swap.tokensIn[0].amount).toBe(amountIn.toString()) + expect(swap.tokensIn[0].token).toBe(tokenIn.token) + expect(swap.tokensIn[0].amount).toBe(tokenIn.amount) expect(swap.destinationChain).toBe(chainId) expect(swap.tokensOut.length).toBe(1) - expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) - expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.toString()) - expect(swap.tokensOut[0].recipient).toBe(user.toString()) + expect(swap.tokensOut[0].token).toBe(tokenOut.token) + expect(swap.tokensOut[0].minAmount).toBe(tokenOut.minAmount) + expect(swap.tokensOut[0].recipient).toBe(tokenOut.recipient) expect(swap.events.length).toBe(0) expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[],"events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.address.toString()}","amount":"${amountIn}"}],"tokensOut":[{"token":"${tokenOut.address.toString()}","minAmount":"${minAmountOut}","recipient":"${user}"}],"destinationChain":${chainId}}` + `{"opType":0,"chainId":${chainId},"user":"${user}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${chainId}}` ) }) it('creates a simple Swap with valid parameters and stringifies it', () => { const chainId = 1 + const destinationChain = 10 + const randomUser = randomEvmAddress() const user = randomEvmAddress() const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(999999) - const tokenIn = ERC20Token.fromAddress(randomEvmAddress(), chainId) - const amountIn = BigInt.fromI32(10) - const tokenOut = ERC20Token.fromAddress(randomEvmAddress(), chainId) - const minAmountOut = BigInt.fromI32(100) + const tokenIn = SwapTokenIn.fromI32(randomERC20Token(chainId), 10) + const tokenOut = SwapTokenOut.fromI32(randomERC20Token(chainId), 100, randomEvmAddress()) setContext(1, 1, user.toString(), [settler], 'trigger-456') - const swap = Swap.create( - chainId, - tokenIn, - amountIn, - tokenOut, - minAmountOut, - Address.fromString(settler.address), - user, - deadline, - null, - [] - ) - expect(swap.op).toBe(OperationType.Swap) - expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe(deadline.toString()) - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) + const swap = new Swap(chainId, [tokenIn], [tokenOut], destinationChain, randomUser, []) + expect(swap.opType).toBe(OperationType.Swap) + expect(swap.user).toBe(randomUser.toString()) expect(swap.sourceChain).toBe(chainId) expect(swap.tokensIn.length).toBe(1) - expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) - expect(swap.tokensIn[0].amount).toBe(amountIn.toString()) - - expect(swap.destinationChain).toBe(chainId) - expect(swap.tokensOut.length).toBe(1) - expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) - expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.toString()) - expect(swap.tokensOut[0].recipient).toBe(user.toString()) - - expect(swap.events.length).toBe(0) - - expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[],"events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.address.toString()}","amount":"${amountIn}"}],"tokensOut":[{"token":"${tokenOut.address.toString()}","minAmount":"${minAmountOut}","recipient":"${user}"}],"destinationChain":${chainId}}` - ) - }) - - it('creates a complex Swap with valid parameters and stringifies it', () => { - const sourceChain = 1 - const destinationChain = 10 - const user = randomEvmAddress() - const settler = randomSettler(sourceChain) - const deadline = BigInt.fromI32(999999) - const tokenIn = SwapTokenIn.fromI32(randomERC20Token(sourceChain), 10) - const tokenOut = SwapTokenOut.fromI32(randomERC20Token(destinationChain), 100, randomEvmAddress()) - - setContext(1, 1, user.toString(), [settler], 'trigger-456') - - const swap = new Swap( - sourceChain, - [tokenIn], - [tokenOut], - destinationChain, - Address.fromString(settler.address), - user, - deadline - ) - expect(swap.op).toBe(OperationType.Swap) - expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe(deadline.toString()) - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) - - expect(swap.sourceChain).toBe(sourceChain) - expect(swap.tokensIn.length).toBe(1) expect(swap.tokensIn[0].token).toBe(tokenIn.token) expect(swap.tokensIn[0].amount).toBe(tokenIn.amount) @@ -140,7 +73,7 @@ describe('Swap', () => { expect(swap.events.length).toBe(0) expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[],"events":[],"sourceChain":${sourceChain},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${destinationChain}}` + `{"opType":0,"chainId":${chainId},"user":"${randomUser}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${destinationChain}}` ) }) @@ -182,7 +115,7 @@ describe('SwapBuilder', () => { builder.addTokenOutFromTokenAmount(tokenOutAmount, recipientAddress) const swap = builder.build() - expect(swap.op).toBe(OperationType.Swap) + expect(swap.opType).toBe(OperationType.Swap) expect(swap.sourceChain).toBe(sourceChain) expect(swap.destinationChain).toBe(destinationChain) expect(swap.tokensIn[0].token).toBe(tokenInAddress.toString()) @@ -275,37 +208,31 @@ describe('Swap - SVM', () => { it('creates a simple Swap with default values', () => { const chainId = ChainId.SOLANA_MAINNET const user = randomSvmAddress() - const tokenIn = SPLToken.fromAddress(randomSvmAddress(), chainId, 6, 'USDC') - const amountIn = BigInt.fromI32(10) - const tokenOut = SPLToken.fromAddress(randomSvmAddress(), chainId, 6, 'USDT') - const minAmountOut = BigInt.fromI32(100) + const tokenIn = SwapTokenIn.fromI32(randomSPLToken(chainId), 10) + const tokenOut = SwapTokenOut.fromI32(randomSPLToken(chainId), 100, randomSvmAddress()) const settler = randomSettler(chainId) setContext(1, 1, user.toString(), [settler], 'trigger-456') - const swap = Swap.create(chainId, tokenIn, amountIn, tokenOut, minAmountOut) - expect(swap.op).toBe(OperationType.Swap) + const swap = new Swap(chainId, [tokenIn], [tokenOut], chainId) + expect(swap.opType).toBe(OperationType.Swap) expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe('300') - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) expect(swap.sourceChain).toBe(chainId) expect(swap.tokensIn.length).toBe(1) - expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) - expect(swap.tokensIn[0].amount).toBe(amountIn.toString()) + expect(swap.tokensIn[0].token).toBe(tokenIn.token) + expect(swap.tokensIn[0].amount).toBe(tokenIn.amount) expect(swap.destinationChain).toBe(chainId) expect(swap.tokensOut.length).toBe(1) - expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) - expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.toString()) - expect(swap.tokensOut[0].recipient).toBe(user.toString()) + expect(swap.tokensOut[0].token).toBe(tokenOut.token) + expect(swap.tokensOut[0].minAmount).toBe(tokenOut.minAmount) + expect(swap.tokensOut[0].recipient).toBe(tokenOut.recipient) expect(swap.events.length).toBe(0) expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[],"events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.address.toString()}","amount":"${amountIn}"}],"tokensOut":[{"token":"${tokenOut.address.toString()}","minAmount":"${minAmountOut}","recipient":"${user}"}],"destinationChain":${chainId}}` + `{"opType":0,"chainId":${chainId},"user":"${user}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${chainId}}` ) }) @@ -313,82 +240,21 @@ describe('Swap - SVM', () => { const chainId = ChainId.SOLANA_MAINNET const user = randomSvmAddress() const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(999999) - const tokenIn = SPLToken.fromAddress(randomSvmAddress(), chainId, 6, 'USDC') - const amountIn = BigInt.fromI32(10) - const tokenOut = SPLToken.fromAddress(randomSvmAddress(), chainId, 6, 'USDT') - const minAmountOut = BigInt.fromI32(100) + const tokenIn = SwapTokenIn.fromI32(randomSPLToken(chainId), 10) + const tokenOut = SwapTokenOut.fromI32(randomSPLToken(chainId), 100, randomSvmAddress()) setContext(1, 1, user.toString(), [settler], 'trigger-456') - const swap = Swap.create( - chainId, - tokenIn, - amountIn, - tokenOut, - minAmountOut, - Address.fromString(settler.address), - user, - deadline - ) - expect(swap.op).toBe(OperationType.Swap) + const swap = new Swap(chainId, [tokenIn], [tokenOut], chainId, user, []) + expect(swap.opType).toBe(OperationType.Swap) expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe(deadline.toString()) - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) expect(swap.sourceChain).toBe(chainId) expect(swap.tokensIn.length).toBe(1) - expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) - expect(swap.tokensIn[0].amount).toBe(amountIn.toString()) - - expect(swap.destinationChain).toBe(chainId) - expect(swap.tokensOut.length).toBe(1) - expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) - expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.toString()) - expect(swap.tokensOut[0].recipient).toBe(user.toString()) - - expect(swap.events.length).toBe(0) - - expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[],"events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.address.toString()}","amount":"${amountIn}"}],"tokensOut":[{"token":"${tokenOut.address.toString()}","minAmount":"${minAmountOut}","recipient":"${user}"}],"destinationChain":${chainId}}` - ) - }) - - it('creates a complex Swap with valid parameters and stringifies it', () => { - const sourceChain = ChainId.SOLANA_MAINNET - const destinationChain = ChainId.SOLANA_MAINNET - const user = randomSvmAddress() - const settler = randomSettler(sourceChain) - const deadline = BigInt.fromI32(999999) - const tokenIn = SwapTokenIn.fromI32(randomSPLToken(sourceChain), 10) - const tokenOut = SwapTokenOut.fromI32(randomSPLToken(destinationChain), 100, randomSvmAddress()) - - setContext(1, 1, user.toString(), [settler], 'trigger-456') - - const swap = new Swap( - sourceChain, - [tokenIn], - [tokenOut], - destinationChain, - Address.fromString(settler.address), - user, - deadline - ) - expect(swap.op).toBe(OperationType.Swap) - expect(swap.user).toBe(user.toString()) - expect(swap.settler).toBe(settler.address.toString()) - expect(swap.deadline).toBe(deadline.toString()) - expect(swap.nonce).toBe('0x') - expect(swap.maxFees.length).toBe(0) - - expect(swap.sourceChain).toBe(sourceChain) - expect(swap.tokensIn.length).toBe(1) expect(swap.tokensIn[0].token).toBe(tokenIn.token) expect(swap.tokensIn[0].amount).toBe(tokenIn.amount) - expect(swap.destinationChain).toBe(destinationChain) + expect(swap.destinationChain).toBe(chainId) expect(swap.tokensOut.length).toBe(1) expect(swap.tokensOut[0].token).toBe(tokenOut.token) expect(swap.tokensOut[0].minAmount).toBe(tokenOut.minAmount) @@ -397,7 +263,7 @@ describe('Swap - SVM', () => { expect(swap.events.length).toBe(0) expect(JSON.stringify(swap)).toBe( - `{"op":0,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[],"events":[],"sourceChain":${sourceChain},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${destinationChain}}` + `{"opType":0,"chainId":${chainId},"user":"${user}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${chainId}}` ) }) @@ -439,7 +305,7 @@ describe('SwapBuilder - SVM', () => { builder.addTokenOutFromTokenAmount(tokenOutAmount, recipientAddress) const swap = builder.build() - expect(swap.op).toBe(OperationType.Swap) + expect(swap.opType).toBe(OperationType.Swap) expect(swap.sourceChain).toBe(sourceChain) expect(swap.destinationChain).toBe(destinationChain) expect(swap.tokensIn[0].token).toBe(tokenInAddress.toString()) diff --git a/packages/lib-ts/tests/intents/Transfer.spec.ts b/packages/lib-ts/tests/intents/Transfer.spec.ts index 72e08144..dd8f7eeb 100644 --- a/packages/lib-ts/tests/intents/Transfer.spec.ts +++ b/packages/lib-ts/tests/intents/Transfer.spec.ts @@ -1,6 +1,6 @@ import { JSON } from 'json-as' -import { IntentEvent, OperationType, Transfer, TransferBuilder, TransferData } from '../../src/intents' +import { OperationEvent, OperationType, Transfer, TransferBuilder, TransferData } from '../../src/intents' import { ERC20Token, SPLToken, TokenAmount } from '../../src/tokens' import { Address, BigInt, Bytes, ChainId } from '../../src/types' import { @@ -20,33 +20,24 @@ describe('Transfer', () => { const tokenAddress = randomEvmAddress() const token = new ERC20Token(tokenAddress, chainId) const amount = BigInt.fromI32(1000) - const fee = BigInt.fromI32(10) const recipient = randomEvmAddress() const settler = randomSettler(chainId) setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = Transfer.create(token, amount, recipient, fee) - expect(transfer.op).toBe(OperationType.Transfer) + const transfer = new Transfer(chainId, [TransferData.fromBigInt(token, amount, recipient)]) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.address.toString()) - expect(transfer.chainId).toBe(chainId) - expect(transfer.deadline).toBe('300') - expect(transfer.nonce).toBe('0x') expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].token).toBe(tokenAddress.toString()) expect(transfer.transfers[0].recipient).toBe(recipient.toString()) expect(transfer.transfers[0].amount).toBe(amount.toString()) - expect(transfer.maxFees.length).toBe(1) - expect(transfer.maxFees[0].token).toBe(tokenAddress.toString()) - expect(transfer.maxFees[0].amount).toBe(fee.toString()) - expect(transfer.events.length).toBe(0) expect(JSON.stringify(transfer)).toBe( - `{"op":1,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[{"token":"${tokenAddress}","amount":"${fee.toString()}"}],"events":[],"chainId":${chainId},"transfers":[{"token":"${tokenAddress}","amount":"${amount}","recipient":"${recipient}"}]}` + `{"opType":1,"chainId":${chainId},"user":"${user}","events":[],"transfers":[{"token":"${transfer.transfers[0].token}","amount":"${transfer.transfers[0].amount}","recipient":"${transfer.transfers[0].recipient}"}]}` ) }) @@ -56,47 +47,30 @@ describe('Transfer', () => { const tokenAddress = randomEvmAddress() const token = new ERC20Token(tokenAddress, chainId) const amount = BigInt.fromI32(1000) - const fee = BigInt.fromI32(10) const recipient = randomEvmAddress() const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(9999999) setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = Transfer.create( - token, - amount, - recipient, - fee, - Address.fromString(settler.address), - user, - deadline, - null, - [new IntentEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data'))] - ) + const transfer = new Transfer(chainId, [TransferData.fromBigInt(token, amount, recipient)], user, [ + new OperationEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data')), + ]) - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.address.toString()) - expect(transfer.deadline).toBe(deadline.toString()) - expect(transfer.nonce).toBe('0x') expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].token).toBe(tokenAddress.toString()) expect(transfer.transfers[0].recipient).toBe(recipient.toString()) expect(transfer.transfers[0].amount).toBe(amount.toString()) - expect(transfer.maxFees.length).toBe(1) - expect(transfer.maxFees[0].token).toBe(tokenAddress.toString()) - expect(transfer.maxFees[0].amount).toBe(fee.toString()) - expect(transfer.events.length).toBe(1) expect(transfer.events[0].topic).toBe('0x746f706963') expect(transfer.events[0].data).toBe('0x64617461') expect(JSON.stringify(transfer)).toBe( - `{"op":1,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${tokenAddress}","amount":"${fee.toString()}"}],"events":[{"topic":"0x746f706963","data":"0x64617461"}],"chainId":${chainId},"transfers":[{"token":"${tokenAddress}","amount":"${amount}","recipient":"${recipient}"}]}` + `{"opType":1,"chainId":${chainId},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"transfers":[{"token":"${transfer.transfers[0].token}","amount":"${transfer.transfers[0].amount}","recipient":"${transfer.transfers[0].recipient}"}]}` ) }) }) @@ -106,60 +80,28 @@ describe('Transfer', () => { const chainId = 1 const user = randomEvmAddress() const transferData = TransferData.fromI32(randomERC20Token(chainId), 5000, randomEvmAddress()) - const fee = TokenAmount.fromI32(randomERC20Token(chainId), 10) const settler = randomSettler(chainId) - const deadline = BigInt.fromI32(9999999) setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = new Transfer( - chainId, - [transferData], - [fee], - Address.fromString(settler.address), - user, - deadline, - '0x' - ) + const transfer = new Transfer(chainId, [transferData], user, []) - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.address.toString()) - expect(transfer.deadline).toBe(deadline.toString()) - expect(transfer.nonce).toBe('0x') expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].token).toBe(transferData.token) expect(transfer.transfers[0].recipient).toBe(transferData.recipient) expect(transfer.transfers[0].amount).toBe(transferData.amount) - expect(transfer.maxFees.length).toBe(1) - expect(transfer.maxFees[0].token).toBe(fee.token.address.toString()) - expect(transfer.maxFees[0].amount).toBe(fee.amount.toString()) - expect(transfer.events.length).toBe(0) expect(JSON.stringify(transfer)).toBe( - `{"op":1,"settler":"${settler.address}","user":"${user}","deadline":"${deadline}","nonce":"0x","maxFees":[{"token":"${fee.token.address.toString()}","amount":"${fee.amount.toString()}"}],"events":[],"chainId":${chainId},"transfers":[{"token":"${transferData.token}","amount":"${transferData.amount}","recipient":"${transferData.recipient}"}]}` + `{"opType":1,"chainId":${chainId},"user":"${user}","events":[],"transfers":[{"token":"${transferData.token}","amount":"${transferData.amount}","recipient":"${transferData.recipient}"}]}` ) }) }) - - describe('validations', () => { - it('throws an error when transfer list is empty', () => { - expect(() => { - new Transfer(1, [], []) - }).toThrow('Transfer list cannot be empty') - }) - - it('throws an error when there is no max fee', () => { - expect(() => { - const transferData = TransferData.fromI32(randomERC20Token(1), 5000, randomEvmAddress()) - new Transfer(1, [transferData], []) - }).toThrow('At least a max fee must be specified') - }) - }) }) describe('TransferBuilder', () => { @@ -176,10 +118,9 @@ describe('TransferBuilder', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransferFromTokenAmount(tokenAmount, recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 9)) const transfer = builder.build() - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].amount).toBe('5000') @@ -194,7 +135,6 @@ describe('TransferBuilder', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransferFromStringDecimal(token, '3000', recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 9)) const transfer = builder.build() expect(transfer.transfers[0].amount).toBe('3000') @@ -213,7 +153,6 @@ describe('TransferBuilder', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransfers([transfer1, transfer2]) - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 9)) const transfer = builder.build() expect(transfer.transfers.length).toBe(2) @@ -230,7 +169,6 @@ describe('TransferBuilder', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransfersFromTokenAmounts(tokenAmounts, recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(randomERC20Token(chainId), 9)) const transfer = builder.build() expect(transfer.transfers.length).toBe(2) @@ -240,13 +178,6 @@ describe('TransferBuilder', () => { describe('validations', () => { describe('chainId', () => { - it('throws if fee token chainId mismatches the transfer chainId', () => { - expect(() => { - const fee = TokenAmount.fromI32(randomERC20Token(ChainId.GNOSIS), 2) - TransferBuilder.forChain(chainId).addMaxFee(fee) - }).toThrow('Fee token must be on the same chain as the one requested for the transfer') - }) - it('throws if addTransferFromStringDecimal has different chainId', () => { expect(() => { const tokenAddress = Address.fromString(tokenAddressStr) @@ -280,34 +211,24 @@ describe('Transfer - SVM support', () => { const tokenAddress = randomSvmAddress() const token = SPLToken.fromAddress(tokenAddress, chainId, 9, 'SOL') const amount = BigInt.fromI32(1000) - const fee = BigInt.fromI32(10) const recipient = randomSvmAddress() const settler = randomSvmSettler() setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = Transfer.create(token, amount, recipient, fee) - expect(transfer.op).toBe(OperationType.Transfer) + const transfer = new Transfer(chainId, [TransferData.fromBigInt(token, amount, recipient)]) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.address.toString()) - expect(transfer.chainId).toBe(chainId) - expect(transfer.deadline).toBe('300') - expect(transfer.nonce).toBe('0x') - expect(transfer.isSVM()).toBe(true) expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].token).toBe(tokenAddress.toString()) expect(transfer.transfers[0].recipient).toBe(recipient.toString()) expect(transfer.transfers[0].amount).toBe(amount.toString()) - expect(transfer.maxFees.length).toBe(1) - expect(transfer.maxFees[0].token).toBe(tokenAddress.toString()) - expect(transfer.maxFees[0].amount).toBe(fee.toString()) - expect(transfer.events.length).toBe(0) expect(JSON.stringify(transfer)).toBe( - `{"op":1,"settler":"${settler.address}","user":"${user}","deadline":"300","nonce":"0x","maxFees":[{"token":"${tokenAddress}","amount":"${fee.toString()}"}],"events":[],"chainId":${chainId},"transfers":[{"token":"${tokenAddress}","amount":"${amount}","recipient":"${recipient}"}]}` + `{"opType":1,"chainId":${chainId},"user":"${user}","events":[],"transfers":[{"token":"${transfer.transfers[0].token}","amount":"${transfer.transfers[0].amount}","recipient":"${transfer.transfers[0].recipient}"}]}` ) }) @@ -315,15 +236,13 @@ describe('Transfer - SVM support', () => { const user = randomSvmAddress() const token = SPLToken.native() const amount = BigInt.fromI32(1000) - const fee = BigInt.fromI32(10) const recipient = randomSvmAddress() const settler = randomSvmSettler() setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = Transfer.create(token, amount, recipient, fee) + const transfer = new Transfer(ChainId.SOLANA_MAINNET, [TransferData.fromBigInt(token, amount, recipient)]) expect(transfer.chainId).toBe(ChainId.SOLANA_MAINNET) - expect(transfer.isSVM()).toBe(true) expect(transfer.transfers[0].token).toBe(token.address.toString()) expect(token.isNative()).toBe(true) }) @@ -335,29 +254,15 @@ describe('Transfer - SVM support', () => { const token2 = SPLToken.fromAddress(randomSvmAddress(), chainId, 6, 'USDC') const transferData1 = TransferData.fromI32(token1, 5000, randomSvmAddress()) const transferData2 = TransferData.fromI32(token2, 1000, randomSvmAddress()) - const fee = TokenAmount.fromI32(token1, 10) const settler = randomSvmSettler() - const deadline = BigInt.fromI32(9999999) setContext(1, 1, user.toString(), [settler], 'trigger-transfer') - const transfer = new Transfer( - chainId, - [transferData1, transferData2], - [fee], - Address.fromString(settler.address), - user, - deadline, - '0x' - ) + const transfer = new Transfer(chainId, [transferData1, transferData2], user, []) - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) - expect(transfer.isSVM()).toBe(true) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.address.toString()) - expect(transfer.deadline).toBe(deadline.toString()) - expect(transfer.nonce).toBe('0x') expect(transfer.transfers.length).toBe(2) expect(transfer.transfers[0].token).toBe(transferData1.token) @@ -366,10 +271,6 @@ describe('Transfer - SVM support', () => { expect(transfer.transfers[1].token).toBe(transferData2.token) expect(transfer.transfers[1].recipient).toBe(transferData2.recipient) expect(transfer.transfers[1].amount).toBe(transferData2.amount) - - expect(transfer.maxFees.length).toBe(1) - expect(transfer.maxFees[0].token).toBe(fee.token.address.toString()) - expect(transfer.maxFees[0].amount).toBe(fee.amount.toString()) }) }) }) @@ -386,12 +287,10 @@ describe('TransferBuilder - SVM support', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransferFromTokenAmount(tokenAmount, recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(token, 9)) const transfer = builder.build() - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) - expect(transfer.isSVM()).toBe(true) expect(transfer.transfers.length).toBe(1) expect(transfer.transfers[0].amount).toBe('5000000000000') expect(transfer.transfers[0].recipient).toBe(recipientAddress.toString()) @@ -405,7 +304,6 @@ describe('TransferBuilder - SVM support', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransferFromStringDecimal(token, '3000', recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(token, 9)) const transfer = builder.build() expect(transfer.transfers[0].amount).toBe('3000000000000') @@ -423,7 +321,6 @@ describe('TransferBuilder - SVM support', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransfers([transfer1, transfer2]) - builder.addMaxFee(TokenAmount.fromI32(token, 9)) const transfer = builder.build() expect(transfer.transfers.length).toBe(2) @@ -439,7 +336,6 @@ describe('TransferBuilder - SVM support', () => { const builder = TransferBuilder.forChain(chainId) builder.addTransfersFromTokenAmounts(tokenAmounts, recipientAddress) - builder.addMaxFee(TokenAmount.fromI32(token, 9)) const transfer = builder.build() expect(transfer.transfers.length).toBe(2) @@ -448,41 +344,24 @@ describe('TransferBuilder - SVM support', () => { it('creates a complete Solana Transfer with all builder methods', () => { const user = randomSvmAddress() - const settler = randomSvmAddress() const token = SPLToken.native() const recipient = randomSvmAddress() - const deadline = BigInt.fromI32(9999999) - const nonce = '0x123456' + const settler = randomSvmSettler() + + setContext(1, 1, user.toString(), [settler], 'trigger-transfer') const builder = TransferBuilder.forChain(chainId) - builder.addSettler(settler) - builder.addUser(user) - builder.addDeadline(deadline) - builder.addNonce(nonce) builder.addTransferFromI32(token, 1000, recipient) - builder.addMaxFee(TokenAmount.fromI32(token, 10)) const transfer = builder.build() - expect(transfer.op).toBe(OperationType.Transfer) + expect(transfer.opType).toBe(OperationType.Transfer) expect(transfer.chainId).toBe(chainId) - expect(transfer.isSVM()).toBe(true) expect(transfer.user).toBe(user.toString()) - expect(transfer.settler).toBe(settler.toString()) - expect(transfer.deadline).toBe(deadline.toString()) - expect(transfer.nonce).toBe(nonce) expect(transfer.transfers.length).toBe(1) - expect(transfer.maxFees.length).toBe(1) }) }) describe('chainId validations', () => { - it('throws if fee token chainId mismatches the Solana transfer chainId', () => { - expect(() => { - const fee = TokenAmount.fromI32(randomERC20Token(ChainId.ETHEREUM), 2) - TransferBuilder.forChain(chainId).addMaxFee(fee) - }).toThrow('Fee token must be on the same chain') - }) - it('throws if addTransferFromStringDecimal has different chainId for Solana', () => { expect(() => { const tokenAddress = randomSvmAddress() diff --git a/packages/test-ts/src/RunnerMock.ts b/packages/test-ts/src/RunnerMock.ts index c4e940ed..14860666 100644 --- a/packages/test-ts/src/RunnerMock.ts +++ b/packages/test-ts/src/RunnerMock.ts @@ -159,10 +159,7 @@ export default class RunnerMock { private getDefaultEnvImports(): WebAssembly.ModuleImports { const defaultResult = (data: unknown) => '{"success":"true","data":' + JSON.stringify(data) + ',"error":""}' return { - _evmCall: this.createLogFn('_evmCall'), - _svmCall: this.createLogFn('_svmCall'), - _swap: this.createLogFn('_swap'), - _transfer: this.createLogFn('_transfer'), + _sendIntent: this.createLogFn('_sendIntent'), _tokenPriceQuery: this.createMockFunction('_tokenPriceQuery', { default: defaultResult([]) }), _relevantTokensQuery: this.createMockFunction('_relevantTokensQuery', { default: defaultResult([]) }), _evmCallQuery: this.createMockFunction('_evmCallQuery', { default: defaultResult('0x') }), diff --git a/packages/test-ts/src/types.ts b/packages/test-ts/src/types.ts index a606db41..29d8ef5c 100644 --- a/packages/test-ts/src/types.ts +++ b/packages/test-ts/src/types.ts @@ -98,34 +98,46 @@ export type GenerateMockParams = { export type RunFunctionOptionalParams = Partial> -export type IntentBase = { - op: number - settler: string +export type OperationBase = { + opType: number user: string - deadline: string - nonce: string - maxFees: { token: string; amount: string }[] + chainId: number events: { topic: string; data: string }[] } -export type Transfer = IntentBase & { - chainId: number +export type TransferOperation = OperationBase & { transfers: { token: string; amount: string; recipient: string }[] } -export type Swap = IntentBase & { +export type SwapOperation = OperationBase & { sourceChain: number destinationChain: number tokensIn: { token: string; amount: string }[] tokensOut: { token: string; minAmount: string; recipient: string }[] } -export type Call = IntentBase & { - chainId: number +export type CallOperation = OperationBase & { calls: { target: string; data: string; value: string }[] } -export type Intent = Transfer | Swap | Call +export type SvmCallOperation = OperationBase & { + instructions: { + programId: string + accountsMeta: { pubkey: string; isSigner: boolean; isWritable: boolean }[] + data: string + }[] +} + +export type Operation = TransferOperation | SwapOperation | CallOperation | SvmCallOperation + +export type Intent = { + settler: string + feePayer: string + deadline: string + nonce: string + maxFees: { token: string; amount: string }[] + operations: Operation[] +} export type OracleResponse = AnyOracleResponse diff --git a/packages/test-ts/src/utils.ts b/packages/test-ts/src/utils.ts index 9950637e..87e542ae 100644 --- a/packages/test-ts/src/utils.ts +++ b/packages/test-ts/src/utils.ts @@ -10,13 +10,15 @@ import { import { Wallet } from 'ethers' import { - Call, + CallOperation, Intent, + Operation, OracleResponse, QueryMock, QueryProcessor, - Swap, - Transfer, + SvmCallOperation, + SwapOperation, + TransferOperation, ValidationErrorContext, } from './types' @@ -24,13 +26,23 @@ const SIGNER = new OracleSigner(EthersSigner.fromPrivateKey(Wallet.createRandom( export function toIntents(intentsJson: string) { const raw = JSON.parse(intentsJson) - return raw.map((intent: Partial) => { - if (intent.op == OpType.Swap) { - const { sourceChain, destinationChain } = intent as Swap - return { ...intent, sourceChain: Number(sourceChain), destinationChain: Number(destinationChain) } - } else { - const { chainId } = intent as Transfer | Call - return { ...intent, chainId: Number(chainId) } + return raw.map((intent: Intent) => { + return { + ...intent, + operations: intent.operations.map((operation: Operation) => { + if (operation.opType == OpType.Swap) { + const swap = operation as SwapOperation + return { + ...swap, + chainId: Number(swap.chainId), + sourceChain: Number(swap.sourceChain), + destinationChain: Number(swap.destinationChain), + } + } + + const nonSwap = operation as TransferOperation | CallOperation | SvmCallOperation + return { ...nonSwap, chainId: Number(nonSwap.chainId) } + }), } }) }