diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 37b14207f..76901f110 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -178,6 +178,17 @@ const agent = createAgent({
+AZZLE + + + + + + + +
post_azzle_taskPosts an AZZLE V2 task on Base using an AZL-wei amount.
claim_azzle_taskClaims a posted AZZLE V2 task.
fund_azzle_taskFunds a claimed AZZLE task in AZL wei.
mark_azzle_task_deliveredMarks an active AZZLE task delivered.
release_azzle_escrowReleases AZL escrow for a delivered task.
complete_azzle_taskCompletes a delivered AZZLE task after release.
+
+
Base Account diff --git a/typescript/agentkit/src/action-providers/azzle/README.md b/typescript/agentkit/src/action-providers/azzle/README.md new file mode 100644 index 000000000..4de74854d --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/README.md @@ -0,0 +1,27 @@ +# AZZLE Action Provider + +`AzzleActionProvider` exposes the supported AZZLE V2 task lifecycle on Base +mainnet: + +- `post_azzle_task` +- `claim_azzle_task` +- `fund_azzle_task` +- `mark_azzle_task_delivered` +- `release_azzle_escrow` +- `complete_azzle_task` + +All amounts are AZL wei. The caller supplies `taskRegistry` from its +runtime-loaded AZZLE V2 manifest; this provider intentionally does not embed +protocol addresses. + +The provider supports only Base mainnet (`8453` / `base-mainnet`). Its +transactions use AgentKit's `EvmWalletProvider`, so AgentKit retains custody +and signing control. + +The intentionally scoped lifecycle is: + +`post -> claim -> fund -> markDelivered -> release / complete` + +Cancellation, expiry, and dispute operations are not exposed by this initial +provider. Agents should validate task state and role permissions before calling +each write action. diff --git a/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.test.ts b/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.test.ts new file mode 100644 index 000000000..6e77df1f4 --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.test.ts @@ -0,0 +1,57 @@ +import { encodeFunctionData } from "viem"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { AZZLE_TASK_REGISTRY_ABI } from "./constants"; +import { azzleActionProvider } from "./azzleActionProvider"; + +const REGISTRY = "0x1111111111111111111111111111111111111111"; +const HASH = "0x1234567890123456789012345678901234567890123456789012345678901234"; + +function wallet(chainId = "8453"): jest.Mocked { + return { + getNetwork: jest.fn().mockReturnValue({ protocolFamily: "evm", chainId }), + sendTransaction: jest.fn().mockResolvedValue(HASH), + waitForTransactionReceipt: jest.fn().mockResolvedValue({ logs: [] }), + } as unknown as jest.Mocked; +} + +describe("AzzleActionProvider", () => { + const provider = azzleActionProvider(); + + it("only supports Base mainnet", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm", chainId: "8453" })).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "evm", chainId: "1" })).toBe(false); + }); + + it("posts AZL-wei tasks through an AgentKit EVM wallet", async () => { + const mockWallet = wallet(); + const deadline = Math.floor(Date.now() / 1000) + 3600; + + const response = await provider.postAzzleTask(mockWallet, { + taskRegistry: REGISTRY, + totalAmountAzlWei: "1000000000000000000", + deadline, + }); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: REGISTRY, + data: encodeFunctionData({ + abi: AZZLE_TASK_REGISTRY_ABI, + functionName: "post", + args: [1000000000000000000n, BigInt(deadline)], + }), + }); + expect(mockWallet.waitForTransactionReceipt).toHaveBeenCalledWith(HASH); + expect(response).toContain("Posted AZZLE task"); + }); + + it("refuses a non-Base wallet before sending", async () => { + const mockWallet = wallet("1"); + const response = await provider.claimAzzleTask(mockWallet, { + taskRegistry: REGISTRY, + taskId: "1", + }); + + expect(mockWallet.sendTransaction).not.toHaveBeenCalled(); + expect(response).toContain("require Base mainnet"); + }); +}); diff --git a/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.ts b/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.ts new file mode 100644 index 000000000..2eb67b5f8 --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/azzleActionProvider.ts @@ -0,0 +1,127 @@ +import { z } from "zod"; +import { encodeFunctionData, type Hex } from "viem"; +import { CreateAction } from "../actionDecorator"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { AZZLE_TASK_REGISTRY_ABI } from "./constants"; +import { + AzzleTaskAmountSchema, + AzzleTaskSchema, + PostAzzleTaskSchema, +} from "./schemas"; + +function isBaseMainnet(network: Network): boolean { + return network.protocolFamily === "evm" && + (network.chainId === "8453" || network.networkId === "base-mainnet"); +} + +function futureDeadline(deadline: number): void { + if (deadline <= Math.floor(Date.now() / 1000)) { + throw new Error("deadline must be a Unix timestamp in the future."); + } +} + +async function send( + walletProvider: EvmWalletProvider, + taskRegistry: string, + data: Hex, +): Promise { + if (!isBaseMainnet(walletProvider.getNetwork())) { + throw new Error("AZZLE V2 actions require Base mainnet (chain ID 8453)."); + } + const hash = await walletProvider.sendTransaction({ + to: taskRegistry as Hex, + data, + }); + await walletProvider.waitForTransactionReceipt(hash); + return hash; +} + +/** + * AgentKit provider for AZZLE V2 task coordination on Base. + * Amounts are AZL wei. Registry addresses are supplied at runtime from the + * caller's current manifest rather than embedded in this provider. + */ +export class AzzleActionProvider extends ActionProvider { + constructor() { + super("azzle", []); + } + + supportsNetwork = isBaseMainnet; + + @CreateAction({ + name: "post_azzle_task", + description: "Post an AZZLE V2 task on Base. totalAmountAzlWei is AZL wei, not USDC.", + schema: PostAzzleTaskSchema, + }) + async postAzzleTask( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + futureDeadline(args.deadline); + const hash = await send(walletProvider, args.taskRegistry, encodeFunctionData({ + abi: AZZLE_TASK_REGISTRY_ABI, + functionName: "post", + args: [BigInt(args.totalAmountAzlWei), BigInt(args.deadline)], + })); + return `Posted AZZLE task. Read TaskPosted from receipt ${hash} for its canonical v2:standard:N or v2:micro:N ID.`; + } catch (error) { + return `Error posting AZZLE task: ${error}`; + } + } + + @CreateAction({ name: "claim_azzle_task", description: "Claim a posted AZZLE V2 task on Base.", schema: AzzleTaskSchema }) + async claimAzzleTask(walletProvider: EvmWalletProvider, args: z.infer): Promise { + return this.call(walletProvider, args.taskRegistry, "claim", [BigInt(args.taskId)], "Claimed AZZLE task"); + } + + @CreateAction({ name: "fund_azzle_task", description: "Fund a claimed AZZLE task in AZL wei; full funding activates it.", schema: AzzleTaskAmountSchema }) + async fundAzzleTask(walletProvider: EvmWalletProvider, args: z.infer): Promise { + return this.call(walletProvider, args.taskRegistry, "fund", [BigInt(args.taskId), BigInt(args.amountAzlWei)], "Funded AZZLE task"); + } + + @CreateAction({ name: "mark_azzle_task_delivered", description: "Mark a fully funded active AZZLE task delivered.", schema: AzzleTaskSchema }) + async markAzzleTaskDelivered(walletProvider: EvmWalletProvider, args: z.infer): Promise { + return this.call(walletProvider, args.taskRegistry, "markDelivered", [BigInt(args.taskId)], "Marked AZZLE task delivered"); + } + + @CreateAction({ name: "release_azzle_escrow", description: "Release AZL wei from a delivered AZZLE task.", schema: AzzleTaskAmountSchema }) + async releaseAzzleEscrow(walletProvider: EvmWalletProvider, args: z.infer): Promise { + return this.call(walletProvider, args.taskRegistry, "release", [BigInt(args.taskId), BigInt(args.amountAzlWei)], "Released AZZLE escrow"); + } + + @CreateAction({ name: "complete_azzle_task", description: "Complete a delivered AZZLE task after release.", schema: AzzleTaskSchema }) + async completeAzzleTask(walletProvider: EvmWalletProvider, args: z.infer): Promise { + return this.call(walletProvider, args.taskRegistry, "complete", [BigInt(args.taskId)], "Completed AZZLE task"); + } + + private async call( + walletProvider: EvmWalletProvider, + taskRegistry: string, + functionName: "claim" | "fund" | "markDelivered" | "release" | "complete", + args: readonly bigint[], + success: string, + ): Promise { + try { + const data = functionName === "fund" || functionName === "release" + ? encodeFunctionData({ + abi: AZZLE_TASK_REGISTRY_ABI, + functionName, + args: [args[0], args[1]], + }) + : encodeFunctionData({ + abi: AZZLE_TASK_REGISTRY_ABI, + functionName, + args: [args[0]], + }); + const hash = await send(walletProvider, taskRegistry, data); + return `${success}. Transaction hash: ${hash}`; + } catch (error) { + return `Error: ${error}`; + } + } +} + +export const azzleActionProvider = () => new AzzleActionProvider(); diff --git a/typescript/agentkit/src/action-providers/azzle/constants.ts b/typescript/agentkit/src/action-providers/azzle/constants.ts new file mode 100644 index 000000000..2cf5e5ab5 --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/constants.ts @@ -0,0 +1,53 @@ +export const AZZLE_TASK_REGISTRY_ABI = [ + { + type: "function", + name: "post", + stateMutability: "nonpayable", + inputs: [ + { name: "totalAmount", type: "uint256" }, + { name: "deadline", type: "uint64" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "claim", + stateMutability: "nonpayable", + inputs: [{ name: "taskId", type: "uint256" }], + outputs: [], + }, + { + type: "function", + name: "fund", + stateMutability: "nonpayable", + inputs: [ + { name: "taskId", type: "uint256" }, + { name: "amount", type: "uint256" }, + ], + outputs: [], + }, + { + type: "function", + name: "markDelivered", + stateMutability: "nonpayable", + inputs: [{ name: "taskId", type: "uint256" }], + outputs: [], + }, + { + type: "function", + name: "release", + stateMutability: "nonpayable", + inputs: [ + { name: "taskId", type: "uint256" }, + { name: "amount", type: "uint256" }, + ], + outputs: [], + }, + { + type: "function", + name: "complete", + stateMutability: "nonpayable", + inputs: [{ name: "taskId", type: "uint256" }], + outputs: [], + }, +] as const; diff --git a/typescript/agentkit/src/action-providers/azzle/index.ts b/typescript/agentkit/src/action-providers/azzle/index.ts new file mode 100644 index 000000000..d5d6a9890 --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/index.ts @@ -0,0 +1,2 @@ +export * from "./azzleActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/azzle/schemas.ts b/typescript/agentkit/src/action-providers/azzle/schemas.ts new file mode 100644 index 000000000..c101f2126 --- /dev/null +++ b/typescript/agentkit/src/action-providers/azzle/schemas.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +const taskId = z.string().regex(/^[1-9]\d*$/, "Task ID must be a positive integer."); +const azlWei = z.string().regex(/^[1-9]\d*$/, "Amount must be a positive AZL-wei integer."); + +export const PostAzzleTaskSchema = z.object({ + taskRegistry: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Task registry must be an address."), + totalAmountAzlWei: azlWei, + deadline: z.number().int().positive().describe("Future Unix timestamp."), +}); + +export const AzzleTaskSchema = z.object({ + taskRegistry: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Task registry must be an address."), + taskId, +}); + +export const AzzleTaskAmountSchema = AzzleTaskSchema.extend({ + amountAzlWei: azlWei, +}); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..d3c3f56d2 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -32,6 +32,7 @@ export * from "./wallet"; export * from "./weth"; export * from "./wow"; export * from "./allora"; +export * from "./azzle"; export * from "./flaunch"; export * from "./onramp"; export * from "./vaultsfyi";