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/tips.ts
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tip Jar / Creator Economy Module
|
|
3
|
+
*
|
|
4
|
+
* Enable Clawdbot operators and agents to receive USDC tips
|
|
5
|
+
* via simple commands. Built for the creator economy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
export interface TipJar {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
|
|
17
|
+
// Owner info
|
|
18
|
+
ownerId: string; // Clawdbot session/agent ID
|
|
19
|
+
ownerName: string; // Display name
|
|
20
|
+
|
|
21
|
+
// Wallet addresses by chain
|
|
22
|
+
addresses: {
|
|
23
|
+
chain: string;
|
|
24
|
+
address: string;
|
|
25
|
+
isDefault: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
|
|
28
|
+
// Settings
|
|
29
|
+
settings: {
|
|
30
|
+
minTip: string; // Minimum tip amount
|
|
31
|
+
suggestedAmounts: string[]; // Quick-select amounts
|
|
32
|
+
thankYouMessage?: string; // Custom thank you message
|
|
33
|
+
allowAnonymous: boolean; // Allow anonymous tips
|
|
34
|
+
notifyOnTip: boolean; // Send notification on tip
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Stats
|
|
38
|
+
stats: {
|
|
39
|
+
totalReceived: string;
|
|
40
|
+
tipCount: number;
|
|
41
|
+
uniqueTippers: number;
|
|
42
|
+
largestTip: string;
|
|
43
|
+
lastTipAt?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Public link
|
|
47
|
+
publicSlug?: string; // e.g., "gustav" for tip.clawd.bot/gustav
|
|
48
|
+
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Tip {
|
|
54
|
+
id: string;
|
|
55
|
+
tipJarId: string;
|
|
56
|
+
|
|
57
|
+
// Sender
|
|
58
|
+
fromAddress: string;
|
|
59
|
+
fromName?: string;
|
|
60
|
+
isAnonymous: boolean;
|
|
61
|
+
|
|
62
|
+
// Payment
|
|
63
|
+
amount: string;
|
|
64
|
+
chain: string;
|
|
65
|
+
txHash: string;
|
|
66
|
+
|
|
67
|
+
// Message
|
|
68
|
+
message?: string;
|
|
69
|
+
|
|
70
|
+
// Status
|
|
71
|
+
status: 'pending' | 'confirmed' | 'failed';
|
|
72
|
+
confirmedAt?: string;
|
|
73
|
+
|
|
74
|
+
createdAt: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Leaderboard {
|
|
78
|
+
tipJarId: string;
|
|
79
|
+
period: 'all-time' | 'monthly' | 'weekly';
|
|
80
|
+
entries: {
|
|
81
|
+
rank: number;
|
|
82
|
+
name: string;
|
|
83
|
+
address: string;
|
|
84
|
+
totalTipped: string;
|
|
85
|
+
tipCount: number;
|
|
86
|
+
}[];
|
|
87
|
+
generatedAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DATA_DIR = process.env.USDC_DATA_DIR || './data';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Tip Jar Manager
|
|
94
|
+
*/
|
|
95
|
+
export class TipJarManager {
|
|
96
|
+
private jarsPath: string;
|
|
97
|
+
private tipsPath: string;
|
|
98
|
+
|
|
99
|
+
constructor(dataDir = DATA_DIR) {
|
|
100
|
+
this.jarsPath = path.join(dataDir, 'tip-jars.json');
|
|
101
|
+
this.tipsPath = path.join(dataDir, 'tips.json');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async loadJars(): Promise<TipJar[]> {
|
|
105
|
+
try {
|
|
106
|
+
const data = await fs.readFile(this.jarsPath, 'utf-8');
|
|
107
|
+
return JSON.parse(data);
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async saveJars(jars: TipJar[]): Promise<void> {
|
|
114
|
+
await fs.mkdir(path.dirname(this.jarsPath), { recursive: true });
|
|
115
|
+
await fs.writeFile(this.jarsPath, JSON.stringify(jars, null, 2));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async loadTips(): Promise<Tip[]> {
|
|
119
|
+
try {
|
|
120
|
+
const data = await fs.readFile(this.tipsPath, 'utf-8');
|
|
121
|
+
return JSON.parse(data);
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async saveTips(tips: Tip[]): Promise<void> {
|
|
128
|
+
await fs.mkdir(path.dirname(this.tipsPath), { recursive: true });
|
|
129
|
+
await fs.writeFile(this.tipsPath, JSON.stringify(tips, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============ Tip Jar Management ============
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a new tip jar
|
|
136
|
+
*/
|
|
137
|
+
async createJar(params: {
|
|
138
|
+
ownerId: string;
|
|
139
|
+
ownerName: string;
|
|
140
|
+
name?: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
addresses: { chain: string; address: string }[];
|
|
143
|
+
publicSlug?: string;
|
|
144
|
+
settings?: Partial<TipJar['settings']>;
|
|
145
|
+
}): Promise<TipJar> {
|
|
146
|
+
const jars = await this.loadJars();
|
|
147
|
+
|
|
148
|
+
// Check for duplicate slug
|
|
149
|
+
if (params.publicSlug) {
|
|
150
|
+
const existing = jars.find(j => j.publicSlug === params.publicSlug);
|
|
151
|
+
if (existing) {
|
|
152
|
+
throw new Error(`Slug "${params.publicSlug}" is already taken`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const jar: TipJar = {
|
|
157
|
+
id: crypto.randomUUID(),
|
|
158
|
+
name: params.name || `${params.ownerName}'s Tip Jar`,
|
|
159
|
+
description: params.description,
|
|
160
|
+
ownerId: params.ownerId,
|
|
161
|
+
ownerName: params.ownerName,
|
|
162
|
+
addresses: params.addresses.map((a, i) => ({
|
|
163
|
+
...a,
|
|
164
|
+
isDefault: i === 0,
|
|
165
|
+
})),
|
|
166
|
+
settings: {
|
|
167
|
+
minTip: params.settings?.minTip || '1',
|
|
168
|
+
suggestedAmounts: params.settings?.suggestedAmounts || ['5', '10', '25', '50'],
|
|
169
|
+
thankYouMessage: params.settings?.thankYouMessage || 'Thanks for the tip! 🙏',
|
|
170
|
+
allowAnonymous: params.settings?.allowAnonymous ?? true,
|
|
171
|
+
notifyOnTip: params.settings?.notifyOnTip ?? true,
|
|
172
|
+
},
|
|
173
|
+
stats: {
|
|
174
|
+
totalReceived: '0',
|
|
175
|
+
tipCount: 0,
|
|
176
|
+
uniqueTippers: 0,
|
|
177
|
+
largestTip: '0',
|
|
178
|
+
},
|
|
179
|
+
publicSlug: params.publicSlug,
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
updatedAt: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
jars.push(jar);
|
|
185
|
+
await this.saveJars(jars);
|
|
186
|
+
|
|
187
|
+
return jar;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get tip jar by ID, slug, or owner
|
|
192
|
+
*/
|
|
193
|
+
async getJar(identifier: string): Promise<TipJar | null> {
|
|
194
|
+
const jars = await this.loadJars();
|
|
195
|
+
return jars.find(j =>
|
|
196
|
+
j.id === identifier ||
|
|
197
|
+
j.publicSlug === identifier ||
|
|
198
|
+
j.ownerId === identifier
|
|
199
|
+
) || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update tip jar settings
|
|
204
|
+
*/
|
|
205
|
+
async updateJar(
|
|
206
|
+
id: string,
|
|
207
|
+
updates: Partial<Pick<TipJar, 'name' | 'description' | 'settings' | 'publicSlug'>>
|
|
208
|
+
): Promise<TipJar | null> {
|
|
209
|
+
const jars = await this.loadJars();
|
|
210
|
+
const jar = jars.find(j => j.id === id);
|
|
211
|
+
|
|
212
|
+
if (jar) {
|
|
213
|
+
if (updates.name) jar.name = updates.name;
|
|
214
|
+
if (updates.description !== undefined) jar.description = updates.description;
|
|
215
|
+
if (updates.publicSlug !== undefined) {
|
|
216
|
+
// Check for duplicate
|
|
217
|
+
if (updates.publicSlug && jars.some(j => j.id !== id && j.publicSlug === updates.publicSlug)) {
|
|
218
|
+
throw new Error(`Slug "${updates.publicSlug}" is already taken`);
|
|
219
|
+
}
|
|
220
|
+
jar.publicSlug = updates.publicSlug;
|
|
221
|
+
}
|
|
222
|
+
if (updates.settings) {
|
|
223
|
+
jar.settings = { ...jar.settings, ...updates.settings };
|
|
224
|
+
}
|
|
225
|
+
jar.updatedAt = new Date().toISOString();
|
|
226
|
+
await this.saveJars(jars);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return jar || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add address to tip jar
|
|
234
|
+
*/
|
|
235
|
+
async addAddress(jarId: string, chain: string, address: string): Promise<TipJar | null> {
|
|
236
|
+
const jars = await this.loadJars();
|
|
237
|
+
const jar = jars.find(j => j.id === jarId);
|
|
238
|
+
|
|
239
|
+
if (jar) {
|
|
240
|
+
if (jar.addresses.some(a => a.chain === chain)) {
|
|
241
|
+
throw new Error(`Address for ${chain} already exists`);
|
|
242
|
+
}
|
|
243
|
+
jar.addresses.push({ chain, address, isDefault: false });
|
|
244
|
+
jar.updatedAt = new Date().toISOString();
|
|
245
|
+
await this.saveJars(jars);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return jar || null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Set default address
|
|
253
|
+
*/
|
|
254
|
+
async setDefaultAddress(jarId: string, chain: string): Promise<TipJar | null> {
|
|
255
|
+
const jars = await this.loadJars();
|
|
256
|
+
const jar = jars.find(j => j.id === jarId);
|
|
257
|
+
|
|
258
|
+
if (jar) {
|
|
259
|
+
jar.addresses.forEach(a => {
|
|
260
|
+
a.isDefault = a.chain === chain;
|
|
261
|
+
});
|
|
262
|
+
jar.updatedAt = new Date().toISOString();
|
|
263
|
+
await this.saveJars(jars);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return jar || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============ Tip Processing ============
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Record a tip
|
|
273
|
+
*/
|
|
274
|
+
async recordTip(params: {
|
|
275
|
+
tipJarId: string;
|
|
276
|
+
fromAddress: string;
|
|
277
|
+
fromName?: string;
|
|
278
|
+
isAnonymous?: boolean;
|
|
279
|
+
amount: string;
|
|
280
|
+
chain: string;
|
|
281
|
+
txHash: string;
|
|
282
|
+
message?: string;
|
|
283
|
+
}): Promise<Tip> {
|
|
284
|
+
const jars = await this.loadJars();
|
|
285
|
+
const jar = jars.find(j => j.id === params.tipJarId);
|
|
286
|
+
|
|
287
|
+
if (!jar) {
|
|
288
|
+
throw new Error('Tip jar not found');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const tips = await this.loadTips();
|
|
292
|
+
|
|
293
|
+
const tip: Tip = {
|
|
294
|
+
id: crypto.randomUUID(),
|
|
295
|
+
tipJarId: params.tipJarId,
|
|
296
|
+
fromAddress: params.fromAddress,
|
|
297
|
+
fromName: params.fromName,
|
|
298
|
+
isAnonymous: params.isAnonymous ?? false,
|
|
299
|
+
amount: params.amount,
|
|
300
|
+
chain: params.chain,
|
|
301
|
+
txHash: params.txHash,
|
|
302
|
+
message: params.message,
|
|
303
|
+
status: 'confirmed',
|
|
304
|
+
confirmedAt: new Date().toISOString(),
|
|
305
|
+
createdAt: new Date().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
tips.push(tip);
|
|
309
|
+
await this.saveTips(tips);
|
|
310
|
+
|
|
311
|
+
// Update jar stats
|
|
312
|
+
const amount = parseFloat(params.amount);
|
|
313
|
+
jar.stats.totalReceived = (parseFloat(jar.stats.totalReceived) + amount).toString();
|
|
314
|
+
jar.stats.tipCount++;
|
|
315
|
+
|
|
316
|
+
// Check if new unique tipper
|
|
317
|
+
const previousTips = tips.filter(t =>
|
|
318
|
+
t.tipJarId === jar.id &&
|
|
319
|
+
t.fromAddress.toLowerCase() === params.fromAddress.toLowerCase() &&
|
|
320
|
+
t.id !== tip.id
|
|
321
|
+
);
|
|
322
|
+
if (previousTips.length === 0) {
|
|
323
|
+
jar.stats.uniqueTippers++;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Update largest tip
|
|
327
|
+
if (amount > parseFloat(jar.stats.largestTip)) {
|
|
328
|
+
jar.stats.largestTip = params.amount;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
jar.stats.lastTipAt = new Date().toISOString();
|
|
332
|
+
jar.updatedAt = new Date().toISOString();
|
|
333
|
+
await this.saveJars(jars);
|
|
334
|
+
|
|
335
|
+
return tip;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get tips for a jar
|
|
340
|
+
*/
|
|
341
|
+
async getTips(jarId: string, options?: {
|
|
342
|
+
limit?: number;
|
|
343
|
+
includeAnonymous?: boolean;
|
|
344
|
+
}): Promise<Tip[]> {
|
|
345
|
+
let tips = await this.loadTips();
|
|
346
|
+
tips = tips.filter(t => t.tipJarId === jarId);
|
|
347
|
+
|
|
348
|
+
if (options?.includeAnonymous === false) {
|
|
349
|
+
tips = tips.filter(t => !t.isAnonymous);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
tips.sort((a, b) =>
|
|
353
|
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (options?.limit) {
|
|
357
|
+
tips = tips.slice(0, options.limit);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return tips;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get recent tips across all jars (for activity feed)
|
|
365
|
+
*/
|
|
366
|
+
async getRecentTips(limit = 10): Promise<(Tip & { jarName: string })[]> {
|
|
367
|
+
const tips = await this.loadTips();
|
|
368
|
+
const jars = await this.loadJars();
|
|
369
|
+
|
|
370
|
+
const jarMap = new Map(jars.map(j => [j.id, j]));
|
|
371
|
+
|
|
372
|
+
return tips
|
|
373
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
374
|
+
.slice(0, limit)
|
|
375
|
+
.map(tip => ({
|
|
376
|
+
...tip,
|
|
377
|
+
jarName: jarMap.get(tip.tipJarId)?.name || 'Unknown',
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============ Leaderboards ============
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generate leaderboard for a tip jar
|
|
385
|
+
*/
|
|
386
|
+
async getLeaderboard(jarId: string, period: Leaderboard['period'] = 'all-time'): Promise<Leaderboard> {
|
|
387
|
+
let tips = await this.loadTips();
|
|
388
|
+
tips = tips.filter(t => t.tipJarId === jarId && !t.isAnonymous);
|
|
389
|
+
|
|
390
|
+
// Filter by period
|
|
391
|
+
const now = new Date();
|
|
392
|
+
if (period === 'weekly') {
|
|
393
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
394
|
+
tips = tips.filter(t => new Date(t.createdAt) >= weekAgo);
|
|
395
|
+
} else if (period === 'monthly') {
|
|
396
|
+
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
397
|
+
tips = tips.filter(t => new Date(t.createdAt) >= monthAgo);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Aggregate by tipper
|
|
401
|
+
const tipperMap = new Map<string, { name: string; total: number; count: number }>();
|
|
402
|
+
|
|
403
|
+
for (const tip of tips) {
|
|
404
|
+
const key = tip.fromAddress.toLowerCase();
|
|
405
|
+
const existing = tipperMap.get(key) || {
|
|
406
|
+
name: tip.fromName || this.shortAddress(tip.fromAddress),
|
|
407
|
+
total: 0,
|
|
408
|
+
count: 0
|
|
409
|
+
};
|
|
410
|
+
existing.total += parseFloat(tip.amount);
|
|
411
|
+
existing.count++;
|
|
412
|
+
if (tip.fromName) existing.name = tip.fromName;
|
|
413
|
+
tipperMap.set(key, existing);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Sort and rank
|
|
417
|
+
const entries = Array.from(tipperMap.entries())
|
|
418
|
+
.map(([address, data]) => ({
|
|
419
|
+
address,
|
|
420
|
+
name: data.name,
|
|
421
|
+
totalTipped: data.total.toFixed(2),
|
|
422
|
+
tipCount: data.count,
|
|
423
|
+
}))
|
|
424
|
+
.sort((a, b) => parseFloat(b.totalTipped) - parseFloat(a.totalTipped))
|
|
425
|
+
.map((entry, i) => ({ rank: i + 1, ...entry }));
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
tipJarId: jarId,
|
|
429
|
+
period,
|
|
430
|
+
entries,
|
|
431
|
+
generatedAt: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============ Tip Commands (for Clawdbot) ============
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Parse tip command and return tip info
|
|
439
|
+
* Supports: "tip @gustav 10", "tip 5 to gustav", etc.
|
|
440
|
+
*/
|
|
441
|
+
parseTipCommand(command: string): {
|
|
442
|
+
recipient: string;
|
|
443
|
+
amount: string;
|
|
444
|
+
message?: string;
|
|
445
|
+
} | null {
|
|
446
|
+
// Patterns:
|
|
447
|
+
// "tip @gustav 10"
|
|
448
|
+
// "tip 10 to @gustav"
|
|
449
|
+
// "tip gustav 10 usdc thanks!"
|
|
450
|
+
|
|
451
|
+
const patterns = [
|
|
452
|
+
// tip @recipient amount [message]
|
|
453
|
+
/^tip\s+@?(\w+)\s+(\d+(?:\.\d+)?)\s*(?:usdc)?\s*(.*)$/i,
|
|
454
|
+
// tip amount to @recipient [message]
|
|
455
|
+
/^tip\s+(\d+(?:\.\d+)?)\s*(?:usdc)?\s+to\s+@?(\w+)\s*(.*)$/i,
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
for (const pattern of patterns) {
|
|
459
|
+
const match = command.match(pattern);
|
|
460
|
+
if (match) {
|
|
461
|
+
if (pattern === patterns[0]) {
|
|
462
|
+
return {
|
|
463
|
+
recipient: match[1],
|
|
464
|
+
amount: match[2],
|
|
465
|
+
message: match[3]?.trim() || undefined,
|
|
466
|
+
};
|
|
467
|
+
} else {
|
|
468
|
+
return {
|
|
469
|
+
recipient: match[2],
|
|
470
|
+
amount: match[1],
|
|
471
|
+
message: match[3]?.trim() || undefined,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Generate tip jar card/embed for display
|
|
482
|
+
*/
|
|
483
|
+
formatTipJarCard(jar: TipJar): string {
|
|
484
|
+
const defaultAddr = jar.addresses.find(a => a.isDefault) || jar.addresses[0];
|
|
485
|
+
|
|
486
|
+
let card = `💰 **${jar.name}**\n`;
|
|
487
|
+
if (jar.description) {
|
|
488
|
+
card += `${jar.description}\n`;
|
|
489
|
+
}
|
|
490
|
+
card += `\n`;
|
|
491
|
+
card += `Owner: ${jar.ownerName}\n`;
|
|
492
|
+
card += `Total Received: **$${parseFloat(jar.stats.totalReceived).toFixed(2)} USDC**\n`;
|
|
493
|
+
card += `Tips: ${jar.stats.tipCount} from ${jar.stats.uniqueTippers} tippers\n`;
|
|
494
|
+
card += `\n`;
|
|
495
|
+
card += `**Quick Tip:**\n`;
|
|
496
|
+
card += jar.settings.suggestedAmounts.map(a => `[$${a}]`).join(' ') + '\n';
|
|
497
|
+
card += `\n`;
|
|
498
|
+
card += `Send to: \`${defaultAddr.address}\`\n`;
|
|
499
|
+
card += `Chain: ${defaultAddr.chain}\n`;
|
|
500
|
+
|
|
501
|
+
if (jar.publicSlug) {
|
|
502
|
+
card += `\nLink: tip.clawd.bot/${jar.publicSlug}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return card;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Format tip notification
|
|
510
|
+
*/
|
|
511
|
+
formatTipNotification(tip: Tip, jar: TipJar): string {
|
|
512
|
+
const senderName = tip.isAnonymous ? 'Anonymous' : (tip.fromName || this.shortAddress(tip.fromAddress));
|
|
513
|
+
|
|
514
|
+
let notif = `🎉 **New Tip!**\n\n`;
|
|
515
|
+
notif += `${senderName} tipped **$${tip.amount} USDC**`;
|
|
516
|
+
|
|
517
|
+
if (tip.message) {
|
|
518
|
+
notif += `\n\n"${tip.message}"`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
notif += `\n\n${jar.settings.thankYouMessage}`;
|
|
522
|
+
|
|
523
|
+
return notif;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private shortAddress(address: string): string {
|
|
527
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ============ Agent-to-Agent Tips ============
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Send tip from one agent to another
|
|
534
|
+
* Returns the transaction to be executed
|
|
535
|
+
*/
|
|
536
|
+
async prepareAgentTip(params: {
|
|
537
|
+
fromAgentId: string;
|
|
538
|
+
fromWalletId: string;
|
|
539
|
+
toAgentId: string;
|
|
540
|
+
amount: string;
|
|
541
|
+
message?: string;
|
|
542
|
+
}): Promise<{
|
|
543
|
+
toJar: TipJar;
|
|
544
|
+
toAddress: string;
|
|
545
|
+
chain: string;
|
|
546
|
+
amount: string;
|
|
547
|
+
message?: string;
|
|
548
|
+
} | null> {
|
|
549
|
+
// Find recipient's tip jar
|
|
550
|
+
const toJar = await this.getJar(params.toAgentId);
|
|
551
|
+
if (!toJar) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const defaultAddr = toJar.addresses.find(a => a.isDefault) || toJar.addresses[0];
|
|
556
|
+
if (!defaultAddr) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
toJar,
|
|
562
|
+
toAddress: defaultAddr.address,
|
|
563
|
+
chain: defaultAddr.chain,
|
|
564
|
+
amount: params.amount,
|
|
565
|
+
message: params.message,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export default TipJarManager;
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pay Lobster Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface LobsterConfig {
|
|
6
|
+
/** Circle API key for wallet operations */
|
|
7
|
+
circleApiKey?: string;
|
|
8
|
+
/** Ethereum private key for direct transactions */
|
|
9
|
+
privateKey?: string;
|
|
10
|
+
/** Network: 'base' | 'base-sepolia' | 'ethereum' */
|
|
11
|
+
network?: 'base' | 'base-sepolia' | 'ethereum';
|
|
12
|
+
/** Existing Circle wallet ID */
|
|
13
|
+
walletId?: string;
|
|
14
|
+
/** Custom RPC URL */
|
|
15
|
+
rpcUrl?: string;
|
|
16
|
+
/** Enable ERC-8004 trust verification */
|
|
17
|
+
enableTrust?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Wallet {
|
|
21
|
+
id: string;
|
|
22
|
+
address: string;
|
|
23
|
+
network: string;
|
|
24
|
+
balance?: string;
|
|
25
|
+
createdAt?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Transfer {
|
|
29
|
+
id: string;
|
|
30
|
+
hash?: string;
|
|
31
|
+
status: 'pending' | 'confirmed' | 'failed';
|
|
32
|
+
amount: string;
|
|
33
|
+
to: string;
|
|
34
|
+
from: string;
|
|
35
|
+
memo?: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Escrow {
|
|
40
|
+
id: string;
|
|
41
|
+
amount: string;
|
|
42
|
+
buyer: string;
|
|
43
|
+
seller: string;
|
|
44
|
+
status: 'funded' | 'released' | 'disputed' | 'refunded';
|
|
45
|
+
conditions?: EscrowConditions;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface EscrowConditions {
|
|
50
|
+
type: 'milestone' | 'time' | 'approval';
|
|
51
|
+
description: string;
|
|
52
|
+
deadline?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TrustScore {
|
|
56
|
+
score: number;
|
|
57
|
+
level: 'new' | 'established' | 'trusted' | 'verified';
|
|
58
|
+
totalTransactions: number;
|
|
59
|
+
successRate: number;
|
|
60
|
+
ratings?: Rating[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Rating {
|
|
64
|
+
rating: number;
|
|
65
|
+
comment?: string;
|
|
66
|
+
from: string;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Agent {
|
|
71
|
+
address: string;
|
|
72
|
+
name: string;
|
|
73
|
+
capabilities: string[];
|
|
74
|
+
pricing?: Record<string, string>;
|
|
75
|
+
trustScore?: TrustScore;
|
|
76
|
+
metadata?: Record<string, any>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TransferOptions {
|
|
80
|
+
to: string;
|
|
81
|
+
amount: string;
|
|
82
|
+
memo?: string;
|
|
83
|
+
idempotencyKey?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface EscrowOptions {
|
|
87
|
+
amount: string;
|
|
88
|
+
recipient: string;
|
|
89
|
+
conditions?: EscrowConditions;
|
|
90
|
+
template?: 'freelance' | 'marketplace' | 'rental';
|
|
91
|
+
deadline?: string;
|
|
92
|
+
milestones?: { name: string; amount: string }[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DiscoverOptions {
|
|
96
|
+
capability?: string;
|
|
97
|
+
minTrustScore?: number;
|
|
98
|
+
maxPrice?: string;
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AutonomousConfig {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
dailyLimit: string;
|
|
105
|
+
perTransactionLimit: string;
|
|
106
|
+
autoApproveBelow: string;
|
|
107
|
+
allowedCapabilities: string[];
|
|
108
|
+
}
|