diff --git a/.changeset/spicy-areas-switch.md b/.changeset/spicy-areas-switch.md new file mode 100644 index 00000000..a2743298 --- /dev/null +++ b/.changeset/spicy-areas-switch.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/test-ts": patch +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +--- + +Add dynamic call operation diff --git a/packages/integration/tests/014-swap-and-dynamic-call/expected.log b/packages/integration/tests/014-swap-and-dynamic-call/expected.log new file mode 100644 index 00000000..dedf9859 --- /dev/null +++ b/packages/integration/tests/014-swap-and-dynamic-call/expected.log @@ -0,0 +1 @@ +_sendIntent: {"settler":"0x6b175474e89094c44da98b954eedeac495271d0f","feePayer":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","deadline":"1438223473","nonce":"0xabcd","maxFees":[{"token":"0xa000000000000000000000000000000000000002","amount":"10"}],"operations":[{"opType":0,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"sourceChain":1,"tokensIn":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"100000000"}],"tokensOut":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","minAmount":"95000000","recipient":"0xa000000000000000000000000000000000000001"}],"destinationChain":1},{"opType":4,"chainId":1,"user":"0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0","events":[],"calls":[{"target":"0xa000000000000000000000000000000000000001","value":"0","selector":"0x12345678","arguments":[{"kind":0,"data":"0x"},{"kind":1,"data":"0x"}]}]}]} diff --git a/packages/integration/tests/014-swap-and-dynamic-call/manifest.yaml b/packages/integration/tests/014-swap-and-dynamic-call/manifest.yaml new file mode 100644 index 00000000..d3531edf --- /dev/null +++ b/packages/integration/tests/014-swap-and-dynamic-call/manifest.yaml @@ -0,0 +1,9 @@ +version: 1.0.0 +name: Example Function +description: Autogenerated Example Function +inputs: + - chainId: int32 + - target: address + - selector: bytes + - maxFeeToken: address + - maxFeeAmount: uint256 diff --git a/packages/integration/tests/014-swap-and-dynamic-call/mock.json b/packages/integration/tests/014-swap-and-dynamic-call/mock.json new file mode 100644 index 00000000..d63f39ad --- /dev/null +++ b/packages/integration/tests/014-swap-and-dynamic-call/mock.json @@ -0,0 +1,12 @@ +{ + "environment": { + "_getContext": "{ \"timestamp\": 1438223173000, \"consensusThreshold\": 1, \"user\": \"0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0\", \"settlers\": [{\"address\": \"0x6b175474e89094c44da98b954eedeac495271d0f\", \"chainId\": 1}], \"triggerSig\": \"682ec8210b1ce912da4d2952\"}" + }, + "inputs": { + "chainId": 1, + "target": "0xA000000000000000000000000000000000000001", + "selector": "0x12345678", + "maxFeeToken": "0xA000000000000000000000000000000000000002", + "maxFeeAmount": "10" + } +} diff --git a/packages/integration/tests/014-swap-and-dynamic-call/src/function.ts b/packages/integration/tests/014-swap-and-dynamic-call/src/function.ts new file mode 100644 index 00000000..e4da9636 --- /dev/null +++ b/packages/integration/tests/014-swap-and-dynamic-call/src/function.ts @@ -0,0 +1,29 @@ +import { + BigInt, + ERC20Token, + EvmDynamicArg, + EvmDynamicCallBuilder, + EvmEncodeParam, + IntentBuilder, + SwapBuilder, + TokenAmount, +} from '@mimicprotocol/lib-ts' + +import { inputs } from './types' + +export default function main(): void { + const chainId = inputs.chainId + const USDC = ERC20Token.fromString('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', chainId, 6, 'USDC') + const maxFee = TokenAmount.fromBigInt(ERC20Token.fromAddress(inputs.maxFeeToken, chainId), inputs.maxFeeAmount) + + const swap = SwapBuilder.forChains(chainId, chainId) + .addTokenInFromTokenAmount(TokenAmount.fromI32(USDC, 100)) + .addTokenOutFromTokenAmount(TokenAmount.fromI32(USDC, 95), inputs.target) + + const call = EvmDynamicCallBuilder.forChain(chainId).addCall(inputs.target, inputs.selector, [ + EvmDynamicArg.literal([EvmEncodeParam.fromValue('uint256', BigInt.fromI32(123))]), + EvmDynamicArg.variable(0, 0), + ]) + + new IntentBuilder().addMaxFee(maxFee).addOperationsBuilders([swap, call]).send() +} diff --git a/packages/lib-ts/src/intents/Call/EvmDynamicCall.ts b/packages/lib-ts/src/intents/Call/EvmDynamicCall.ts new file mode 100644 index 00000000..5caa6f6a --- /dev/null +++ b/packages/lib-ts/src/intents/Call/EvmDynamicCall.ts @@ -0,0 +1,269 @@ +import { environment } from '../../environment' +import { evm } from '../../evm' +import { TokenAmount } from '../../tokens' +import { Address, BigInt, Bytes, ChainId, EvmEncodeParam } from '../../types' +import { IntentBuilder } from '../Intent' +import { Operation, OperationBuilder, OperationEvent, OperationType } from '../Operation' + +export enum EvmDynamicArgKind { + Literal = 0, + Variable = 1, +} + +/** + * Builder for creating EVM dynamic call operations. + */ +export class EvmDynamicCallBuilder extends OperationBuilder { + protected chainId: ChainId + protected calls: EvmDynamicCallData[] = [] + + /** + * Creates an EvmDynamicCallBuilder for the specified EVM blockchain network. + * @param chainId - The blockchain network identifier + * @returns A new EvmDynamicCallBuilder instance + */ + static forChain(chainId: ChainId): EvmDynamicCallBuilder { + return new EvmDynamicCallBuilder(chainId) + } + + /** + * Creates a new EvmDynamicCallBuilder instance. + * @param chainId - The EVM blockchain network identifier + */ + private constructor(chainId: ChainId) { + super() + this.chainId = chainId + } + + /** + * Adds a dynamic contract call to the operation. + * @param target - The contract address to call + * @param selector - The function selector to call + * @param args - The dynamic call arguments + * @param value - The native token value to send + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addCall( + target: Address, + selector: Bytes, + args: EvmDynamicArg[] = [], + value: BigInt = BigInt.zero() + ): EvmDynamicCallBuilder { + this.calls.push(new EvmDynamicCallData(target, selector, args, value)) + return this + } + + /** + * Adds multiple dynamic contract calls to the operation. + * @param calls - The contract calls to add + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addCalls(calls: EvmDynamicCallData[]): EvmDynamicCallBuilder { + for (let i = 0; i < calls.length; i++) { + this.addCall( + Address.fromString(calls[i].target), + Bytes.fromHexString(calls[i].selector), + calls[i].arguments, + BigInt.fromString(calls[i].value) + ) + } + return this + } + + /** + * Adds the calls from another EvmDynamicCallBuilder to this EvmDynamicCallBuilder. + * @param builder - The EvmDynamicCallBuilder to add the calls from + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addCallsFromBuilder(builder: EvmDynamicCallBuilder): EvmDynamicCallBuilder { + return this.addCalls(builder.getCalls()) + } + + /** + * Adds the calls from multiple EvmDynamicCallBuilders to this EvmDynamicCallBuilder. + * @param builders - The EvmDynamicCallBuilders to add the calls from + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addCallsFromBuilders(builders: EvmDynamicCallBuilder[]): EvmDynamicCallBuilder { + for (let i = 0; i < builders.length; i++) this.addCallsFromBuilder(builders[i]) + return this + } + + /** + * Returns a copy of the calls array. + * @returns A copy of the calls array + */ + getCalls(): EvmDynamicCallData[] { + return this.calls.slice(0) + } + + /** + * Sets the user address for this operation. + * @param user - The user address + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addUser(user: Address): EvmDynamicCallBuilder { + return changetype(super.addUser(user)) + } + + /** + * Sets the user address from a string. + * @param user - The user address as a hex string + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addUserAsString(user: string): EvmDynamicCallBuilder { + return changetype(super.addUserAsString(user)) + } + + /** + * Sets an event for the operation. + * @param topic - The topic to be indexed in the event + * @param data - The event data + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addEvent(topic: Bytes, data: Bytes): EvmDynamicCallBuilder { + return changetype(super.addEvent(topic, data)) + } + + /** + * Sets multiple events for the operation. + * @param events - The list of events to be added + * @returns This EvmDynamicCallBuilder instance for method chaining + */ + addEvents(events: OperationEvent[]): EvmDynamicCallBuilder { + return changetype(super.addEvents(events)) + } + + /** + * Builds and returns the final EvmDynamicCall operation. + * @returns A new EvmDynamicCall instance with all configured parameters + */ + build(): EvmDynamicCall { + return new EvmDynamicCall(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 a single dynamic argument in a dynamic call. + */ +@json +export class EvmDynamicArg { + public kind: EvmDynamicArgKind + public data: string + + /** + * Creates a literal dynamic argument from ABI-encoded parameters. + * @param parameters - The ABI parameters to encode as a literal argument + * @returns A new literal dynamic argument + */ + static literal(parameters: EvmEncodeParam[]): EvmDynamicArg { + const encodedParameters = new Array(parameters.length + 1) + encodedParameters[0] = EvmEncodeParam.fromValue('string', Bytes.fromUTF8('')) + for (let i = 0; i < parameters.length; i++) encodedParameters[i + 1] = parameters[i] + return new EvmDynamicArg(EvmDynamicArgKind.Literal, Bytes.fromHexString(evm.encode(encodedParameters))) + } + + /** + * Creates a variable reference dynamic argument. + * @param opIndex - The referenced operation index + * @param subIndex - The referenced output index within the operation + * @returns A new variable dynamic argument + */ + static variable(opIndex: u32, subIndex: u32): EvmDynamicArg { + return new EvmDynamicArg( + EvmDynamicArgKind.Variable, + Bytes.fromHexString( + evm.encode([ + EvmEncodeParam.fromValue('uint256', BigInt.fromU32(opIndex)), + EvmEncodeParam.fromValue('uint256', BigInt.fromU32(subIndex)), + ]) + ) + ) + } + + /** + * Creates a new EvmDynamicArg instance. + * @param kind - The argument resolution strategy + * @param data - The ABI-encoded argument data + */ + constructor(kind: EvmDynamicArgKind, data: Bytes) { + this.kind = kind + this.data = data.toHexString() + } +} + +/** + * Represents data for a single dynamic contract call within an EVM dynamic call operation. + */ +@json +export class EvmDynamicCallData { + public target: string + public value: string + public selector: string + public arguments: EvmDynamicArg[] + + /** + * Creates a new EvmDynamicCallData instance. + * @param target - The contract address to call + * @param selector - The function selector to call + * @param args - The dynamic arguments for the call + * @param value - The native token value to send + */ + constructor(target: Address, selector: Bytes, args: EvmDynamicArg[] = [], value: BigInt = BigInt.zero()) { + if (selector.length !== 4) throw new Error('Selector must be 4 bytes') + this.target = target.toString() + this.value = value.toString() + this.selector = selector.toHexString() + this.arguments = new Array(args.length) + for (let i = 0; i < args.length; i++) { + const argument = args[i] + this.arguments[i] = new EvmDynamicArg(argument.kind, Bytes.fromHexString(argument.data)) + } + } +} + +/** + * Represents an EVM dynamic call operation containing one or more dynamic contract calls. + */ +@json +export class EvmDynamicCall extends Operation { + public calls: EvmDynamicCallData[] + + /** + * Creates a new EvmDynamicCall operation. + * @param chainId - The blockchain network identifier + * @param calls - Array of dynamic contract calls to execute + * @param user - The user address + * @param events - The operation events to emit + */ + constructor( + chainId: ChainId, + calls: EvmDynamicCallData[], + user: Address | null = null, + events: OperationEvent[] | null = null + ) { + super(OperationType.EvmDynamicCall, chainId, user, events) + if (calls.length === 0) throw new Error('Call list cannot be empty') + this.calls = calls + } + + /** + * Sends this EvmDynamicCall 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(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/index.ts b/packages/lib-ts/src/intents/Call/index.ts index 2c576fec..62d2dc2a 100644 --- a/packages/lib-ts/src/intents/Call/index.ts +++ b/packages/lib-ts/src/intents/Call/index.ts @@ -1,2 +1,3 @@ export * from './EvmCall' +export * from './EvmDynamicCall' export * from './SvmCall' diff --git a/packages/lib-ts/src/intents/Intent.ts b/packages/lib-ts/src/intents/Intent.ts index b6aff772..0c884ca3 100644 --- a/packages/lib-ts/src/intents/Intent.ts +++ b/packages/lib-ts/src/intents/Intent.ts @@ -6,6 +6,7 @@ import { Address, BigInt, Bytes, ChainId } from '../types' import { SvmAccountMeta } from '../types/svm/SvmAccountMeta' import { EvmCall, EvmCallData } from './Call/EvmCall' +import { EvmDynamicArg, EvmDynamicCall, EvmDynamicCallData } from './Call/EvmDynamicCall' import { SvmCall, SvmInstruction } from './Call/SvmCall' import { Operation, OperationBuilder, OperationEvent, OperationType } from './Operation' import { Swap, SwapTokenIn, SwapTokenOut } from './Swap' @@ -84,6 +85,31 @@ export class IntentBuilder { return this.addOperation(new EvmCall(chainId, [new EvmCallData(target, data, value)], user, events)) } + /** + * Adds a single EVM dynamic call operation to this intent from raw parameters. + * @param chainId - The blockchain network identifier + * @param target - The contract address to call + * @param selector - The function selector to call + * @param args - The dynamic arguments to resolve at execution time + * @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 + */ + addEvmDynamicCallOperation( + chainId: ChainId, + target: Address, + selector: Bytes, + args: EvmDynamicArg[] = [], + value: BigInt = BigInt.zero(), + user: Address | null = null, + events: OperationEvent[] | null = null + ): IntentBuilder { + return this.addOperation( + new EvmDynamicCall(chainId, [new EvmDynamicCallData(target, selector, args, value)], user, events) + ) + } + /** * Adds a single swap operation to this intent from raw parameters. * @param sourceChain - The source blockchain network identifier diff --git a/packages/lib-ts/src/intents/Operation.ts b/packages/lib-ts/src/intents/Operation.ts index cf93f7cc..7ebe5178 100644 --- a/packages/lib-ts/src/intents/Operation.ts +++ b/packages/lib-ts/src/intents/Operation.ts @@ -7,6 +7,7 @@ export enum OperationType { Transfer, EvmCall, CrossChainSwap, + EvmDynamicCall, SvmCall, } diff --git a/packages/lib-ts/tests/intents/EvmDynamicCall.spec.ts b/packages/lib-ts/tests/intents/EvmDynamicCall.spec.ts new file mode 100644 index 00000000..18ea4f40 --- /dev/null +++ b/packages/lib-ts/tests/intents/EvmDynamicCall.spec.ts @@ -0,0 +1,191 @@ +import { JSON } from 'json-as' + +import { + EvmDynamicArg, + EvmDynamicArgKind, + EvmDynamicCall, + EvmDynamicCallBuilder, + EvmDynamicCallData, + OperationEvent, + OperationType, +} from '../../src/intents' +import { Address, BigInt, Bytes, EvmEncodeParam } from '../../src/types' +import { randomBytes, randomEvmAddress, randomSettler, setContext, setEvmEncode } from '../helpers' + +describe('EvmDynamicCall', () => { + it('creates a simple operation with default values and stringifies it', () => { + const chainId = 1 + const user = randomEvmAddress() + const target = randomEvmAddress() + const selector = Bytes.fromHexString('0x12345678') + const argument = new EvmDynamicArg(EvmDynamicArgKind.Literal, randomBytes(64)) + const settler = randomSettler(chainId) + + setContext(1, 1, user.toString(), [settler], 'trigger-123') + + const call = new EvmDynamicCall(chainId, [new EvmDynamicCallData(target, selector, [argument])]) + expect(call.opType).toBe(OperationType.EvmDynamicCall) + expect(call.user).toBe(user.toString()) + expect(call.chainId).toBe(chainId) + expect(call.events.length).toBe(0) + expect(call.calls.length).toBe(1) + expect(call.calls[0].target).toBe(target.toString()) + expect(call.calls[0].value).toBe('0') + expect(call.calls[0].selector).toBe(selector.toHexString()) + expect(call.calls[0].arguments.length).toBe(1) + expect(call.calls[0].arguments[0].kind).toBe(EvmDynamicArgKind.Literal) + expect(call.calls[0].arguments[0].data).toBe(argument.data) + + expect(JSON.stringify(call)).toBe( + `{"opType":4,"chainId":${chainId},"user":"${user}","events":[],"calls":[{"target":"${target}","value":"0","selector":"${selector.toHexString()}","arguments":[{"kind":0,"data":"${argument.data}"}]}]}` + ) + }) + + it('creates an operation with explicit user and events', () => { + const chainId = 1 + const user = randomEvmAddress() + const settler = randomSettler(chainId) + const target = randomEvmAddress() + const selector = Bytes.fromHexString('0x90abcdef') + const argument = new EvmDynamicArg(EvmDynamicArgKind.Variable, randomBytes(64)) + const value = BigInt.fromI32(10) + + setContext(1, 1, user.toString(), [settler], 'trigger-123') + + const call = new EvmDynamicCall(chainId, [new EvmDynamicCallData(target, selector, [argument], value)], user, [ + new OperationEvent(Bytes.fromUTF8('topic'), Bytes.fromUTF8('data')), + ]) + + expect(call.opType).toBe(OperationType.EvmDynamicCall) + expect(call.user).toBe(user.toString()) + expect(call.chainId).toBe(chainId) + expect(call.calls[0].value).toBe(value.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( + `{"opType":4,"chainId":${chainId},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"calls":[{"target":"${target}","value":"${value.toString()}","selector":"${selector.toHexString()}","arguments":[{"kind":1,"data":"${argument.data}"}]}]}` + ) + }) + + it('creates a complex operation with multiple calls', () => { + const chainId = 1 + const user = randomEvmAddress() + const settler = randomSettler(chainId) + const target1 = randomEvmAddress() + const target2 = randomEvmAddress() + const selector1 = Bytes.fromHexString('0x12345678') + const selector2 = Bytes.fromHexString('0x90abcdef') + const argument1 = new EvmDynamicArg(EvmDynamicArgKind.Literal, randomBytes(64)) + const argument2 = new EvmDynamicArg(EvmDynamicArgKind.Variable, randomBytes(64)) + + setContext(1, 1, user.toString(), [settler], 'trigger-123') + + const call = new EvmDynamicCall( + chainId, + [ + new EvmDynamicCallData(target1, selector1, [argument1], BigInt.fromI32(1)), + new EvmDynamicCallData(target2, selector2, [argument2], BigInt.fromI32(2)), + ], + user + ) + + expect(call.calls.length).toBe(2) + expect(call.calls[0].target).toBe(target1.toString()) + expect(call.calls[0].selector).toBe(selector1.toHexString()) + expect(call.calls[0].arguments.length).toBe(1) + expect(call.calls[0].arguments[0].kind).toBe(EvmDynamicArgKind.Literal) + expect(call.calls[0].arguments[0].data).toBe(argument1.data) + expect(call.calls[0].value).toBe('1') + + expect(call.calls[1].target).toBe(target2.toString()) + expect(call.calls[1].selector).toBe(selector2.toHexString()) + expect(call.calls[1].arguments.length).toBe(1) + expect(call.calls[1].arguments[0].kind).toBe(EvmDynamicArgKind.Variable) + expect(call.calls[1].arguments[0].data).toBe(argument2.data) + expect(call.calls[1].value).toBe('2') + + expect(JSON.stringify(call)).toBe( + `{"opType":4,"chainId":${chainId},"user":"${user}","events":[],"calls":[{"target":"${target1}","value":"1","selector":"${selector1.toHexString()}","arguments":[{"kind":0,"data":"${argument1.data}"}]},{"target":"${target2}","value":"2","selector":"${selector2.toHexString()}","arguments":[{"kind":1,"data":"${argument2.data}"}]}]}` + ) + }) + + it('throws an error when there is no call data', () => { + expect(() => { + new EvmDynamicCall(1, []) + }).toThrow('Call list cannot be empty') + }) + + it('throws an error when the selector is not 4 bytes', () => { + expect(() => { + new EvmDynamicCallData(randomEvmAddress(), Bytes.fromHexString('0x1234')) + }).toThrow('Selector must be 4 bytes') + }) +}) + +describe('EvmDynamicArg', () => { + it('encodes literal arguments', () => { + const emptyString = Bytes.fromUTF8('').toHexString() + setEvmEncode('string', emptyString, '0x1234') + + const argument = EvmDynamicArg.literal([EvmEncodeParam.fromValue('uint256', BigInt.fromI32(1))]) + + expect(argument.kind).toBe(EvmDynamicArgKind.Literal) + expect(argument.data).toBe('0x1234') + }) + + it('encodes variable references', () => { + setEvmEncode('uint256', '1', '0x5678') + + const argument = EvmDynamicArg.variable(1, 0) + + expect(argument.kind).toBe(EvmDynamicArgKind.Variable) + expect(argument.data).toBe('0x5678') + }) +}) + +describe('EvmDynamicCallBuilder', () => { + const chainId = 1 + const target1Str = '0x0000000000000000000000000000000000000001' + const target2Str = '0x0000000000000000000000000000000000000002' + + it('adds multiple calls and builds an operation', () => { + const target1 = Address.fromString(target1Str) + const target2 = Address.fromString(target2Str) + const selector1 = Bytes.fromHexString('0x12345678') + const selector2 = Bytes.fromHexString('0x90abcdef') + + const builder = EvmDynamicCallBuilder.forChain(chainId) + builder.addCall( + target1, + selector1, + [new EvmDynamicArg(EvmDynamicArgKind.Literal, randomBytes(64))], + BigInt.fromString('1') + ) + builder.addCall( + target2, + selector2, + [new EvmDynamicArg(EvmDynamicArgKind.Variable, randomBytes(64))], + BigInt.fromString('2') + ) + + const call = builder.build() + expect(call.calls.length).toBe(2) + expect(call.calls[0].target).toBe(target1Str) + expect(call.calls[0].selector).toBe(selector1.toHexString()) + expect(call.calls[1].target).toBe(target2Str) + expect(call.calls[1].selector).toBe(selector2.toHexString()) + }) + + it('adds call with default arguments and value', () => { + const target = Address.fromString(target1Str) + const selector = Bytes.fromHexString('0x12345678') + + const builder = EvmDynamicCallBuilder.forChain(chainId) + builder.addCall(target, selector) + + const call = builder.build() + expect(call.calls[0].arguments.length).toBe(0) + expect(call.calls[0].value).toBe('0') + }) +}) diff --git a/packages/lib-ts/tests/intents/Intent.spec.ts b/packages/lib-ts/tests/intents/Intent.spec.ts index 710f574b..103c7610 100644 --- a/packages/lib-ts/tests/intents/Intent.spec.ts +++ b/packages/lib-ts/tests/intents/Intent.spec.ts @@ -2,11 +2,20 @@ import { JSON } from 'json-as' import { SerializableSettler } from '../../src/context' import { NULL_ADDRESS } from '../../src/helpers' -import { EvmCallBuilder, IntentBuilder, SwapBuilder } from '../../src/intents' +import { + EvmCallBuilder, + EvmDynamicArg, + EvmDynamicArgKind, + EvmDynamicCall, + IntentBuilder, + SwapBuilder, +} from '../../src/intents' import { TokenAmount } from '../../src/tokens' import { Address, BigInt, Bytes } from '../../src/types' import { randomERC20Token, randomSettler, setContext } from '../helpers' +/* eslint-disable no-secrets/no-secrets */ + describe('IntentBuilder', () => { const chainId = 1 const targetAddressStr = '0x0000000000000000000000000000000000000001' @@ -157,4 +166,31 @@ describe('IntentBuilder', () => { }).toThrow('Cross-chain swap must be the only operation in an intent') }) }) + + describe('addEvmDynamicCallOperation', () => { + it('adds a dynamic call operation from raw parameters', () => { + const target = Address.fromString(targetAddressStr) + const settler = randomSettler(chainId) + const userAddressStr = '0x0000000000000000000000000000000000000002' + + setContext(0, 1, userAddressStr, [settler], 'trigger-dynamic-call') + + const intent = new IntentBuilder() + .addEvmDynamicCallOperation( + chainId, + target, + Bytes.fromHexString('0x12345678'), + [new EvmDynamicArg(EvmDynamicArgKind.Literal, Bytes.fromHexString('0x1234'))], + BigInt.fromI32(7) + ) + .build() + + expect(intent.operations.length).toBe(1) + expect(intent.operations[0].opType).toBe(4) + + const operation = changetype(intent.operations[0]) + expect(operation.calls[0].selector).toBe('0x12345678') + expect(operation.calls[0].value).toBe('7') + }) + }) }) diff --git a/packages/lib-ts/tests/intents/SvmCall.spec.ts b/packages/lib-ts/tests/intents/SvmCall.spec.ts index d54bb33e..b91ae505 100644 --- a/packages/lib-ts/tests/intents/SvmCall.spec.ts +++ b/packages/lib-ts/tests/intents/SvmCall.spec.ts @@ -36,7 +36,7 @@ describe('SvmCall', () => { expect(svmCall.events.length).toBe(0) expect(JSON.stringify(svmCall)).toBe( - `{"opType":4,"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()}"}]}` + `{"opType":5,"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()}"}]}` ) }) @@ -69,7 +69,7 @@ describe('SvmCall', () => { expect(svmCall.events[0].data).toBe('0x64617461') expect(JSON.stringify(svmCall)).toBe( - `{"opType":4,"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()}"}]}` + `{"opType":5,"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,7 +107,7 @@ describe('SvmCall', () => { expect(svmCall.events.length).toBe(0) expect(JSON.stringify(svmCall)).toBe( - `{"opType":4,"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()}"}]}` + `{"opType":5,"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()}"}]}` ) }) }) diff --git a/packages/test-ts/src/types.ts b/packages/test-ts/src/types.ts index 29d8ef5c..03666118 100644 --- a/packages/test-ts/src/types.ts +++ b/packages/test-ts/src/types.ts @@ -120,6 +120,10 @@ export type CallOperation = OperationBase & { calls: { target: string; data: string; value: string }[] } +export type DynamicCallOperation = OperationBase & { + calls: { target: string; value: string; selector: string; arguments: { kind: number; data: string }[] }[] +} + export type SvmCallOperation = OperationBase & { instructions: { programId: string @@ -128,7 +132,7 @@ export type SvmCallOperation = OperationBase & { }[] } -export type Operation = TransferOperation | SwapOperation | CallOperation | SvmCallOperation +export type Operation = TransferOperation | SwapOperation | CallOperation | DynamicCallOperation | SvmCallOperation export type Intent = { settler: string