@ton-agent-kit/plugin-escrow 1.0.3 → 1.1.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 +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/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)",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Address, fromNano } from "@ton/core";
|
|
3
|
+
import { defineAction, toFriendlyAddress } from "@ton-agent-kit/core";
|
|
4
|
+
import { loadEscrows, sellerStakeOnContract, getContractState, getLatestTxHash } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const sellerStakeAction = defineAction({
|
|
7
|
+
name: "seller_stake_escrow",
|
|
8
|
+
description:
|
|
9
|
+
"Seller (beneficiary) deposits their stake into an escrow that requires reputation collateral. " +
|
|
10
|
+
"Must be called before buyer deposits. Stake amount can be adjusted by reputation: " +
|
|
11
|
+
"score 90-100 = 50% of base, 60-89 = 100%, 30-59 = 150%, below minRepScore = blocked.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
escrowId: z.string().optional().describe("Escrow ID"),
|
|
14
|
+
contractAddress: z.string().optional().describe("Contract address (alternative to escrowId)"),
|
|
15
|
+
stakeAmount: z.string().optional().describe("Stake amount in TON. If not provided, uses baseSellerStake from escrow."),
|
|
16
|
+
}),
|
|
17
|
+
handler: async (agent, params) => {
|
|
18
|
+
const escrows = loadEscrows();
|
|
19
|
+
let escrow: any;
|
|
20
|
+
if (params.escrowId) {
|
|
21
|
+
escrow = escrows[params.escrowId];
|
|
22
|
+
} else if (params.contractAddress) {
|
|
23
|
+
escrow = Object.values(escrows).find((e: any) => e.contractAddress === params.contractAddress);
|
|
24
|
+
}
|
|
25
|
+
if (!escrow) {
|
|
26
|
+
return { staked: false, message: "Escrow not found" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const contractAddress = Address.parse(escrow.contractAddress);
|
|
30
|
+
|
|
31
|
+
// Read on-chain state to check requirements
|
|
32
|
+
try {
|
|
33
|
+
const state = await getContractState(agent, contractAddress);
|
|
34
|
+
|
|
35
|
+
if (!state.requireSellerStake) {
|
|
36
|
+
return { staked: false, message: "This escrow does not require seller stake" };
|
|
37
|
+
}
|
|
38
|
+
if (state.sellerStaked) {
|
|
39
|
+
return { staked: false, message: "Seller has already staked" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stakeAmount = params.stakeAmount || fromNano(state.baseSellerStake);
|
|
43
|
+
|
|
44
|
+
await sellerStakeOnContract(agent, contractAddress, stakeAmount);
|
|
45
|
+
|
|
46
|
+
const txHash = await getLatestTxHash(
|
|
47
|
+
agent.wallet.address.toRawString(),
|
|
48
|
+
agent.network,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
staked: true,
|
|
53
|
+
amount: stakeAmount + " TON",
|
|
54
|
+
escrowId: escrow.id,
|
|
55
|
+
contractAddress: escrow.contractAddress,
|
|
56
|
+
friendlyContract: toFriendlyAddress(contractAddress, agent.network),
|
|
57
|
+
stakeTxHash: txHash,
|
|
58
|
+
message: `Seller staked ${stakeAmount} TON. Buyer can now deposit.`,
|
|
59
|
+
};
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
return {
|
|
62
|
+
staked: false,
|
|
63
|
+
escrowId: escrow.id,
|
|
64
|
+
error: err.message,
|
|
65
|
+
message: `Failed to stake: ${err.message}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|