From a10c9009b3ebecdaa79c48c8211e3a7c19eb40f4 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Tue, 30 Dec 2025 08:47:57 +0100 Subject: [PATCH 01/15] wip: cross chain service --- package-lock.json | 49 +++-- package.json | 4 +- src/abis/acpAbiV2.ts | 197 ++++++++++++++++--- src/acpClient.ts | 9 +- src/acpFare.ts | 33 +++- src/acpJob.ts | 76 ++++++- src/acpMemo.ts | 4 +- src/configs/acpConfigs.ts | 25 ++- src/constants.ts | 24 ++- src/contractClients/acpContractClientV2.ts | 87 +++++++- src/contractClients/baseAcpContractClient.ts | 57 +++++- src/interfaces.ts | 19 ++ src/utils.ts | 59 ++++++ 13 files changed, 574 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac3b44d..1e51c39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "license": "ISC", "dependencies": { "@aa-sdk/core": "^4.73.0", - "@account-kit/infra": "^4.73.0", - "@account-kit/smart-contracts": "^4.73.0", + "@account-kit/infra": "^4.81.2", + "@account-kit/smart-contracts": "^4.81.2", "@virtuals-protocol/acp-node": "^0.3.0-beta.9", "ajv": "^8.17.1", + "dotenv": "^17.2.3", "socket.io-client": "^4.8.1", "tsup": "^8.5.0", "viem": "^2.28.2" @@ -23,9 +24,9 @@ } }, "node_modules/@aa-sdk/core": { - "version": "4.73.0", - "resolved": "https://registry.npmjs.org/@aa-sdk/core/-/core-4.73.0.tgz", - "integrity": "sha512-Fm9ko4GS6jHtDJq552pebl4Yp3zMWFnFAvhTds0eejDxEDScq7bzAajxh57kjitNPHowW+msY1xpe9lxFi6qCQ==", + "version": "4.81.2", + "resolved": "https://registry.npmjs.org/@aa-sdk/core/-/core-4.81.2.tgz", + "integrity": "sha512-0iUeu66U+fEUCMLpDTaCYLCZleVOoM5jVUakTqKBIbXZjCHi8XqaQiToUYsC/iUYJ93UsTSOMv5d5oxXboptHQ==", "license": "MIT", "dependencies": { "abitype": "^0.8.3", @@ -37,13 +38,13 @@ } }, "node_modules/@account-kit/infra": { - "version": "4.73.0", - "resolved": "https://registry.npmjs.org/@account-kit/infra/-/infra-4.73.0.tgz", - "integrity": "sha512-uX5h6MwQkj0KWeixKwTcyKAciHHC/mUNNGuLVEPAIoNSOOvrR2DdI04HDKVhFwHhQphO0iK09bNL2WcafRNDMA==", + "version": "4.81.2", + "resolved": "https://registry.npmjs.org/@account-kit/infra/-/infra-4.81.2.tgz", + "integrity": "sha512-kUHPpx5j5XUIuChEWRBj1nCHT6ObdGM4E3WUV/Ou3pOWvs6RBvw1DA8tYSbTJX3Txt5iiSIrITOoeRIqR3spuA==", "license": "MIT", "dependencies": { - "@aa-sdk/core": "^4.73.0", - "@account-kit/logging": "^4.73.0", + "@aa-sdk/core": "^4.81.2", + "@account-kit/logging": "^4.81.2", "eventemitter3": "^5.0.1", "zod": "^3.22.4" }, @@ -55,9 +56,9 @@ } }, "node_modules/@account-kit/logging": { - "version": "4.73.0", - "resolved": "https://registry.npmjs.org/@account-kit/logging/-/logging-4.73.0.tgz", - "integrity": "sha512-dS/C8vM9z0t3UoCI05F8yBjBTdu+alYn9Zfu3I6IiFeT1Uz7z8i5V+Tiobipa6Dx37tAx1mErgu4kG0TCqWQJw==", + "version": "4.81.2", + "resolved": "https://registry.npmjs.org/@account-kit/logging/-/logging-4.81.2.tgz", + "integrity": "sha512-vajr624gm58n2+Z2USOF2nAGr/wBY7XwczsBNFX54aTIFrYfzCD+z/+/vAGmN1lopZuQvwKGMc4nNxDE/TnC1Q==", "license": "MIT", "dependencies": { "@segment/analytics-next": "1.74.0", @@ -65,13 +66,13 @@ } }, "node_modules/@account-kit/smart-contracts": { - "version": "4.73.0", - "resolved": "https://registry.npmjs.org/@account-kit/smart-contracts/-/smart-contracts-4.73.0.tgz", - "integrity": "sha512-YwHvIhfEtg2i3JbxNPVznHePGGXgPNU5WBPjvtC9I2n4N6lcIRRbTV+D4RETEO5gPMM+0vU3mR+tK3RXM9lS9w==", + "version": "4.81.2", + "resolved": "https://registry.npmjs.org/@account-kit/smart-contracts/-/smart-contracts-4.81.2.tgz", + "integrity": "sha512-PzojTNIssltqfywfgXrtbODg9xWMtsxhE+RHZdUQG0SeqGlglZ+8WQOY46z2+71jKqpV7a2tper43uy5PqxRKw==", "license": "MIT", "dependencies": { - "@aa-sdk/core": "^4.73.0", - "@account-kit/infra": "^4.73.0", + "@aa-sdk/core": "^4.81.2", + "@account-kit/infra": "^4.81.2", "webauthn-p256": "^0.0.10" }, "peerDependencies": { @@ -2328,6 +2329,18 @@ "node": ">=0.4.0" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", diff --git a/package.json b/package.json index faab8e5..8d89853 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ }, "dependencies": { "@aa-sdk/core": "^4.73.0", - "@account-kit/infra": "^4.73.0", - "@account-kit/smart-contracts": "^4.73.0", + "@account-kit/infra": "^4.81.2", + "@account-kit/smart-contracts": "^4.81.2", "@virtuals-protocol/acp-node": "^0.3.0-beta.9", "ajv": "^8.17.1", "socket.io-client": "^4.8.1", diff --git a/src/abis/acpAbiV2.ts b/src/abis/acpAbiV2.ts index a44f582..4471829 100644 --- a/src/abis/acpAbiV2.ts +++ b/src/abis/acpAbiV2.ts @@ -78,7 +78,12 @@ const ACP_V2_ABI = [ name: "accountId", type: "uint256", }, - { indexed: false, internalType: "bool", name: "isActive", type: "bool" }, + { + indexed: false, + internalType: "bool", + name: "isActive", + type: "bool", + }, ], name: "AccountStatusUpdated", type: "event", @@ -212,7 +217,12 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, { indexed: true, internalType: "bytes32", @@ -232,7 +242,12 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, { indexed: true, internalType: "address", @@ -252,7 +267,12 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, { indexed: true, internalType: "address", @@ -334,7 +354,11 @@ const ACP_V2_ABI = [ inputs: [], name: "accountManager", outputs: [ - { internalType: "contract IAccountManager", name: "", type: "address" }, + { + internalType: "contract IAccountManager", + name: "", + type: "address", + }, ], stateMutability: "view", type: "function", @@ -414,6 +438,28 @@ const ACP_V2_ABI = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "string", name: "metadata", type: "string" }, + ], + name: "createMemoWithMetadata", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { internalType: "uint256", name: "jobId", type: "uint256" }, @@ -422,7 +468,11 @@ const ACP_V2_ABI = [ { internalType: "uint256", name: "amount", type: "uint256" }, { internalType: "address", name: "recipient", type: "address" }, { internalType: "uint256", name: "feeAmount", type: "uint256" }, - { internalType: "enum ACPTypes.FeeType", name: "feeType", type: "uint8" }, + { + internalType: "enum ACPTypes.FeeType", + name: "feeType", + type: "uint8", + }, { internalType: "enum ACPTypes.MemoType", name: "memoType", @@ -540,9 +590,21 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { internalType: "address", name: "approvedBy", type: "address" }, - { internalType: "uint256", name: "approvedAt", type: "uint256" }, - { internalType: "bool", name: "requiresApproval", type: "bool" }, + { + internalType: "address", + name: "approvedBy", + type: "address", + }, + { + internalType: "uint256", + name: "approvedAt", + type: "uint256", + }, + { + internalType: "bool", + name: "requiresApproval", + type: "bool", + }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -551,6 +613,11 @@ const ACP_V2_ABI = [ type: "uint8", }, { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, ], internalType: "struct ACPTypes.Memo[]", name: "memos", @@ -587,9 +654,21 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { internalType: "address", name: "approvedBy", type: "address" }, - { internalType: "uint256", name: "approvedAt", type: "uint256" }, - { internalType: "bool", name: "requiresApproval", type: "bool" }, + { + internalType: "address", + name: "approvedBy", + type: "address", + }, + { + internalType: "uint256", + name: "approvedAt", + type: "uint256", + }, + { + internalType: "bool", + name: "requiresApproval", + type: "bool", + }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -598,6 +677,11 @@ const ACP_V2_ABI = [ type: "uint8", }, { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, ], internalType: "struct ACPTypes.Memo[]", name: "memos", @@ -611,7 +695,11 @@ const ACP_V2_ABI = [ { inputs: [ { internalType: "uint256", name: "jobId", type: "uint256" }, - { internalType: "enum ACPTypes.JobPhase", name: "phase", type: "uint8" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "phase", + type: "uint8", + }, { internalType: "uint256", name: "offset", type: "uint256" }, { internalType: "uint256", name: "limit", type: "uint256" }, ], @@ -630,9 +718,21 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { internalType: "address", name: "approvedBy", type: "address" }, - { internalType: "uint256", name: "approvedAt", type: "uint256" }, - { internalType: "bool", name: "requiresApproval", type: "bool" }, + { + internalType: "address", + name: "approvedBy", + type: "address", + }, + { + internalType: "uint256", + name: "approvedAt", + type: "uint256", + }, + { + internalType: "bool", + name: "requiresApproval", + type: "bool", + }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -641,6 +741,11 @@ const ACP_V2_ABI = [ type: "uint8", }, { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, ], internalType: "struct ACPTypes.Memo[]", name: "memos", @@ -692,9 +797,21 @@ const ACP_V2_ABI = [ name: "defaultPaymentToken_", type: "address", }, - { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, - { internalType: "address", name: "platformTreasury_", type: "address" }, - { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + { + internalType: "uint256", + name: "platformFeeBP_", + type: "uint256", + }, + { + internalType: "address", + name: "platformTreasury_", + type: "address", + }, + { + internalType: "uint256", + name: "evaluatorFeeBP_", + type: "uint256", + }, ], name: "initialize", outputs: [], @@ -747,7 +864,11 @@ const ACP_V2_ABI = [ inputs: [], name: "paymentManager", outputs: [ - { internalType: "contract IPaymentManager", name: "", type: "address" }, + { + internalType: "contract IPaymentManager", + name: "", + type: "address", + }, ], stateMutability: "view", type: "function", @@ -776,7 +897,11 @@ const ACP_V2_ABI = [ { inputs: [ { internalType: "bytes32", name: "role", type: "bytes32" }, - { internalType: "address", name: "callerConfirmation", type: "address" }, + { + internalType: "address", + name: "callerConfirmation", + type: "address", + }, ], name: "renounceRole", outputs: [], @@ -851,7 +976,11 @@ const ACP_V2_ABI = [ }, { inputs: [ - { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + { + internalType: "uint256", + name: "evaluatorFeeBP_", + type: "uint256", + }, ], name: "updateEvaluatorFee", outputs: [], @@ -870,9 +999,21 @@ const ACP_V2_ABI = [ }, { inputs: [ - { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, - { internalType: "address", name: "platformTreasury_", type: "address" }, - { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, + { + internalType: "uint256", + name: "platformFeeBP_", + type: "uint256", + }, + { + internalType: "address", + name: "platformTreasury_", + type: "address", + }, + { + internalType: "uint256", + name: "evaluatorFeeBP_", + type: "uint256", + }, ], name: "updatePlatformConfig", outputs: [], @@ -881,7 +1022,11 @@ const ACP_V2_ABI = [ }, { inputs: [ - { internalType: "address", name: "newImplementation", type: "address" }, + { + internalType: "address", + name: "newImplementation", + type: "address", + }, { internalType: "bytes", name: "data", type: "bytes" }, ], name: "upgradeToAndCall", diff --git a/src/acpClient.ts b/src/acpClient.ts index 522bcd8..e59e059 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -82,7 +82,7 @@ class AcpClient { this.onNewTask = options.onNewTask; this.onEvaluate = options.onEvaluate || this.defaultOnEvaluate; - this.init(); + this.init(options.skipSocketConnection); } public contractClientByAddress(address: Address | undefined) { @@ -121,7 +121,11 @@ class AcpClient { return this.acpContractClient.walletAddress; } - async init() { + async init(skipSocketConnection: boolean = false) { + if (skipSocketConnection) { + return; + } + const socket = io(this.acpUrl, { auth: { walletAddress: this.walletAddress, @@ -230,6 +234,7 @@ class AcpClient { } process.exit(0); }; + process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); } diff --git a/src/acpFare.ts b/src/acpFare.ts index 60e971b..2e1f0e3 100644 --- a/src/acpFare.ts +++ b/src/acpFare.ts @@ -10,7 +10,11 @@ import AcpError from "./acpError"; import { AcpContractConfig, baseAcpConfig } from "./configs/acpConfigs"; class Fare { - constructor(public contractAddress: Address, public decimals: number) {} + constructor( + public contractAddress: Address, + public decimals: number, + public chainId?: number + ) {} formatAmount(amount: number) { return parseUnits(amount.toString(), this.decimals); @@ -18,15 +22,34 @@ class Fare { static async fromContractAddress( contractAddress: Address, - config: AcpContractConfig = baseAcpConfig + config: AcpContractConfig = baseAcpConfig, + chainId: number = config.chain.id ) { if (contractAddress === config.baseFare.contractAddress) { return config.baseFare; } + let chainConfig = config.chain; + let rpcUrl = config.rpcEndpoint; + + if (chainId !== config.chain.id) { + const selectedConfig = config.chains?.find( + (chain) => chain.chain.id === chainId + ); + + if (!selectedConfig) { + throw new AcpError( + `Chain configuration for chainId ${chainId} not found.` + ); + } + + chainConfig = selectedConfig.chain; + rpcUrl = selectedConfig.rpcUrl; + } + const publicClient = createPublicClient({ - chain: config.chain, - transport: http(config.rpcEndpoint), + chain: chainConfig, + transport: http(rpcUrl), }); const decimals = await publicClient.readContract({ @@ -35,7 +58,7 @@ class Fare { functionName: "decimals", }); - return new Fare(contractAddress, decimals as number); + return new Fare(contractAddress, decimals as number, chainId); } } diff --git a/src/acpJob.ts b/src/acpJob.ts index 0dcc0ce..38e3e9a 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -1,4 +1,4 @@ -import { Address, formatUnits } from "viem"; +import { Address } from "viem"; import AcpClient from "./acpClient"; import { AcpJobPhases, @@ -8,10 +8,15 @@ import { } from "./contractClients/baseAcpContractClient"; import AcpMemo from "./acpMemo"; import { DeliverablePayload, AcpMemoStatus } from "./interfaces"; -import { preparePayload, tryParseJson } from "./utils"; +import { + encodeTransferEventMetadata, + preparePayload, + tryParseJson, +} from "./utils"; import { FareAmount, FareAmountBase } from "./acpFare"; import AcpError from "./acpError"; import { PriceType } from "./acpJobOffering"; +import { ASSET_MANAGER_ADDRESSES } from "./constants"; class AcpJob { public name: string | undefined; @@ -358,6 +363,11 @@ class AcpJob { throw new AcpError("No transaction memo found"); } + // If payable chain belongs to non ACP native chain, we route to transfer service + if (amount.fare.chainId !== this.acpContractClient.config.chain.id) { + return await this.deliverCrossChainPayable(this.clientAddress, amount); + } + const operations: OperationPayload[] = []; operations.push( @@ -525,6 +535,68 @@ class AcpJob { waitMs = Math.min(waitMs * 2, maxWaitMs); } } + + private async deliverCrossChainPayable( + recipient: Address, + amount: FareAmountBase + ) { + if (!amount.fare.chainId) { + throw new AcpError("Chain ID is required for cross chain payable"); + } + + const chainId = amount.fare.chainId; + + // Check if wallet has enough balance on destination chain + const tokenBalance = await this.acpContractClient.getERC20Balance( + chainId, + amount.fare.contractAddress, + this.acpContractClient.agentWalletAddress + ); + + if (tokenBalance < amount.amount) { + throw new AcpError("Insufficient token balance for cross chain payable"); + } + + // Approve allowance to asset manager on destination chain + const approveAllowanceOperation = this.acpContractClient.approveAllowance( + amount.amount, + amount.fare.contractAddress, + ASSET_MANAGER_ADDRESSES[ + chainId as unknown as keyof typeof ASSET_MANAGER_ADDRESSES + ] as Address + ); + + const { userOpHash, txnHash } = + await this.acpContractClient.handleOperation( + [approveAllowanceOperation], + chainId + ); + + const encodedTransferEventMetadata = encodeTransferEventMetadata( + amount.fare.contractAddress, + amount.amount, + recipient, + chainId + ); + + // Create transfer event memo + const transferEventMemoOperation = + this.acpContractClient.createMemoWithMetadata( + this.id, + preparePayload({ + tokenAddress: amount.fare.contractAddress, + tokenAmount: amount.amount.toString(), + recipient, + chainId, + }), + MemoType.TRANSFER_EVENT, + true, + AcpJobPhases.COMPLETED, + encodedTransferEventMetadata + ); + + await this.acpContractClient.handleOperation([transferEventMemoOperation]); + } } export default AcpJob; diff --git a/src/acpMemo.ts b/src/acpMemo.ts index d748f23..5bc0378 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -4,6 +4,7 @@ import BaseAcpContractClient, { MemoType, } from "./contractClients/baseAcpContractClient"; import { + AcpMemoState, AcpMemoStatus, GenericPayload, PayableDetails, @@ -24,7 +25,8 @@ class AcpMemo { public senderAddress: Address, public signedReason?: string, public expiry?: Date, - public payableDetails?: PayableDetails + public payableDetails?: PayableDetails, + public state?: AcpMemoState ) { if (this.payableDetails) { this.payableDetails.amount = BigInt(this.payableDetails.amount); diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index ec823da..f176016 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,10 +1,32 @@ import { Address } from "@aa-sdk/core"; import { baseSepolia, base } from "@account-kit/infra"; +import { + mainnet, + polygon, + bsc, + arbitrum, + sepolia, + polygonAmoy, + bscTestnet, + arbitrumSepolia, +} from "viem/chains"; import { Fare } from "../acpFare"; import ACP_ABI from "../abis/acpAbi"; import ACP_V2_ABI from "../abis/acpAbiV2"; import { X402Config } from "../interfaces"; +type SupportedChain = + | typeof mainnet + | typeof sepolia + | typeof polygon + | typeof polygonAmoy + | typeof bsc + | typeof bscTestnet + | typeof arbitrum + | typeof arbitrumSepolia; + +type ChainConfig = { chain: SupportedChain; rpcUrl?: string }; + class AcpContractConfig { constructor( public chain: typeof baseSepolia | typeof base, @@ -14,7 +36,8 @@ class AcpContractConfig { public acpUrl: string, public abi: typeof ACP_ABI | typeof ACP_V2_ABI, public rpcEndpoint?: string, - public x402Config?: X402Config + public x402Config?: X402Config, + public chains: ChainConfig[] = [] ) {} } diff --git a/src/constants.ts b/src/constants.ts index f983e17..c213410 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,16 @@ import { Address } from "viem"; -import { base, baseSepolia } from "viem/chains"; +import { + arbitrum, + arbitrumSepolia, + base, + baseSepolia, + bsc, + bscTestnet, + mainnet, + polygon, + polygonAmoy, + sepolia, +} from "viem/chains"; export const USDC_TOKEN_ADDRESS = { [baseSepolia.id]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address, @@ -19,3 +30,14 @@ export const HTTP_STATUS_CODES = { OK: 200, PAYMENT_REQUIRED: 402, }; + +export const ASSET_MANAGER_ADDRESSES = { + [bscTestnet.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [bsc.id]: "", + [polygonAmoy.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [polygon.id]: "", + [arbitrum.id]: "", + [arbitrumSepolia.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [sepolia.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [mainnet.id]: "", +}; diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index 4a7bf80..eb941db 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -1,6 +1,7 @@ import { Address, LocalAccountSigner, SmartAccountSigner } from "@aa-sdk/core"; -import { alchemy } from "@account-kit/infra"; +import { alchemy, defineAlchemyChain } from "@account-kit/infra"; import { + createModularAccountV2, createModularAccountV2Client, ModularAccountV2Client, } from "@account-kit/smart-contracts"; @@ -18,16 +19,20 @@ import { X402PayableRequirements, X402Payment, X402PaymentResponse, + CheckTransactionConfig, } from "../interfaces"; import { AcpX402 } from "../acpX402"; +import { base, baseSepolia } from "viem/chains"; class AcpContractClientV2 extends BaseAcpContractClient { private MAX_RETRIES: number; private PRIORITY_FEE_MULTIPLIER = 2; private MAX_FEE_PER_GAS = 20000000; private MAX_PRIORITY_FEE_PER_GAS = 21000000; + private GAS_FEE_MULTIPLIER = 0.5; private _sessionKeyClient: ModularAccountV2Client | undefined; + private _sessionKeyClients: Record = {}; private _acpX402: AcpX402 | undefined; constructor( @@ -48,6 +53,17 @@ class AcpContractClientV2 extends BaseAcpContractClient { agentWalletAddress: Address, config: AcpContractConfig = baseAcpConfigV2 ) { + const publicClients: Record< + number, + ReturnType + > = {}; + for (const chain of config.chains) { + publicClients[chain.chain.id] = createPublicClient({ + chain: chain.chain, + transport: http(chain.rpcUrl), + }); + } + const publicClient = createPublicClient({ chain: config.chain, transport: http(config.rpcEndpoint), @@ -88,6 +104,8 @@ class AcpContractClientV2 extends BaseAcpContractClient { config ); + acpContractClient.publicClients = publicClients; + await acpContractClient.init(walletPrivateKey, sessionEntityKeyId); return acpContractClient; @@ -111,6 +129,27 @@ class AcpContractClientV2 extends BaseAcpContractClient { }, }); + // initialize all session key clients for all chains in the config + for (const chain of this.config.chains) { + this._sessionKeyClients[chain.chain.id] = + await createModularAccountV2Client({ + chain: defineAlchemyChain({ + chain: chain.chain, + rpcBaseUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, + }), + transport: alchemy({ + rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, + }), + signer: sessionKeySigner, + policyId: "186aaa4a-5f57-4156-83fb-e456365a8820", + accountAddress: this.agentWalletAddress, + signerEntity: { + entityId: sessionEntityKeyId, + isGlobalValidation: true, + }, + }); + } + this._acpX402 = new AcpX402( this.config, this.sessionKeyClient, @@ -145,7 +184,20 @@ class AcpContractClientV2 extends BaseAcpContractClient { return this._acpX402; } - private async calculateGasFees() { + private async calculateGasFees(chainId?: number) { + if (chainId) { + const { maxFeePerGas } = await this.publicClients[ + chainId + ].estimateFeesPerGas(); + + const increasedMaxFeePerGas = + BigInt(maxFeePerGas) + + (BigInt(maxFeePerGas) * BigInt(this.GAS_FEE_MULTIPLIER * 100)) / + BigInt(100); + + return increasedMaxFeePerGas; + } + const finalMaxFeePerGas = BigInt(this.MAX_FEE_PER_GAS) + BigInt(this.MAX_PRIORITY_FEE_PER_GAS) * @@ -154,7 +206,18 @@ class AcpContractClientV2 extends BaseAcpContractClient { return finalMaxFeePerGas; } - async handleOperation(operations: OperationPayload[]): Promise<{ userOpHash: Address , txnHash: Address }> { + async handleOperation( + operations: OperationPayload[], + chainId?: number + ): Promise<{ userOpHash: Address; txnHash: Address }> { + const sessionKeyClient = chainId + ? this._sessionKeyClients[chainId] + : this.sessionKeyClient; + + if (!sessionKeyClient) { + throw new AcpError("Session key client not initialized"); + } + const payload: any = { uo: operations.map((operation) => ({ target: operation.contractAddress, @@ -172,24 +235,32 @@ class AcpContractClientV2 extends BaseAcpContractClient { while (retries > 0) { try { if (this.MAX_RETRIES > retries) { - const gasFees = await this.calculateGasFees(); + const gasFees = await this.calculateGasFees(chainId); payload["overrides"] = { maxFeePerGas: `0x${gasFees.toString(16)}`, }; } - const { hash } = await this.sessionKeyClient.sendUserOperation(payload); + const { hash } = await sessionKeyClient.sendUserOperation(payload); - const txnHash = await this.sessionKeyClient.waitForUserOperationTransaction({ + const checkTransactionConfig: CheckTransactionConfig = { hash, - tag: "pending", retries: { intervalMs: 200, multiplier: 1.1, maxRetries: 10, }, - }); + }; + + // Only base / base sepolia supports preconfirmed transactions + if (!chainId || chainId === baseSepolia.id || chainId === base.id) { + checkTransactionConfig["tag"] = "pending"; + } + + const txnHash = await sessionKeyClient.waitForUserOperationTransaction( + checkTransactionConfig + ); return { userOpHash: hash, txnHash }; } catch (error) { diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 3fd5f3e..255b03c 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -38,6 +38,7 @@ export enum MemoType { PAYABLE_TRANSFER_ESCROW, // 8 - Escrowed payment transfer NOTIFICATION, // 9 - Notification PAYABLE_NOTIFICATION, // 10 - Payable notification + TRANSFER_EVENT, } export enum AcpJobPhases { @@ -69,6 +70,8 @@ abstract class BaseAcpContractClient { public abi: typeof ACP_ABI | typeof ACP_V2_ABI; public jobCreatedSignature: string; public publicClient: ReturnType; + public publicClients: Record> = + {}; constructor( public agentWalletAddress: Address, @@ -89,7 +92,10 @@ abstract class BaseAcpContractClient { }); } - abstract handleOperation(operations: OperationPayload[]): Promise<{ userOpHash: Address , txnHash: Address }>; + abstract handleOperation( + operations: OperationPayload[], + chainId?: number + ): Promise<{ userOpHash: Address; txnHash: Address }>; abstract getJobId( createJobUserOpHash: Address, @@ -171,13 +177,14 @@ abstract class BaseAcpContractClient { approveAllowance( amountBaseUnit: bigint, - paymentTokenAddress: Address = this.config.baseFare.contractAddress + paymentTokenAddress: Address = this.config.baseFare.contractAddress, + targetAddress?: Address ): OperationPayload { try { const data = encodeFunctionData({ abi: erc20Abi, functionName: "approve", - args: [this.contractAddress, amountBaseUnit], + args: [targetAddress ?? this.contractAddress, amountBaseUnit], }); const payload: OperationPayload = { @@ -263,6 +270,32 @@ abstract class BaseAcpContractClient { } } + createMemoWithMetadata( + jobId: number, + content: string, + type: MemoType, + isSecured: boolean, + nextPhase: AcpJobPhases, + metadata: string + ): OperationPayload { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createMemoWithMetadata", + args: [jobId, content, type, isSecured, nextPhase, metadata], + }); + + const payload: OperationPayload = { + data: data, + contractAddress: this.contractAddress, + }; + + return payload; + } catch (error) { + throw new AcpError("Failed to create memo with metadata", error); + } + } + signMemo( memoId: number, isApproved: boolean, @@ -400,6 +433,24 @@ abstract class BaseAcpContractClient { } } + async getERC20Balance( + chainId: number, + tokenAddress: Address, + walletAddress: Address + ): Promise { + const publicClient = this.publicClients[chainId]; + if (!publicClient) { + throw new AcpError(`Public client for chainId ${chainId} not found`); + } + + return await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + args: [walletAddress], + }); + } + abstract getAcpVersion(): string; } diff --git a/src/interfaces.ts b/src/interfaces.ts index 2aba1df..d987890 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -18,6 +18,14 @@ export enum AcpMemoStatus { REJECTED = "REJECTED", } +export enum AcpMemoState { + NONE = "NONE", + PENDING = "PENDING", + IN_PROGRESS = "IN_PROGRESS", + COMPLETED = "COMPLETED", + REJECTED = "REJECTED", +} + export interface PayableDetails { amount: bigint; token: Address; @@ -107,6 +115,7 @@ export interface IAcpClientOptions { onNewTask?: (job: AcpJob, memoToSign?: acpMemo) => void; onEvaluate?: (job: AcpJob) => void; customRpcUrl?: string; + skipSocketConnection?: boolean; } export type AcpAgent = { @@ -344,3 +353,13 @@ export type X402PaymentResponse = { isPaymentRequired: boolean; data: X402PayableRequirements; }; + +export type CheckTransactionConfig = { + hash: Address; + retries: { + intervalMs: number; + multiplier: number; + maxRetries: number; + }; + tag?: "pending"; +}; diff --git a/src/utils.ts b/src/utils.ts index 6571967..6083d32 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,17 @@ +import { Address, encodeAbiParameters } from "viem"; +import { + arbitrum, + arbitrumSepolia, + base, + baseSepolia, + bsc, + bscTestnet, + mainnet, + polygon, + polygonAmoy, + sepolia, +} from "viem/chains"; + export function tryParseJson(content: string): T | null { try { return JSON.parse(content) as T; @@ -19,3 +33,48 @@ export function safeBase64Encode(data: string): string { } return Buffer.from(data).toString("base64"); } + +export function getDestinationEndpointId(chainId: number): number { + switch (chainId) { + case baseSepolia.id: + return 40245; + case sepolia.id: + return 40161; + case polygonAmoy.id: + return 40267; + case arbitrumSepolia.id: + return 40231; + case bscTestnet.id: + return 40102; + case base.id: + return 30184; + case mainnet.id: + return 30101; + case polygon.id: + return 30109; + case arbitrum.id: + return 30110; + case bsc.id: + return 30102; + } + + throw new Error(`Unsupported chain ID: ${chainId}`); +} + +export function encodeTransferEventMetadata( + tokenAddress: Address, + amount: bigint, + recipient: Address, + chainId: number +): string { + return encodeAbiParameters( + [ + { type: "address", name: "token" }, + { type: "uint256", name: "amount" }, + { type: "address", name: "recipient" }, + { type: "uint32", name: "dstEid" }, + { type: "bytes", name: "lzOptions" }, + ], + [tokenAddress, amount, recipient, getDestinationEndpointId(chainId), "0x"] + ); +} From 5f4b39ef8b2072df6a7b88ff40210f1819c73685 Mon Sep 17 00:00:00 2001 From: Zuhwa Date: Fri, 9 Jan 2026 16:18:40 +0800 Subject: [PATCH 02/15] implement cross chain payable --- package-lock.json | 49 ++--- package.json | 4 +- src/abis/acpAbiV2.ts | 210 +++++-------------- src/acpJob.ts | 33 ++- src/configs/acpConfigs.ts | 13 +- src/constants.ts | 8 +- src/contractClients/acpContractClientV2.ts | 8 +- src/contractClients/baseAcpContractClient.ts | 48 +++++ src/utils.ts | 17 +- 9 files changed, 164 insertions(+), 226 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e51c39..736d75c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,10 @@ "license": "ISC", "dependencies": { "@aa-sdk/core": "^4.73.0", - "@account-kit/infra": "^4.81.2", - "@account-kit/smart-contracts": "^4.81.2", + "@account-kit/infra": "^4.82.0", + "@account-kit/smart-contracts": "^4.82.0", "@virtuals-protocol/acp-node": "^0.3.0-beta.9", "ajv": "^8.17.1", - "dotenv": "^17.2.3", "socket.io-client": "^4.8.1", "tsup": "^8.5.0", "viem": "^2.28.2" @@ -24,9 +23,9 @@ } }, "node_modules/@aa-sdk/core": { - "version": "4.81.2", - "resolved": "https://registry.npmjs.org/@aa-sdk/core/-/core-4.81.2.tgz", - "integrity": "sha512-0iUeu66U+fEUCMLpDTaCYLCZleVOoM5jVUakTqKBIbXZjCHi8XqaQiToUYsC/iUYJ93UsTSOMv5d5oxXboptHQ==", + "version": "4.82.0", + "resolved": "https://registry.npmjs.org/@aa-sdk/core/-/core-4.82.0.tgz", + "integrity": "sha512-SwFtrrl09R6UgKZaS2MijRc0ISA+POfl625+XvJY+BTo1uGj0KJX+mTQZG493jZnh8TErEqj5Ownlv2ObNFmPw==", "license": "MIT", "dependencies": { "abitype": "^0.8.3", @@ -38,13 +37,13 @@ } }, "node_modules/@account-kit/infra": { - "version": "4.81.2", - "resolved": "https://registry.npmjs.org/@account-kit/infra/-/infra-4.81.2.tgz", - "integrity": "sha512-kUHPpx5j5XUIuChEWRBj1nCHT6ObdGM4E3WUV/Ou3pOWvs6RBvw1DA8tYSbTJX3Txt5iiSIrITOoeRIqR3spuA==", + "version": "4.82.0", + "resolved": "https://registry.npmjs.org/@account-kit/infra/-/infra-4.82.0.tgz", + "integrity": "sha512-GGmYCiHilvbqCU1X4IVZ/ZyTkUeWVi7nWaP8aGB7sHgS17X0U1gRbdyjUxL1MtAv4DGW5S9RjEY9HKJuVfND3w==", "license": "MIT", "dependencies": { - "@aa-sdk/core": "^4.81.2", - "@account-kit/logging": "^4.81.2", + "@aa-sdk/core": "^4.82.0", + "@account-kit/logging": "^4.82.0", "eventemitter3": "^5.0.1", "zod": "^3.22.4" }, @@ -56,9 +55,9 @@ } }, "node_modules/@account-kit/logging": { - "version": "4.81.2", - "resolved": "https://registry.npmjs.org/@account-kit/logging/-/logging-4.81.2.tgz", - "integrity": "sha512-vajr624gm58n2+Z2USOF2nAGr/wBY7XwczsBNFX54aTIFrYfzCD+z/+/vAGmN1lopZuQvwKGMc4nNxDE/TnC1Q==", + "version": "4.82.0", + "resolved": "https://registry.npmjs.org/@account-kit/logging/-/logging-4.82.0.tgz", + "integrity": "sha512-W3Fxdiq5ck5llmJp5oi8NlD368NuQx0TsM7L3XHTExYFumx3hIdML9uNVY1eQgV5yfoLBtzkPrdcPAL3tVntLA==", "license": "MIT", "dependencies": { "@segment/analytics-next": "1.74.0", @@ -66,13 +65,13 @@ } }, "node_modules/@account-kit/smart-contracts": { - "version": "4.81.2", - "resolved": "https://registry.npmjs.org/@account-kit/smart-contracts/-/smart-contracts-4.81.2.tgz", - "integrity": "sha512-PzojTNIssltqfywfgXrtbODg9xWMtsxhE+RHZdUQG0SeqGlglZ+8WQOY46z2+71jKqpV7a2tper43uy5PqxRKw==", + "version": "4.82.0", + "resolved": "https://registry.npmjs.org/@account-kit/smart-contracts/-/smart-contracts-4.82.0.tgz", + "integrity": "sha512-tkCNOO+iKyZK/j1tcws/tdeGpu609AlVMLqJ4RzxIqKpsKovYkMaKU588UF5PpyXnzHLS5EZFFEgaLbzmKt+mQ==", "license": "MIT", "dependencies": { - "@aa-sdk/core": "^4.81.2", - "@account-kit/infra": "^4.81.2", + "@aa-sdk/core": "^4.82.0", + "@account-kit/infra": "^4.82.0", "webauthn-p256": "^0.0.10" }, "peerDependencies": { @@ -2329,18 +2328,6 @@ "node": ">=0.4.0" } }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", diff --git a/package.json b/package.json index 8d89853..161f3ae 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ }, "dependencies": { "@aa-sdk/core": "^4.73.0", - "@account-kit/infra": "^4.81.2", - "@account-kit/smart-contracts": "^4.81.2", + "@account-kit/infra": "^4.82.0", + "@account-kit/smart-contracts": "^4.82.0", "@virtuals-protocol/acp-node": "^0.3.0-beta.9", "ajv": "^8.17.1", "socket.io-client": "^4.8.1", diff --git a/src/abis/acpAbiV2.ts b/src/abis/acpAbiV2.ts index 4471829..a2438c9 100644 --- a/src/abis/acpAbiV2.ts +++ b/src/abis/acpAbiV2.ts @@ -78,12 +78,7 @@ const ACP_V2_ABI = [ name: "accountId", type: "uint256", }, - { - indexed: false, - internalType: "bool", - name: "isActive", - type: "bool", - }, + { indexed: false, internalType: "bool", name: "isActive", type: "bool" }, ], name: "AccountStatusUpdated", type: "event", @@ -217,12 +212,7 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { - indexed: true, - internalType: "bytes32", - name: "role", - type: "bytes32", - }, + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, { indexed: true, internalType: "bytes32", @@ -242,12 +232,7 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { - indexed: true, - internalType: "bytes32", - name: "role", - type: "bytes32", - }, + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, { indexed: true, internalType: "address", @@ -267,12 +252,7 @@ const ACP_V2_ABI = [ { anonymous: false, inputs: [ - { - indexed: true, - internalType: "bytes32", - name: "role", - type: "bytes32", - }, + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, { indexed: true, internalType: "address", @@ -354,11 +334,7 @@ const ACP_V2_ABI = [ inputs: [], name: "accountManager", outputs: [ - { - internalType: "contract IAccountManager", - name: "", - type: "address", - }, + { internalType: "contract IAccountManager", name: "", type: "address" }, ], stateMutability: "view", type: "function", @@ -390,6 +366,34 @@ const ACP_V2_ABI = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "string", name: "content", type: "string" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "enum ACPTypes.FeeType", name: "feeType", type: "uint8" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint32", name: "lzDstEid", type: "uint32" }, + ], + name: "createCrossChainPayableMemo", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { internalType: "address", name: "provider", type: "address" }, @@ -438,28 +442,6 @@ const ACP_V2_ABI = [ stateMutability: "nonpayable", type: "function", }, - { - inputs: [ - { internalType: "uint256", name: "jobId", type: "uint256" }, - { internalType: "string", name: "content", type: "string" }, - { - internalType: "enum ACPTypes.MemoType", - name: "memoType", - type: "uint8", - }, - { internalType: "bool", name: "isSecured", type: "bool" }, - { - internalType: "enum ACPTypes.JobPhase", - name: "nextPhase", - type: "uint8", - }, - { internalType: "string", name: "metadata", type: "string" }, - ], - name: "createMemoWithMetadata", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "nonpayable", - type: "function", - }, { inputs: [ { internalType: "uint256", name: "jobId", type: "uint256" }, @@ -468,11 +450,7 @@ const ACP_V2_ABI = [ { internalType: "uint256", name: "amount", type: "uint256" }, { internalType: "address", name: "recipient", type: "address" }, { internalType: "uint256", name: "feeAmount", type: "uint256" }, - { - internalType: "enum ACPTypes.FeeType", - name: "feeType", - type: "uint8", - }, + { internalType: "enum ACPTypes.FeeType", name: "feeType", type: "uint8" }, { internalType: "enum ACPTypes.MemoType", name: "memoType", @@ -590,21 +568,9 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { - internalType: "address", - name: "approvedBy", - type: "address", - }, - { - internalType: "uint256", - name: "approvedAt", - type: "uint256", - }, - { - internalType: "bool", - name: "requiresApproval", - type: "bool", - }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -654,21 +620,9 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { - internalType: "address", - name: "approvedBy", - type: "address", - }, - { - internalType: "uint256", - name: "approvedAt", - type: "uint256", - }, - { - internalType: "bool", - name: "requiresApproval", - type: "bool", - }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -695,11 +649,7 @@ const ACP_V2_ABI = [ { inputs: [ { internalType: "uint256", name: "jobId", type: "uint256" }, - { - internalType: "enum ACPTypes.JobPhase", - name: "phase", - type: "uint8", - }, + { internalType: "enum ACPTypes.JobPhase", name: "phase", type: "uint8" }, { internalType: "uint256", name: "offset", type: "uint256" }, { internalType: "uint256", name: "limit", type: "uint256" }, ], @@ -718,21 +668,9 @@ const ACP_V2_ABI = [ }, { internalType: "uint256", name: "createdAt", type: "uint256" }, { internalType: "bool", name: "isApproved", type: "bool" }, - { - internalType: "address", - name: "approvedBy", - type: "address", - }, - { - internalType: "uint256", - name: "approvedAt", - type: "uint256", - }, - { - internalType: "bool", - name: "requiresApproval", - type: "bool", - }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, { internalType: "string", name: "metadata", type: "string" }, { internalType: "bool", name: "isSecured", type: "bool" }, { @@ -797,21 +735,9 @@ const ACP_V2_ABI = [ name: "defaultPaymentToken_", type: "address", }, - { - internalType: "uint256", - name: "platformFeeBP_", - type: "uint256", - }, - { - internalType: "address", - name: "platformTreasury_", - type: "address", - }, - { - internalType: "uint256", - name: "evaluatorFeeBP_", - type: "uint256", - }, + { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, + { internalType: "address", name: "platformTreasury_", type: "address" }, + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, ], name: "initialize", outputs: [], @@ -864,11 +790,7 @@ const ACP_V2_ABI = [ inputs: [], name: "paymentManager", outputs: [ - { - internalType: "contract IPaymentManager", - name: "", - type: "address", - }, + { internalType: "contract IPaymentManager", name: "", type: "address" }, ], stateMutability: "view", type: "function", @@ -897,11 +819,7 @@ const ACP_V2_ABI = [ { inputs: [ { internalType: "bytes32", name: "role", type: "bytes32" }, - { - internalType: "address", - name: "callerConfirmation", - type: "address", - }, + { internalType: "address", name: "callerConfirmation", type: "address" }, ], name: "renounceRole", outputs: [], @@ -976,11 +894,7 @@ const ACP_V2_ABI = [ }, { inputs: [ - { - internalType: "uint256", - name: "evaluatorFeeBP_", - type: "uint256", - }, + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, ], name: "updateEvaluatorFee", outputs: [], @@ -999,21 +913,9 @@ const ACP_V2_ABI = [ }, { inputs: [ - { - internalType: "uint256", - name: "platformFeeBP_", - type: "uint256", - }, - { - internalType: "address", - name: "platformTreasury_", - type: "address", - }, - { - internalType: "uint256", - name: "evaluatorFeeBP_", - type: "uint256", - }, + { internalType: "uint256", name: "platformFeeBP_", type: "uint256" }, + { internalType: "address", name: "platformTreasury_", type: "address" }, + { internalType: "uint256", name: "evaluatorFeeBP_", type: "uint256" }, ], name: "updatePlatformConfig", outputs: [], @@ -1022,11 +924,7 @@ const ACP_V2_ABI = [ }, { inputs: [ - { - internalType: "address", - name: "newImplementation", - type: "address", - }, + { internalType: "address", name: "newImplementation", type: "address" }, { internalType: "bytes", name: "data", type: "bytes" }, ], name: "upgradeToAndCall", diff --git a/src/acpJob.ts b/src/acpJob.ts index 38e3e9a..cea832e 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -10,6 +10,7 @@ import AcpMemo from "./acpMemo"; import { DeliverablePayload, AcpMemoStatus } from "./interfaces"; import { encodeTransferEventMetadata, + getDestinationEndpointId, preparePayload, tryParseJson, } from "./utils"; @@ -572,30 +573,22 @@ class AcpJob { chainId ); - const encodedTransferEventMetadata = encodeTransferEventMetadata( - amount.fare.contractAddress, - amount.amount, - recipient, - chainId - ); - - // Create transfer event memo - const transferEventMemoOperation = - this.acpContractClient.createMemoWithMetadata( + const createMemoOperation = + this.acpContractClient.createCrossChainPayableMemo( this.id, - preparePayload({ - tokenAddress: amount.fare.contractAddress, - tokenAmount: amount.amount.toString(), - recipient, - chainId, - }), - MemoType.TRANSFER_EVENT, - true, + "test", + amount.fare.contractAddress, + amount.amount, + recipient, + BigInt(0), + FeeType.NO_FEE, + MemoType.PAYABLE_TRANSFER, + new Date(Date.now() + 1000 * 60 * 5), AcpJobPhases.COMPLETED, - encodedTransferEventMetadata + getDestinationEndpointId(chainId) ); - await this.acpContractClient.handleOperation([transferEventMemoOperation]); + await this.acpContractClient.handleOperation([createMemoOperation]); } } diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index f176016..1e4d8d1 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,15 +1,16 @@ import { Address } from "@aa-sdk/core"; -import { baseSepolia, base } from "@account-kit/infra"; import { - mainnet, - polygon, + baseSepolia, + base, + bscTestnet, bsc, - arbitrum, + mainnet, sepolia, + polygon, polygonAmoy, - bscTestnet, + arbitrum, arbitrumSepolia, -} from "viem/chains"; +} from "@account-kit/infra"; import { Fare } from "../acpFare"; import ACP_ABI from "../abis/acpAbi"; import ACP_V2_ABI from "../abis/acpAbiV2"; diff --git a/src/constants.ts b/src/constants.ts index c213410..cbffc82 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -32,12 +32,12 @@ export const HTTP_STATUS_CODES = { }; export const ASSET_MANAGER_ADDRESSES = { - [bscTestnet.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [bscTestnet.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", [bsc.id]: "", - [polygonAmoy.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [polygonAmoy.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", [polygon.id]: "", [arbitrum.id]: "", - [arbitrumSepolia.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", - [sepolia.id]: "0xd576a839576a9f86eeef068206f3ead562cf0fac", + [arbitrumSepolia.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", + [sepolia.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", [mainnet.id]: "", }; diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index eb941db..448410d 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -5,7 +5,7 @@ import { createModularAccountV2Client, ModularAccountV2Client, } from "@account-kit/smart-contracts"; -import { createPublicClient, decodeEventLog, http } from "viem"; +import { createPublicClient, decodeEventLog, http, zeroAddress } from "viem"; import { AcpContractConfig, baseAcpConfigV2 } from "../configs/acpConfigs"; import AcpError from "../acpError"; import BaseAcpContractClient, { @@ -133,10 +133,8 @@ class AcpContractClientV2 extends BaseAcpContractClient { for (const chain of this.config.chains) { this._sessionKeyClients[chain.chain.id] = await createModularAccountV2Client({ - chain: defineAlchemyChain({ - chain: chain.chain, - rpcBaseUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, - }), + chain: chain.chain, + transport: alchemy({ rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, }), diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 255b03c..ddfc24a 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -245,6 +245,54 @@ abstract class BaseAcpContractClient { } } + createCrossChainPayableMemo( + jobId: number, + content: string, + token: Address, + amountBaseUnit: bigint, + recipient: Address, + feeAmountBaseUnit: bigint, + feeType: FeeType, + type: + | MemoType.PAYABLE_REQUEST + | MemoType.PAYABLE_TRANSFER + | MemoType.PAYABLE_NOTIFICATION, + expiredAt: Date, + nextPhase: AcpJobPhases, + destinationEid: number, + secured: boolean = true + ): OperationPayload { + try { + const data = encodeFunctionData({ + abi: this.abi, + functionName: "createCrossChainPayableMemo", + args: [ + jobId, + content, + token, + amountBaseUnit, + recipient, + feeAmountBaseUnit, + feeType, + type, + expiredAt, + secured, + nextPhase, + destinationEid, + ], + }); + + const payload: OperationPayload = { + data: data, + contractAddress: this.contractAddress, + }; + + return payload; + } catch (error) { + throw new AcpError("Failed to create cross chain payable memo", error); + } + } + createMemo( jobId: number, content: string, diff --git a/src/utils.ts b/src/utils.ts index 6083d32..05900b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Address, encodeAbiParameters } from "viem"; +import { Address, decodeAbiParameters, encodeAbiParameters } from "viem"; import { arbitrum, arbitrumSepolia, @@ -67,7 +67,7 @@ export function encodeTransferEventMetadata( recipient: Address, chainId: number ): string { - return encodeAbiParameters( + const result = encodeAbiParameters( [ { type: "address", name: "token" }, { type: "uint256", name: "amount" }, @@ -77,4 +77,17 @@ export function encodeTransferEventMetadata( ], [tokenAddress, amount, recipient, getDestinationEndpointId(chainId), "0x"] ); + + const decoded = decodeAbiParameters( + [ + { type: "address", name: "token" }, + { type: "uint256", name: "amount" }, + { type: "address", name: "recipient" }, + { type: "uint32", name: "dstEid" }, + { type: "bytes", name: "lzOptions" }, + ], + result + ); + + return result; } From adb6132a472f1b1fceae084e9ab5bb9defc54d76 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Tue, 13 Jan 2026 17:22:42 +0800 Subject: [PATCH 03/15] feat: create cross chain payable memo --- src/acpClient.ts | 25 ++++-- src/acpJob.ts | 93 ++++++++++++++------ src/contractClients/acpContractClientV2.ts | 1 - src/contractClients/baseAcpContractClient.ts | 35 ++++++++ src/interfaces.ts | 4 + 5 files changed, 123 insertions(+), 35 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index e59e059..22734cb 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -12,6 +12,7 @@ import { AcpAgent, AcpAgentSort, AcpGraduationStatus, + AcpMemoState, AcpOnlineStatus, IAcpAccount, IAcpClientOptions, @@ -170,7 +171,8 @@ class AcpClient { memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), data.phase, @@ -211,7 +213,8 @@ class AcpClient { memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), data.phase, @@ -456,7 +459,8 @@ class AcpClient { memo.senderAddress, memo.signedReason, memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), job.phase, @@ -508,7 +512,8 @@ class AcpClient { memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, typeof memo.payableDetails === "string" ? tryParseJson(memo.payableDetails) || undefined - : memo.payableDetails + : memo.payableDetails, + memo.state ); }), job.phase, @@ -558,7 +563,8 @@ class AcpClient { memo.senderAddress, memo.signedReason, memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), job.phase, @@ -607,7 +613,8 @@ class AcpClient { memo.senderAddress, memo.signedReason, memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), job.phase, @@ -661,7 +668,8 @@ class AcpClient { memo.senderAddress, memo.signedReason, memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); }), job.phase, @@ -705,7 +713,8 @@ class AcpClient { memo.senderAddress, memo.signedReason, memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, - memo.payableDetails + memo.payableDetails, + memo.state ); } catch (error) { throw new AcpError("Failed to get memo by id", error); diff --git a/src/acpJob.ts b/src/acpJob.ts index cea832e..f569949 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -1,4 +1,4 @@ -import { Address } from "viem"; +import { Address, formatUnits } from "viem"; import AcpClient from "./acpClient"; import { AcpJobPhases, @@ -166,24 +166,49 @@ class AcpJob { const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); - operations.push( - this.acpContractClient.createPayableMemo( - this.id, - content, - amount.amount, - recipient, - this.priceType === PriceType.PERCENTAGE - ? BigInt(this.priceValue * 10000) // convert to basis points - : feeAmount.amount, - this.priceType === PriceType.PERCENTAGE - ? FeeType.PERCENTAGE_FEE - : FeeType.NO_FEE, - AcpJobPhases.TRANSACTION, - type, - expiredAt, - amount.fare.contractAddress - ) - ); + if ( + amount.fare.chainId && + amount.fare.chainId !== this.acpContractClient.config.chain.id + ) { + operations.push( + this.acpContractClient.createCrossChainPayableMemo( + this.id, + content, + amount.fare.contractAddress, + amount.amount, + recipient, + this.priceType === PriceType.PERCENTAGE + ? BigInt(this.priceValue * 10000) // convert to basis points + : feeAmount.amount, + this.priceType === PriceType.PERCENTAGE + ? FeeType.PERCENTAGE_FEE + : FeeType.NO_FEE, + type as MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER, + expiredAt, + AcpJobPhases.TRANSACTION, + getDestinationEndpointId(amount.fare.chainId as number) + ) + ); + } else { + operations.push( + this.acpContractClient.createPayableMemo( + this.id, + content, + amount.amount, + recipient, + this.priceType === PriceType.PERCENTAGE + ? BigInt(this.priceValue * 10000) // convert to basis points + : feeAmount.amount, + this.priceType === PriceType.PERCENTAGE + ? FeeType.PERCENTAGE_FEE + : FeeType.NO_FEE, + AcpJobPhases.TRANSACTION, + type, + expiredAt, + amount.fare.contractAddress + ) + ); + } return await this.acpContractClient.handleOperation(operations); } @@ -558,25 +583,41 @@ class AcpJob { throw new AcpError("Insufficient token balance for cross chain payable"); } + const currentAllowance = await this.acpContractClient.getERC20Allowance( + chainId, + amount.fare.contractAddress, + this.acpContractClient.agentWalletAddress, + ASSET_MANAGER_ADDRESSES[ + chainId as unknown as keyof typeof ASSET_MANAGER_ADDRESSES + ] as Address + ); + // Approve allowance to asset manager on destination chain const approveAllowanceOperation = this.acpContractClient.approveAllowance( - amount.amount, + amount.amount + currentAllowance, amount.fare.contractAddress, ASSET_MANAGER_ADDRESSES[ chainId as unknown as keyof typeof ASSET_MANAGER_ADDRESSES ] as Address ); - const { userOpHash, txnHash } = - await this.acpContractClient.handleOperation( - [approveAllowanceOperation], - chainId - ); + await this.acpContractClient.handleOperation( + [approveAllowanceOperation], + chainId + ); + + const tokenSymbol = await this.acpContractClient.getERC20Symbol( + chainId, + amount.fare.contractAddress + ); const createMemoOperation = this.acpContractClient.createCrossChainPayableMemo( this.id, - "test", + `Performing cross chain payable transfer of ${formatUnits( + amount.amount, + amount.fare.decimals + )} ${tokenSymbol} to ${recipient}`, amount.fare.contractAddress, amount.amount, recipient, diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index 448410d..e8a7ce2 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -134,7 +134,6 @@ class AcpContractClientV2 extends BaseAcpContractClient { this._sessionKeyClients[chain.chain.id] = await createModularAccountV2Client({ chain: chain.chain, - transport: alchemy({ rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, }), diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index ddfc24a..c03dfe7 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -499,6 +499,41 @@ abstract class BaseAcpContractClient { }); } + async getERC20Allowance( + chainId: number, + tokenAddress: Address, + walletAddress: Address, + spenderAddress: Address + ): Promise { + const publicClient = this.publicClients[chainId]; + if (!publicClient) { + throw new AcpError(`Public client for chainId ${chainId} not found`); + } + + return await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "allowance", + args: [walletAddress, spenderAddress], + }); + } + + async getERC20Symbol( + chainId: number, + tokenAddress: Address + ): Promise { + const publicClient = this.publicClients[chainId]; + if (!publicClient) { + throw new AcpError(`Public client for chainId ${chainId} not found`); + } + + return await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "symbol", + }); + } + abstract getAcpVersion(): string; } diff --git a/src/interfaces.ts b/src/interfaces.ts index d987890..23df016 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -22,6 +22,7 @@ export enum AcpMemoState { NONE = "NONE", PENDING = "PENDING", IN_PROGRESS = "IN_PROGRESS", + READY = "READY", COMPLETED = "COMPLETED", REJECTED = "REJECTED", } @@ -31,6 +32,8 @@ export interface PayableDetails { token: Address; recipient: Address; feeAmount: bigint; + lzSrcEid?: number; + lzDstEid?: number; } export interface IAcpMemoData { @@ -46,6 +49,7 @@ export interface IAcpMemoData { expiry?: string; payableDetails?: PayableDetails; contractAddress?: Address; + state?: AcpMemoState; } export interface IAcpMemo { data: IAcpMemoData; From b649eb5e6972e47adb5343bd9290b77f465d4f09 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Tue, 13 Jan 2026 18:03:45 +0800 Subject: [PATCH 04/15] feat: export AcpMemoState --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index de474fa..f0d02f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { AcpMemoStatus, ResponseSwapTokenPayload, SwapTokenPayload, + AcpMemoState, } from "./interfaces"; import { AcpContractConfig, @@ -31,7 +32,7 @@ import { baseAcpX402Config, baseAcpX402ConfigV2, baseSepoliaAcpConfig, - baseSepoliaAcpConfigV2 + baseSepoliaAcpConfigV2, } from "./configs/acpConfigs"; import { ethFare, Fare, FareAmount, FareBigInt, wethFare } from "./acpFare"; import AcpError from "./acpError"; @@ -75,4 +76,5 @@ export { ClosePositionPayload, RequestClosePositionPayload, AcpMemoStatus, + AcpMemoState, }; From ee849b986f2b98190fc29a7bcd15f8eb0ceed63a Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 14 Jan 2026 11:34:27 +0800 Subject: [PATCH 05/15] fix: populate memo state --- src/acpClient.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index ec6e402..807682d 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -610,7 +610,8 @@ class AcpClient { memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, memo.payableDetails, memo.txHash, - memo.signedTxHash + memo.signedTxHash, + memo.state ) ); @@ -679,7 +680,8 @@ class AcpClient { memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined, memo.payableDetails, memo.txHash, - memo.signedTxHash + memo.signedTxHash, + memo.state ); } catch (err) { throw new AcpError( From 009f5caa4c6eb1f89f5d04f75247f818e69b7f4e Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 14 Jan 2026 16:13:27 +0800 Subject: [PATCH 06/15] feat: dynamically get asset manager --- src/abis/memoManagerAbi.ts | 1130 ++++++++++++++++++ src/acpJob.ts | 31 +- src/constants.ts | 11 - src/contractClients/acpContractClient.ts | 39 +- src/contractClients/acpContractClientV2.ts | 9 + src/contractClients/baseAcpContractClient.ts | 3 +- src/index.ts | 2 + src/interfaces.ts | 12 +- 8 files changed, 1191 insertions(+), 46 deletions(-) create mode 100644 src/abis/memoManagerAbi.ts diff --git a/src/abis/memoManagerAbi.ts b/src/abis/memoManagerAbi.ts new file mode 100644 index 0000000..ab1e23f --- /dev/null +++ b/src/abis/memoManagerAbi.ts @@ -0,0 +1,1130 @@ +const MEMO_MANAGER_ABI = [ + { inputs: [], stateMutability: "nonpayable", type: "constructor" }, + { inputs: [], name: "AccessControlBadConfirmation", type: "error" }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "bytes32", name: "neededRole", type: "bytes32" }, + ], + name: "AccessControlUnauthorizedAccount", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "target", type: "address" }], + name: "AddressEmptyCode", + type: "error", + }, + { inputs: [], name: "AlreadyVoted", type: "error" }, + { inputs: [], name: "CannotApproveMemo", type: "error" }, + { inputs: [], name: "CannotUpdateApprovedMemo", type: "error" }, + { inputs: [], name: "CannotUpdateMemo", type: "error" }, + { inputs: [], name: "CannotWithdrawYet", type: "error" }, + { inputs: [], name: "DestinationChainNotConfigured", type: "error" }, + { + inputs: [ + { internalType: "address", name: "implementation", type: "address" }, + ], + name: "ERC1967InvalidImplementation", + type: "error", + }, + { inputs: [], name: "ERC1967NonPayable", type: "error" }, + { inputs: [], name: "EmptyContent", type: "error" }, + { inputs: [], name: "FailedInnerCall", type: "error" }, + { inputs: [], name: "InvalidInitialization", type: "error" }, + { inputs: [], name: "InvalidMemoState", type: "error" }, + { inputs: [], name: "InvalidMemoStateTransition", type: "error" }, + { inputs: [], name: "InvalidMemoType", type: "error" }, + { inputs: [], name: "JobAlreadyCompleted", type: "error" }, + { inputs: [], name: "JobDoesNotExist", type: "error" }, + { inputs: [], name: "MemoAlreadyApproved", type: "error" }, + { inputs: [], name: "MemoAlreadyExecuted", type: "error" }, + { inputs: [], name: "MemoAlreadySigned", type: "error" }, + { inputs: [], name: "MemoCannotBeSigned", type: "error" }, + { inputs: [], name: "MemoDoesNotExist", type: "error" }, + { inputs: [], name: "MemoDoesNotRequireApproval", type: "error" }, + { inputs: [], name: "MemoExpired", type: "error" }, + { inputs: [], name: "MemoNotApproved", type: "error" }, + { inputs: [], name: "MemoNotReadyToBeSigned", type: "error" }, + { inputs: [], name: "MemoStateUnchanged", type: "error" }, + { inputs: [], name: "NoAmountToTransfer", type: "error" }, + { inputs: [], name: "NoPaymentAmount", type: "error" }, + { inputs: [], name: "NotEscrowTransferMemoType", type: "error" }, + { inputs: [], name: "NotInitializing", type: "error" }, + { inputs: [], name: "NotPayableMemoType", type: "error" }, + { inputs: [], name: "OnlyACPContract", type: "error" }, + { inputs: [], name: "OnlyAssetManager", type: "error" }, + { inputs: [], name: "OnlyClientOrProvider", type: "error" }, + { inputs: [], name: "OnlyCounterParty", type: "error" }, + { inputs: [], name: "OnlyEvaluator", type: "error" }, + { inputs: [], name: "OnlyMemoSender", type: "error" }, + { inputs: [], name: "ReentrancyGuardReentrantCall", type: "error" }, + { inputs: [], name: "UUPSUnauthorizedCallContext", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "slot", type: "bytes32" }], + name: "UUPSUnsupportedProxiableUUID", + type: "error", + }, + { inputs: [], name: "ZeroAcpContractAddress", type: "error" }, + { inputs: [], name: "ZeroAddressRecipient", type: "error" }, + { inputs: [], name: "ZeroAddressToken", type: "error" }, + { inputs: [], name: "ZeroAssetManagerAddress", type: "error" }, + { inputs: [], name: "ZeroJobManagerAddress", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "version", + type: "uint64", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "approver", + type: "address", + }, + { indexed: false, internalType: "bool", name: "approved", type: "bool" }, + { + indexed: false, + internalType: "string", + name: "reason", + type: "string", + }, + ], + name: "MemoSigned", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: false, + internalType: "enum ACPTypes.MemoState", + name: "oldState", + type: "uint8", + }, + { + indexed: false, + internalType: "enum ACPTypes.MemoState", + name: "newState", + type: "uint8", + }, + ], + name: "MemoStateUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { + indexed: false, + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { + indexed: false, + internalType: "string", + name: "content", + type: "string", + }, + ], + name: "NewMemo", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "PayableFeeRefunded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "PayableFundsRefunded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "memoId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "jobId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "executor", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "PayableMemoExecuted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + inputs: [], + name: "ACP_CONTRACT_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPGRADE_INTERFACE_VERSION", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acpContract", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "approveMemo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "assetManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256[]", name: "memoIds", type: "uint256[]" }, + { internalType: "bool", name: "approved", type: "bool" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "bulkApproveMemos", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "address", name: "user", type: "address" }, + ], + name: "canApproveMemo", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "string", name: "metadata", type: "string" }, + ], + name: "createMemo", + outputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { + internalType: "enum ACPTypes.FeeType", + name: "feeType", + type: "uint8", + }, + { internalType: "bool", name: "isExecuted", type: "bool" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint32", name: "lzSrcEid", type: "uint32" }, + { internalType: "uint32", name: "lzDstEid", type: "uint32" }, + ], + internalType: "struct ACPTypes.PayableDetails", + name: "payableDetails_", + type: "tuple", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + ], + name: "createPayableMemo", + outputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "emergencyApproveMemo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "executePayableMemo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getAssetManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getJobMemos", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, + ], + internalType: "struct ACPTypes.Memo[]", + name: "memoArray", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "enum ACPTypes.JobPhase", name: "phase", type: "uint8" }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getJobMemosByPhase", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, + ], + internalType: "struct ACPTypes.Memo[]", + name: "memoArray", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "offset", type: "uint256" }, + { internalType: "uint256", name: "limit", type: "uint256" }, + ], + name: "getJobMemosByType", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, + ], + internalType: "struct ACPTypes.Memo[]", + name: "memoArray", + type: "tuple[]", + }, + { internalType: "uint256", name: "total", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLocalEid", + outputs: [{ internalType: "uint32", name: "", type: "uint32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "getMemo", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, + ], + internalType: "struct ACPTypes.Memo", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "getMemoApprovalStatus", + outputs: [ + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "getMemoWithPayableDetails", + outputs: [ + { + components: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "state", + type: "uint8", + }, + ], + internalType: "struct ACPTypes.Memo", + name: "memo", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { + internalType: "enum ACPTypes.FeeType", + name: "feeType", + type: "uint8", + }, + { internalType: "bool", name: "isExecuted", type: "bool" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint32", name: "lzSrcEid", type: "uint32" }, + { internalType: "uint32", name: "lzDstEid", type: "uint32" }, + ], + internalType: "struct ACPTypes.PayableDetails", + name: "details", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "getPayableDetails", + outputs: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { + internalType: "enum ACPTypes.FeeType", + name: "feeType", + type: "uint8", + }, + { internalType: "bool", name: "isExecuted", type: "bool" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint32", name: "lzSrcEid", type: "uint32" }, + { internalType: "uint32", name: "lzDstEid", type: "uint32" }, + ], + internalType: "struct ACPTypes.PayableDetails", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], + name: "getRoleAdmin", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "hasRole", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "acpContract_", type: "address" }, + { internalType: "address", name: "jobManager_", type: "address" }, + { internalType: "address", name: "paymentManager_", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "user", type: "address" }, + ], + name: "isJobEvaluator", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "address", name: "user", type: "address" }, + ], + name: "isMemoSigner", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "isPayable", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "jobManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "jobMemos", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "enum ACPTypes.JobPhase", name: "", type: "uint8" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "jobMemosByPhase", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "enum ACPTypes.MemoType", name: "", type: "uint8" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "jobMemosByType", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "address", name: "", type: "address" }, + ], + name: "memoApprovals", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "memoCounter", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "", type: "uint256" }], + name: "memos", + outputs: [ + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "jobId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "string", name: "content", type: "string" }, + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "createdAt", type: "uint256" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "address", name: "approvedBy", type: "address" }, + { internalType: "uint256", name: "approvedAt", type: "uint256" }, + { internalType: "bool", name: "requiresApproval", type: "bool" }, + { internalType: "string", name: "metadata", type: "string" }, + { internalType: "bool", name: "isSecured", type: "bool" }, + { + internalType: "enum ACPTypes.JobPhase", + name: "nextPhase", + type: "uint8", + }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "enum ACPTypes.MemoState", name: "state", type: "uint8" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "", type: "uint256" }], + name: "payableDetails", + outputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "enum ACPTypes.FeeType", name: "feeType", type: "uint8" }, + { internalType: "bool", name: "isExecuted", type: "bool" }, + { internalType: "uint256", name: "expiredAt", type: "uint256" }, + { internalType: "uint32", name: "lzSrcEid", type: "uint32" }, + { internalType: "uint32", name: "lzDstEid", type: "uint32" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paymentManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "callerConfirmation", type: "address" }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "", type: "uint256" }], + name: "requiredApprovals", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "requiresApproval", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "role", type: "bytes32" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "enum ACPTypes.MemoType", + name: "memoType", + type: "uint8", + }, + { internalType: "uint256", name: "requiredApprovals_", type: "uint256" }, + ], + name: "setApprovalRequirements", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "assetManager_", type: "address" }, + ], + name: "setAssetManager", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "bool", name: "isApproved", type: "bool" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "signMemo", + outputs: [{ internalType: "uint256", name: "jobId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "acpContract_", type: "address" }, + { internalType: "address", name: "jobManager_", type: "address" }, + { internalType: "address", name: "paymentManager_", type: "address" }, + { internalType: "address", name: "assetManager_", type: "address" }, + ], + name: "updateContracts", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { internalType: "string", name: "newContent", type: "string" }, + ], + name: "updateMemoContent", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "memoId", type: "uint256" }, + { + internalType: "enum ACPTypes.MemoState", + name: "newMemoState", + type: "uint8", + }, + ], + name: "updateMemoState", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newImplementation", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "memoId", type: "uint256" }], + name: "withdrawEscrowedFunds", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; + +export default MEMO_MANAGER_ABI; diff --git a/src/acpJob.ts b/src/acpJob.ts index 2dd89a7..b7f1c89 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -17,7 +17,6 @@ import { import { FareAmount, FareAmountBase } from "./acpFare"; import AcpError from "./acpError"; import { PriceType } from "./acpJobOffering"; -import { ASSET_MANAGER_ADDRESSES } from "./constants"; import * as util from "util"; class AcpJob { @@ -149,7 +148,10 @@ class AcpJob { async createPayableRequirement( content: string, - type: MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER_ESCROW, + type: + | MemoType.PAYABLE_REQUEST + | MemoType.PAYABLE_TRANSFER_ESCROW + | MemoType.PAYABLE_TRANSFER, amount: FareAmountBase, recipient: Address, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes @@ -184,7 +186,7 @@ class AcpJob { ? BigInt(Math.round(this.priceValue * 10000)) // convert to basis points : feeAmount.amount, isPercentagePricing ? FeeType.PERCENTAGE_FEE : FeeType.NO_FEE, - type as MemoType.PAYABLE_REQUEST | MemoType.PAYABLE_TRANSFER, + type as MemoType.PAYABLE_REQUEST, expiredAt, AcpJobPhases.TRANSACTION, getDestinationEndpointId(amount.fare.chainId as number) @@ -214,7 +216,9 @@ class AcpJob { async payAndAcceptRequirement(reason?: string) { const memo = this.memos.find( - (m) => m.nextPhase === AcpJobPhases.TRANSACTION + (m) => + m.nextPhase === AcpJobPhases.TRANSACTION || + m.nextPhase === AcpJobPhases.COMPLETED ); if (!memo) { @@ -573,9 +577,10 @@ class AcpJob { } } - private async deliverCrossChainPayable( + async deliverCrossChainPayable( recipient: Address, - amount: FareAmountBase + amount: FareAmountBase, + isRequest: boolean = false ) { if (!amount.fare.chainId) { throw new AcpError("Chain ID is required for cross chain payable"); @@ -583,6 +588,8 @@ class AcpJob { const chainId = amount.fare.chainId; + const assetManagerAddress = await this.acpContractClient.getAssetManager(); + // Check if wallet has enough balance on destination chain const tokenBalance = await this.acpContractClient.getERC20Balance( chainId, @@ -598,18 +605,14 @@ class AcpJob { chainId, amount.fare.contractAddress, this.acpContractClient.agentWalletAddress, - ASSET_MANAGER_ADDRESSES[ - chainId as unknown as keyof typeof ASSET_MANAGER_ADDRESSES - ] as Address + assetManagerAddress ); // Approve allowance to asset manager on destination chain const approveAllowanceOperation = this.acpContractClient.approveAllowance( amount.amount + currentAllowance, amount.fare.contractAddress, - ASSET_MANAGER_ADDRESSES[ - chainId as unknown as keyof typeof ASSET_MANAGER_ADDRESSES - ] as Address + assetManagerAddress ); await this.acpContractClient.handleOperation( @@ -634,9 +637,9 @@ class AcpJob { recipient, BigInt(0), FeeType.NO_FEE, - MemoType.PAYABLE_TRANSFER, + isRequest ? MemoType.PAYABLE_REQUEST : MemoType.PAYABLE_TRANSFER, new Date(Date.now() + 1000 * 60 * 5), - AcpJobPhases.COMPLETED, + isRequest ? AcpJobPhases.TRANSACTION : AcpJobPhases.COMPLETED, getDestinationEndpointId(chainId) ); diff --git a/src/constants.ts b/src/constants.ts index 293681a..48d6cf7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,14 +33,3 @@ export const HTTP_STATUS_CODES = { export const SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS: Address = "0x00000000000099DE0BF6fA90dEB851E2A2df7d83"; - -export const ASSET_MANAGER_ADDRESSES = { - [bscTestnet.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", - [bsc.id]: "", - [polygonAmoy.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", - [polygon.id]: "", - [arbitrum.id]: "", - [arbitrumSepolia.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", - [sepolia.id]: "0xfCf52B02936623852dd5132007E9414f9060168b", - [mainnet.id]: "", -}; diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index 90806ed..22cc6c5 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -32,7 +32,7 @@ class AcpContractClient extends BaseAcpContractClient { constructor( agentWalletAddress: Address, - config: AcpContractConfig = baseAcpConfig, + config: AcpContractConfig = baseAcpConfig ) { super(agentWalletAddress, config); } @@ -41,12 +41,9 @@ class AcpContractClient extends BaseAcpContractClient { walletPrivateKey: Address, sessionEntityKeyId: number, agentWalletAddress: Address, - config: AcpContractConfig = baseAcpConfig, + config: AcpContractConfig = baseAcpConfig ) { - const acpContractClient = new AcpContractClient( - agentWalletAddress, - config, - ); + const acpContractClient = new AcpContractClient(agentWalletAddress, config); await acpContractClient.init(walletPrivateKey, sessionEntityKeyId); return acpContractClient; } @@ -76,15 +73,20 @@ class AcpContractClient extends BaseAcpContractClient { ); const account = this.sessionKeyClient.account; - const sessionSignerAddress: Address = await account.getSigner().getAddress(); + const sessionSignerAddress: Address = await account + .getSigner() + .getAddress(); - if (!await account.isAccountDeployed()) { + if (!(await account.isAccountDeployed())) { throw new AcpError( `ACP Contract Client validation failed: agent account ${this.agentWalletAddress} is not deployed on-chain` ); } - await this.validateSessionKeyOnChain(sessionSignerAddress, sessionEntityKeyId); + await this.validateSessionKeyOnChain( + sessionSignerAddress, + sessionEntityKeyId + ); console.log("Connected to ACP with v1 Contract Client (Legacy):", { agentWalletAddress: this.agentWalletAddress, @@ -129,7 +131,9 @@ class AcpContractClient extends BaseAcpContractClient { return finalMaxFeePerGas; } - async handleOperation(operations: OperationPayload[]): Promise<{ userOpHash: Address , txnHash: Address }> { + async handleOperation( + operations: OperationPayload[] + ): Promise<{ userOpHash: Address; txnHash: Address }> { const payload: any = { uo: operations.map((op) => ({ target: op.contractAddress, @@ -156,9 +160,10 @@ class AcpContractClient extends BaseAcpContractClient { const { hash } = await this.sessionKeyClient.sendUserOperation(payload); - const txnHash = await this.sessionKeyClient.waitForUserOperationTransaction({ - hash, - }); + const txnHash = + await this.sessionKeyClient.waitForUserOperationTransaction({ + hash, + }); return { userOpHash: hash, txnHash }; } catch (error) { @@ -180,7 +185,9 @@ class AcpContractClient extends BaseAcpContractClient { clientAddress: Address, providerAddress: Address ) { - const result = await this.sessionKeyClient.getUserOperationReceipt(createJobUserOpHash); + const result = await this.sessionKeyClient.getUserOperationReceipt( + createJobUserOpHash + ); if (!result) { throw new AcpError("Failed to get user operation receipt"); @@ -347,6 +354,10 @@ class AcpContractClient extends BaseAcpContractClient { return await this.acpX402.performRequest(url, version, budget, signature); } + async getAssetManager(): Promise
{ + throw new Error("Asset Manager not supported"); + } + getAcpVersion(): string { return "1"; } diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index f17dd70..52aa9a2 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -23,6 +23,7 @@ import { } from "../interfaces"; import { AcpX402 } from "../acpX402"; import { base, baseSepolia } from "viem/chains"; +import MEMO_MANAGER_ABI from "../abis/memoManagerAbi"; class AcpContractClientV2 extends BaseAcpContractClient { private PRIORITY_FEE_MULTIPLIER = 2; @@ -379,6 +380,14 @@ class AcpContractClientV2 extends BaseAcpContractClient { } } + async getAssetManager(): Promise
{ + return (await this.publicClient.readContract({ + address: this.memoManagerAddress, + abi: MEMO_MANAGER_ABI, + functionName: "assetManager", + })) as Address; + } + getAcpVersion(): string { return "2"; } diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index c8d34ba..fdd2578 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -41,7 +41,6 @@ export enum MemoType { PAYABLE_TRANSFER_ESCROW, // 8 - Escrowed payment transfer NOTIFICATION, // 9 - Notification PAYABLE_NOTIFICATION, // 10 - Payable notification - TRANSFER_EVENT, } export enum AcpJobPhases { @@ -581,6 +580,8 @@ abstract class BaseAcpContractClient { }); } + abstract getAssetManager(): Promise
; + abstract getAcpVersion(): string; } diff --git a/src/index.ts b/src/index.ts index f0d02f7..de2bc69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import { baseAcpX402ConfigV2, baseSepoliaAcpConfig, baseSepoliaAcpConfigV2, + baseSepoliaAcpX402ConfigV2, } from "./configs/acpConfigs"; import { ethFare, Fare, FareAmount, FareBigInt, wethFare } from "./acpFare"; import AcpError from "./acpError"; @@ -55,6 +56,7 @@ export { ethFare, baseSepoliaAcpConfig, baseSepoliaAcpConfigV2, + baseSepoliaAcpX402ConfigV2, baseAcpConfig, baseAcpConfigV2, baseAcpX402Config, diff --git a/src/interfaces.ts b/src/interfaces.ts index 7497e5e..6c439ef 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -19,12 +19,12 @@ export enum AcpMemoStatus { } export enum AcpMemoState { - NONE = "NONE", - PENDING = "PENDING", - IN_PROGRESS = "IN_PROGRESS", - READY = "READY", - COMPLETED = "COMPLETED", - REJECTED = "REJECTED", + NONE, + PENDING, + IN_PROGRESS, + READY, + COMPLETED, + REJECTED, } export interface PayableDetails { From ad8b1f1a76982e5d6c3e4d2f949b8ee0c3842663 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 21 Jan 2026 17:50:16 +0800 Subject: [PATCH 07/15] feat: custom retry config, maxFeePerGas & maxPriorityFeePerGas multiplier --- src/configs/acpConfigs.ts | 5 +++ src/contractClients/acpContractClient.ts | 46 +++++++++++++------- src/contractClients/acpContractClientV2.ts | 50 +++++++++++++--------- 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index a8e8a3c..d349cef 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -41,6 +41,11 @@ class AcpContractConfig { public maxRetries: number, public rpcEndpoint?: string, public x402Config?: X402Config, + public retryConfig?: { + intervalMs: number; + multiplier: number; + maxRetries: number; + }, public chains: ChainConfig[] = [] ) {} } diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index 22cc6c5..197350a 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -26,6 +26,11 @@ class AcpContractClient extends BaseAcpContractClient { protected PRIORITY_FEE_MULTIPLIER = 2; protected MAX_FEE_PER_GAS = 20000000; protected MAX_PRIORITY_FEE_PER_GAS = 21000000; + private RETRY_CONFIG = { + intervalMs: 200, + multiplier: 1.1, + maxRetries: 10, + }; private _sessionKeyClient: ModularAccountV2Client | undefined; private _acpX402: AcpX402 | undefined; @@ -88,6 +93,8 @@ class AcpContractClient extends BaseAcpContractClient { sessionEntityKeyId ); + this.RETRY_CONFIG = this.config.retryConfig || this.RETRY_CONFIG; + console.log("Connected to ACP with v1 Contract Client (Legacy):", { agentWalletAddress: this.agentWalletAddress, whitelistedWalletAddress: sessionSignerAddress, @@ -134,46 +141,53 @@ class AcpContractClient extends BaseAcpContractClient { async handleOperation( operations: OperationPayload[] ): Promise<{ userOpHash: Address; txnHash: Address }> { - const payload: any = { + const basePayload: any = { uo: operations.map((op) => ({ target: op.contractAddress, data: op.data, value: op.value, })), - overrides: { - nonceKey: this.getRandomNonce(), - }, }; - let retries = this.config.maxRetries; + let iteration = 0; let finalError: unknown; - while (retries > 0) { + while (iteration < this.config.maxRetries) { try { - if (this.config.maxRetries > retries) { - const gasFees = await this.calculateGasFees(); - - payload["overrides"] = { - maxFeePerGas: `0x${gasFees.toString(16)}`, - }; - } + const currentMultiplier = 1 + 0.1 * (iteration + 1); + + const payload: any = { + ...basePayload, + overrides: { + nonceKey: this.getRandomNonce(), + maxFeePerGas: { + multiplier: currentMultiplier, + }, + maxPriorityFeePerGas: { + multiplier: currentMultiplier, + }, + }, + }; const { hash } = await this.sessionKeyClient.sendUserOperation(payload); const txnHash = await this.sessionKeyClient.waitForUserOperationTransaction({ hash, + tag: "pending", + retries: this.RETRY_CONFIG, }); return { userOpHash: hash, txnHash }; } catch (error) { - retries -= 1; - if (retries === 0) { + iteration++; + + if (iteration === this.config.maxRetries) { finalError = error; break; } - await new Promise((resolve) => setTimeout(resolve, 2000 * retries)); + await new Promise((resolve) => setTimeout(resolve, 2000 * iteration)); } } diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index 52aa9a2..a258656 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -30,6 +30,11 @@ class AcpContractClientV2 extends BaseAcpContractClient { private MAX_FEE_PER_GAS = 20000000; private MAX_PRIORITY_FEE_PER_GAS = 21000000; private GAS_FEE_MULTIPLIER = 0.5; + private RETRY_CONFIG = { + intervalMs: 200, + multiplier: 1.1, + maxRetries: 10, + }; private _sessionKeyClient: ModularAccountV2Client | undefined; private _sessionKeyClients: Record = {}; @@ -167,6 +172,8 @@ class AcpContractClientV2 extends BaseAcpContractClient { sessionEntityKeyId ); + this.RETRY_CONFIG = this.config.retryConfig || this.RETRY_CONFIG; + console.log("Connected to ACP:", { agentWalletAddress: this.agentWalletAddress, whitelistedWalletAddress: sessionSignerAddress, @@ -235,39 +242,39 @@ class AcpContractClientV2 extends BaseAcpContractClient { throw new AcpError("Session key client not initialized"); } - const payload: any = { + const basePayload: any = { uo: operations.map((operation) => ({ target: operation.contractAddress, data: operation.data, value: operation.value, })), - overrides: { - nonceKey: this.getRandomNonce(), - }, }; - let retries = this.config.maxRetries; + let iteration = 0; let finalError: unknown; - while (retries > 0) { + while (iteration < this.config.maxRetries) { try { - if (this.config.maxRetries > retries) { - const gasFees = await this.calculateGasFees(); - - payload["overrides"] = { - maxFeePerGas: `0x${gasFees.toString(16)}`, - }; - } + const currentMultiplier = 1 + 0.1 * (iteration + 1); + + const payload: any = { + ...basePayload, + overrides: { + nonceKey: this.getRandomNonce(), + maxFeePerGas: { + multiplier: currentMultiplier, + }, + maxPriorityFeePerGas: { + multiplier: currentMultiplier, + }, + }, + }; const { hash } = await sessionKeyClient.sendUserOperation(payload); const checkTransactionConfig: CheckTransactionConfig = { hash, - retries: { - intervalMs: 200, - multiplier: 1.1, - maxRetries: 10, - }, + retries: this.RETRY_CONFIG, }; // Only base / base sepolia supports preconfirmed transactions @@ -281,13 +288,14 @@ class AcpContractClientV2 extends BaseAcpContractClient { return { userOpHash: hash, txnHash }; } catch (error) { - retries -= 1; - if (retries === 0) { + iteration++; + + if (iteration === this.config.maxRetries) { finalError = error; break; } - await new Promise((resolve) => setTimeout(resolve, 2000 * retries)); + await new Promise((resolve) => setTimeout(resolve, 2000 * iteration)); } } From 089d30737f6b72acb4fe4f231257f8bd37e85e87 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Thu, 22 Jan 2026 10:33:34 +0800 Subject: [PATCH 08/15] feat: test case for maxFeePerGas multiplier during retries --- test/unit/acpContractClientV2.test.ts | 65 ++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/test/unit/acpContractClientV2.test.ts b/test/unit/acpContractClientV2.test.ts index da88901..e7d060c 100644 --- a/test/unit/acpContractClientV2.test.ts +++ b/test/unit/acpContractClientV2.test.ts @@ -18,7 +18,7 @@ describe("AcpContractClient V2 Unit Testing", () => { "0x2222222222222222222222222222222222222222" as Address, "0x3333333333333333333333333333333333333333" as Address, "0x4444444444444444444444444444444444444444" as Address, - baseAcpConfigV2, + baseAcpConfigV2 ); }); describe("Random Nonce Generation", () => { @@ -149,7 +149,7 @@ describe("AcpContractClient V2 Unit Testing", () => { // Start the operation and immediately set up the expectation const operationPromise = expect( - contractClient.handleOperation([mockOperation]), + contractClient.handleOperation([mockOperation]) ).rejects.toThrow(AcpError); await jest.runAllTimersAsync(); @@ -205,7 +205,7 @@ describe("AcpContractClient V2 Unit Testing", () => { }); }); - it("should able to invoke calculateGasFees during retries", async () => { + it("should able to increase maxFeePerGas multiplier during retries", async () => { jest.useFakeTimers(); const mockOperation: OperationPayload = { @@ -234,23 +234,48 @@ describe("AcpContractClient V2 Unit Testing", () => { waitForUserOperationTransaction: mockWaitForUserOperation, } as any; - // Spy on calculateGasFees to track if it's called - const calculateGasFeesSpy = jest.spyOn( - contractClient as any, - "calculateGasFees", - ); - const operationPromise = contractClient.handleOperation([mockOperation]); await jest.runAllTimersAsync(); await operationPromise; - expect(calculateGasFeesSpy).toHaveBeenCalledTimes(2); - expect(mockSendUserOperation).toHaveBeenCalledTimes(3); - calculateGasFeesSpy.mockRestore(); + // Verify multipliers increase with each iteration + // iteration 0: multiplier = 1 + 0.1 * (0 + 1) = 1.1 + // iteration 1: multiplier = 1 + 0.1 * (1 + 1) = 1.2 + // iteration 2: multiplier = 1 + 0.1 * (2 + 1) = 1.3 + expect(mockSendUserOperation).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + overrides: expect.objectContaining({ + maxFeePerGas: { multiplier: 1.1 }, + maxPriorityFeePerGas: { multiplier: 1.1 }, + }), + }) + ); + + expect(mockSendUserOperation).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + overrides: expect.objectContaining({ + maxFeePerGas: { multiplier: 1.2 }, + maxPriorityFeePerGas: { multiplier: 1.2 }, + }), + }) + ); + + expect(mockSendUserOperation).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + overrides: expect.objectContaining({ + maxFeePerGas: { multiplier: 1.3 }, + maxPriorityFeePerGas: { multiplier: 1.3 }, + }), + }) + ); + jest.useRealTimers(); }); }); @@ -273,14 +298,14 @@ describe("AcpContractClient V2 Unit Testing", () => { mockUrl, mockVersion, mockBudget, - mockSignature, + mockSignature ); expect(mockPerformRequest).toHaveBeenCalledWith( mockUrl, mockVersion, mockBudget, - mockSignature, + mockSignature ); expect(results).toBe(mockResponse); @@ -301,12 +326,12 @@ describe("AcpContractClient V2 Unit Testing", () => { const results = await contractClient.generateX402Payment( mockX402PayableRequest, - mockX402PayableRequirements, + mockX402PayableRequirements ); expect(mockGenerateX402Payment).toHaveBeenCalledWith( mockX402PayableRequest, - mockX402PayableRequirements, + mockX402PayableRequirements ); expect(results).toBe(mockResponse); @@ -351,12 +376,12 @@ describe("AcpContractClient V2 Unit Testing", () => { // Expect it to throw AcpError await expect( - contractClient.getX402PaymentDetails(mockJobId), + contractClient.getX402PaymentDetails(mockJobId) ).rejects.toThrow(AcpError); // Also verify the error message await expect( - contractClient.getX402PaymentDetails(mockJobId), + contractClient.getX402PaymentDetails(mockJobId) ).rejects.toThrow("Failed to get X402 payment details"); }); @@ -373,12 +398,12 @@ describe("AcpContractClient V2 Unit Testing", () => { const results = await contractClient.updateJobX402Nonce( mockJobIdNumber, - mockNonce, + mockNonce ); expect(mockUpdateJobNonce).toHaveBeenCalledWith( mockJobIdNumber, - mockNonce, + mockNonce ); expect(results).toBe(mockResponse); }); From 9af0df24a625d007bbd8d2858727bab18fee35b7 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Thu, 22 Jan 2026 19:17:48 +0800 Subject: [PATCH 09/15] fix: memo expiry in seconds unit --- src/contractClients/baseAcpContractClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index fdd2578..60a3ca0 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -321,7 +321,7 @@ abstract class BaseAcpContractClient { feeAmountBaseUnit, feeType, type, - expiredAt, + Math.floor(expiredAt.getTime() / 1000), secured, nextPhase, destinationEid, From d100a7b35c29a12396bd088267ba384bb27dc35d Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Fri, 23 Jan 2026 11:17:12 +0800 Subject: [PATCH 10/15] feat: remove evaluation memo checking for deliverables --- src/acpJob.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index b7f1c89..f3a95e6 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -366,10 +366,6 @@ class AcpJob { } async deliver(deliverable: DeliverablePayload) { - if (this.latestMemo?.nextPhase !== AcpJobPhases.EVALUATION) { - throw new AcpError("No transaction memo found"); - } - const operations: OperationPayload[] = []; operations.push( @@ -391,10 +387,6 @@ class AcpJob { skipFee: boolean = false, expiredAt: Date = new Date(Date.now() + 1000 * 60 * 5) // 5 minutes ) { - if (this.latestMemo?.nextPhase !== AcpJobPhases.EVALUATION) { - throw new AcpError("No transaction memo found"); - } - // If payable chain belongs to non ACP native chain, we route to transfer service if (amount.fare.chainId !== this.acpContractClient.config.chain.id) { return await this.deliverCrossChainPayable(this.clientAddress, amount); From a3eeaa587fcb49f93640bea263450b8f29bb69f1 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Fri, 23 Jan 2026 20:42:43 +0800 Subject: [PATCH 11/15] feat: cross chain transfer service example --- .../cross-chain-transfer-service/env.ts | 37 +++++++ .../cross-chain-transfer-service/seller.ts | 104 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 examples/acp-base/cross-chain-transfer-service/env.ts create mode 100644 examples/acp-base/cross-chain-transfer-service/seller.ts diff --git a/examples/acp-base/cross-chain-transfer-service/env.ts b/examples/acp-base/cross-chain-transfer-service/env.ts new file mode 100644 index 0000000..612821b --- /dev/null +++ b/examples/acp-base/cross-chain-transfer-service/env.ts @@ -0,0 +1,37 @@ +import dotenv from "dotenv"; +import { Address } from "viem"; + +dotenv.config({ path: __dirname + "/.env" }); + +function getEnvVar(key: string, required = true): T { + const value = process.env[key]; + if (required && (value === undefined || value === "")) { + throw new Error(`${key} is not defined or is empty in the .env file`); + } + return value as T; +} + +export const WHITELISTED_WALLET_PRIVATE_KEY = getEnvVar
( + "WHITELISTED_WALLET_PRIVATE_KEY" +); + +export const BUYER_AGENT_WALLET_ADDRESS = getEnvVar
( + "BUYER_AGENT_WALLET_ADDRESS" +); + +export const BUYER_ENTITY_ID = parseInt(getEnvVar("BUYER_ENTITY_ID")); + +export const SELLER_AGENT_WALLET_ADDRESS = getEnvVar
( + "SELLER_AGENT_WALLET_ADDRESS" +); + +export const SELLER_ENTITY_ID = parseInt(getEnvVar("SELLER_ENTITY_ID")); + +const entities = { + BUYER_ENTITY_ID, + SELLER_ENTITY_ID, +}; + +for (const [key, value] of Object.entries(entities)) { + if (isNaN(value)) throw new Error(`${key} must be a valid number`); +} diff --git a/examples/acp-base/cross-chain-transfer-service/seller.ts b/examples/acp-base/cross-chain-transfer-service/seller.ts new file mode 100644 index 0000000..604778b --- /dev/null +++ b/examples/acp-base/cross-chain-transfer-service/seller.ts @@ -0,0 +1,104 @@ +import { Address } from "viem"; +import AcpClient, { + AcpContractClientV2, + AcpJob, + AcpJobPhases, + AcpMemo, + Fare, + FareAmount, + MemoType, + baseSepoliaAcpX402ConfigV2, +} from "../../../src/index"; +import { + SELLER_AGENT_WALLET_ADDRESS, + SELLER_ENTITY_ID, + WHITELISTED_WALLET_PRIVATE_KEY, +} from "./env"; +import { bscTestnet } from "@account-kit/infra"; + +const REJECT_JOB = false; +const SOURCE_TOKEN_ADDRESS = "" as Address; +const TARGET_TOKEN_ADDRESS = "" as Address; +const TARGET_CHAIN = bscTestnet; + +async function seller() { + const config = { + ...baseSepoliaAcpX402ConfigV2, + chains: [ + { + chain: bscTestnet, + }, + ], + }; + + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + config + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.REQUEST && + memoToSign?.nextPhase === AcpJobPhases.NEGOTIATION + ) { + const response = true; + console.log( + `Responding to job ${job.id} with requirement`, + job.requirement + ); + if (response) { + await job.accept("Job requirement matches agent capability"); + + const swappedToken = new FareAmount( + 1, + await Fare.fromContractAddress( + SOURCE_TOKEN_ADDRESS, + config, + TARGET_CHAIN.id + ) + ); + + await job.createPayableRequirement( + "Requesting token from client on destination chain", + MemoType.PAYABLE_REQUEST, + swappedToken, + job.providerAddress + ); + } else { + await job.reject("Job requirement does not meet agent capability"); + } + console.log(`Job ${job.id} responded with ${response}`); + } else if (job.phase === AcpJobPhases.TRANSACTION) { + console.log("Delivering swapped token"); + + // to cater cases where agent decide to reject job after payment has been madep + if (REJECT_JOB) { + // conditional check for job rejection logic + const reason = "Job requirement does not meet agent capability"; + console.log(`Rejecting job ${job.id} with reason: ${reason}`); + await job.reject(reason); + console.log(`Job ${job.id} rejected`); + return; + } + + const swappedToken = new FareAmount( + 1, + await Fare.fromContractAddress( + TARGET_TOKEN_ADDRESS, + config, + TARGET_CHAIN.id + ) + ); + + await job.deliverPayable( + "Delivered swapped token on destination chain", + swappedToken + ); + } + }, + }); +} + +seller(); From 0ba1e76695e8461d1b4a59e04ad41d7a5a6b4c6b Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Mon, 26 Jan 2026 19:13:41 +0800 Subject: [PATCH 12/15] feat: cross chain transfer service buyer example --- .../cross-chain-transfer-service/buyer.ts | 102 ++++++++++++++++++ src/acpJob.ts | 84 +++++++++++++-- src/contractClients/baseAcpContractClient.ts | 16 +++ src/utils.ts | 27 +++++ 4 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 examples/acp-base/cross-chain-transfer-service/buyer.ts diff --git a/examples/acp-base/cross-chain-transfer-service/buyer.ts b/examples/acp-base/cross-chain-transfer-service/buyer.ts new file mode 100644 index 0000000..7716641 --- /dev/null +++ b/examples/acp-base/cross-chain-transfer-service/buyer.ts @@ -0,0 +1,102 @@ +import { bscTestnet } from "@account-kit/infra"; +import AcpClient, { + AcpContractClientV2, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus, + baseSepoliaAcpX402ConfigV2, + AcpMemoState, +} from "../../../src/index"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, +} from "./env"; +import { Address } from "viem"; + +async function buyer() { + const config = { + ...baseSepoliaAcpX402ConfigV2, + chains: [ + { + chain: bscTestnet, + }, + ], + }; + + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + config + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.NEGOTIATION && + (memoToSign?.nextPhase === AcpJobPhases.TRANSACTION || + memoToSign?.nextPhase === AcpJobPhases.COMPLETED) + ) { + console.log( + `Memo to sign ${memoToSign?.id} for job ${job.id} is in state ${memoToSign?.state}` + ); + if (memoToSign?.state === AcpMemoState.PENDING) { + console.log(`Paying for job ${job.id}`); + // Internally approves allowance on destination chain for cross chain payable memo + await job.payAndAcceptRequirement(); + console.log(`Job ${job.id} paid`); + } + } else if ( + job.phase === AcpJobPhases.TRANSACTION && + memoToSign?.nextPhase === AcpJobPhases.REJECTED + ) { + console.log( + `Signing job ${job.id} rejection memo, rejection reason: ${memoToSign?.content}` + ); + await memoToSign?.sign(true, "Accepts job rejection"); + console.log(`Job ${job.id} rejection memo signed`); + } else if (job.phase === AcpJobPhases.COMPLETED) { + console.log( + `Job ${job.id} completed, received deliverable:`, + job.deliverable + ); + } else if (job.phase === AcpJobPhases.REJECTED) { + console.log(`Job ${job.id} rejected by seller`); + } else if (job.phase === AcpJobPhases.TRANSACTION) { + // console.log(`Memo to sign ${memoToSign?.id} for job ${job.id}`); + await memoToSign?.sign(true, "Accepts transaction memo"); + } + }, + }); + + // Browse available agents based on a keyword + const relevantAgents = await acpClient.browseAgents("cross chain transfer", { + sort_by: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], + top_k: 5, + graduationStatus: AcpGraduationStatus.ALL, + onlineStatus: AcpOnlineStatus.ALL, + // showHiddenOfferings: true, + }); + + console.log("Relevant agents:", relevantAgents); + + // Pick one of the agents based on your criteria (in this example we just pick the first one) + const chosenAgent = relevantAgents[0]; + // Pick one of the service offerings based on your criteria (in this example we just pick the first one) + const chosenJobOffering = chosenAgent.jobOfferings[1]; + + const jobId = await chosenJobOffering.initiateJob( + // can be found in your ACP Visualiser's "Edit Service" pop-up. + // Reference: (./images/specify_requirement_toggle_switch.png) + {}, + undefined, // evaluator address, undefined fallback to empty address - skip-evaluation + new Date(Date.now() + 1000 * 60 * 15) // job expiry duration, minimum 5 minutes + ); + + console.log(`Job ${jobId} initiated`); +} + +buyer(); diff --git a/src/acpJob.ts b/src/acpJob.ts index f3a95e6..a840569 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -7,9 +7,9 @@ import { OperationPayload, } from "./contractClients/baseAcpContractClient"; import AcpMemo from "./acpMemo"; -import { DeliverablePayload, AcpMemoStatus } from "./interfaces"; +import { DeliverablePayload, AcpMemoStatus, AcpMemoState } from "./interfaces"; import { - encodeTransferEventMetadata, + getDestinationChainId, getDestinationEndpointId, preparePayload, tryParseJson, @@ -225,6 +225,16 @@ class AcpJob { throw new AcpError("No notification memo found"); } + if ( + memo.type === MemoType.PAYABLE_REQUEST && + memo.state !== AcpMemoState.PENDING && + memo.payableDetails?.lzDstEid !== undefined && + memo.payableDetails?.lzDstEid !== 0 + ) { + // Payable request memo required to be in pending state + return; + } + const operations: OperationPayload[] = []; const baseFareAmount = new FareAmount(this.price, this.baseFare); @@ -273,6 +283,66 @@ class AcpJob { ) ); + if (memo.payableDetails) { + const destinationChainId = memo.payableDetails.lzDstEid + ? getDestinationChainId(memo.payableDetails.lzDstEid) + : this.config.chain.id; + + if (destinationChainId !== this.config.chain.id) { + if (memo.type === MemoType.PAYABLE_REQUEST) { + const tokenBalance = await this.acpContractClient.getERC20Balance( + destinationChainId, + memo.payableDetails.token, + this.acpContractClient.agentWalletAddress + ); + + if (tokenBalance < memo.payableDetails.amount) { + const tokenDecimals = await this.acpContractClient.getERC20Decimals( + destinationChainId, + memo.payableDetails.token + ); + + const tokenSymbol = await this.acpContractClient.getERC20Symbol( + destinationChainId, + memo.payableDetails.token + ); + + throw new Error( + `You do not have enough funds to pay for the job which costs ${formatUnits( + memo.payableDetails.amount, + tokenDecimals + )} ${tokenSymbol} on chainId ${destinationChainId}` + ); + } + + const assetManagerAddress = + await this.acpContractClient.getAssetManager(); + + const allowance = await this.acpContractClient.getERC20Allowance( + destinationChainId, + memo.payableDetails.token, + this.acpContractClient.agentWalletAddress, + assetManagerAddress + ); + + const destinationChainOperations: OperationPayload[] = []; + + destinationChainOperations.push( + this.acpContractClient.approveAllowance( + memo.payableDetails.amount + allowance, + memo.payableDetails.token, + assetManagerAddress + ) + ); + + await this.acpContractClient.handleOperation( + destinationChainOperations, + destinationChainId + ); + } + } + } + if (this.price > 0) { const x402PaymentDetails = await this.acpContractClient.getX402PaymentDetails(this.id); @@ -569,11 +639,7 @@ class AcpJob { } } - async deliverCrossChainPayable( - recipient: Address, - amount: FareAmountBase, - isRequest: boolean = false - ) { + async deliverCrossChainPayable(recipient: Address, amount: FareAmountBase) { if (!amount.fare.chainId) { throw new AcpError("Chain ID is required for cross chain payable"); } @@ -629,9 +695,9 @@ class AcpJob { recipient, BigInt(0), FeeType.NO_FEE, - isRequest ? MemoType.PAYABLE_REQUEST : MemoType.PAYABLE_TRANSFER, + MemoType.PAYABLE_TRANSFER, new Date(Date.now() + 1000 * 60 * 5), - isRequest ? AcpJobPhases.TRANSACTION : AcpJobPhases.COMPLETED, + AcpJobPhases.COMPLETED, getDestinationEndpointId(chainId) ); diff --git a/src/contractClients/baseAcpContractClient.ts b/src/contractClients/baseAcpContractClient.ts index 60a3ca0..f3da542 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -580,6 +580,22 @@ abstract class BaseAcpContractClient { }); } + async getERC20Decimals( + chainId: number, + tokenAddress: Address + ): Promise { + const publicClient = this.publicClients[chainId]; + if (!publicClient) { + throw new AcpError(`Public client for chainId ${chainId} not found`); + } + + return await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "decimals", + }); + } + abstract getAssetManager(): Promise
; abstract getAcpVersion(): string; diff --git a/src/utils.ts b/src/utils.ts index 05900b3..1fe3f91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,6 +61,33 @@ export function getDestinationEndpointId(chainId: number): number { throw new Error(`Unsupported chain ID: ${chainId}`); } +export function getDestinationChainId(endpointId: number): number { + switch (endpointId) { + case 40245: + return baseSepolia.id; + case 40161: + return sepolia.id; + case 40267: + return polygonAmoy.id; + case 40231: + return arbitrumSepolia.id; + case 40102: + return bscTestnet.id; + case 30184: + return base.id; + case 30101: + return mainnet.id; + case 30109: + return polygon.id; + case 30110: + return arbitrum.id; + case 30102: + return bsc.id; + } + + throw new Error(`Unsupported endpoint ID: ${endpointId}`); +} + export function encodeTransferEventMetadata( tokenAddress: Address, amount: bigint, From c97ab20ac16667ead1bad187a56c1dfe211e243b Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Tue, 27 Jan 2026 17:58:09 +0800 Subject: [PATCH 13/15] feat: percentage pricing for payable transfer --- src/acpJob.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index a840569..78cc50b 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -459,7 +459,11 @@ class AcpJob { ) { // If payable chain belongs to non ACP native chain, we route to transfer service if (amount.fare.chainId !== this.acpContractClient.config.chain.id) { - return await this.deliverCrossChainPayable(this.clientAddress, amount); + return await this.deliverCrossChainPayable( + this.clientAddress, + amount, + skipFee + ); } const operations: OperationPayload[] = []; @@ -639,7 +643,11 @@ class AcpJob { } } - async deliverCrossChainPayable(recipient: Address, amount: FareAmountBase) { + async deliverCrossChainPayable( + recipient: Address, + amount: FareAmountBase, + skipFee: boolean = false + ) { if (!amount.fare.chainId) { throw new AcpError("Chain ID is required for cross chain payable"); } @@ -683,6 +691,10 @@ class AcpJob { amount.fare.contractAddress ); + const feeAmount = new FareAmount(0, this.acpContractClient.config.baseFare); + const isPercentagePricing: boolean = + this.priceType === PriceType.PERCENTAGE && !skipFee; + const createMemoOperation = this.acpContractClient.createCrossChainPayableMemo( this.id, @@ -693,8 +705,10 @@ class AcpJob { amount.fare.contractAddress, amount.amount, recipient, - BigInt(0), - FeeType.NO_FEE, + isPercentagePricing + ? BigInt(Math.round(this.priceValue * 10000)) + : feeAmount.amount, + isPercentagePricing ? FeeType.PERCENTAGE_FEE : FeeType.NO_FEE, MemoType.PAYABLE_TRANSFER, new Date(Date.now() + 1000 * 60 * 5), AcpJobPhases.COMPLETED, From 54dbc2f3c6f81e2348f9dcb4af999e232bd0f02f Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 28 Jan 2026 14:15:30 +0800 Subject: [PATCH 14/15] feat: simplified multichain config, update memo state enum --- .../cross-chain-transfer-service/buyer.ts | 20 +------ .../cross-chain-transfer-service/seller.ts | 16 +----- src/acpFare.ts | 10 ++-- src/configs/acpConfigs.ts | 57 +++++++++++++++---- src/contractClients/acpContractClientV2.ts | 12 ++-- src/interfaces.ts | 5 +- 6 files changed, 64 insertions(+), 56 deletions(-) diff --git a/examples/acp-base/cross-chain-transfer-service/buyer.ts b/examples/acp-base/cross-chain-transfer-service/buyer.ts index 7716641..96e7d3d 100644 --- a/examples/acp-base/cross-chain-transfer-service/buyer.ts +++ b/examples/acp-base/cross-chain-transfer-service/buyer.ts @@ -1,4 +1,3 @@ -import { bscTestnet } from "@account-kit/infra"; import AcpClient, { AcpContractClientV2, AcpJobPhases, @@ -8,31 +7,20 @@ import AcpClient, { AcpGraduationStatus, AcpOnlineStatus, baseSepoliaAcpX402ConfigV2, - AcpMemoState, } from "../../../src/index"; import { BUYER_AGENT_WALLET_ADDRESS, WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, } from "./env"; -import { Address } from "viem"; async function buyer() { - const config = { - ...baseSepoliaAcpX402ConfigV2, - chains: [ - { - chain: bscTestnet, - }, - ], - }; - const acpClient = new AcpClient({ acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, BUYER_ENTITY_ID, BUYER_AGENT_WALLET_ADDRESS, - config + baseSepoliaAcpX402ConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( @@ -40,15 +28,9 @@ async function buyer() { (memoToSign?.nextPhase === AcpJobPhases.TRANSACTION || memoToSign?.nextPhase === AcpJobPhases.COMPLETED) ) { - console.log( - `Memo to sign ${memoToSign?.id} for job ${job.id} is in state ${memoToSign?.state}` - ); - if (memoToSign?.state === AcpMemoState.PENDING) { console.log(`Paying for job ${job.id}`); - // Internally approves allowance on destination chain for cross chain payable memo await job.payAndAcceptRequirement(); console.log(`Job ${job.id} paid`); - } } else if ( job.phase === AcpJobPhases.TRANSACTION && memoToSign?.nextPhase === AcpJobPhases.REJECTED diff --git a/examples/acp-base/cross-chain-transfer-service/seller.ts b/examples/acp-base/cross-chain-transfer-service/seller.ts index 604778b..4ca79f5 100644 --- a/examples/acp-base/cross-chain-transfer-service/seller.ts +++ b/examples/acp-base/cross-chain-transfer-service/seller.ts @@ -22,21 +22,12 @@ const TARGET_TOKEN_ADDRESS = "" as Address; const TARGET_CHAIN = bscTestnet; async function seller() { - const config = { - ...baseSepoliaAcpX402ConfigV2, - chains: [ - { - chain: bscTestnet, - }, - ], - }; - new AcpClient({ acpContractClient: await AcpContractClientV2.build( WHITELISTED_WALLET_PRIVATE_KEY, SELLER_ENTITY_ID, SELLER_AGENT_WALLET_ADDRESS, - config + baseSepoliaAcpX402ConfigV2 ), onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { if ( @@ -50,12 +41,11 @@ async function seller() { ); if (response) { await job.accept("Job requirement matches agent capability"); - const swappedToken = new FareAmount( 1, await Fare.fromContractAddress( SOURCE_TOKEN_ADDRESS, - config, + baseSepoliaAcpX402ConfigV2, TARGET_CHAIN.id ) ); @@ -87,7 +77,7 @@ async function seller() { 1, await Fare.fromContractAddress( TARGET_TOKEN_ADDRESS, - config, + baseSepoliaAcpX402ConfigV2, TARGET_CHAIN.id ) ); diff --git a/src/acpFare.ts b/src/acpFare.ts index 2e1f0e3..6ab15e5 100644 --- a/src/acpFare.ts +++ b/src/acpFare.ts @@ -33,18 +33,18 @@ class Fare { let rpcUrl = config.rpcEndpoint; if (chainId !== config.chain.id) { - const selectedConfig = config.chains?.find( - (chain) => chain.chain.id === chainId + const selectedChainConfig = config.chains.find( + (chain) => chain.id === chainId ); - if (!selectedConfig) { + if (!selectedChainConfig) { throw new AcpError( `Chain configuration for chainId ${chainId} not found.` ); } - chainConfig = selectedConfig.chain; - rpcUrl = selectedConfig.rpcUrl; + chainConfig = selectedChainConfig; + rpcUrl = `${config.alchemyRpcUrl}?chainId=${chainId}`; } const publicClient = createPublicClient({ diff --git a/src/configs/acpConfigs.ts b/src/configs/acpConfigs.ts index d349cef..24495e7 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -26,10 +26,31 @@ type SupportedChain = | typeof arbitrum | typeof arbitrumSepolia; -type ChainConfig = { chain: SupportedChain; rpcUrl?: string }; const V1_MAX_RETRIES = 10; // temp fix, while alchemy taking a look into it const V2_MAX_RETRIES = 3; +const TESTNET_CHAINS = [ + baseSepolia, + sepolia, + polygonAmoy, + arbitrumSepolia, + bscTestnet, +]; + +const MAINNET_CHAINS = [ + base, + mainnet, + polygon, + arbitrum, + bsc, +]; + +const DEFAULT_RETRY_CONFIG = { + intervalMs: 200, + multiplier: 1.1, + maxRetries: 10, +}; + class AcpContractConfig { constructor( public chain: typeof baseSepolia | typeof base, @@ -46,7 +67,7 @@ class AcpContractConfig { multiplier: number; maxRetries: number; }, - public chains: ChainConfig[] = [] + public chains: SupportedChain[] = [] ) {} } @@ -59,7 +80,9 @@ const baseSepoliaAcpConfig = new AcpContractConfig( ACP_ABI, V1_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseSepoliaAcpX402Config = new AcpContractConfig( @@ -73,7 +96,9 @@ const baseSepoliaAcpX402Config = new AcpContractConfig( undefined, { url: "https://dev-acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseSepoliaAcpConfigV2 = new AcpContractConfig( @@ -85,7 +110,9 @@ const baseSepoliaAcpConfigV2 = new AcpContractConfig( ACP_V2_ABI, V2_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseSepoliaAcpX402ConfigV2 = new AcpContractConfig( @@ -99,7 +126,9 @@ const baseSepoliaAcpX402ConfigV2 = new AcpContractConfig( undefined, { url: "https://dev-acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseAcpConfig = new AcpContractConfig( @@ -111,7 +140,9 @@ const baseAcpConfig = new AcpContractConfig( ACP_ABI, V1_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); const baseAcpX402Config = new AcpContractConfig( @@ -125,7 +156,9 @@ const baseAcpX402Config = new AcpContractConfig( undefined, { url: "https://acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); const baseAcpConfigV2 = new AcpContractConfig( @@ -137,7 +170,9 @@ const baseAcpConfigV2 = new AcpContractConfig( ACP_V2_ABI, V2_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); const baseAcpX402ConfigV2 = new AcpContractConfig( @@ -151,7 +186,9 @@ const baseAcpX402ConfigV2 = new AcpContractConfig( undefined, { url: "https://acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); export { diff --git a/src/contractClients/acpContractClientV2.ts b/src/contractClients/acpContractClientV2.ts index a258656..33a9e27 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -61,9 +61,9 @@ class AcpContractClientV2 extends BaseAcpContractClient { ReturnType > = {}; for (const chain of config.chains) { - publicClients[chain.chain.id] = createPublicClient({ - chain: chain.chain, - transport: http(chain.rpcUrl), + publicClients[chain.id] = createPublicClient({ + chain: chain, + transport: http(`${config.alchemyRpcUrl}?chainId=${chain.id}`), }); } @@ -134,11 +134,11 @@ class AcpContractClientV2 extends BaseAcpContractClient { // initialize all session key clients for all chains in the config for (const chain of this.config.chains) { - this._sessionKeyClients[chain.chain.id] = + this._sessionKeyClients[chain.id] = await createModularAccountV2Client({ - chain: chain.chain, + chain: chain, transport: alchemy({ - rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.chain.id}`, + rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${chain.id}`, }), signer: sessionKeySigner, policyId: "186aaa4a-5f57-4156-83fb-e456365a8820", diff --git a/src/interfaces.ts b/src/interfaces.ts index 6c439ef..35bd877 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -22,9 +22,8 @@ export enum AcpMemoState { NONE, PENDING, IN_PROGRESS, - READY, - COMPLETED, - REJECTED, + FAILED, + COMPLETED } export interface PayableDetails { From 2ef5d2a038710808b0a1dcbfedd97eeb34007e12 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Thu, 29 Jan 2026 15:43:02 +0800 Subject: [PATCH 15/15] fix: remove fixed content for cross chain delivery --- src/acpJob.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/acpJob.ts b/src/acpJob.ts index 78cc50b..adc4397 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -461,6 +461,7 @@ class AcpJob { if (amount.fare.chainId !== this.acpContractClient.config.chain.id) { return await this.deliverCrossChainPayable( this.clientAddress, + preparePayload(deliverable), amount, skipFee ); @@ -645,6 +646,7 @@ class AcpJob { async deliverCrossChainPayable( recipient: Address, + content: string, amount: FareAmountBase, skipFee: boolean = false ) { @@ -698,10 +700,7 @@ class AcpJob { const createMemoOperation = this.acpContractClient.createCrossChainPayableMemo( this.id, - `Performing cross chain payable transfer of ${formatUnits( - amount.amount, - amount.fare.decimals - )} ${tokenSymbol} to ${recipient}`, + content, amount.fare.contractAddress, amount.amount, recipient,