@ton-agent-kit/plugin-escrow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@ton-agent-kit/plugin-escrow",
3
+ "version": "1.0.0",
4
+ "description": "Escrow plugin for TON Agent Kit — create, fund, release, and refund escrow deals",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "scripts": { "build": "tsc" },
8
+ "dependencies": {
9
+ "@ton-agent-kit/core": "^1.0.0",
10
+ "@ton/ton": "^16.2.0",
11
+ "@ton/core": "^0.63.1"
12
+ },
13
+ "keywords": ["ton", "blockchain", "ai", "agent", "sdk"],
14
+ "license": "MIT",
15
+ "publishConfig": { "access": "public" },
16
+ "repository": { "type": "git", "url": "https://github.com/Andy00L/ton-agent-kit.git" }
17
+ }
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+ import { Address } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
+ import {
5
+ loadEscrows,
6
+ saveEscrows,
7
+ deployEscrowContract,
8
+ type EscrowRecord,
9
+ } from "../utils";
10
+
11
+ export const createEscrowAction = defineAction<
12
+ {
13
+ beneficiary: string;
14
+ amount: string;
15
+ description?: string;
16
+ deadlineTimestamp?: number;
17
+ deadlineDays?: number;
18
+ deadlineHours?: number;
19
+ deadlineMinutes?: number;
20
+ },
21
+ any
22
+ >({
23
+ name: "create_escrow",
24
+ description:
25
+ "Create a new on-chain escrow deal. Deploys a Tact Escrow contract to TON. " +
26
+ "The beneficiary receives funds when released. " +
27
+ "Deadline can be set via deadlineTimestamp (exact Unix timestamp), deadlineDays, deadlineHours, or deadlineMinutes. " +
28
+ "Priority: timestamp > days > hours > minutes. Defaults to 60 minutes if none provided.",
29
+ schema: z.object({
30
+ beneficiary: z
31
+ .string()
32
+ .describe("Address of the beneficiary (who receives funds)"),
33
+ amount: z.string().describe("Amount of TON to escrow"),
34
+ description: z.string().optional().describe("Description of the deal"),
35
+ deadlineTimestamp: z.coerce
36
+ .number()
37
+ .optional()
38
+ .describe("Exact deadline as Unix timestamp (seconds). Takes highest priority."),
39
+ deadlineDays: z.coerce
40
+ .number()
41
+ .optional()
42
+ .describe("Deadline in days from now (e.g., 90 for ~3 months)"),
43
+ deadlineHours: z.coerce
44
+ .number()
45
+ .optional()
46
+ .describe("Deadline in hours from now (e.g., 24 for 1 day)"),
47
+ deadlineMinutes: z.coerce
48
+ .number()
49
+ .optional()
50
+ .describe("Deadline in minutes from now (e.g., 60 for 1 hour). Default if nothing else set."),
51
+ }) as any,
52
+ handler: async (agent, params) => {
53
+ const escrowId = `escrow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
54
+ const now = Math.floor(Date.now() / 1000);
55
+
56
+ let deadline: number;
57
+ if (params.deadlineTimestamp) {
58
+ deadline = params.deadlineTimestamp;
59
+ } else if (params.deadlineDays) {
60
+ deadline = now + params.deadlineDays * 86400;
61
+ } else if (params.deadlineHours) {
62
+ deadline = now + params.deadlineHours * 3600;
63
+ } else {
64
+ deadline = now + (params.deadlineMinutes || 60) * 60;
65
+ }
66
+
67
+ const depositor = agent.wallet.address;
68
+ const beneficiary = Address.parse(params.beneficiary);
69
+ const arbiter = agent.wallet.address; // Agent is arbiter by default
70
+
71
+ // Deploy a new Escrow contract on-chain
72
+ const contractAddress = await deployEscrowContract(
73
+ agent,
74
+ depositor,
75
+ beneficiary,
76
+ arbiter,
77
+ BigInt(deadline),
78
+ );
79
+
80
+ // Save to JSON index (escrowId → contract address mapping)
81
+ const escrow: EscrowRecord = {
82
+ id: escrowId,
83
+ contractAddress: contractAddress.toRawString(),
84
+ depositor: depositor.toRawString(),
85
+ beneficiary: params.beneficiary,
86
+ arbiter: arbiter.toRawString(),
87
+ amount: params.amount,
88
+ deadline,
89
+ deadlineISO: new Date(deadline * 1000).toISOString(),
90
+ description: params.description || "",
91
+ status: "created",
92
+ createdAt: new Date().toISOString(),
93
+ };
94
+
95
+ const escrows = loadEscrows();
96
+ escrows[escrowId] = escrow;
97
+ saveEscrows(escrows);
98
+
99
+ return {
100
+ escrowId,
101
+ status: "created (contract deployed on-chain)",
102
+ contractAddress: contractAddress.toRawString(),
103
+ friendlyContract: toFriendlyAddress(contractAddress, agent.network),
104
+ beneficiary: params.beneficiary,
105
+ friendlyBeneficiary: toFriendlyAddress(beneficiary, agent.network),
106
+ amount: params.amount + " TON",
107
+ deadline: escrow.deadlineISO,
108
+ description: params.description || "",
109
+ nextStep: `Deposit ${params.amount} TON using deposit_to_escrow with escrowId: ${escrowId}`,
110
+ };
111
+ },
112
+ });
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { Address } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
+ import {
5
+ loadEscrows,
6
+ saveEscrows,
7
+ depositToContract,
8
+ getLatestTxHash,
9
+ } from "../utils";
10
+
11
+ export const depositToEscrowAction = defineAction<{ escrowId: string }, any>({
12
+ name: "deposit_to_escrow",
13
+ description:
14
+ "Deposit TON into an on-chain escrow contract. Sends TON to the deployed Escrow smart contract.",
15
+ schema: z.object({
16
+ escrowId: z.string().describe("Escrow ID from create_escrow"),
17
+ }) as any,
18
+ handler: async (agent, params) => {
19
+ const escrows = loadEscrows();
20
+ const escrow = escrows[params.escrowId];
21
+ if (!escrow) throw new Error(`Escrow not found: ${params.escrowId}`);
22
+ if (escrow.status !== "created")
23
+ throw new Error(`Escrow already ${escrow.status}`);
24
+
25
+ const contractAddress = Address.parse(escrow.contractAddress);
26
+
27
+ // Send Deposit message with TON to the escrow contract
28
+ await depositToContract(agent, contractAddress, escrow.amount);
29
+
30
+ // Wait for confirmation
31
+ await new Promise((r) => setTimeout(r, 10000));
32
+
33
+ const txHash = await getLatestTxHash(
34
+ agent.wallet.address.toRawString(),
35
+ agent.network,
36
+ );
37
+
38
+ // Update index
39
+ escrow.status = "funded";
40
+ saveEscrows(escrows);
41
+
42
+ return {
43
+ escrowId: params.escrowId,
44
+ status: "funded (on-chain)",
45
+ contractAddress: escrow.contractAddress,
46
+ friendlyContract: toFriendlyAddress(contractAddress, agent.network),
47
+ amount: escrow.amount + " TON",
48
+ depositTxHash: txHash,
49
+ beneficiary: escrow.beneficiary,
50
+ friendlyBeneficiary: toFriendlyAddress(Address.parse(escrow.beneficiary), agent.network),
51
+ deadline: escrow.deadlineISO,
52
+ };
53
+ },
54
+ });
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { Address, fromNano } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
+ import { loadEscrows, getContractState } from "../utils";
5
+
6
+ export const getEscrowInfoAction = defineAction<{ escrowId?: string }, any>({
7
+ name: "get_escrow_info",
8
+ description:
9
+ "Get escrow details. Reads on-chain contract state if available. " +
10
+ "If escrowId is provided, returns that escrow. If no escrowId, lists ALL escrows.",
11
+ schema: z.object({
12
+ escrowId: z
13
+ .string()
14
+ .optional()
15
+ .describe("Escrow ID. If not provided, lists all escrows."),
16
+ }) as any,
17
+ handler: async (agent, params) => {
18
+ const escrows = loadEscrows();
19
+
20
+ if (params.escrowId) {
21
+ const escrow = escrows[params.escrowId];
22
+ if (!escrow) throw new Error(`Escrow not found: ${params.escrowId}`);
23
+
24
+ // Try to read on-chain state
25
+ try {
26
+ const contractAddress = Address.parse(escrow.contractAddress);
27
+ const onChain = await getContractState(agent, contractAddress);
28
+
29
+ return {
30
+ id: escrow.id,
31
+ contractAddress: escrow.contractAddress,
32
+ friendlyContract: toFriendlyAddress(Address.parse(escrow.contractAddress), agent.network),
33
+ description: escrow.description,
34
+ createdAt: escrow.createdAt,
35
+ onChain: {
36
+ depositor: toFriendlyAddress(onChain.depositor, agent.network),
37
+ beneficiary: toFriendlyAddress(onChain.beneficiary, agent.network),
38
+ arbiter: toFriendlyAddress(onChain.arbiter, agent.network),
39
+ amount: fromNano(onChain.amount) + " TON",
40
+ balance: fromNano(onChain.balance) + " TON",
41
+ deadline: new Date(Number(onChain.deadline) * 1000).toISOString(),
42
+ released: onChain.released,
43
+ refunded: onChain.refunded,
44
+ status: onChain.released
45
+ ? "released"
46
+ : onChain.refunded
47
+ ? "refunded"
48
+ : onChain.amount > BigInt(0)
49
+ ? "funded"
50
+ : "created",
51
+ },
52
+ };
53
+ } catch (e: any) {
54
+ // Contract not yet active or network error — fall back to index
55
+ return {
56
+ ...escrow,
57
+ onChainError: e.message || "Could not read contract state (may not be deployed yet)",
58
+ };
59
+ }
60
+ }
61
+
62
+ // List all escrows from index
63
+ const list = Object.values(escrows);
64
+ return {
65
+ count: list.length,
66
+ escrows: list.map((e) => ({
67
+ id: e.id,
68
+ status: e.status,
69
+ contractAddress: e.contractAddress || null,
70
+ friendlyContract: e.contractAddress
71
+ ? toFriendlyAddress(Address.parse(e.contractAddress), agent.network)
72
+ : null,
73
+ amount: e.amount + " TON",
74
+ beneficiary: e.beneficiary,
75
+ friendlyBeneficiary: e.beneficiary
76
+ ? toFriendlyAddress(Address.parse(e.beneficiary), agent.network)
77
+ : null,
78
+ deadline: e.deadlineISO,
79
+ description: e.description,
80
+ })),
81
+ };
82
+ },
83
+ });
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { Address } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
+ import {
5
+ loadEscrows,
6
+ saveEscrows,
7
+ refundContract,
8
+ getLatestTxHash,
9
+ } from "../utils";
10
+
11
+ export const refundEscrowAction = defineAction<{ escrowId: string }, any>({
12
+ name: "refund_escrow",
13
+ description:
14
+ "Refund escrowed funds back to the depositor via the on-chain Escrow contract. " +
15
+ "Can refund if authorized (depositor/arbiter) or after deadline has passed.",
16
+ schema: z.object({
17
+ escrowId: z.string().describe("Escrow ID to refund"),
18
+ }) as any,
19
+ handler: async (agent, params) => {
20
+ const escrows = loadEscrows();
21
+ const escrow = escrows[params.escrowId];
22
+ if (!escrow) throw new Error(`Escrow not found: ${params.escrowId}`);
23
+ if (escrow.status !== "funded")
24
+ throw new Error(`Escrow is ${escrow.status}, must be funded`);
25
+
26
+ const contractAddress = Address.parse(escrow.contractAddress);
27
+
28
+ // Send Refund message to the escrow contract
29
+ // The contract itself enforces authorization (depositor/arbiter/past deadline)
30
+ await refundContract(agent, contractAddress);
31
+
32
+ // Wait for confirmation
33
+ await new Promise((r) => setTimeout(r, 10000));
34
+
35
+ const txHash = await getLatestTxHash(
36
+ agent.wallet.address.toRawString(),
37
+ agent.network,
38
+ );
39
+
40
+ const now = Math.floor(Date.now() / 1000);
41
+ const pastDeadline = now > escrow.deadline;
42
+
43
+ // Update index
44
+ escrow.status = "refunded";
45
+ saveEscrows(escrows);
46
+
47
+ return {
48
+ escrowId: params.escrowId,
49
+ status: "refunded (on-chain)",
50
+ contractAddress: escrow.contractAddress,
51
+ friendlyContract: toFriendlyAddress(contractAddress, agent.network),
52
+ amount: escrow.amount + " TON",
53
+ depositor: escrow.depositor,
54
+ friendlyDepositor: toFriendlyAddress(Address.parse(escrow.depositor), agent.network),
55
+ reason: pastDeadline ? "Deadline passed" : "Authorized refund",
56
+ refundTxHash: txHash,
57
+ };
58
+ },
59
+ });
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { Address } from "@ton/core";
3
+ import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
4
+ import {
5
+ loadEscrows,
6
+ saveEscrows,
7
+ releaseContract,
8
+ getLatestTxHash,
9
+ } from "../utils";
10
+
11
+ export const releaseEscrowAction = defineAction<{ escrowId: string }, any>({
12
+ name: "release_escrow",
13
+ description:
14
+ "Release escrowed funds to the beneficiary via the on-chain Escrow contract. Only the depositor or arbiter can release.",
15
+ schema: z.object({
16
+ escrowId: z.string().describe("Escrow ID to release"),
17
+ }) as any,
18
+ handler: async (agent, params) => {
19
+ const escrows = loadEscrows();
20
+ const escrow = escrows[params.escrowId];
21
+ if (!escrow) throw new Error(`Escrow not found: ${params.escrowId}`);
22
+ if (escrow.status !== "funded")
23
+ throw new Error(`Escrow is ${escrow.status}, must be funded`);
24
+
25
+ const contractAddress = Address.parse(escrow.contractAddress);
26
+
27
+ // Send Release message to the escrow contract
28
+ await releaseContract(agent, contractAddress);
29
+
30
+ // Wait for confirmation
31
+ await new Promise((r) => setTimeout(r, 10000));
32
+
33
+ const txHash = await getLatestTxHash(
34
+ agent.wallet.address.toRawString(),
35
+ agent.network,
36
+ );
37
+
38
+ // Update index
39
+ escrow.status = "released";
40
+ saveEscrows(escrows);
41
+
42
+ return {
43
+ escrowId: params.escrowId,
44
+ status: "released (on-chain)",
45
+ contractAddress: escrow.contractAddress,
46
+ friendlyContract: toFriendlyAddress(contractAddress, agent.network),
47
+ amount: escrow.amount + " TON",
48
+ beneficiary: escrow.beneficiary,
49
+ friendlyBeneficiary: toFriendlyAddress(Address.parse(escrow.beneficiary), agent.network),
50
+ releaseTxHash: txHash,
51
+ };
52
+ },
53
+ });
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { definePlugin } from "@ton-agent-kit/core";
2
+ import { createEscrowAction } from "./actions/create-escrow";
3
+ import { depositToEscrowAction } from "./actions/deposit-to-escrow";
4
+ import { releaseEscrowAction } from "./actions/release-escrow";
5
+ import { refundEscrowAction } from "./actions/refund-escrow";
6
+ import { getEscrowInfoAction } from "./actions/get-escrow-info";
7
+
8
+ /**
9
+ * Escrow Plugin — TON escrow deal management
10
+ *
11
+ * Actions:
12
+ * - create_escrow: Create a new escrow deal
13
+ * - deposit_to_escrow: Fund an escrow with TON
14
+ * - release_escrow: Release funds to beneficiary
15
+ * - refund_escrow: Refund funds to depositor
16
+ * - get_escrow_info: Get escrow details or list all
17
+ */
18
+ const EscrowPlugin = definePlugin({
19
+ name: "escrow",
20
+ actions: [
21
+ createEscrowAction,
22
+ depositToEscrowAction,
23
+ releaseEscrowAction,
24
+ refundEscrowAction,
25
+ getEscrowInfoAction,
26
+ ],
27
+ });
28
+
29
+ export default EscrowPlugin;
30
+
31
+ export {
32
+ createEscrowAction,
33
+ depositToEscrowAction,
34
+ releaseEscrowAction,
35
+ refundEscrowAction,
36
+ getEscrowInfoAction,
37
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { Address, internal, toNano, beginCell } from "@ton/core";
3
+ import { TonClient4 } from "@ton/ton";
4
+ import type { AgentContext } from "@ton-agent-kit/core";
5
+ import {
6
+ Escrow,
7
+ storeDeploy,
8
+ storeDeposit,
9
+ storeRelease,
10
+ storeRefund,
11
+ } from "../../../contracts/output/Escrow_Escrow";
12
+ import { sendTransaction } from "@ton-agent-kit/core";
13
+
14
+ const ESCROW_FILE = ".escrow-store.json";
15
+
16
+ // ── JSON index (escrowId → metadata + contract address) ───────────────────
17
+
18
+ export interface EscrowRecord {
19
+ id: string;
20
+ contractAddress: string;
21
+ depositor: string;
22
+ beneficiary: string;
23
+ arbiter: string;
24
+ amount: string;
25
+ deadline: number;
26
+ deadlineISO: string;
27
+ description: string;
28
+ status: string;
29
+ createdAt: string;
30
+ }
31
+
32
+ export function loadEscrows(): Record<string, EscrowRecord> {
33
+ try {
34
+ if (existsSync(ESCROW_FILE)) {
35
+ return JSON.parse(readFileSync(ESCROW_FILE, "utf-8"));
36
+ }
37
+ } catch (err: any) {
38
+ console.error(`Failed to load escrow store: ${err.message}`);
39
+ }
40
+ return {};
41
+ }
42
+
43
+ export function saveEscrows(escrows: Record<string, EscrowRecord>): void {
44
+ try {
45
+ writeFileSync(ESCROW_FILE, JSON.stringify(escrows, null, 2), "utf-8");
46
+ } catch (err: any) {
47
+ console.error(`Failed to save escrow store: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ // ── On-chain helpers ──────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Deploy a new Escrow contract instance and return the contract address.
55
+ */
56
+ export async function deployEscrowContract(
57
+ agent: AgentContext,
58
+ depositor: Address,
59
+ beneficiary: Address,
60
+ arbiter: Address,
61
+ deadline: bigint,
62
+ ): Promise<Address> {
63
+ const escrow = await Escrow.fromInit(depositor, beneficiary, arbiter, deadline);
64
+
65
+ const deployBody = beginCell()
66
+ .store(storeDeploy({ $$type: "Deploy", queryId: BigInt(0) }))
67
+ .endCell();
68
+
69
+ await sendTransaction(agent, [
70
+ internal({
71
+ to: escrow.address,
72
+ value: toNano("0.05"),
73
+ bounce: false,
74
+ init: escrow.init!,
75
+ body: deployBody,
76
+ }),
77
+ ]);
78
+
79
+ return escrow.address;
80
+ }
81
+
82
+ /**
83
+ * Send a Deposit message (with TON attached) to an escrow contract.
84
+ */
85
+ export async function depositToContract(
86
+ agent: AgentContext,
87
+ contractAddress: Address,
88
+ amount: string,
89
+ ): Promise<void> {
90
+ const body = beginCell()
91
+ .store(storeDeposit({ $$type: "Deposit", queryId: BigInt(0) }))
92
+ .endCell();
93
+
94
+ await sendTransaction(agent, [
95
+ internal({
96
+ to: contractAddress,
97
+ value: toNano(amount),
98
+ bounce: true,
99
+ body,
100
+ }),
101
+ ]);
102
+ }
103
+
104
+ /**
105
+ * Send a Release message to an escrow contract.
106
+ */
107
+ export async function releaseContract(
108
+ agent: AgentContext,
109
+ contractAddress: Address,
110
+ ): Promise<void> {
111
+ const body = beginCell()
112
+ .store(storeRelease({ $$type: "Release", queryId: BigInt(0) }))
113
+ .endCell();
114
+
115
+ await sendTransaction(agent, [
116
+ internal({
117
+ to: contractAddress,
118
+ value: toNano("0.02"),
119
+ bounce: true,
120
+ body,
121
+ }),
122
+ ]);
123
+ }
124
+
125
+ /**
126
+ * Send a Refund message to an escrow contract.
127
+ */
128
+ export async function refundContract(
129
+ agent: AgentContext,
130
+ contractAddress: Address,
131
+ ): Promise<void> {
132
+ const body = beginCell()
133
+ .store(storeRefund({ $$type: "Refund", queryId: BigInt(0) }))
134
+ .endCell();
135
+
136
+ await sendTransaction(agent, [
137
+ internal({
138
+ to: contractAddress,
139
+ value: toNano("0.02"),
140
+ bounce: true,
141
+ body,
142
+ }),
143
+ ]);
144
+ }
145
+
146
+ /**
147
+ * Read on-chain escrow state via get method.
148
+ */
149
+ export async function getContractState(agent: AgentContext, contractAddress: Address) {
150
+ const client = new TonClient4({ endpoint: agent.rpcUrl });
151
+ const contract = client.open(Escrow.fromAddress(contractAddress));
152
+ const data = await contract.getEscrowData();
153
+ const balance = await contract.getBalance();
154
+ return { ...data, balance };
155
+ }
156
+
157
+ /**
158
+ * Look up the latest tx hash for an address via TonAPI.
159
+ */
160
+ export async function getLatestTxHash(
161
+ address: string,
162
+ network: "testnet" | "mainnet",
163
+ ): Promise<string> {
164
+ const apiBase =
165
+ network === "testnet"
166
+ ? "https://testnet.tonapi.io/v2"
167
+ : "https://tonapi.io/v2";
168
+ try {
169
+ const res = await fetch(
170
+ `${apiBase}/accounts/${encodeURIComponent(address)}/events?limit=1`,
171
+ );
172
+ const data = await res.json();
173
+ return data.events?.[0]?.event_id || "pending";
174
+ } catch {
175
+ return "pending";
176
+ }
177
+ }