Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/spicy-areas-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mimicprotocol/test-ts": patch
"@mimicprotocol/lib-ts": patch
"@mimicprotocol/cli": patch
---

Add dynamic call operation
Original file line number Diff line number Diff line change
@@ -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"}]}]}]}
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions packages/integration/tests/014-swap-and-dynamic-call/mock.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
269 changes: 269 additions & 0 deletions packages/lib-ts/src/intents/Call/EvmDynamicCall.ts
Original file line number Diff line number Diff line change
@@ -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<EvmDynamicCallBuilder>(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<EvmDynamicCallBuilder>(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<EvmDynamicCallBuilder>(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<EvmDynamicCallBuilder>(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<EvmEncodeParam>(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<EvmDynamicArg>(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())
}
}
1 change: 1 addition & 0 deletions packages/lib-ts/src/intents/Call/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './EvmCall'
export * from './EvmDynamicCall'
export * from './SvmCall'
26 changes: 26 additions & 0 deletions packages/lib-ts/src/intents/Intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/lib-ts/src/intents/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum OperationType {
Transfer,
EvmCall,
CrossChainSwap,
EvmDynamicCall,
SvmCall,
}

Expand Down
Loading