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..96e7d3d --- /dev/null +++ b/examples/acp-base/cross-chain-transfer-service/buyer.ts @@ -0,0 +1,84 @@ +import AcpClient, { + AcpContractClientV2, + AcpJobPhases, + AcpJob, + AcpMemo, + AcpAgentSort, + AcpGraduationStatus, + AcpOnlineStatus, + baseSepoliaAcpX402ConfigV2, +} from "../../../src/index"; +import { + BUYER_AGENT_WALLET_ADDRESS, + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, +} from "./env"; + +async function buyer() { + const acpClient = new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + BUYER_ENTITY_ID, + BUYER_AGENT_WALLET_ADDRESS, + baseSepoliaAcpX402ConfigV2 + ), + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { + if ( + job.phase === AcpJobPhases.NEGOTIATION && + (memoToSign?.nextPhase === AcpJobPhases.TRANSACTION || + memoToSign?.nextPhase === AcpJobPhases.COMPLETED) + ) { + console.log(`Paying for job ${job.id}`); + 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/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..4ca79f5 --- /dev/null +++ b/examples/acp-base/cross-chain-transfer-service/seller.ts @@ -0,0 +1,94 @@ +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() { + new AcpClient({ + acpContractClient: await AcpContractClientV2.build( + WHITELISTED_WALLET_PRIVATE_KEY, + SELLER_ENTITY_ID, + SELLER_AGENT_WALLET_ADDRESS, + baseSepoliaAcpX402ConfigV2 + ), + 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, + baseSepoliaAcpX402ConfigV2, + 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, + baseSepoliaAcpX402ConfigV2, + TARGET_CHAIN.id + ) + ); + + await job.deliverPayable( + "Delivered swapped token on destination chain", + swappedToken + ); + } + }, + }); +} + +seller(); diff --git a/package-lock.json b/package-lock.json index aa1380a..7ffb1bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,9 +33,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.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", @@ -47,13 +47,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.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.73.0", - "@account-kit/logging": "^4.73.0", + "@aa-sdk/core": "^4.82.0", + "@account-kit/logging": "^4.82.0", "eventemitter3": "^5.0.1", "zod": "^3.22.4" }, @@ -65,9 +65,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.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", @@ -75,13 +75,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.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.73.0", - "@account-kit/infra": "^4.73.0", + "@aa-sdk/core": "^4.82.0", + "@account-kit/infra": "^4.82.0", "webauthn-p256": "^0.0.10" }, "peerDependencies": { diff --git a/src/abis/acpAbiV2.ts b/src/abis/acpAbiV2.ts index a44f582..a2438c9 100644 --- a/src/abis/acpAbiV2.ts +++ b/src/abis/acpAbiV2.ts @@ -366,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" }, @@ -551,6 +579,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", @@ -598,6 +631,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", @@ -641,6 +679,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", 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/acpClient.ts b/src/acpClient.ts index 13dfe7d..37a829f 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -12,6 +12,7 @@ import { IAcpAgent, AcpAgentSort, AcpGraduationStatus, + AcpMemoState, AcpOnlineStatus, IAcpAccount, IAcpClientOptions, @@ -183,6 +184,7 @@ class AcpClient { } process.exit(0); }; + process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); } @@ -231,7 +233,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(`Failed to hydrate memo ${memo.id}`, err); diff --git a/src/acpFare.ts b/src/acpFare.ts index 60e971b..6ab15e5 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 selectedChainConfig = config.chains.find( + (chain) => chain.id === chainId + ); + + if (!selectedChainConfig) { + throw new AcpError( + `Chain configuration for chainId ${chainId} not found.` + ); + } + + chainConfig = selectedChainConfig; + rpcUrl = `${config.alchemyRpcUrl}?chainId=${chainId}`; + } + 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 e963001..adc4397 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -7,8 +7,13 @@ import { OperationPayload, } from "./contractClients/baseAcpContractClient"; import AcpMemo from "./acpMemo"; -import { DeliverablePayload, AcpMemoStatus } from "./interfaces"; -import { preparePayload, tryParseJson } from "./utils"; +import { DeliverablePayload, AcpMemoStatus, AcpMemoState } from "./interfaces"; +import { + getDestinationChainId, + getDestinationEndpointId, + preparePayload, + tryParseJson, +} from "./utils"; import { FareAmount, FareAmountBase } from "./acpFare"; import AcpError from "./acpError"; import { PriceType } from "./acpJobOffering"; @@ -143,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 @@ -163,35 +171,70 @@ class AcpJob { const isPercentagePricing: boolean = this.priceType === PriceType.PERCENTAGE; - operations.push( - this.acpContractClient.createPayableMemo( - this.id, - content, - amount.amount, - recipient, - isPercentagePricing - ? BigInt(Math.round(this.priceValue * 10000)) // convert to basis points - : feeAmount.amount, - isPercentagePricing ? 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, + isPercentagePricing + ? BigInt(Math.round(this.priceValue * 10000)) // convert to basis points + : feeAmount.amount, + isPercentagePricing ? FeeType.PERCENTAGE_FEE : FeeType.NO_FEE, + type as MemoType.PAYABLE_REQUEST, + expiredAt, + AcpJobPhases.TRANSACTION, + getDestinationEndpointId(amount.fare.chainId as number) + ) + ); + } else { + operations.push( + this.acpContractClient.createPayableMemo( + this.id, + content, + amount.amount, + recipient, + isPercentagePricing + ? BigInt(Math.round(this.priceValue * 10000)) // convert to basis points + : feeAmount.amount, + isPercentagePricing ? FeeType.PERCENTAGE_FEE : FeeType.NO_FEE, + AcpJobPhases.TRANSACTION, + type, + expiredAt, + amount.fare.contractAddress + ) + ); + } return await this.acpContractClient.handleOperation(operations); } 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) { 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); @@ -240,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); @@ -333,10 +436,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( @@ -358,8 +457,14 @@ 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, + preparePayload(deliverable), + amount, + skipFee + ); } const operations: OperationPayload[] = []; @@ -539,6 +644,79 @@ class AcpJob { } } + async deliverCrossChainPayable( + recipient: Address, + content: string, + amount: FareAmountBase, + skipFee: boolean = false + ) { + if (!amount.fare.chainId) { + throw new AcpError("Chain ID is required for cross chain payable"); + } + + 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, + amount.fare.contractAddress, + this.acpContractClient.agentWalletAddress + ); + + if (tokenBalance < amount.amount) { + throw new AcpError("Insufficient token balance for cross chain payable"); + } + + const currentAllowance = await this.acpContractClient.getERC20Allowance( + chainId, + amount.fare.contractAddress, + this.acpContractClient.agentWalletAddress, + assetManagerAddress + ); + + // Approve allowance to asset manager on destination chain + const approveAllowanceOperation = this.acpContractClient.approveAllowance( + amount.amount + currentAllowance, + amount.fare.contractAddress, + assetManagerAddress + ); + + await this.acpContractClient.handleOperation( + [approveAllowanceOperation], + chainId + ); + + const tokenSymbol = await this.acpContractClient.getERC20Symbol( + chainId, + 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, + content, + amount.fare.contractAddress, + amount.amount, + recipient, + 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, + getDestinationEndpointId(chainId) + ); + + await this.acpContractClient.handleOperation([createMemoOperation]); + } + [util.inspect.custom]() { return { id: this.id, diff --git a/src/acpMemo.ts b/src/acpMemo.ts index cf29a22..bb4d69e 100644 --- a/src/acpMemo.ts +++ b/src/acpMemo.ts @@ -4,6 +4,7 @@ import BaseAcpContractClient, { MemoType, } from "./contractClients/baseAcpContractClient"; import { + AcpMemoState, AcpMemoStatus, PayableDetails, } from "./interfaces"; @@ -23,6 +24,7 @@ class AcpMemo { public payableDetails?: PayableDetails, public txHash?: `0x${string}`, public signedTxHash?: `0x${string}`, + 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 cebbb65..24495e7 100644 --- a/src/configs/acpConfigs.ts +++ b/src/configs/acpConfigs.ts @@ -1,13 +1,56 @@ import { Address } from "@aa-sdk/core"; -import { baseSepolia, base } from "@account-kit/infra"; +import { + baseSepolia, + base, + bscTestnet, + bsc, + mainnet, + sepolia, + polygon, + polygonAmoy, + arbitrum, + arbitrumSepolia, +} from "@account-kit/infra"; 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; + 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, @@ -23,7 +66,8 @@ class AcpContractConfig { intervalMs: number; multiplier: number; maxRetries: number; - } + }, + public chains: SupportedChain[] = [] ) {} } @@ -36,7 +80,9 @@ const baseSepoliaAcpConfig = new AcpContractConfig( ACP_ABI, V1_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseSepoliaAcpX402Config = new AcpContractConfig( @@ -50,7 +96,9 @@ const baseSepoliaAcpX402Config = new AcpContractConfig( undefined, { url: "https://dev-acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseSepoliaAcpConfigV2 = new AcpContractConfig( @@ -62,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( @@ -76,7 +126,9 @@ const baseSepoliaAcpX402ConfigV2 = new AcpContractConfig( undefined, { url: "https://dev-acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + TESTNET_CHAINS ); const baseAcpConfig = new AcpContractConfig( @@ -88,7 +140,9 @@ const baseAcpConfig = new AcpContractConfig( ACP_ABI, V1_MAX_RETRIES, undefined, - undefined + undefined, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); const baseAcpX402Config = new AcpContractConfig( @@ -102,7 +156,9 @@ const baseAcpX402Config = new AcpContractConfig( undefined, { url: "https://acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); const baseAcpConfigV2 = new AcpContractConfig( @@ -114,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( @@ -128,7 +186,9 @@ const baseAcpX402ConfigV2 = new AcpContractConfig( undefined, { url: "https://acp-x402.virtuals.io", - } + }, + DEFAULT_RETRY_CONFIG, + MAINNET_CHAINS ); export { diff --git a/src/constants.ts b/src/constants.ts index 361464a..48d6cf7 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, @@ -20,4 +31,5 @@ export const HTTP_STATUS_CODES = { PAYMENT_REQUIRED: 402, }; -export const SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS: Address = "0x00000000000099DE0BF6fA90dEB851E2A2df7d83"; +export const SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS: Address = + "0x00000000000099DE0BF6fA90dEB851E2A2df7d83"; diff --git a/src/contractClients/acpContractClient.ts b/src/contractClients/acpContractClient.ts index 43d6a4d..197350a 100644 --- a/src/contractClients/acpContractClient.ts +++ b/src/contractClients/acpContractClient.ts @@ -368,6 +368,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 0bd6ade..33a9e27 100644 --- a/src/contractClients/acpContractClientV2.ts +++ b/src/contractClients/acpContractClientV2.ts @@ -1,10 +1,11 @@ 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"; -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, { @@ -18,8 +19,11 @@ import { X402PayableRequirements, X402Payment, X402PaymentResponse, + CheckTransactionConfig, } 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; @@ -33,6 +37,7 @@ class AcpContractClientV2 extends BaseAcpContractClient { }; private _sessionKeyClient: ModularAccountV2Client | undefined; + private _sessionKeyClients: Record = {}; private _acpX402: AcpX402 | undefined; constructor( @@ -51,6 +56,17 @@ class AcpContractClientV2 extends BaseAcpContractClient { agentWalletAddress: Address, config: AcpContractConfig = baseAcpConfigV2 ) { + const publicClients: Record< + number, + ReturnType + > = {}; + for (const chain of config.chains) { + publicClients[chain.id] = createPublicClient({ + chain: chain, + transport: http(`${config.alchemyRpcUrl}?chainId=${chain.id}`), + }); + } + const publicClient = createPublicClient({ chain: config.chain, transport: http(config.rpcEndpoint), @@ -91,6 +107,8 @@ class AcpContractClientV2 extends BaseAcpContractClient { config ); + acpContractClient.publicClients = publicClients; + await acpContractClient.init(walletPrivateKey, sessionEntityKeyId); return acpContractClient; @@ -114,6 +132,24 @@ 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.id] = + await createModularAccountV2Client({ + chain: chain, + transport: alchemy({ + rpcUrl: `${this.config.alchemyRpcUrl}?chainId=${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, @@ -172,7 +208,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) * @@ -182,8 +231,17 @@ class AcpContractClientV2 extends BaseAcpContractClient { } async handleOperation( - operations: OperationPayload[] + 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 basePayload: any = { uo: operations.map((operation) => ({ target: operation.contractAddress, @@ -212,14 +270,21 @@ class AcpContractClientV2 extends BaseAcpContractClient { }, }; - const { hash } = await this.sessionKeyClient.sendUserOperation(payload); + const { hash } = await sessionKeyClient.sendUserOperation(payload); - const txnHash = - await this.sessionKeyClient.waitForUserOperationTransaction({ - hash, - tag: "pending", - retries: this.RETRY_CONFIG, - }); + const checkTransactionConfig: CheckTransactionConfig = { + hash, + retries: this.RETRY_CONFIG, + }; + + // 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) { @@ -323,6 +388,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 2e0e8fa..f3da542 100644 --- a/src/contractClients/baseAcpContractClient.ts +++ b/src/contractClients/baseAcpContractClient.ts @@ -72,6 +72,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, @@ -96,12 +98,14 @@ abstract class BaseAcpContractClient { sessionSignerAddress: Address, sessionEntityKeyId: number ): Promise { - const onChainSignerAddress = ((await this.publicClient.readContract({ - address: SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS, - abi: SINGLE_SIGNER_VALIDATION_MODULE_ABI, - functionName: "signers", - args: [sessionEntityKeyId, this.agentWalletAddress], - })) as Address).toLowerCase(); + const onChainSignerAddress = ( + (await this.publicClient.readContract({ + address: SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS, + abi: SINGLE_SIGNER_VALIDATION_MODULE_ABI, + functionName: "signers", + args: [sessionEntityKeyId, this.agentWalletAddress], + })) as Address + ).toLowerCase(); if (onChainSignerAddress === zeroAddress.toLowerCase()) { throw new AcpError( @@ -134,7 +138,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, @@ -216,13 +223,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 = { @@ -283,6 +291,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, + Math.floor(expiredAt.getTime() / 1000), + 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, @@ -308,6 +364,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, @@ -445,6 +527,77 @@ 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], + }); + } + + 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", + }); + } + + 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/index.ts b/src/index.ts index 1430086..8051b7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,8 +14,8 @@ import { AcpGraduationStatus, AcpOnlineStatus, AcpMemoStatus, + AcpMemoState, DeliverablePayload, - IAcpAgent, } from "./interfaces"; import { AcpContractConfig, @@ -24,7 +24,8 @@ import { baseAcpX402Config, baseAcpX402ConfigV2, baseSepoliaAcpConfig, - baseSepoliaAcpConfigV2 + baseSepoliaAcpConfigV2, + baseSepoliaAcpX402ConfigV2, } from "./configs/acpConfigs"; import { ethFare, Fare, FareAmount, FareBigInt, wethFare } from "./acpFare"; import AcpError from "./acpError"; @@ -46,6 +47,7 @@ export { ethFare, baseSepoliaAcpConfig, baseSepoliaAcpConfigV2, + baseSepoliaAcpX402ConfigV2, baseAcpConfig, baseAcpConfigV2, baseAcpX402Config, @@ -60,4 +62,5 @@ export { AcpGraduationStatus, AcpOnlineStatus, AcpMemoStatus, + AcpMemoState, }; diff --git a/src/interfaces.ts b/src/interfaces.ts index 842aa34..71f4bc0 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -15,11 +15,21 @@ export enum AcpMemoStatus { REJECTED = "REJECTED", } +export enum AcpMemoState { + NONE, + PENDING, + IN_PROGRESS, + FAILED, + COMPLETED +} + export interface PayableDetails { amount: bigint; token: Address; recipient: Address; feeAmount: bigint; + lzSrcEid?: number; + lzDstEid?: number; } export interface IAcpMemoData { @@ -35,6 +45,7 @@ export interface IAcpMemoData { expiry?: string; payableDetails?: PayableDetails; contractAddress?: Address; + state?: AcpMemoState; txHash?: `0x${string}`; signedTxHash?: `0x${string}`; } @@ -233,3 +244,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..1fe3f91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,17 @@ +import { Address, decodeAbiParameters, 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,88 @@ 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 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, + recipient: Address, + chainId: number +): string { + const result = 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"] + ); + + 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; +}