@ton-agent-kit/plugin-escrow 1.0.3 → 1.1.1
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/README.md +61 -0
- package/package.json +1 -1
- package/src/actions/auto-release-escrow.ts +72 -0
- package/src/actions/claim-reward.ts +48 -0
- package/src/actions/confirm-delivery.ts +60 -0
- package/src/actions/create-escrow.ts +44 -26
- package/src/actions/fallback-settle.ts +51 -0
- package/src/actions/get-escrow-info.ts +22 -4
- package/src/actions/join-dispute.ts +56 -0
- package/src/actions/open-dispute.ts +56 -0
- package/src/actions/refund-escrow.ts +31 -0
- package/src/actions/release-escrow.ts +30 -0
- package/src/actions/seller-stake.ts +69 -0
- package/src/actions/vote-refund.ts +50 -0
- package/src/actions/vote-release.ts +50 -0
- package/src/contracts/Escrow_Escrow.code.boc +0 -0
- package/src/contracts/Escrow_Escrow.ts +887 -37
- package/src/index.ts +60 -6
- package/src/utils.ts +75 -84
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @ton-agent-kit/plugin-escrow
|
|
2
|
+
|
|
3
|
+
Escrow plugin for on-chain escrow with self-selecting arbiters, staking, and dispute-resolution voting on TON. Manages the full lifecycle of escrow deals from creation through settlement.
|
|
4
|
+
|
|
5
|
+
Part of [TON Agent Kit](https://github.com/Andy00L/ton-agent-kit).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ton-agent-kit/plugin-escrow @ton-agent-kit/core zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { TonAgentKit, KeypairWallet } from "@ton-agent-kit/core";
|
|
17
|
+
import EscrowPlugin from "@ton-agent-kit/plugin-escrow";
|
|
18
|
+
|
|
19
|
+
const agent = new TonAgentKit(wallet).use(EscrowPlugin);
|
|
20
|
+
|
|
21
|
+
// Create an escrow deal
|
|
22
|
+
const escrow = await agent.runAction("create_escrow", {
|
|
23
|
+
beneficiary: "EQBx...",
|
|
24
|
+
amount: "1.5",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Release funds after delivery
|
|
28
|
+
await agent.runAction("release_escrow", { escrowId: escrow.id });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Actions
|
|
32
|
+
|
|
33
|
+
| Action | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `create_escrow` | Create a new escrow deal (arbiters self-select during disputes). |
|
|
36
|
+
| `deposit_to_escrow` | Fund an escrow with TON. |
|
|
37
|
+
| `release_escrow` | Release funds to beneficiary (depositor only, non-dispute). |
|
|
38
|
+
| `refund_escrow` | Refund funds to depositor. |
|
|
39
|
+
| `get_escrow_info` | Get escrow details or list all escrows. |
|
|
40
|
+
| `confirm_delivery` | Confirm service delivery on-chain (buyer only). |
|
|
41
|
+
| `auto_release_escrow` | Release after deadline (requires delivery confirmation). |
|
|
42
|
+
| `open_dispute` | Open a dispute, freezing the escrow for arbiter voting. |
|
|
43
|
+
| `join_dispute` | Stake TON to join as an arbiter in a dispute. |
|
|
44
|
+
| `vote_release` | Arbiter votes to release funds during a dispute. |
|
|
45
|
+
| `vote_refund` | Arbiter votes to refund funds during a dispute. |
|
|
46
|
+
| `claim_reward` | Claim arbiter reward after settlement. |
|
|
47
|
+
| `fallback_settle` | Settle after the 72h voting deadline expires. |
|
|
48
|
+
| `seller_stake_escrow` | Seller stakes TON to signal commitment to the deal. |
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
See [docs/escrow-system.md](https://github.com/Andy00L/ton-agent-kit/blob/main/docs/escrow-system.md) for the full escrow system design.
|
|
53
|
+
|
|
54
|
+
## Links
|
|
55
|
+
|
|
56
|
+
- [GitHub](https://github.com/Andy00L/ton-agent-kit)
|
|
57
|
+
- [npm](https://www.npmjs.com/package/@ton-agent-kit/plugin-escrow)
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, saveEscrows, autoReleaseOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const autoReleaseEscrowAction = defineAction({
|
|
7
|
+
name: "auto_release_escrow",
|
|
8
|
+
description:
|
|
9
|
+
"Auto-release escrow funds to the beneficiary after the deadline has passed. Requires delivery confirmation from the buyer (confirm_delivery). Fails if delivery was never confirmed or if escrow is disputed.",
|
|
10
|
+
schema: z.object({
|
|
11
|
+
escrowId: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Escrow ID to auto-release after deadline"),
|
|
14
|
+
}),
|
|
15
|
+
handler: async (agent, params) => {
|
|
16
|
+
const escrows = loadEscrows();
|
|
17
|
+
const escrow = escrows[params.escrowId];
|
|
18
|
+
if (!escrow) throw new Error(`Escrow not found: ${params.escrowId}`);
|
|
19
|
+
|
|
20
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
21
|
+
|
|
22
|
+
await autoReleaseOnContract(agent, contractAddress);
|
|
23
|
+
|
|
24
|
+
escrow.status = "auto-released";
|
|
25
|
+
saveEscrows(escrows);
|
|
26
|
+
|
|
27
|
+
const txHash = await getLatestTxHash(
|
|
28
|
+
contractAddress.toRawString(),
|
|
29
|
+
agent.network,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Save pending bidirectional ratings (non-critical)
|
|
33
|
+
try {
|
|
34
|
+
await (agent as any).runAction("save_context", {
|
|
35
|
+
key: `pending_rating_${params.escrowId}_buyer`,
|
|
36
|
+
namespace: "pending_ratings",
|
|
37
|
+
value: JSON.stringify({
|
|
38
|
+
escrowId: params.escrowId,
|
|
39
|
+
raterRole: "buyer",
|
|
40
|
+
targetAddress: escrow.beneficiary,
|
|
41
|
+
suggestedSuccess: true,
|
|
42
|
+
escrowOutcome: "auto-released",
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
} catch {}
|
|
47
|
+
try {
|
|
48
|
+
await (agent as any).runAction("save_context", {
|
|
49
|
+
key: `pending_rating_${params.escrowId}_seller`,
|
|
50
|
+
namespace: "pending_ratings",
|
|
51
|
+
value: JSON.stringify({
|
|
52
|
+
escrowId: params.escrowId,
|
|
53
|
+
raterRole: "seller",
|
|
54
|
+
targetAddress: escrow.depositor,
|
|
55
|
+
suggestedSuccess: true,
|
|
56
|
+
escrowOutcome: "auto-released",
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
released: true,
|
|
64
|
+
escrowId: params.escrowId,
|
|
65
|
+
contractAddress: escrow.contractAddress,
|
|
66
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
67
|
+
beneficiary: escrow.beneficiary,
|
|
68
|
+
autoReleaseTxHash: txHash,
|
|
69
|
+
message: `Escrow ${params.escrowId} auto-released to beneficiary after deadline.`,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, claimRewardOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const claimRewardAction = defineAction({
|
|
7
|
+
name: "claim_reward",
|
|
8
|
+
description:
|
|
9
|
+
"Claim arbiter reward after an escrow dispute is settled. Winners get back their stake + share of losers' stakes. Losers forfeit their stake.",
|
|
10
|
+
schema: z.object({
|
|
11
|
+
escrowId: z.string().describe("Escrow ID to claim arbiter reward from"),
|
|
12
|
+
}),
|
|
13
|
+
handler: async (agent, params) => {
|
|
14
|
+
const escrows = loadEscrows();
|
|
15
|
+
const escrow = escrows[params.escrowId];
|
|
16
|
+
if (!escrow) {
|
|
17
|
+
return { claimed: false, message: `Escrow not found: ${params.escrowId}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await claimRewardOnContract(agent, contractAddress);
|
|
24
|
+
|
|
25
|
+
const txHash = await getLatestTxHash(
|
|
26
|
+
agent.wallet.address.toRawString(),
|
|
27
|
+
agent.network,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
claimed: true,
|
|
32
|
+
escrowId: params.escrowId,
|
|
33
|
+
contractAddress: escrow.contractAddress,
|
|
34
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
35
|
+
arbiter: agent.wallet.address.toRawString(),
|
|
36
|
+
claimTxHash: txHash,
|
|
37
|
+
message: `Claim submitted for escrow ${params.escrowId}. If you voted with the majority, reward is sent to your wallet.`,
|
|
38
|
+
};
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
return {
|
|
41
|
+
claimed: false,
|
|
42
|
+
escrowId: params.escrowId,
|
|
43
|
+
error: err.message,
|
|
44
|
+
message: `Failed to claim reward: ${err.message}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, saveEscrows, confirmDeliveryOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const confirmDeliveryAction = defineAction({
|
|
7
|
+
name: "confirm_delivery",
|
|
8
|
+
description:
|
|
9
|
+
"Confirm that a service was delivered for an escrow deal. Only the depositor (buyer) can call this. Once confirmed, auto-release becomes available after the deadline. The x402TxHash serves as an audit trail linking the payment to the delivery.",
|
|
10
|
+
schema: z.object({
|
|
11
|
+
escrowId: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Escrow ID to confirm delivery for"),
|
|
14
|
+
x402TxHash: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Optional x402 payment TX hash as proof of service delivery"),
|
|
18
|
+
}),
|
|
19
|
+
handler: async (agent, params) => {
|
|
20
|
+
const escrows = loadEscrows();
|
|
21
|
+
const escrow = escrows[params.escrowId];
|
|
22
|
+
if (!escrow) {
|
|
23
|
+
return {
|
|
24
|
+
confirmed: false,
|
|
25
|
+
message: `Escrow not found: ${params.escrowId}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await confirmDeliveryOnContract(agent, contractAddress, params.x402TxHash || "");
|
|
33
|
+
|
|
34
|
+
escrow.status = "delivery-confirmed";
|
|
35
|
+
saveEscrows(escrows);
|
|
36
|
+
|
|
37
|
+
const txHash = await getLatestTxHash(
|
|
38
|
+
contractAddress.toRawString(),
|
|
39
|
+
agent.network,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
confirmed: true,
|
|
44
|
+
escrowId: params.escrowId,
|
|
45
|
+
contractAddress: escrow.contractAddress,
|
|
46
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
47
|
+
x402TxHash: params.x402TxHash || null,
|
|
48
|
+
confirmTxHash: txHash,
|
|
49
|
+
message: `Delivery confirmed on-chain for escrow ${params.escrowId}. Auto-release will be available after deadline.`,
|
|
50
|
+
};
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
return {
|
|
53
|
+
confirmed: false,
|
|
54
|
+
escrowId: params.escrowId,
|
|
55
|
+
error: err.message,
|
|
56
|
+
message: `Failed to confirm delivery: ${err.message}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { Address } from "@ton/core";
|
|
2
|
+
import { Address, toNano } from "@ton/core";
|
|
3
3
|
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
4
|
import {
|
|
5
5
|
loadEscrows,
|
|
@@ -7,11 +7,14 @@ import {
|
|
|
7
7
|
deployEscrowContract,
|
|
8
8
|
type EscrowRecord,
|
|
9
9
|
} from "../utils";
|
|
10
|
+
import { resolveContractAddress } from "../../../plugin-identity/src/reputation-config";
|
|
10
11
|
|
|
11
12
|
export const createEscrowAction = defineAction<
|
|
12
13
|
{
|
|
13
14
|
beneficiary: string;
|
|
14
15
|
amount: string;
|
|
16
|
+
minArbiters?: number;
|
|
17
|
+
minStake?: string;
|
|
15
18
|
description?: string;
|
|
16
19
|
deadlineTimestamp?: number;
|
|
17
20
|
deadlineDays?: number;
|
|
@@ -23,31 +26,24 @@ export const createEscrowAction = defineAction<
|
|
|
23
26
|
name: "create_escrow",
|
|
24
27
|
description:
|
|
25
28
|
"Create a new on-chain escrow deal. Deploys a Tact Escrow contract to TON. " +
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
+
"No arbiters needed upfront — they self-select by staking during disputes. " +
|
|
30
|
+
"minArbiters sets the minimum arbiters needed for voting (default 3). " +
|
|
31
|
+
"minStake sets the minimum stake per arbiter in TON (default 0.5).",
|
|
29
32
|
schema: z.object({
|
|
30
33
|
beneficiary: z
|
|
31
34
|
.string()
|
|
32
35
|
.describe("Address of the beneficiary (who receives funds)"),
|
|
33
36
|
amount: z.string().describe("Amount of TON to escrow"),
|
|
37
|
+
minArbiters: z.coerce.number().optional().describe("Minimum arbiters for dispute voting. Default 3."),
|
|
38
|
+
minStake: z.string().optional().describe("Minimum stake per arbiter in TON. Default 0.5."),
|
|
39
|
+
requireRepCollateral: z.boolean().optional().describe("Require reputation-based collateral from seller. Default false."),
|
|
40
|
+
minRepScore: z.coerce.number().optional().describe("Minimum rep score for seller (0-100). Default 30. Only if requireRepCollateral=true."),
|
|
41
|
+
baseSellerStake: z.string().optional().describe("Base seller stake in TON before rep adjustment. Default 0.5. Only if requireRepCollateral=true."),
|
|
34
42
|
description: z.string().optional().describe("Description of the deal"),
|
|
35
|
-
deadlineTimestamp: z.coerce
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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."),
|
|
43
|
+
deadlineTimestamp: z.coerce.number().optional().describe("Exact deadline as Unix timestamp (seconds)."),
|
|
44
|
+
deadlineDays: z.coerce.number().optional().describe("Deadline in days from now"),
|
|
45
|
+
deadlineHours: z.coerce.number().optional().describe("Deadline in hours from now"),
|
|
46
|
+
deadlineMinutes: z.coerce.number().optional().describe("Deadline in minutes from now. Default 60."),
|
|
51
47
|
}) as any,
|
|
52
48
|
handler: async (agent, params) => {
|
|
53
49
|
const escrowId = `escrow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -66,27 +62,42 @@ export const createEscrowAction = defineAction<
|
|
|
66
62
|
|
|
67
63
|
const depositor = agent.wallet.address;
|
|
68
64
|
const beneficiary = Address.parse(params.beneficiary);
|
|
69
|
-
const
|
|
65
|
+
const minArbiters = params.minArbiters || 3;
|
|
66
|
+
const minStakeNano = toNano(params.minStake || "0.5");
|
|
67
|
+
|
|
68
|
+
// Layer 3+4: Rep collateral
|
|
69
|
+
const requireRepCollateral = params.requireRepCollateral || false;
|
|
70
|
+
const minRepScore = params.minRepScore || 30;
|
|
71
|
+
const baseSellerStake = params.baseSellerStake || "0.5";
|
|
72
|
+
const baseSellerStakeNano = toNano(baseSellerStake);
|
|
73
|
+
|
|
74
|
+
// Resolve reputation contract for cross-contract notifications
|
|
75
|
+
const repAddr = resolveContractAddress(undefined, agent.network);
|
|
76
|
+
const reputationContract = repAddr ? Address.parse(repAddr) : depositor; // fallback
|
|
70
77
|
|
|
71
|
-
// Deploy a new Escrow contract on-chain
|
|
72
78
|
const contractAddress = await deployEscrowContract(
|
|
73
79
|
agent,
|
|
74
80
|
depositor,
|
|
75
81
|
beneficiary,
|
|
76
|
-
arbiter,
|
|
77
82
|
BigInt(deadline),
|
|
83
|
+
BigInt(minArbiters),
|
|
84
|
+
minStakeNano,
|
|
85
|
+
reputationContract,
|
|
86
|
+
requireRepCollateral,
|
|
87
|
+
BigInt(minRepScore),
|
|
88
|
+
baseSellerStakeNano,
|
|
78
89
|
);
|
|
79
90
|
|
|
80
|
-
// Save to JSON index (escrowId → contract address mapping)
|
|
81
91
|
const escrow: EscrowRecord = {
|
|
82
92
|
id: escrowId,
|
|
83
93
|
contractAddress: contractAddress.toRawString(),
|
|
84
94
|
depositor: depositor.toRawString(),
|
|
85
95
|
beneficiary: params.beneficiary,
|
|
86
|
-
arbiter: arbiter.toRawString(),
|
|
87
96
|
amount: params.amount,
|
|
88
97
|
deadline,
|
|
89
98
|
deadlineISO: new Date(deadline * 1000).toISOString(),
|
|
99
|
+
minArbiters,
|
|
100
|
+
minStake: params.minStake || "0.5",
|
|
90
101
|
description: params.description || "",
|
|
91
102
|
status: "created",
|
|
92
103
|
createdAt: new Date().toISOString(),
|
|
@@ -103,10 +114,17 @@ export const createEscrowAction = defineAction<
|
|
|
103
114
|
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
104
115
|
beneficiary: params.beneficiary,
|
|
105
116
|
friendlyBeneficiary: toFriendlyAddress(beneficiary, agent.network),
|
|
117
|
+
minArbiters,
|
|
118
|
+
minStake: params.minStake || "0.5",
|
|
106
119
|
amount: params.amount + " TON",
|
|
107
120
|
deadline: escrow.deadlineISO,
|
|
121
|
+
requireRepCollateral,
|
|
122
|
+
minRepScore: requireRepCollateral ? minRepScore : "disabled",
|
|
123
|
+
baseSellerStake: requireRepCollateral ? baseSellerStake + " TON" : "not required",
|
|
108
124
|
description: params.description || "",
|
|
109
|
-
nextStep:
|
|
125
|
+
nextStep: requireRepCollateral
|
|
126
|
+
? `Seller must stake first using seller_stake_escrow, then deposit ${params.amount} TON`
|
|
127
|
+
: `Deposit ${params.amount} TON using deposit_to_escrow with escrowId: ${escrowId}`,
|
|
110
128
|
};
|
|
111
129
|
},
|
|
112
130
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, saveEscrows, fallbackSettleOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const fallbackSettleAction = defineAction({
|
|
7
|
+
name: "fallback_settle",
|
|
8
|
+
description:
|
|
9
|
+
"Settle a disputed escrow after the 72h voting deadline. If not enough arbiters joined or votes are tied, " +
|
|
10
|
+
"settles based on delivery status. Anyone can call after votingDeadline.",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
escrowId: z.string().describe("Escrow ID to settle after voting timeout"),
|
|
13
|
+
}),
|
|
14
|
+
handler: async (agent, params) => {
|
|
15
|
+
const escrows = loadEscrows();
|
|
16
|
+
const escrow = escrows[params.escrowId];
|
|
17
|
+
if (!escrow) {
|
|
18
|
+
return { settled: false, message: `Escrow not found: ${params.escrowId}` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await fallbackSettleOnContract(agent, contractAddress);
|
|
25
|
+
|
|
26
|
+
escrow.status = "fallback-settled";
|
|
27
|
+
saveEscrows(escrows);
|
|
28
|
+
|
|
29
|
+
const txHash = await getLatestTxHash(
|
|
30
|
+
contractAddress.toRawString(),
|
|
31
|
+
agent.network,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
settled: true,
|
|
36
|
+
escrowId: params.escrowId,
|
|
37
|
+
contractAddress: escrow.contractAddress,
|
|
38
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
39
|
+
settleTxHash: txHash,
|
|
40
|
+
message: `Escrow ${params.escrowId} settled via fallback after voting deadline.`,
|
|
41
|
+
};
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
return {
|
|
44
|
+
settled: false,
|
|
45
|
+
escrowId: params.escrowId,
|
|
46
|
+
error: err.message,
|
|
47
|
+
message: `Fallback settle failed: ${err.message}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -35,19 +35,37 @@ export const getEscrowInfoAction = defineAction<{ escrowId?: string }, any>({
|
|
|
35
35
|
onChain: {
|
|
36
36
|
depositor: toFriendlyAddress(onChain.depositor, agent.network),
|
|
37
37
|
beneficiary: toFriendlyAddress(onChain.beneficiary, agent.network),
|
|
38
|
-
|
|
38
|
+
reputationContract: toFriendlyAddress(onChain.reputationContract, agent.network),
|
|
39
39
|
amount: fromNano(onChain.amount) + " TON",
|
|
40
40
|
balance: fromNano(onChain.balance) + " TON",
|
|
41
41
|
deadline: new Date(Number(onChain.deadline) * 1000).toISOString(),
|
|
42
42
|
released: onChain.released,
|
|
43
43
|
refunded: onChain.refunded,
|
|
44
|
+
deliveryConfirmed: onChain.deliveryConfirmed,
|
|
45
|
+
disputed: onChain.disputed,
|
|
46
|
+
votingDeadline: onChain.votingDeadline ? new Date(Number(onChain.votingDeadline) * 1000).toISOString() : null,
|
|
47
|
+
arbiterCount: Number(onChain.arbiterCount),
|
|
48
|
+
votesRelease: Number(onChain.votesRelease),
|
|
49
|
+
votesRefund: Number(onChain.votesRefund),
|
|
50
|
+
minArbiters: Number(onChain.minArbiters),
|
|
51
|
+
minStake: fromNano(onChain.minStake) + " TON",
|
|
52
|
+
sellerStake: fromNano(onChain.sellerStake) + " TON",
|
|
53
|
+
sellerStaked: onChain.sellerStaked,
|
|
54
|
+
requireSellerStake: onChain.requireSellerStake,
|
|
55
|
+
baseSellerStake: fromNano(onChain.baseSellerStake) + " TON",
|
|
56
|
+
requireRepCollateral: onChain.requireRepCollateral,
|
|
57
|
+
minRepScore: Number(onChain.minRepScore),
|
|
58
|
+
autoReleaseAvailable: onChain.autoReleaseAvailable,
|
|
59
|
+
refundAvailable: onChain.refundAvailable,
|
|
44
60
|
status: onChain.released
|
|
45
61
|
? "released"
|
|
46
62
|
: onChain.refunded
|
|
47
63
|
? "refunded"
|
|
48
|
-
: onChain.
|
|
49
|
-
? "
|
|
50
|
-
:
|
|
64
|
+
: onChain.disputed
|
|
65
|
+
? "disputed"
|
|
66
|
+
: onChain.amount > BigInt(0)
|
|
67
|
+
? "funded"
|
|
68
|
+
: "created",
|
|
51
69
|
},
|
|
52
70
|
};
|
|
53
71
|
} catch (e: any) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, joinDisputeOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const joinDisputeAction = defineAction({
|
|
7
|
+
name: "join_dispute",
|
|
8
|
+
description:
|
|
9
|
+
"Join an escrow dispute as an arbiter by staking TON. Self-selecting: any agent can join (except depositor/beneficiary). " +
|
|
10
|
+
"A 2% fee is deducted from the stake. Winners get back their stake + share of losers' stakes. Losers forfeit.",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
escrowId: z.string().describe("Escrow ID to join as arbiter"),
|
|
13
|
+
stake: z.string().optional().describe("Stake amount in TON (sent to contract, 2% fee deducted). Default 0.5."),
|
|
14
|
+
}),
|
|
15
|
+
handler: async (agent, params) => {
|
|
16
|
+
const escrows = loadEscrows();
|
|
17
|
+
const escrow = escrows[params.escrowId];
|
|
18
|
+
if (!escrow) {
|
|
19
|
+
return { joined: false, message: `Escrow not found: ${params.escrowId}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
23
|
+
const stakeAmount = params.stake || escrow.minStake || "0.5";
|
|
24
|
+
|
|
25
|
+
// Add 3% buffer to cover the 2% fee + gas
|
|
26
|
+
const stakeNum = parseFloat(stakeAmount);
|
|
27
|
+
const withBuffer = (stakeNum * 1.03).toFixed(9);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await joinDisputeOnContract(agent, contractAddress, withBuffer);
|
|
31
|
+
|
|
32
|
+
const txHash = await getLatestTxHash(
|
|
33
|
+
agent.wallet.address.toRawString(),
|
|
34
|
+
agent.network,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
joined: true,
|
|
39
|
+
escrowId: params.escrowId,
|
|
40
|
+
contractAddress: escrow.contractAddress,
|
|
41
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
42
|
+
arbiter: agent.wallet.address.toRawString(),
|
|
43
|
+
stakeAmount: stakeAmount + " TON",
|
|
44
|
+
stakeTxHash: txHash,
|
|
45
|
+
message: `Joined dispute on escrow ${params.escrowId} with ${stakeAmount} TON stake. Vote with vote_release or vote_refund.`,
|
|
46
|
+
};
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
return {
|
|
49
|
+
joined: false,
|
|
50
|
+
escrowId: params.escrowId,
|
|
51
|
+
error: err.message,
|
|
52
|
+
message: `Failed to join dispute: ${err.message}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, saveEscrows, openDisputeOnContract, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const openDisputeAction = defineAction({
|
|
7
|
+
name: "open_dispute",
|
|
8
|
+
description:
|
|
9
|
+
"Open a dispute on an escrow deal. Freezes the escrow so only arbiter voting (or single-arbiter decision) can settle it. Only the depositor or beneficiary can open a dispute.",
|
|
10
|
+
schema: z.object({
|
|
11
|
+
escrowId: z.string().describe("Escrow ID to open a dispute on"),
|
|
12
|
+
}),
|
|
13
|
+
handler: async (agent, params) => {
|
|
14
|
+
const escrows = loadEscrows();
|
|
15
|
+
const escrow = escrows[params.escrowId];
|
|
16
|
+
if (!escrow) {
|
|
17
|
+
return { disputed: false, message: `Escrow not found: ${params.escrowId}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await openDisputeOnContract(agent, contractAddress);
|
|
24
|
+
|
|
25
|
+
escrow.status = "disputed";
|
|
26
|
+
saveEscrows(escrows);
|
|
27
|
+
|
|
28
|
+
const txHash = await getLatestTxHash(
|
|
29
|
+
agent.wallet.address.toRawString(),
|
|
30
|
+
agent.network,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
disputed: true,
|
|
35
|
+
escrowId: params.escrowId,
|
|
36
|
+
contractAddress: escrow.contractAddress,
|
|
37
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
38
|
+
arbiterCount: escrow.arbiterCount || 1,
|
|
39
|
+
arbiters: escrow.arbiters || [escrow.arbiter],
|
|
40
|
+
disputeTxHash: txHash,
|
|
41
|
+
message: `Dispute opened on escrow ${params.escrowId}. ${
|
|
42
|
+
(escrow.arbiterCount || 1) > 1
|
|
43
|
+
? `Waiting for ${Math.floor((escrow.arbiterCount || 1) / 2) + 1}/${escrow.arbiterCount} arbiter votes.`
|
|
44
|
+
: "Single arbiter can now release or refund."
|
|
45
|
+
}`,
|
|
46
|
+
};
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
return {
|
|
49
|
+
disputed: false,
|
|
50
|
+
escrowId: params.escrowId,
|
|
51
|
+
error: err.message,
|
|
52
|
+
message: `Failed to open dispute: ${err.message}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -44,6 +44,37 @@ export const refundEscrowAction = defineAction<{ escrowId: string }, any>({
|
|
|
44
44
|
escrow.status = "refunded";
|
|
45
45
|
saveEscrows(escrows);
|
|
46
46
|
|
|
47
|
+
// Save pending bidirectional ratings (non-critical)
|
|
48
|
+
// Refund = deal failed — negative ratings for both
|
|
49
|
+
try {
|
|
50
|
+
await (agent as any).runAction("save_context", {
|
|
51
|
+
key: `pending_rating_${params.escrowId}_buyer`,
|
|
52
|
+
namespace: "pending_ratings",
|
|
53
|
+
value: JSON.stringify({
|
|
54
|
+
escrowId: params.escrowId,
|
|
55
|
+
raterRole: "buyer",
|
|
56
|
+
targetAddress: escrow.beneficiary,
|
|
57
|
+
suggestedSuccess: false,
|
|
58
|
+
escrowOutcome: "refunded",
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
} catch {}
|
|
63
|
+
try {
|
|
64
|
+
await (agent as any).runAction("save_context", {
|
|
65
|
+
key: `pending_rating_${params.escrowId}_seller`,
|
|
66
|
+
namespace: "pending_ratings",
|
|
67
|
+
value: JSON.stringify({
|
|
68
|
+
escrowId: params.escrowId,
|
|
69
|
+
raterRole: "seller",
|
|
70
|
+
targetAddress: escrow.depositor,
|
|
71
|
+
suggestedSuccess: false,
|
|
72
|
+
escrowOutcome: "refunded",
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
47
78
|
return {
|
|
48
79
|
escrowId: params.escrowId,
|
|
49
80
|
status: "refunded (on-chain)",
|
|
@@ -39,6 +39,36 @@ export const releaseEscrowAction = defineAction<{ escrowId: string }, any>({
|
|
|
39
39
|
escrow.status = "released";
|
|
40
40
|
saveEscrows(escrows);
|
|
41
41
|
|
|
42
|
+
// Save pending bidirectional ratings (non-critical)
|
|
43
|
+
try {
|
|
44
|
+
await (agent as any).runAction("save_context", {
|
|
45
|
+
key: `pending_rating_${params.escrowId}_buyer`,
|
|
46
|
+
namespace: "pending_ratings",
|
|
47
|
+
value: JSON.stringify({
|
|
48
|
+
escrowId: params.escrowId,
|
|
49
|
+
raterRole: "buyer",
|
|
50
|
+
targetAddress: escrow.beneficiary,
|
|
51
|
+
suggestedSuccess: true,
|
|
52
|
+
escrowOutcome: "released",
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
} catch {}
|
|
57
|
+
try {
|
|
58
|
+
await (agent as any).runAction("save_context", {
|
|
59
|
+
key: `pending_rating_${params.escrowId}_seller`,
|
|
60
|
+
namespace: "pending_ratings",
|
|
61
|
+
value: JSON.stringify({
|
|
62
|
+
escrowId: params.escrowId,
|
|
63
|
+
raterRole: "seller",
|
|
64
|
+
targetAddress: escrow.depositor,
|
|
65
|
+
suggestedSuccess: true,
|
|
66
|
+
escrowOutcome: "released",
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
42
72
|
return {
|
|
43
73
|
escrowId: params.escrowId,
|
|
44
74
|
status: "released (on-chain)",
|