pay-lobster 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/README.md +401 -0
- package/README.md.bak +401 -0
- package/dist/agent.d.ts +132 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +224 -0
- package/dist/agent.js.map +1 -0
- package/dist/analytics.d.ts +120 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +345 -0
- package/dist/analytics.js.map +1 -0
- package/dist/approvals.d.ts +168 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +406 -0
- package/dist/approvals.js.map +1 -0
- package/dist/circle-client.d.ts +152 -0
- package/dist/circle-client.d.ts.map +1 -0
- package/dist/circle-client.js +266 -0
- package/dist/circle-client.js.map +1 -0
- package/dist/commission.d.ts +191 -0
- package/dist/commission.d.ts.map +1 -0
- package/dist/commission.js +475 -0
- package/dist/commission.js.map +1 -0
- package/dist/condition-builder.d.ts +98 -0
- package/dist/condition-builder.d.ts.map +1 -0
- package/dist/condition-builder.js +193 -0
- package/dist/condition-builder.js.map +1 -0
- package/dist/contacts.d.ts +179 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +445 -0
- package/dist/contacts.js.map +1 -0
- package/dist/easy.d.ts +22 -0
- package/dist/easy.d.ts.map +1 -0
- package/dist/easy.js +40 -0
- package/dist/easy.js.map +1 -0
- package/dist/erc8004/constants.d.ts +152 -0
- package/dist/erc8004/constants.d.ts.map +1 -0
- package/dist/erc8004/constants.js +114 -0
- package/dist/erc8004/constants.js.map +1 -0
- package/dist/erc8004/discovery.d.ts +84 -0
- package/dist/erc8004/discovery.d.ts.map +1 -0
- package/dist/erc8004/discovery.js +217 -0
- package/dist/erc8004/discovery.js.map +1 -0
- package/dist/erc8004/identity.d.ts +91 -0
- package/dist/erc8004/identity.d.ts.map +1 -0
- package/dist/erc8004/identity.js +250 -0
- package/dist/erc8004/identity.js.map +1 -0
- package/dist/erc8004/index.d.ts +147 -0
- package/dist/erc8004/index.d.ts.map +1 -0
- package/dist/erc8004/index.js +225 -0
- package/dist/erc8004/index.js.map +1 -0
- package/dist/erc8004/reputation.d.ts +133 -0
- package/dist/erc8004/reputation.d.ts.map +1 -0
- package/dist/erc8004/reputation.js +277 -0
- package/dist/erc8004/reputation.js.map +1 -0
- package/dist/escrow-templates.d.ts +38 -0
- package/dist/escrow-templates.d.ts.map +1 -0
- package/dist/escrow-templates.js +419 -0
- package/dist/escrow-templates.js.map +1 -0
- package/dist/escrow.d.ts +320 -0
- package/dist/escrow.d.ts.map +1 -0
- package/dist/escrow.js +854 -0
- package/dist/escrow.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/invoices.d.ts +212 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +393 -0
- package/dist/invoices.js.map +1 -0
- package/dist/notifications.d.ts +141 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +350 -0
- package/dist/notifications.js.map +1 -0
- package/dist/tips.d.ts +171 -0
- package/dist/tips.d.ts.map +1 -0
- package/dist/tips.js +390 -0
- package/dist/tips.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/x402-client.d.ts +127 -0
- package/dist/x402-client.d.ts.map +1 -0
- package/dist/x402-client.js +350 -0
- package/dist/x402-client.js.map +1 -0
- package/dist/x402-server.d.ts +133 -0
- package/dist/x402-server.d.ts.map +1 -0
- package/dist/x402-server.js +330 -0
- package/dist/x402-server.js.map +1 -0
- package/lib/agent.ts +273 -0
- package/lib/analytics.ts +474 -0
- package/lib/analytics.ts.bak +474 -0
- package/lib/approvals.ts +585 -0
- package/lib/approvals.ts.bak +585 -0
- package/lib/circle-client.ts +376 -0
- package/lib/circle-client.ts.bak +376 -0
- package/lib/commission.ts +680 -0
- package/lib/commission.ts.bak +680 -0
- package/lib/condition-builder.ts +223 -0
- package/lib/condition-builder.ts.bak +223 -0
- package/lib/contacts.ts +615 -0
- package/lib/contacts.ts.bak +615 -0
- package/lib/easy.ts +46 -0
- package/lib/easy.ts.bak +352 -0
- package/lib/erc8004/constants.ts +175 -0
- package/lib/erc8004/discovery.ts +299 -0
- package/lib/erc8004/identity.ts +327 -0
- package/lib/erc8004/index.ts +285 -0
- package/lib/erc8004/reputation.ts +368 -0
- package/lib/escrow-templates.ts +462 -0
- package/lib/escrow.ts +1216 -0
- package/lib/index.ts +13 -0
- package/lib/invoices.ts +588 -0
- package/lib/notifications.ts +484 -0
- package/lib/tips.ts +570 -0
- package/lib/types.ts +108 -0
- package/lib/x402-client.ts +471 -0
- package/lib/x402-server.ts +462 -0
- package/package.json +58 -0
package/lib/escrow.ts
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Escrow Module (Escrow as a Service - EaaS)
|
|
3
|
+
*
|
|
4
|
+
* Smart contract-style escrow for multiple verticals:
|
|
5
|
+
* - Real estate (earnest money, security deposits, closing)
|
|
6
|
+
* - Freelance/Services (milestones, deliverables)
|
|
7
|
+
* - Commerce (purchases, trades, swaps)
|
|
8
|
+
* - P2P (OTC trades, loans)
|
|
9
|
+
* - Digital (licenses, subscriptions, content)
|
|
10
|
+
* - Custom (user-defined)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import type { EscrowTemplate, EscrowVertical } from './escrow-templates';
|
|
17
|
+
import { getTemplate } from './escrow-templates';
|
|
18
|
+
import type { BuiltCondition } from './condition-builder';
|
|
19
|
+
|
|
20
|
+
export type EscrowType =
|
|
21
|
+
// Real Estate
|
|
22
|
+
| 'earnest_money'
|
|
23
|
+
| 'security_deposit'
|
|
24
|
+
| 'closing_funds'
|
|
25
|
+
// Freelance / Services
|
|
26
|
+
| 'milestone'
|
|
27
|
+
| 'freelance'
|
|
28
|
+
// Commerce
|
|
29
|
+
| 'purchase'
|
|
30
|
+
| 'trade'
|
|
31
|
+
// Custom
|
|
32
|
+
| 'general';
|
|
33
|
+
|
|
34
|
+
export type EscrowStatus =
|
|
35
|
+
| 'created'
|
|
36
|
+
| 'funded'
|
|
37
|
+
| 'pending_release'
|
|
38
|
+
| 'released'
|
|
39
|
+
| 'refunded'
|
|
40
|
+
| 'disputed'
|
|
41
|
+
| 'cancelled';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Standard roles across verticals
|
|
45
|
+
*/
|
|
46
|
+
export type StandardRole =
|
|
47
|
+
| 'depositor' | 'recipient' // Universal
|
|
48
|
+
| 'buyer' | 'seller' // Commerce
|
|
49
|
+
| 'client' | 'provider' // Services
|
|
50
|
+
| 'landlord' | 'tenant' // Real estate
|
|
51
|
+
| 'agent' | 'title_company' // Real estate agents
|
|
52
|
+
| 'arbiter' | 'witness' // Dispute resolution
|
|
53
|
+
| 'lender' | 'borrower' // P2P lending
|
|
54
|
+
| 'party_a' | 'party_b' // Generic parties
|
|
55
|
+
| 'marketplace' | 'platform' // Third parties
|
|
56
|
+
| 'consultant' | 'contractor' | 'inspector' | 'vendor' | 'subscriber' | 'creator'; // Specific roles
|
|
57
|
+
|
|
58
|
+
export interface EscrowParty {
|
|
59
|
+
role: StandardRole | string; // Standard or custom role
|
|
60
|
+
name: string;
|
|
61
|
+
email?: string;
|
|
62
|
+
phone?: string;
|
|
63
|
+
walletAddress?: string;
|
|
64
|
+
sessionId?: string; // For Clawdbot agent approval
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface EscrowCondition {
|
|
68
|
+
id: string;
|
|
69
|
+
description: string;
|
|
70
|
+
type:
|
|
71
|
+
// Real Estate
|
|
72
|
+
| 'inspection' | 'financing' | 'appraisal' | 'title' | 'closing' | 'move_out'
|
|
73
|
+
// Freelance/Milestones
|
|
74
|
+
| 'milestone' | 'delivery' | 'approval' | 'revision'
|
|
75
|
+
// Commerce
|
|
76
|
+
| 'shipping' | 'receipt' | 'verification'
|
|
77
|
+
// Document/Time-based
|
|
78
|
+
| 'document' | 'deadline'
|
|
79
|
+
// Custom
|
|
80
|
+
| 'custom';
|
|
81
|
+
status: 'pending' | 'satisfied' | 'waived' | 'failed';
|
|
82
|
+
deadline?: string;
|
|
83
|
+
satisfiedAt?: string;
|
|
84
|
+
satisfiedBy?: string;
|
|
85
|
+
evidence?: string; // URL or description of proof
|
|
86
|
+
|
|
87
|
+
// For milestone escrows - partial release
|
|
88
|
+
releaseAmount?: string; // Amount to release when this condition is met
|
|
89
|
+
releasePercentage?: string; // Or percentage of total
|
|
90
|
+
|
|
91
|
+
// Additional metadata
|
|
92
|
+
metadata?: Record<string, any>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Escrow {
|
|
96
|
+
id: string;
|
|
97
|
+
type: EscrowType;
|
|
98
|
+
status: EscrowStatus;
|
|
99
|
+
|
|
100
|
+
// Property/transaction info
|
|
101
|
+
property?: {
|
|
102
|
+
address: string;
|
|
103
|
+
city: string;
|
|
104
|
+
state: string;
|
|
105
|
+
zip: string;
|
|
106
|
+
mlsNumber?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Parties
|
|
110
|
+
parties: EscrowParty[];
|
|
111
|
+
|
|
112
|
+
// Funds
|
|
113
|
+
amount: string;
|
|
114
|
+
chain: string;
|
|
115
|
+
escrowAddress: string; // Smart contract or custody address
|
|
116
|
+
fundingTxHash?: string;
|
|
117
|
+
fundedAt?: string;
|
|
118
|
+
|
|
119
|
+
// Conditions for release
|
|
120
|
+
conditions: EscrowCondition[];
|
|
121
|
+
releaseRequires: 'all_conditions' | 'majority_approval' | 'any_party';
|
|
122
|
+
|
|
123
|
+
// Approvals for release
|
|
124
|
+
approvals: {
|
|
125
|
+
partyRole: string;
|
|
126
|
+
approved: boolean;
|
|
127
|
+
timestamp: string;
|
|
128
|
+
note?: string;
|
|
129
|
+
}[];
|
|
130
|
+
requiredApprovals: string[]; // Roles that must approve
|
|
131
|
+
|
|
132
|
+
// Release/refund
|
|
133
|
+
releaseTo?: string; // Address
|
|
134
|
+
releaseToRole?: string;
|
|
135
|
+
releaseTxHash?: string;
|
|
136
|
+
releasedAt?: string;
|
|
137
|
+
|
|
138
|
+
// Dispute
|
|
139
|
+
dispute?: {
|
|
140
|
+
raisedBy: string;
|
|
141
|
+
reason: string;
|
|
142
|
+
raisedAt: string;
|
|
143
|
+
resolution?: string;
|
|
144
|
+
resolvedAt?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Deadlines
|
|
148
|
+
fundingDeadline?: string;
|
|
149
|
+
closingDate?: string;
|
|
150
|
+
leaseEndDate?: string;
|
|
151
|
+
|
|
152
|
+
// Metadata
|
|
153
|
+
notes?: string;
|
|
154
|
+
documents: { name: string; url: string; uploadedAt: string }[];
|
|
155
|
+
|
|
156
|
+
createdAt: string;
|
|
157
|
+
updatedAt: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const DATA_DIR = process.env.USDC_DATA_DIR || './data';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Escrow Manager
|
|
164
|
+
*/
|
|
165
|
+
export class EscrowManager {
|
|
166
|
+
private dataPath: string;
|
|
167
|
+
|
|
168
|
+
constructor(dataDir = DATA_DIR) {
|
|
169
|
+
this.dataPath = path.join(dataDir, 'escrows.json');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async loadEscrows(): Promise<Escrow[]> {
|
|
173
|
+
try {
|
|
174
|
+
const data = await fs.readFile(this.dataPath, 'utf-8');
|
|
175
|
+
return JSON.parse(data);
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async saveEscrows(escrows: Escrow[]): Promise<void> {
|
|
182
|
+
await fs.mkdir(path.dirname(this.dataPath), { recursive: true });
|
|
183
|
+
await fs.writeFile(this.dataPath, JSON.stringify(escrows, null, 2));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============ Escrow Creation ============
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create earnest money escrow
|
|
190
|
+
*/
|
|
191
|
+
async createEarnestMoney(params: {
|
|
192
|
+
property: Escrow['property'];
|
|
193
|
+
amount: string;
|
|
194
|
+
chain: string;
|
|
195
|
+
buyer: Omit<EscrowParty, 'role'>;
|
|
196
|
+
seller: Omit<EscrowParty, 'role'>;
|
|
197
|
+
agent?: Omit<EscrowParty, 'role'>;
|
|
198
|
+
closingDate?: string;
|
|
199
|
+
conditions?: Omit<EscrowCondition, 'id' | 'status'>[];
|
|
200
|
+
}): Promise<Escrow> {
|
|
201
|
+
const escrows = await this.loadEscrows();
|
|
202
|
+
|
|
203
|
+
const parties: EscrowParty[] = [
|
|
204
|
+
{ ...params.buyer, role: 'buyer' },
|
|
205
|
+
{ ...params.seller, role: 'seller' },
|
|
206
|
+
];
|
|
207
|
+
if (params.agent) {
|
|
208
|
+
parties.push({ ...params.agent, role: 'agent' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Default conditions for earnest money
|
|
212
|
+
const defaultConditions: EscrowCondition[] = [
|
|
213
|
+
{
|
|
214
|
+
id: crypto.randomUUID(),
|
|
215
|
+
description: 'Home inspection satisfactory',
|
|
216
|
+
type: 'inspection',
|
|
217
|
+
status: 'pending',
|
|
218
|
+
deadline: this.addDays(new Date(), 10).toISOString(),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: crypto.randomUUID(),
|
|
222
|
+
description: 'Financing approved',
|
|
223
|
+
type: 'financing',
|
|
224
|
+
status: 'pending',
|
|
225
|
+
deadline: this.addDays(new Date(), 21).toISOString(),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: crypto.randomUUID(),
|
|
229
|
+
description: 'Title clear',
|
|
230
|
+
type: 'title',
|
|
231
|
+
status: 'pending',
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const customConditions = (params.conditions || []).map(c => ({
|
|
236
|
+
...c,
|
|
237
|
+
id: crypto.randomUUID(),
|
|
238
|
+
status: 'pending' as const,
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
const escrow: Escrow = {
|
|
242
|
+
id: `EM-${Date.now().toString(36).toUpperCase()}`,
|
|
243
|
+
type: 'earnest_money',
|
|
244
|
+
status: 'created',
|
|
245
|
+
property: params.property,
|
|
246
|
+
parties,
|
|
247
|
+
amount: params.amount,
|
|
248
|
+
chain: params.chain,
|
|
249
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
250
|
+
conditions: [...defaultConditions, ...customConditions],
|
|
251
|
+
releaseRequires: 'all_conditions',
|
|
252
|
+
approvals: [],
|
|
253
|
+
requiredApprovals: ['buyer', 'seller'],
|
|
254
|
+
closingDate: params.closingDate,
|
|
255
|
+
fundingDeadline: this.addDays(new Date(), 3).toISOString(),
|
|
256
|
+
documents: [],
|
|
257
|
+
createdAt: new Date().toISOString(),
|
|
258
|
+
updatedAt: new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
escrows.push(escrow);
|
|
262
|
+
await this.saveEscrows(escrows);
|
|
263
|
+
|
|
264
|
+
return escrow;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create rental security deposit escrow
|
|
269
|
+
*/
|
|
270
|
+
async createSecurityDeposit(params: {
|
|
271
|
+
property: Escrow['property'];
|
|
272
|
+
amount: string;
|
|
273
|
+
chain: string;
|
|
274
|
+
landlord: Omit<EscrowParty, 'role'>;
|
|
275
|
+
tenant: Omit<EscrowParty, 'role'>;
|
|
276
|
+
leaseEndDate: string;
|
|
277
|
+
}): Promise<Escrow> {
|
|
278
|
+
const escrows = await this.loadEscrows();
|
|
279
|
+
|
|
280
|
+
const escrow: Escrow = {
|
|
281
|
+
id: `SD-${Date.now().toString(36).toUpperCase()}`,
|
|
282
|
+
type: 'security_deposit',
|
|
283
|
+
status: 'created',
|
|
284
|
+
property: params.property,
|
|
285
|
+
parties: [
|
|
286
|
+
{ ...params.landlord, role: 'landlord' },
|
|
287
|
+
{ ...params.tenant, role: 'tenant' },
|
|
288
|
+
],
|
|
289
|
+
amount: params.amount,
|
|
290
|
+
chain: params.chain,
|
|
291
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
292
|
+
conditions: [
|
|
293
|
+
{
|
|
294
|
+
id: crypto.randomUUID(),
|
|
295
|
+
description: 'Lease term completed',
|
|
296
|
+
type: 'move_out',
|
|
297
|
+
status: 'pending',
|
|
298
|
+
deadline: params.leaseEndDate,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: crypto.randomUUID(),
|
|
302
|
+
description: 'Move-out inspection passed',
|
|
303
|
+
type: 'inspection',
|
|
304
|
+
status: 'pending',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
releaseRequires: 'majority_approval',
|
|
308
|
+
approvals: [],
|
|
309
|
+
requiredApprovals: ['landlord'], // Landlord approval releases to tenant
|
|
310
|
+
leaseEndDate: params.leaseEndDate,
|
|
311
|
+
documents: [],
|
|
312
|
+
createdAt: new Date().toISOString(),
|
|
313
|
+
updatedAt: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
escrows.push(escrow);
|
|
317
|
+
await this.saveEscrows(escrows);
|
|
318
|
+
|
|
319
|
+
return escrow;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Create general escrow
|
|
324
|
+
*/
|
|
325
|
+
async createGeneral(params: {
|
|
326
|
+
amount: string;
|
|
327
|
+
chain: string;
|
|
328
|
+
depositor: Omit<EscrowParty, 'role'>;
|
|
329
|
+
recipient: Omit<EscrowParty, 'role'>;
|
|
330
|
+
conditions?: Omit<EscrowCondition, 'id' | 'status'>[];
|
|
331
|
+
description?: string;
|
|
332
|
+
}): Promise<Escrow> {
|
|
333
|
+
const escrows = await this.loadEscrows();
|
|
334
|
+
|
|
335
|
+
const escrow: Escrow = {
|
|
336
|
+
id: `GE-${Date.now().toString(36).toUpperCase()}`,
|
|
337
|
+
type: 'general',
|
|
338
|
+
status: 'created',
|
|
339
|
+
parties: [
|
|
340
|
+
{ ...params.depositor, role: 'depositor' },
|
|
341
|
+
{ ...params.recipient, role: 'recipient' },
|
|
342
|
+
],
|
|
343
|
+
amount: params.amount,
|
|
344
|
+
chain: params.chain,
|
|
345
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
346
|
+
conditions: (params.conditions || []).map(c => ({
|
|
347
|
+
...c,
|
|
348
|
+
id: crypto.randomUUID(),
|
|
349
|
+
status: 'pending' as const,
|
|
350
|
+
})),
|
|
351
|
+
releaseRequires: 'all_conditions',
|
|
352
|
+
approvals: [],
|
|
353
|
+
requiredApprovals: ['depositor'],
|
|
354
|
+
notes: params.description,
|
|
355
|
+
documents: [],
|
|
356
|
+
createdAt: new Date().toISOString(),
|
|
357
|
+
updatedAt: new Date().toISOString(),
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
escrows.push(escrow);
|
|
361
|
+
await this.saveEscrows(escrows);
|
|
362
|
+
|
|
363
|
+
return escrow;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Create milestone escrow for freelance/service work
|
|
368
|
+
*/
|
|
369
|
+
async createMilestone(params: {
|
|
370
|
+
amount: string;
|
|
371
|
+
chain: string;
|
|
372
|
+
client: Omit<EscrowParty, 'role'>;
|
|
373
|
+
freelancer: Omit<EscrowParty, 'role'>;
|
|
374
|
+
projectName: string;
|
|
375
|
+
milestones: {
|
|
376
|
+
description: string;
|
|
377
|
+
amount?: string; // Fixed amount for this milestone
|
|
378
|
+
percentage?: string; // Or percentage of total
|
|
379
|
+
deadline?: string;
|
|
380
|
+
}[];
|
|
381
|
+
}): Promise<Escrow> {
|
|
382
|
+
const escrows = await this.loadEscrows();
|
|
383
|
+
const totalAmount = parseFloat(params.amount);
|
|
384
|
+
|
|
385
|
+
// Convert milestones to conditions with release amounts
|
|
386
|
+
const conditions: EscrowCondition[] = params.milestones.map((m, i) => ({
|
|
387
|
+
id: crypto.randomUUID(),
|
|
388
|
+
description: m.description,
|
|
389
|
+
type: 'milestone' as const,
|
|
390
|
+
status: 'pending' as const,
|
|
391
|
+
deadline: m.deadline,
|
|
392
|
+
releaseAmount: m.amount,
|
|
393
|
+
releasePercentage: m.percentage,
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
// Validate amounts/percentages add up
|
|
397
|
+
let totalAllocated = 0;
|
|
398
|
+
for (const cond of conditions) {
|
|
399
|
+
if (cond.releaseAmount) {
|
|
400
|
+
totalAllocated += parseFloat(cond.releaseAmount);
|
|
401
|
+
} else if (cond.releasePercentage) {
|
|
402
|
+
totalAllocated += totalAmount * (parseFloat(cond.releasePercentage) / 100);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Math.abs(totalAllocated - totalAmount) > 0.01) {
|
|
407
|
+
throw new Error(`Milestone amounts ($${totalAllocated.toFixed(2)}) don't match total ($${params.amount})`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const escrow: Escrow = {
|
|
411
|
+
id: `MS-${Date.now().toString(36).toUpperCase()}`,
|
|
412
|
+
type: 'milestone',
|
|
413
|
+
status: 'created',
|
|
414
|
+
parties: [
|
|
415
|
+
{ ...params.client, role: 'depositor' },
|
|
416
|
+
{ ...params.freelancer, role: 'recipient' },
|
|
417
|
+
],
|
|
418
|
+
amount: params.amount,
|
|
419
|
+
chain: params.chain,
|
|
420
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
421
|
+
conditions,
|
|
422
|
+
releaseRequires: 'any_party', // Each milestone releases independently
|
|
423
|
+
approvals: [],
|
|
424
|
+
requiredApprovals: ['depositor'], // Client approves each milestone
|
|
425
|
+
notes: `Project: ${params.projectName}`,
|
|
426
|
+
documents: [],
|
|
427
|
+
createdAt: new Date().toISOString(),
|
|
428
|
+
updatedAt: new Date().toISOString(),
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
escrows.push(escrow);
|
|
432
|
+
await this.saveEscrows(escrows);
|
|
433
|
+
|
|
434
|
+
return escrow;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Create purchase escrow (buyer/seller goods transaction)
|
|
439
|
+
*/
|
|
440
|
+
async createPurchase(params: {
|
|
441
|
+
amount: string;
|
|
442
|
+
chain: string;
|
|
443
|
+
buyer: Omit<EscrowParty, 'role'>;
|
|
444
|
+
seller: Omit<EscrowParty, 'role'>;
|
|
445
|
+
itemDescription: string;
|
|
446
|
+
requiresShipping?: boolean;
|
|
447
|
+
inspectionPeriodDays?: number;
|
|
448
|
+
}): Promise<Escrow> {
|
|
449
|
+
const escrows = await this.loadEscrows();
|
|
450
|
+
|
|
451
|
+
const conditions: EscrowCondition[] = [];
|
|
452
|
+
|
|
453
|
+
if (params.requiresShipping) {
|
|
454
|
+
conditions.push({
|
|
455
|
+
id: crypto.randomUUID(),
|
|
456
|
+
description: 'Item shipped by seller',
|
|
457
|
+
type: 'shipping',
|
|
458
|
+
status: 'pending',
|
|
459
|
+
});
|
|
460
|
+
conditions.push({
|
|
461
|
+
id: crypto.randomUUID(),
|
|
462
|
+
description: 'Item received by buyer',
|
|
463
|
+
type: 'receipt',
|
|
464
|
+
status: 'pending',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (params.inspectionPeriodDays) {
|
|
469
|
+
conditions.push({
|
|
470
|
+
id: crypto.randomUUID(),
|
|
471
|
+
description: `Inspection period (${params.inspectionPeriodDays} days)`,
|
|
472
|
+
type: 'inspection',
|
|
473
|
+
status: 'pending',
|
|
474
|
+
deadline: this.addDays(new Date(), params.inspectionPeriodDays).toISOString(),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Always require buyer approval
|
|
479
|
+
conditions.push({
|
|
480
|
+
id: crypto.randomUUID(),
|
|
481
|
+
description: 'Buyer approves release',
|
|
482
|
+
type: 'approval',
|
|
483
|
+
status: 'pending',
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const escrow: Escrow = {
|
|
487
|
+
id: `PU-${Date.now().toString(36).toUpperCase()}`,
|
|
488
|
+
type: 'purchase',
|
|
489
|
+
status: 'created',
|
|
490
|
+
parties: [
|
|
491
|
+
{ ...params.buyer, role: 'buyer' },
|
|
492
|
+
{ ...params.seller, role: 'seller' },
|
|
493
|
+
],
|
|
494
|
+
amount: params.amount,
|
|
495
|
+
chain: params.chain,
|
|
496
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
497
|
+
conditions,
|
|
498
|
+
releaseRequires: 'all_conditions',
|
|
499
|
+
approvals: [],
|
|
500
|
+
requiredApprovals: ['buyer'],
|
|
501
|
+
notes: params.itemDescription,
|
|
502
|
+
documents: [],
|
|
503
|
+
createdAt: new Date().toISOString(),
|
|
504
|
+
updatedAt: new Date().toISOString(),
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
escrows.push(escrow);
|
|
508
|
+
await this.saveEscrows(escrows);
|
|
509
|
+
|
|
510
|
+
return escrow;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Release partial amount (for milestone escrows)
|
|
515
|
+
*/
|
|
516
|
+
async releasePartial(
|
|
517
|
+
escrowId: string,
|
|
518
|
+
conditionId: string,
|
|
519
|
+
toAddress: string,
|
|
520
|
+
txHash: string
|
|
521
|
+
): Promise<Escrow | null> {
|
|
522
|
+
const escrows = await this.loadEscrows();
|
|
523
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
524
|
+
|
|
525
|
+
if (!escrow) return null;
|
|
526
|
+
|
|
527
|
+
const condition = escrow.conditions.find(c => c.id === conditionId);
|
|
528
|
+
if (!condition || condition.status !== 'satisfied') {
|
|
529
|
+
throw new Error('Condition must be satisfied before partial release');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Calculate release amount
|
|
533
|
+
let releaseAmount: string;
|
|
534
|
+
if (condition.releaseAmount) {
|
|
535
|
+
releaseAmount = condition.releaseAmount;
|
|
536
|
+
} else if (condition.releasePercentage) {
|
|
537
|
+
const total = parseFloat(escrow.amount);
|
|
538
|
+
releaseAmount = (total * parseFloat(condition.releasePercentage) / 100).toFixed(2);
|
|
539
|
+
} else {
|
|
540
|
+
throw new Error('Condition has no release amount defined');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Record the partial release
|
|
544
|
+
if (!escrow.releaseTo) {
|
|
545
|
+
escrow.releaseTo = toAddress;
|
|
546
|
+
escrow.releaseTxHash = txHash;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check if all milestones released
|
|
550
|
+
const allReleased = escrow.conditions
|
|
551
|
+
.filter(c => c.type === 'milestone')
|
|
552
|
+
.every(c => c.status === 'satisfied');
|
|
553
|
+
|
|
554
|
+
if (allReleased) {
|
|
555
|
+
escrow.status = 'released';
|
|
556
|
+
escrow.releasedAt = new Date().toISOString();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
escrow.updatedAt = new Date().toISOString();
|
|
560
|
+
await this.saveEscrows(escrows);
|
|
561
|
+
|
|
562
|
+
return escrow;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ============ Escrow Operations ============
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get escrow by ID
|
|
569
|
+
*/
|
|
570
|
+
async get(id: string): Promise<Escrow | null> {
|
|
571
|
+
const escrows = await this.loadEscrows();
|
|
572
|
+
return escrows.find(e => e.id === id) || null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* List escrows with filters
|
|
577
|
+
*/
|
|
578
|
+
async list(filters?: {
|
|
579
|
+
type?: EscrowType;
|
|
580
|
+
status?: EscrowStatus;
|
|
581
|
+
partyAddress?: string;
|
|
582
|
+
propertyAddress?: string;
|
|
583
|
+
}): Promise<Escrow[]> {
|
|
584
|
+
let escrows = await this.loadEscrows();
|
|
585
|
+
|
|
586
|
+
if (filters?.type) {
|
|
587
|
+
escrows = escrows.filter(e => e.type === filters.type);
|
|
588
|
+
}
|
|
589
|
+
if (filters?.status) {
|
|
590
|
+
escrows = escrows.filter(e => e.status === filters.status);
|
|
591
|
+
}
|
|
592
|
+
if (filters?.partyAddress) {
|
|
593
|
+
escrows = escrows.filter(e =>
|
|
594
|
+
e.parties.some(p => p.walletAddress?.toLowerCase() === filters.partyAddress!.toLowerCase())
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (filters?.propertyAddress) {
|
|
598
|
+
escrows = escrows.filter(e =>
|
|
599
|
+
e.property?.address.toLowerCase().includes(filters.propertyAddress!.toLowerCase())
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return escrows.sort((a, b) =>
|
|
604
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Mark escrow as funded
|
|
610
|
+
*/
|
|
611
|
+
async markFunded(id: string, txHash: string): Promise<Escrow | null> {
|
|
612
|
+
const escrows = await this.loadEscrows();
|
|
613
|
+
const escrow = escrows.find(e => e.id === id);
|
|
614
|
+
|
|
615
|
+
if (escrow && escrow.status === 'created') {
|
|
616
|
+
escrow.status = 'funded';
|
|
617
|
+
escrow.fundingTxHash = txHash;
|
|
618
|
+
escrow.fundedAt = new Date().toISOString();
|
|
619
|
+
escrow.updatedAt = new Date().toISOString();
|
|
620
|
+
await this.saveEscrows(escrows);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return escrow || null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Satisfy a condition
|
|
628
|
+
*/
|
|
629
|
+
async satisfyCondition(
|
|
630
|
+
escrowId: string,
|
|
631
|
+
conditionId: string,
|
|
632
|
+
satisfiedBy: string,
|
|
633
|
+
evidence?: string
|
|
634
|
+
): Promise<Escrow | null> {
|
|
635
|
+
const escrows = await this.loadEscrows();
|
|
636
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
637
|
+
|
|
638
|
+
if (!escrow) return null;
|
|
639
|
+
|
|
640
|
+
const condition = escrow.conditions.find(c => c.id === conditionId);
|
|
641
|
+
if (condition && condition.status === 'pending') {
|
|
642
|
+
condition.status = 'satisfied';
|
|
643
|
+
condition.satisfiedAt = new Date().toISOString();
|
|
644
|
+
condition.satisfiedBy = satisfiedBy;
|
|
645
|
+
condition.evidence = evidence;
|
|
646
|
+
escrow.updatedAt = new Date().toISOString();
|
|
647
|
+
|
|
648
|
+
// Check if all conditions satisfied
|
|
649
|
+
const allSatisfied = escrow.conditions.every(c =>
|
|
650
|
+
c.status === 'satisfied' || c.status === 'waived'
|
|
651
|
+
);
|
|
652
|
+
if (allSatisfied && escrow.status === 'funded') {
|
|
653
|
+
escrow.status = 'pending_release';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
await this.saveEscrows(escrows);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return escrow;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Waive a condition
|
|
664
|
+
*/
|
|
665
|
+
async waiveCondition(escrowId: string, conditionId: string, waivedBy: string): Promise<Escrow | null> {
|
|
666
|
+
const escrows = await this.loadEscrows();
|
|
667
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
668
|
+
|
|
669
|
+
if (!escrow) return null;
|
|
670
|
+
|
|
671
|
+
const condition = escrow.conditions.find(c => c.id === conditionId);
|
|
672
|
+
if (condition && condition.status === 'pending') {
|
|
673
|
+
condition.status = 'waived';
|
|
674
|
+
condition.satisfiedAt = new Date().toISOString();
|
|
675
|
+
condition.satisfiedBy = waivedBy;
|
|
676
|
+
escrow.updatedAt = new Date().toISOString();
|
|
677
|
+
await this.saveEscrows(escrows);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return escrow;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Fail a condition (triggers refund flow)
|
|
685
|
+
*/
|
|
686
|
+
async failCondition(escrowId: string, conditionId: string, reason: string): Promise<Escrow | null> {
|
|
687
|
+
const escrows = await this.loadEscrows();
|
|
688
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
689
|
+
|
|
690
|
+
if (!escrow) return null;
|
|
691
|
+
|
|
692
|
+
const condition = escrow.conditions.find(c => c.id === conditionId);
|
|
693
|
+
if (condition && condition.status === 'pending') {
|
|
694
|
+
condition.status = 'failed';
|
|
695
|
+
condition.evidence = reason;
|
|
696
|
+
escrow.updatedAt = new Date().toISOString();
|
|
697
|
+
|
|
698
|
+
// Initiate refund process for earnest money
|
|
699
|
+
if (escrow.type === 'earnest_money') {
|
|
700
|
+
escrow.status = 'pending_release';
|
|
701
|
+
escrow.releaseToRole = 'buyer'; // Failed conditions = refund to buyer
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
await this.saveEscrows(escrows);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return escrow;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Submit approval for release
|
|
712
|
+
*/
|
|
713
|
+
async approve(escrowId: string, partyRole: string, note?: string): Promise<Escrow | null> {
|
|
714
|
+
const escrows = await this.loadEscrows();
|
|
715
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
716
|
+
|
|
717
|
+
if (!escrow) return null;
|
|
718
|
+
|
|
719
|
+
// Check if party is authorized
|
|
720
|
+
if (!escrow.requiredApprovals.includes(partyRole)) {
|
|
721
|
+
throw new Error(`Party role "${partyRole}" is not required for approval`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Check if already approved
|
|
725
|
+
if (escrow.approvals.some(a => a.partyRole === partyRole)) {
|
|
726
|
+
throw new Error('Already approved');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
escrow.approvals.push({
|
|
730
|
+
partyRole,
|
|
731
|
+
approved: true,
|
|
732
|
+
timestamp: new Date().toISOString(),
|
|
733
|
+
note,
|
|
734
|
+
});
|
|
735
|
+
escrow.updatedAt = new Date().toISOString();
|
|
736
|
+
|
|
737
|
+
// Check if all required approvals received
|
|
738
|
+
const allApproved = escrow.requiredApprovals.every(role =>
|
|
739
|
+
escrow.approvals.some(a => a.partyRole === role && a.approved)
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
if (allApproved && (escrow.status === 'funded' || escrow.status === 'pending_release')) {
|
|
743
|
+
escrow.status = 'pending_release';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
await this.saveEscrows(escrows);
|
|
747
|
+
return escrow;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Execute release
|
|
752
|
+
*/
|
|
753
|
+
async release(escrowId: string, toAddress: string, txHash: string): Promise<Escrow | null> {
|
|
754
|
+
const escrows = await this.loadEscrows();
|
|
755
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
756
|
+
|
|
757
|
+
if (escrow && escrow.status === 'pending_release') {
|
|
758
|
+
escrow.status = 'released';
|
|
759
|
+
escrow.releaseTo = toAddress;
|
|
760
|
+
escrow.releaseTxHash = txHash;
|
|
761
|
+
escrow.releasedAt = new Date().toISOString();
|
|
762
|
+
escrow.updatedAt = new Date().toISOString();
|
|
763
|
+
await this.saveEscrows(escrows);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return escrow || null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Execute refund
|
|
771
|
+
*/
|
|
772
|
+
async refund(escrowId: string, txHash: string): Promise<Escrow | null> {
|
|
773
|
+
const escrows = await this.loadEscrows();
|
|
774
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
775
|
+
|
|
776
|
+
if (escrow && (escrow.status === 'funded' || escrow.status === 'pending_release')) {
|
|
777
|
+
// Find buyer/depositor address
|
|
778
|
+
const refundParty = escrow.parties.find(p =>
|
|
779
|
+
p.role === 'buyer' || p.role === 'tenant' || p.role === 'depositor'
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
escrow.status = 'refunded';
|
|
783
|
+
escrow.releaseTo = refundParty?.walletAddress;
|
|
784
|
+
escrow.releaseTxHash = txHash;
|
|
785
|
+
escrow.releasedAt = new Date().toISOString();
|
|
786
|
+
escrow.updatedAt = new Date().toISOString();
|
|
787
|
+
await this.saveEscrows(escrows);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return escrow || null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Raise dispute
|
|
795
|
+
*/
|
|
796
|
+
async raiseDispute(escrowId: string, raisedBy: string, reason: string): Promise<Escrow | null> {
|
|
797
|
+
const escrows = await this.loadEscrows();
|
|
798
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
799
|
+
|
|
800
|
+
if (escrow && escrow.status === 'funded') {
|
|
801
|
+
escrow.status = 'disputed';
|
|
802
|
+
escrow.dispute = {
|
|
803
|
+
raisedBy,
|
|
804
|
+
reason,
|
|
805
|
+
raisedAt: new Date().toISOString(),
|
|
806
|
+
};
|
|
807
|
+
escrow.updatedAt = new Date().toISOString();
|
|
808
|
+
await this.saveEscrows(escrows);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return escrow || null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Cancel escrow (before funding)
|
|
816
|
+
*/
|
|
817
|
+
async cancel(escrowId: string): Promise<Escrow | null> {
|
|
818
|
+
const escrows = await this.loadEscrows();
|
|
819
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
820
|
+
|
|
821
|
+
if (escrow && escrow.status === 'created') {
|
|
822
|
+
escrow.status = 'cancelled';
|
|
823
|
+
escrow.updatedAt = new Date().toISOString();
|
|
824
|
+
await this.saveEscrows(escrows);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return escrow || null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Add document to escrow
|
|
832
|
+
*/
|
|
833
|
+
async addDocument(escrowId: string, name: string, url: string): Promise<Escrow | null> {
|
|
834
|
+
const escrows = await this.loadEscrows();
|
|
835
|
+
const escrow = escrows.find(e => e.id === escrowId);
|
|
836
|
+
|
|
837
|
+
if (escrow) {
|
|
838
|
+
escrow.documents.push({
|
|
839
|
+
name,
|
|
840
|
+
url,
|
|
841
|
+
uploadedAt: new Date().toISOString(),
|
|
842
|
+
});
|
|
843
|
+
escrow.updatedAt = new Date().toISOString();
|
|
844
|
+
await this.saveEscrows(escrows);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return escrow || null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ============ UNIVERSAL ESCROW API (EaaS) ============
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Create escrow from template
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* await escrowManager.create({
|
|
857
|
+
* template: 'project_milestone',
|
|
858
|
+
* amount: '1000',
|
|
859
|
+
* chain: 'polygon',
|
|
860
|
+
* parties: [
|
|
861
|
+
* { role: 'client', name: 'Alice' },
|
|
862
|
+
* { role: 'provider', name: 'Bob' }
|
|
863
|
+
* ]
|
|
864
|
+
* })
|
|
865
|
+
*/
|
|
866
|
+
async create(params: {
|
|
867
|
+
template: string;
|
|
868
|
+
amount: string;
|
|
869
|
+
chain: string;
|
|
870
|
+
parties: Omit<EscrowParty, 'role'> & { role: string }[];
|
|
871
|
+
customConditions?: Omit<EscrowCondition, 'id' | 'status'>[];
|
|
872
|
+
metadata?: Record<string, any>;
|
|
873
|
+
autoReleaseDays?: number;
|
|
874
|
+
}): Promise<Escrow> {
|
|
875
|
+
const template = getTemplate(params.template);
|
|
876
|
+
if (!template) {
|
|
877
|
+
throw new Error(`Template '${params.template}' not found`);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const escrows = await this.loadEscrows();
|
|
881
|
+
|
|
882
|
+
// Convert template conditions to EscrowCondition format
|
|
883
|
+
const conditions: EscrowCondition[] = template.conditions.map(c => ({
|
|
884
|
+
id: crypto.randomUUID(),
|
|
885
|
+
description: c.description,
|
|
886
|
+
type: c.type,
|
|
887
|
+
status: 'pending',
|
|
888
|
+
...(c.deadline && { deadline: c.deadline }),
|
|
889
|
+
...(c.releaseAmount && { releaseAmount: c.releaseAmount }),
|
|
890
|
+
...(c.releasePercentage && { releasePercentage: c.releasePercentage }),
|
|
891
|
+
...(c.metadata && { metadata: c.metadata }),
|
|
892
|
+
}));
|
|
893
|
+
|
|
894
|
+
// Add custom conditions if provided
|
|
895
|
+
const customConditions = (params.customConditions || []).map(c => ({
|
|
896
|
+
...c,
|
|
897
|
+
id: crypto.randomUUID(),
|
|
898
|
+
status: 'pending' as const,
|
|
899
|
+
}));
|
|
900
|
+
|
|
901
|
+
const escrow: Escrow = {
|
|
902
|
+
id: `${template.vertical.toUpperCase().slice(0, 2)}-${Date.now().toString(36).toUpperCase()}`,
|
|
903
|
+
type: this.mapVerticalToType(template.vertical),
|
|
904
|
+
status: 'created',
|
|
905
|
+
parties: params.parties as EscrowParty[],
|
|
906
|
+
amount: params.amount,
|
|
907
|
+
chain: params.chain,
|
|
908
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
909
|
+
conditions: [...conditions, ...customConditions],
|
|
910
|
+
releaseRequires: template.releaseRequires,
|
|
911
|
+
approvals: [],
|
|
912
|
+
requiredApprovals: template.recommendedPartyRoles,
|
|
913
|
+
documents: [],
|
|
914
|
+
notes: `Created from template: ${template.name}`,
|
|
915
|
+
createdAt: new Date().toISOString(),
|
|
916
|
+
updatedAt: new Date().toISOString(),
|
|
917
|
+
fundingDeadline: this.addDays(new Date(), 3).toISOString(),
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Add auto-release if specified
|
|
921
|
+
if (params.autoReleaseDays || template.autoReleaseDays) {
|
|
922
|
+
const days = params.autoReleaseDays || template.autoReleaseDays!;
|
|
923
|
+
conditions.push({
|
|
924
|
+
id: crypto.randomUUID(),
|
|
925
|
+
description: `Auto-release after ${days} days if no disputes`,
|
|
926
|
+
type: 'deadline',
|
|
927
|
+
status: 'pending',
|
|
928
|
+
deadline: this.addDays(new Date(), days).toISOString(),
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
escrows.push(escrow);
|
|
933
|
+
await this.saveEscrows(escrows);
|
|
934
|
+
|
|
935
|
+
return escrow;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Create custom escrow with builder conditions
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* import { ConditionBuilder } from './condition-builder';
|
|
943
|
+
*
|
|
944
|
+
* await escrowManager.createCustom({
|
|
945
|
+
* amount: '5000',
|
|
946
|
+
* chain: 'ethereum',
|
|
947
|
+
* parties: [
|
|
948
|
+
* { role: 'buyer', name: 'Alice' },
|
|
949
|
+
* { role: 'seller', name: 'Bob' }
|
|
950
|
+
* ],
|
|
951
|
+
* conditions: [
|
|
952
|
+
* ConditionBuilder.milestone('Phase 1', 30),
|
|
953
|
+
* ConditionBuilder.milestone('Phase 2', 70),
|
|
954
|
+
* ],
|
|
955
|
+
* releaseRequires: 'condition_based'
|
|
956
|
+
* })
|
|
957
|
+
*/
|
|
958
|
+
async createCustom(params: {
|
|
959
|
+
amount: string;
|
|
960
|
+
chain: string;
|
|
961
|
+
parties: Omit<EscrowParty, 'role'> & { role: string }[];
|
|
962
|
+
conditions: BuiltCondition[];
|
|
963
|
+
releaseRequires?: 'all_conditions' | 'majority_approval' | 'condition_based' | 'any_party';
|
|
964
|
+
requiredApprovals?: string[];
|
|
965
|
+
metadata?: Record<string, any>;
|
|
966
|
+
autoReleaseDays?: number;
|
|
967
|
+
}): Promise<Escrow> {
|
|
968
|
+
const escrows = await this.loadEscrows();
|
|
969
|
+
|
|
970
|
+
const conditions: EscrowCondition[] = params.conditions.map(c => ({
|
|
971
|
+
...c,
|
|
972
|
+
status: 'pending',
|
|
973
|
+
}));
|
|
974
|
+
|
|
975
|
+
// Add auto-release if specified
|
|
976
|
+
if (params.autoReleaseDays) {
|
|
977
|
+
conditions.push({
|
|
978
|
+
id: crypto.randomUUID(),
|
|
979
|
+
description: `Auto-release after ${params.autoReleaseDays} days if no disputes`,
|
|
980
|
+
type: 'deadline',
|
|
981
|
+
status: 'pending',
|
|
982
|
+
deadline: this.addDays(new Date(), params.autoReleaseDays).toISOString(),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const escrow: Escrow = {
|
|
987
|
+
id: `CUSTOM-${Date.now().toString(36).toUpperCase()}`,
|
|
988
|
+
type: 'general',
|
|
989
|
+
status: 'created',
|
|
990
|
+
parties: params.parties as EscrowParty[],
|
|
991
|
+
amount: params.amount,
|
|
992
|
+
chain: params.chain,
|
|
993
|
+
escrowAddress: this.generateEscrowAddress(),
|
|
994
|
+
conditions,
|
|
995
|
+
releaseRequires: params.releaseRequires || 'all_conditions',
|
|
996
|
+
approvals: [],
|
|
997
|
+
requiredApprovals: params.requiredApprovals || params.parties.map(p => p.role),
|
|
998
|
+
documents: [],
|
|
999
|
+
createdAt: new Date().toISOString(),
|
|
1000
|
+
updatedAt: new Date().toISOString(),
|
|
1001
|
+
fundingDeadline: this.addDays(new Date(), 3).toISOString(),
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
escrows.push(escrow);
|
|
1005
|
+
await this.saveEscrows(escrows);
|
|
1006
|
+
|
|
1007
|
+
return escrow;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Map vertical to legacy EscrowType
|
|
1012
|
+
*/
|
|
1013
|
+
private mapVerticalToType(vertical: EscrowVertical): EscrowType {
|
|
1014
|
+
switch (vertical) {
|
|
1015
|
+
case 'real_estate': return 'earnest_money';
|
|
1016
|
+
case 'freelance': return 'milestone';
|
|
1017
|
+
case 'commerce': return 'purchase';
|
|
1018
|
+
case 'p2p': return 'trade';
|
|
1019
|
+
case 'digital': return 'purchase';
|
|
1020
|
+
case 'services': return 'freelance';
|
|
1021
|
+
default: return 'general';
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ============ Formatting ============
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Format escrow summary for display
|
|
1029
|
+
*/
|
|
1030
|
+
formatEscrowSummary(escrow: Escrow): string {
|
|
1031
|
+
const statusEmoji = {
|
|
1032
|
+
created: '📝',
|
|
1033
|
+
funded: '💰',
|
|
1034
|
+
pending_release: '⏳',
|
|
1035
|
+
released: '✅',
|
|
1036
|
+
refunded: '↩️',
|
|
1037
|
+
disputed: '⚠️',
|
|
1038
|
+
cancelled: '❌',
|
|
1039
|
+
}[escrow.status];
|
|
1040
|
+
|
|
1041
|
+
const typeLabel = {
|
|
1042
|
+
earnest_money: 'Earnest Money',
|
|
1043
|
+
security_deposit: 'Security Deposit',
|
|
1044
|
+
closing_funds: 'Closing Funds',
|
|
1045
|
+
general: 'Escrow',
|
|
1046
|
+
}[escrow.type];
|
|
1047
|
+
|
|
1048
|
+
let summary = `${statusEmoji} **${typeLabel} ${escrow.id}**\n\n`;
|
|
1049
|
+
|
|
1050
|
+
if (escrow.property) {
|
|
1051
|
+
summary += `📍 ${escrow.property.address}, ${escrow.property.city}, ${escrow.property.state}\n`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
summary += `💵 **$${escrow.amount} USDC** (${escrow.chain})\n`;
|
|
1055
|
+
summary += `Status: ${escrow.status.replace('_', ' ').toUpperCase()}\n\n`;
|
|
1056
|
+
|
|
1057
|
+
// Parties
|
|
1058
|
+
summary += `**Parties:**\n`;
|
|
1059
|
+
for (const party of escrow.parties) {
|
|
1060
|
+
summary += `• ${party.role}: ${party.name}\n`;
|
|
1061
|
+
}
|
|
1062
|
+
summary += '\n';
|
|
1063
|
+
|
|
1064
|
+
// Conditions
|
|
1065
|
+
if (escrow.conditions.length > 0) {
|
|
1066
|
+
summary += `**Conditions:**\n`;
|
|
1067
|
+
for (const cond of escrow.conditions) {
|
|
1068
|
+
const condEmoji = {
|
|
1069
|
+
pending: '⏳',
|
|
1070
|
+
satisfied: '✅',
|
|
1071
|
+
waived: '⏭️',
|
|
1072
|
+
failed: '❌',
|
|
1073
|
+
}[cond.status];
|
|
1074
|
+
summary += `${condEmoji} ${cond.description}\n`;
|
|
1075
|
+
}
|
|
1076
|
+
summary += '\n';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Approvals
|
|
1080
|
+
if (escrow.requiredApprovals.length > 0) {
|
|
1081
|
+
summary += `**Approvals:** `;
|
|
1082
|
+
const approved = escrow.approvals.filter(a => a.approved).map(a => a.partyRole);
|
|
1083
|
+
summary += `${approved.length}/${escrow.requiredApprovals.length} `;
|
|
1084
|
+
summary += `(${escrow.requiredApprovals.map(r => approved.includes(r) ? '✓' : '○').join('')})\n`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return summary;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// ============ Helpers ============
|
|
1091
|
+
|
|
1092
|
+
private generateEscrowAddress(): string {
|
|
1093
|
+
// In production, this would deploy/derive a smart contract address
|
|
1094
|
+
// For now, generate a deterministic address
|
|
1095
|
+
return '0x' + crypto.randomBytes(20).toString('hex');
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private addDays(date: Date, days: number): Date {
|
|
1099
|
+
const result = new Date(date);
|
|
1100
|
+
result.setDate(result.getDate() + days);
|
|
1101
|
+
return result;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* x402 Premium Escrow Features
|
|
1107
|
+
*
|
|
1108
|
+
* Gate premium escrow operations behind x402 payments
|
|
1109
|
+
*/
|
|
1110
|
+
export interface PremiumEscrowFeatures {
|
|
1111
|
+
yieldOptimization?: boolean; // Route funds through yield-bearing protocols
|
|
1112
|
+
insurance?: boolean; // Add escrow insurance coverage
|
|
1113
|
+
prioritySupport?: boolean; // Priority dispute resolution
|
|
1114
|
+
analytics?: boolean; // Advanced analytics and reporting
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export interface X402EscrowEndpoints {
|
|
1118
|
+
optimize: string; // Yield optimization endpoint
|
|
1119
|
+
insure: string; // Insurance coverage endpoint
|
|
1120
|
+
analytics: string; // Analytics endpoint
|
|
1121
|
+
support: string; // Priority support endpoint
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Premium feature pricing (in USDC)
|
|
1125
|
+
export const PREMIUM_ESCROW_PRICING = {
|
|
1126
|
+
yieldOptimization: '0.25', // Enable yield-bearing escrow
|
|
1127
|
+
insurance: '0.50', // Add insurance coverage
|
|
1128
|
+
prioritySupport: '1.00', // Priority support package
|
|
1129
|
+
analytics: '0.10', // Advanced analytics
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Generate x402 premium feature URLs for escrow
|
|
1134
|
+
*/
|
|
1135
|
+
export function generateX402EscrowUrls(
|
|
1136
|
+
escrowId: string,
|
|
1137
|
+
baseUrl?: string
|
|
1138
|
+
): X402EscrowEndpoints {
|
|
1139
|
+
const base = baseUrl || process.env.X402_BASE_URL || 'https://api.lobster-pay.com';
|
|
1140
|
+
|
|
1141
|
+
return {
|
|
1142
|
+
optimize: `${base}/escrow/${escrowId}/optimize`,
|
|
1143
|
+
insure: `${base}/escrow/${escrowId}/insure`,
|
|
1144
|
+
analytics: `${base}/escrow/${escrowId}/analytics`,
|
|
1145
|
+
support: `${base}/escrow/${escrowId}/support`,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Example: Enable premium features on escrow
|
|
1151
|
+
*/
|
|
1152
|
+
export async function enablePremiumFeatures(
|
|
1153
|
+
escrow: Escrow,
|
|
1154
|
+
features: PremiumEscrowFeatures,
|
|
1155
|
+
x402Fetch: (url: string) => Promise<Response>
|
|
1156
|
+
): Promise<{ enabled: string[]; failed: string[] }> {
|
|
1157
|
+
const urls = generateX402EscrowUrls(escrow.id);
|
|
1158
|
+
const enabled: string[] = [];
|
|
1159
|
+
const failed: string[] = [];
|
|
1160
|
+
|
|
1161
|
+
if (features.yieldOptimization) {
|
|
1162
|
+
try {
|
|
1163
|
+
const response = await x402Fetch(urls.optimize);
|
|
1164
|
+
if (response.ok) {
|
|
1165
|
+
enabled.push('yieldOptimization');
|
|
1166
|
+
} else {
|
|
1167
|
+
failed.push('yieldOptimization');
|
|
1168
|
+
}
|
|
1169
|
+
} catch {
|
|
1170
|
+
failed.push('yieldOptimization');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (features.insurance) {
|
|
1175
|
+
try {
|
|
1176
|
+
const response = await x402Fetch(urls.insure);
|
|
1177
|
+
if (response.ok) {
|
|
1178
|
+
enabled.push('insurance');
|
|
1179
|
+
} else {
|
|
1180
|
+
failed.push('insurance');
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
failed.push('insurance');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (features.analytics) {
|
|
1188
|
+
try {
|
|
1189
|
+
const response = await x402Fetch(urls.analytics);
|
|
1190
|
+
if (response.ok) {
|
|
1191
|
+
enabled.push('analytics');
|
|
1192
|
+
} else {
|
|
1193
|
+
failed.push('analytics');
|
|
1194
|
+
}
|
|
1195
|
+
} catch {
|
|
1196
|
+
failed.push('analytics');
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (features.prioritySupport) {
|
|
1201
|
+
try {
|
|
1202
|
+
const response = await x402Fetch(urls.support);
|
|
1203
|
+
if (response.ok) {
|
|
1204
|
+
enabled.push('prioritySupport');
|
|
1205
|
+
} else {
|
|
1206
|
+
failed.push('prioritySupport');
|
|
1207
|
+
}
|
|
1208
|
+
} catch {
|
|
1209
|
+
failed.push('prioritySupport');
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return { enabled, failed };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
export default EscrowManager;
|