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
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Approval System
|
|
3
|
+
*
|
|
4
|
+
* Multi-step approval workflow for large or sensitive transactions.
|
|
5
|
+
* Integrates with Clawdbot for conversational approvals.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
export interface ApprovalPolicy {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
|
|
17
|
+
// Conditions that trigger approval requirement
|
|
18
|
+
conditions: {
|
|
19
|
+
minAmount?: string; // Require approval above this amount
|
|
20
|
+
maxDailyLimit?: string; // Require approval if daily limit exceeded
|
|
21
|
+
addresses?: string[]; // Require approval for these addresses
|
|
22
|
+
chains?: string[]; // Require approval for these chains
|
|
23
|
+
always?: boolean; // Always require approval
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Approval requirements
|
|
27
|
+
approvers: string[]; // List of approver IDs (Clawdbot sessions, emails, etc.)
|
|
28
|
+
requiredApprovals: number; // Number of approvals needed
|
|
29
|
+
timeout: number; // Timeout in seconds (default 24h)
|
|
30
|
+
|
|
31
|
+
// Actions on timeout
|
|
32
|
+
timeoutAction: 'cancel' | 'auto-approve';
|
|
33
|
+
|
|
34
|
+
createdAt: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PendingTransaction {
|
|
39
|
+
id: string;
|
|
40
|
+
policyId: string;
|
|
41
|
+
|
|
42
|
+
// Transaction details
|
|
43
|
+
type: 'send' | 'bridge' | 'recurring';
|
|
44
|
+
fromWalletId: string;
|
|
45
|
+
toAddress: string;
|
|
46
|
+
toName?: string;
|
|
47
|
+
amount: string;
|
|
48
|
+
chain: string;
|
|
49
|
+
targetChain?: string; // For bridges
|
|
50
|
+
memo?: string;
|
|
51
|
+
|
|
52
|
+
// Approval status
|
|
53
|
+
status: 'pending' | 'approved' | 'rejected' | 'cancelled' | 'expired' | 'executed';
|
|
54
|
+
approvals: {
|
|
55
|
+
approverId: string;
|
|
56
|
+
decision: 'approve' | 'reject';
|
|
57
|
+
timestamp: string;
|
|
58
|
+
note?: string;
|
|
59
|
+
}[];
|
|
60
|
+
rejectionReason?: string;
|
|
61
|
+
|
|
62
|
+
// Execution
|
|
63
|
+
txHash?: string;
|
|
64
|
+
executedAt?: string;
|
|
65
|
+
|
|
66
|
+
// Metadata
|
|
67
|
+
requestedBy: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
expiresAt: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DailySpending {
|
|
73
|
+
date: string;
|
|
74
|
+
walletId: string;
|
|
75
|
+
total: string;
|
|
76
|
+
transactions: { amount: string; txHash: string; timestamp: string }[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const DATA_DIR = process.env.USDC_DATA_DIR || './data';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Approval Manager
|
|
83
|
+
*/
|
|
84
|
+
export class ApprovalManager {
|
|
85
|
+
private policiesPath: string;
|
|
86
|
+
private pendingPath: string;
|
|
87
|
+
private spendingPath: string;
|
|
88
|
+
|
|
89
|
+
constructor(dataDir = DATA_DIR) {
|
|
90
|
+
this.policiesPath = path.join(dataDir, 'approval-policies.json');
|
|
91
|
+
this.pendingPath = path.join(dataDir, 'pending-transactions.json');
|
|
92
|
+
this.spendingPath = path.join(dataDir, 'daily-spending.json');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async loadPolicies(): Promise<ApprovalPolicy[]> {
|
|
96
|
+
try {
|
|
97
|
+
const data = await fs.readFile(this.policiesPath, 'utf-8');
|
|
98
|
+
return JSON.parse(data);
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async savePolicies(policies: ApprovalPolicy[]): Promise<void> {
|
|
105
|
+
await fs.mkdir(path.dirname(this.policiesPath), { recursive: true });
|
|
106
|
+
await fs.writeFile(this.policiesPath, JSON.stringify(policies, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async loadPending(): Promise<PendingTransaction[]> {
|
|
110
|
+
try {
|
|
111
|
+
const data = await fs.readFile(this.pendingPath, 'utf-8');
|
|
112
|
+
return JSON.parse(data);
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async savePending(pending: PendingTransaction[]): Promise<void> {
|
|
119
|
+
await fs.mkdir(path.dirname(this.pendingPath), { recursive: true });
|
|
120
|
+
await fs.writeFile(this.pendingPath, JSON.stringify(pending, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async loadSpending(): Promise<DailySpending[]> {
|
|
124
|
+
try {
|
|
125
|
+
const data = await fs.readFile(this.spendingPath, 'utf-8');
|
|
126
|
+
return JSON.parse(data);
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async saveSpending(spending: DailySpending[]): Promise<void> {
|
|
133
|
+
await fs.mkdir(path.dirname(this.spendingPath), { recursive: true });
|
|
134
|
+
await fs.writeFile(this.spendingPath, JSON.stringify(spending, null, 2));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============ Policy Management ============
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create approval policy
|
|
141
|
+
*/
|
|
142
|
+
async createPolicy(params: {
|
|
143
|
+
name: string;
|
|
144
|
+
conditions: ApprovalPolicy['conditions'];
|
|
145
|
+
approvers: string[];
|
|
146
|
+
requiredApprovals?: number;
|
|
147
|
+
timeout?: number;
|
|
148
|
+
timeoutAction?: ApprovalPolicy['timeoutAction'];
|
|
149
|
+
}): Promise<ApprovalPolicy> {
|
|
150
|
+
const policies = await this.loadPolicies();
|
|
151
|
+
|
|
152
|
+
const policy: ApprovalPolicy = {
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
name: params.name,
|
|
155
|
+
enabled: true,
|
|
156
|
+
conditions: params.conditions,
|
|
157
|
+
approvers: params.approvers,
|
|
158
|
+
requiredApprovals: params.requiredApprovals || 1,
|
|
159
|
+
timeout: params.timeout || 86400, // 24 hours default
|
|
160
|
+
timeoutAction: params.timeoutAction || 'cancel',
|
|
161
|
+
createdAt: new Date().toISOString(),
|
|
162
|
+
updatedAt: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
policies.push(policy);
|
|
166
|
+
await this.savePolicies(policies);
|
|
167
|
+
|
|
168
|
+
return policy;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get active policies for a transaction
|
|
173
|
+
*/
|
|
174
|
+
async getMatchingPolicies(tx: {
|
|
175
|
+
amount: string;
|
|
176
|
+
toAddress: string;
|
|
177
|
+
chain: string;
|
|
178
|
+
walletId: string;
|
|
179
|
+
}): Promise<ApprovalPolicy[]> {
|
|
180
|
+
const policies = await this.loadPolicies();
|
|
181
|
+
const spending = await this.getDailySpending(tx.walletId);
|
|
182
|
+
const matchingPolicies: ApprovalPolicy[] = [];
|
|
183
|
+
|
|
184
|
+
for (const policy of policies) {
|
|
185
|
+
if (!policy.enabled) continue;
|
|
186
|
+
|
|
187
|
+
const { conditions } = policy;
|
|
188
|
+
let matches = false;
|
|
189
|
+
|
|
190
|
+
// Check amount threshold
|
|
191
|
+
if (conditions.minAmount && parseFloat(tx.amount) >= parseFloat(conditions.minAmount)) {
|
|
192
|
+
matches = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check daily limit
|
|
196
|
+
if (conditions.maxDailyLimit) {
|
|
197
|
+
const todayTotal = parseFloat(spending?.total || '0') + parseFloat(tx.amount);
|
|
198
|
+
if (todayTotal > parseFloat(conditions.maxDailyLimit)) {
|
|
199
|
+
matches = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check specific addresses
|
|
204
|
+
if (conditions.addresses && conditions.addresses.length > 0) {
|
|
205
|
+
if (conditions.addresses.some(a => a.toLowerCase() === tx.toAddress.toLowerCase())) {
|
|
206
|
+
matches = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check chains
|
|
211
|
+
if (conditions.chains && conditions.chains.length > 0) {
|
|
212
|
+
if (conditions.chains.includes(tx.chain)) {
|
|
213
|
+
matches = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Always require approval
|
|
218
|
+
if (conditions.always) {
|
|
219
|
+
matches = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (matches) {
|
|
223
|
+
matchingPolicies.push(policy);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return matchingPolicies;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* List all policies
|
|
232
|
+
*/
|
|
233
|
+
async listPolicies(): Promise<ApprovalPolicy[]> {
|
|
234
|
+
return this.loadPolicies();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Toggle policy enabled state
|
|
239
|
+
*/
|
|
240
|
+
async togglePolicy(id: string, enabled: boolean): Promise<ApprovalPolicy | null> {
|
|
241
|
+
const policies = await this.loadPolicies();
|
|
242
|
+
const policy = policies.find(p => p.id === id);
|
|
243
|
+
|
|
244
|
+
if (policy) {
|
|
245
|
+
policy.enabled = enabled;
|
|
246
|
+
policy.updatedAt = new Date().toISOString();
|
|
247
|
+
await this.savePolicies(policies);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return policy || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Delete policy
|
|
255
|
+
*/
|
|
256
|
+
async deletePolicy(id: string): Promise<boolean> {
|
|
257
|
+
const policies = await this.loadPolicies();
|
|
258
|
+
const index = policies.findIndex(p => p.id === id);
|
|
259
|
+
|
|
260
|
+
if (index >= 0) {
|
|
261
|
+
policies.splice(index, 1);
|
|
262
|
+
await this.savePolicies(policies);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============ Pending Transactions ============
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Submit transaction for approval
|
|
273
|
+
*/
|
|
274
|
+
async submitForApproval(tx: {
|
|
275
|
+
type: PendingTransaction['type'];
|
|
276
|
+
fromWalletId: string;
|
|
277
|
+
toAddress: string;
|
|
278
|
+
toName?: string;
|
|
279
|
+
amount: string;
|
|
280
|
+
chain: string;
|
|
281
|
+
targetChain?: string;
|
|
282
|
+
memo?: string;
|
|
283
|
+
requestedBy: string;
|
|
284
|
+
}): Promise<PendingTransaction | { approved: true }> {
|
|
285
|
+
// Check if approval is required
|
|
286
|
+
const policies = await this.getMatchingPolicies({
|
|
287
|
+
amount: tx.amount,
|
|
288
|
+
toAddress: tx.toAddress,
|
|
289
|
+
chain: tx.chain,
|
|
290
|
+
walletId: tx.fromWalletId,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// No policies match - auto-approve
|
|
294
|
+
if (policies.length === 0) {
|
|
295
|
+
return { approved: true };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Use the strictest policy (most approvals required)
|
|
299
|
+
const policy = policies.reduce((strictest, p) =>
|
|
300
|
+
p.requiredApprovals > strictest.requiredApprovals ? p : strictest
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const pending = await this.loadPending();
|
|
304
|
+
|
|
305
|
+
const transaction: PendingTransaction = {
|
|
306
|
+
id: crypto.randomUUID(),
|
|
307
|
+
policyId: policy.id,
|
|
308
|
+
type: tx.type,
|
|
309
|
+
fromWalletId: tx.fromWalletId,
|
|
310
|
+
toAddress: tx.toAddress,
|
|
311
|
+
toName: tx.toName,
|
|
312
|
+
amount: tx.amount,
|
|
313
|
+
chain: tx.chain,
|
|
314
|
+
targetChain: tx.targetChain,
|
|
315
|
+
memo: tx.memo,
|
|
316
|
+
status: 'pending',
|
|
317
|
+
approvals: [],
|
|
318
|
+
requestedBy: tx.requestedBy,
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
expiresAt: new Date(Date.now() + policy.timeout * 1000).toISOString(),
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
pending.push(transaction);
|
|
324
|
+
await this.savePending(pending);
|
|
325
|
+
|
|
326
|
+
return transaction;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Approve or reject a pending transaction
|
|
331
|
+
*/
|
|
332
|
+
async decide(
|
|
333
|
+
txId: string,
|
|
334
|
+
approverId: string,
|
|
335
|
+
decision: 'approve' | 'reject',
|
|
336
|
+
note?: string
|
|
337
|
+
): Promise<PendingTransaction | null> {
|
|
338
|
+
const pending = await this.loadPending();
|
|
339
|
+
const tx = pending.find(t => t.id === txId);
|
|
340
|
+
|
|
341
|
+
if (!tx || tx.status !== 'pending') {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check if already decided by this approver
|
|
346
|
+
if (tx.approvals.some(a => a.approverId === approverId)) {
|
|
347
|
+
throw new Error('Already submitted decision');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if approver is authorized
|
|
351
|
+
const policies = await this.loadPolicies();
|
|
352
|
+
const policy = policies.find(p => p.id === tx.policyId);
|
|
353
|
+
|
|
354
|
+
if (!policy || !policy.approvers.includes(approverId)) {
|
|
355
|
+
throw new Error('Not authorized to approve this transaction');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Record decision
|
|
359
|
+
tx.approvals.push({
|
|
360
|
+
approverId,
|
|
361
|
+
decision,
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
363
|
+
note,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Check if rejected
|
|
367
|
+
if (decision === 'reject') {
|
|
368
|
+
tx.status = 'rejected';
|
|
369
|
+
tx.rejectionReason = note || 'Rejected by approver';
|
|
370
|
+
}
|
|
371
|
+
// Check if enough approvals
|
|
372
|
+
else {
|
|
373
|
+
const approvalCount = tx.approvals.filter(a => a.decision === 'approve').length;
|
|
374
|
+
if (approvalCount >= policy.requiredApprovals) {
|
|
375
|
+
tx.status = 'approved';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await this.savePending(pending);
|
|
380
|
+
return tx;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Mark transaction as executed
|
|
385
|
+
*/
|
|
386
|
+
async markExecuted(txId: string, txHash: string): Promise<PendingTransaction | null> {
|
|
387
|
+
const pending = await this.loadPending();
|
|
388
|
+
const tx = pending.find(t => t.id === txId);
|
|
389
|
+
|
|
390
|
+
if (tx && tx.status === 'approved') {
|
|
391
|
+
tx.status = 'executed';
|
|
392
|
+
tx.txHash = txHash;
|
|
393
|
+
tx.executedAt = new Date().toISOString();
|
|
394
|
+
|
|
395
|
+
// Record spending
|
|
396
|
+
await this.recordSpending(tx.fromWalletId, tx.amount, txHash);
|
|
397
|
+
|
|
398
|
+
await this.savePending(pending);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return tx || null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Cancel a pending transaction
|
|
406
|
+
*/
|
|
407
|
+
async cancel(txId: string, requesterId: string): Promise<PendingTransaction | null> {
|
|
408
|
+
const pending = await this.loadPending();
|
|
409
|
+
const tx = pending.find(t => t.id === txId);
|
|
410
|
+
|
|
411
|
+
if (tx && tx.status === 'pending' && tx.requestedBy === requesterId) {
|
|
412
|
+
tx.status = 'cancelled';
|
|
413
|
+
await this.savePending(pending);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return tx || null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get pending transactions
|
|
421
|
+
*/
|
|
422
|
+
async getPending(options?: {
|
|
423
|
+
status?: PendingTransaction['status'];
|
|
424
|
+
approverId?: string;
|
|
425
|
+
}): Promise<PendingTransaction[]> {
|
|
426
|
+
let pending = await this.loadPending();
|
|
427
|
+
|
|
428
|
+
if (options?.status) {
|
|
429
|
+
pending = pending.filter(t => t.status === options.status);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (options?.approverId) {
|
|
433
|
+
const policies = await this.loadPolicies();
|
|
434
|
+
const approverPolicies = policies.filter(p =>
|
|
435
|
+
p.approvers.includes(options.approverId!)
|
|
436
|
+
).map(p => p.id);
|
|
437
|
+
|
|
438
|
+
pending = pending.filter(t => approverPolicies.includes(t.policyId));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return pending.sort((a, b) =>
|
|
442
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Check and expire old transactions
|
|
448
|
+
*/
|
|
449
|
+
async processExpired(): Promise<PendingTransaction[]> {
|
|
450
|
+
const pending = await this.loadPending();
|
|
451
|
+
const policies = await this.loadPolicies();
|
|
452
|
+
const now = new Date();
|
|
453
|
+
const expired: PendingTransaction[] = [];
|
|
454
|
+
|
|
455
|
+
for (const tx of pending) {
|
|
456
|
+
if (tx.status !== 'pending') continue;
|
|
457
|
+
if (new Date(tx.expiresAt) > now) continue;
|
|
458
|
+
|
|
459
|
+
const policy = policies.find(p => p.id === tx.policyId);
|
|
460
|
+
|
|
461
|
+
if (policy?.timeoutAction === 'auto-approve') {
|
|
462
|
+
tx.status = 'approved';
|
|
463
|
+
} else {
|
|
464
|
+
tx.status = 'expired';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
expired.push(tx);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (expired.length > 0) {
|
|
471
|
+
await this.savePending(pending);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return expired;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ============ Daily Spending Tracking ============
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get daily spending for a wallet
|
|
481
|
+
*/
|
|
482
|
+
async getDailySpending(walletId: string): Promise<DailySpending | null> {
|
|
483
|
+
const spending = await this.loadSpending();
|
|
484
|
+
const today = new Date().toISOString().split('T')[0];
|
|
485
|
+
return spending.find(s => s.date === today && s.walletId === walletId) || null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Record spending
|
|
490
|
+
*/
|
|
491
|
+
private async recordSpending(walletId: string, amount: string, txHash: string): Promise<void> {
|
|
492
|
+
const spending = await this.loadSpending();
|
|
493
|
+
const today = new Date().toISOString().split('T')[0];
|
|
494
|
+
|
|
495
|
+
let todaySpending = spending.find(s => s.date === today && s.walletId === walletId);
|
|
496
|
+
|
|
497
|
+
if (!todaySpending) {
|
|
498
|
+
todaySpending = {
|
|
499
|
+
date: today,
|
|
500
|
+
walletId,
|
|
501
|
+
total: '0',
|
|
502
|
+
transactions: [],
|
|
503
|
+
};
|
|
504
|
+
spending.push(todaySpending);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
todaySpending.total = (parseFloat(todaySpending.total) + parseFloat(amount)).toString();
|
|
508
|
+
todaySpending.transactions.push({
|
|
509
|
+
amount,
|
|
510
|
+
txHash,
|
|
511
|
+
timestamp: new Date().toISOString(),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Keep only last 30 days
|
|
515
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
516
|
+
const filtered = spending.filter(s => s.date >= thirtyDaysAgo);
|
|
517
|
+
|
|
518
|
+
await this.saveSpending(filtered);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get spending history
|
|
523
|
+
*/
|
|
524
|
+
async getSpendingHistory(walletId: string, days = 30): Promise<DailySpending[]> {
|
|
525
|
+
const spending = await this.loadSpending();
|
|
526
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
527
|
+
|
|
528
|
+
return spending
|
|
529
|
+
.filter(s => s.walletId === walletId && s.date >= cutoff)
|
|
530
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ============ Helpers ============
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Format pending transaction for display
|
|
537
|
+
*/
|
|
538
|
+
formatPendingTx(tx: PendingTransaction): string {
|
|
539
|
+
const typeEmoji = tx.type === 'send' ? '📤' : tx.type === 'bridge' ? '🌉' : '🔄';
|
|
540
|
+
const statusEmoji = {
|
|
541
|
+
pending: '⏳',
|
|
542
|
+
approved: '✅',
|
|
543
|
+
rejected: '❌',
|
|
544
|
+
cancelled: '🚫',
|
|
545
|
+
expired: '⌛',
|
|
546
|
+
executed: '✓',
|
|
547
|
+
}[tx.status];
|
|
548
|
+
|
|
549
|
+
let text = `${typeEmoji} **${tx.amount} USDC** → ${tx.toName || this.shortAddress(tx.toAddress)}\n`;
|
|
550
|
+
text += `Status: ${statusEmoji} ${tx.status.toUpperCase()}\n`;
|
|
551
|
+
text += `Chain: ${tx.chain}${tx.targetChain ? ` → ${tx.targetChain}` : ''}\n`;
|
|
552
|
+
|
|
553
|
+
if (tx.approvals.length > 0) {
|
|
554
|
+
text += `Approvals: ${tx.approvals.filter(a => a.decision === 'approve').length}/${tx.approvals.length}\n`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
text += `Expires: ${new Date(tx.expiresAt).toLocaleString()}\n`;
|
|
558
|
+
|
|
559
|
+
if (tx.memo) {
|
|
560
|
+
text += `Memo: ${tx.memo}\n`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return text;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private shortAddress(address: string): string {
|
|
567
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Create default approval policy for new users
|
|
572
|
+
*/
|
|
573
|
+
async createDefaultPolicy(approverIds: string[]): Promise<ApprovalPolicy> {
|
|
574
|
+
return this.createPolicy({
|
|
575
|
+
name: 'Large Transactions (>500 USDC)',
|
|
576
|
+
conditions: { minAmount: '500' },
|
|
577
|
+
approvers: approverIds,
|
|
578
|
+
requiredApprovals: 1,
|
|
579
|
+
timeout: 86400, // 24 hours
|
|
580
|
+
timeoutAction: 'cancel',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export default ApprovalManager;
|