clawdentials-mcp 0.1.0 → 0.7.2
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 +310 -58
- package/dist/index.d.ts +1 -1
- package/dist/index.js +225 -18
- package/dist/schemas/index.d.ts +141 -0
- package/dist/schemas/index.js +54 -0
- package/dist/services/firestore.d.ts +45 -2
- package/dist/services/firestore.js +410 -6
- package/dist/services/payments/alby.d.ts +104 -0
- package/dist/services/payments/alby.js +239 -0
- package/dist/services/payments/breez.d.ts +91 -0
- package/dist/services/payments/breez.js +267 -0
- package/dist/services/payments/cashu.d.ts +127 -0
- package/dist/services/payments/cashu.js +248 -0
- package/dist/services/payments/coinremitter.d.ts +84 -0
- package/dist/services/payments/coinremitter.js +176 -0
- package/dist/services/payments/index.d.ts +132 -0
- package/dist/services/payments/index.js +180 -0
- package/dist/services/payments/oxapay.d.ts +89 -0
- package/dist/services/payments/oxapay.js +221 -0
- package/dist/services/payments/x402.d.ts +61 -0
- package/dist/services/payments/x402.js +94 -0
- package/dist/services/payments/zbd.d.ts +88 -0
- package/dist/services/payments/zbd.js +221 -0
- package/dist/tools/admin.d.ts +195 -0
- package/dist/tools/admin.js +210 -0
- package/dist/tools/agent.d.ts +197 -0
- package/dist/tools/agent.js +200 -0
- package/dist/tools/escrow.d.ts +74 -16
- package/dist/tools/escrow.js +139 -28
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/payment.d.ts +144 -0
- package/dist/tools/payment.js +376 -0
- package/dist/types/index.d.ts +44 -1
- package/package.json +18 -2
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { initializeApp, getApps, applicationDefault } from 'firebase-admin/app';
|
|
2
2
|
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
|
|
3
|
+
import { createHash, randomBytes } from 'crypto';
|
|
4
|
+
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
|
5
|
+
import * as nip19 from 'nostr-tools/nip19';
|
|
6
|
+
// Platform fee rate (10%)
|
|
7
|
+
export const FEE_RATE = 0.10;
|
|
8
|
+
// Admin secret (in production, use environment variable)
|
|
9
|
+
export const ADMIN_SECRET = process.env.CLAWDENTIALS_ADMIN_SECRET || 'clawdentials-admin-secret-change-me';
|
|
10
|
+
// API Key utilities
|
|
11
|
+
export function generateApiKey() {
|
|
12
|
+
return `clw_${randomBytes(24).toString('hex')}`;
|
|
13
|
+
}
|
|
14
|
+
export function hashApiKey(apiKey) {
|
|
15
|
+
return createHash('sha256').update(apiKey).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
// Nostr identity utilities
|
|
18
|
+
export function generateNostrKeypair() {
|
|
19
|
+
const secretKey = generateSecretKey();
|
|
20
|
+
const publicKey = getPublicKey(secretKey); // hex format
|
|
21
|
+
const nsec = nip19.nsecEncode(secretKey);
|
|
22
|
+
const npub = nip19.npubEncode(publicKey);
|
|
23
|
+
return { secretKey, publicKey, nsec, npub };
|
|
24
|
+
}
|
|
3
25
|
let app;
|
|
4
26
|
let db;
|
|
5
27
|
export function initFirestore() {
|
|
@@ -30,16 +52,22 @@ export const collections = {
|
|
|
30
52
|
agents: () => getDb().collection('agents'),
|
|
31
53
|
tasks: () => getDb().collection('tasks'),
|
|
32
54
|
subscriptions: () => getDb().collection('subscriptions'),
|
|
55
|
+
withdrawals: () => getDb().collection('withdrawals'),
|
|
56
|
+
deposits: () => getDb().collection('deposits'),
|
|
33
57
|
};
|
|
34
58
|
// Escrow operations
|
|
35
59
|
export async function createEscrow(data) {
|
|
36
60
|
const docRef = collections.escrows().doc();
|
|
61
|
+
const fee = data.amount * FEE_RATE;
|
|
37
62
|
const escrow = {
|
|
38
63
|
...data,
|
|
64
|
+
fee,
|
|
65
|
+
feeRate: FEE_RATE,
|
|
39
66
|
status: 'pending',
|
|
40
67
|
createdAt: new Date(),
|
|
41
68
|
completedAt: null,
|
|
42
69
|
proofOfWork: null,
|
|
70
|
+
disputeReason: null,
|
|
43
71
|
};
|
|
44
72
|
await docRef.set({
|
|
45
73
|
...escrow,
|
|
@@ -59,11 +87,14 @@ export async function getEscrow(escrowId) {
|
|
|
59
87
|
providerAgentId: data.providerAgentId,
|
|
60
88
|
taskDescription: data.taskDescription,
|
|
61
89
|
amount: data.amount,
|
|
90
|
+
fee: data.fee ?? 0,
|
|
91
|
+
feeRate: data.feeRate ?? FEE_RATE,
|
|
62
92
|
currency: data.currency,
|
|
63
93
|
status: data.status,
|
|
64
94
|
createdAt: data.createdAt.toDate(),
|
|
65
95
|
completedAt: data.completedAt?.toDate() ?? null,
|
|
66
96
|
proofOfWork: data.proofOfWork ?? null,
|
|
97
|
+
disputeReason: data.disputeReason ?? null,
|
|
67
98
|
};
|
|
68
99
|
}
|
|
69
100
|
export async function completeEscrow(escrowId, proofOfWork) {
|
|
@@ -85,15 +116,388 @@ export async function completeEscrow(escrowId, proofOfWork) {
|
|
|
85
116
|
}
|
|
86
117
|
return escrow;
|
|
87
118
|
}
|
|
119
|
+
// Default stats for new agents
|
|
120
|
+
function defaultAgentStats() {
|
|
121
|
+
return {
|
|
122
|
+
tasksCompleted: 0,
|
|
123
|
+
totalEarned: 0,
|
|
124
|
+
successRate: 100,
|
|
125
|
+
avgCompletionTime: 0,
|
|
126
|
+
disputeCount: 0,
|
|
127
|
+
disputeRate: 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Agent operations
|
|
131
|
+
export async function createAgent(data) {
|
|
132
|
+
const docRef = collections.agents().doc(data.name); // Use name as ID for simplicity
|
|
133
|
+
const existingDoc = await docRef.get();
|
|
134
|
+
if (existingDoc.exists) {
|
|
135
|
+
throw new Error(`Agent with name "${data.name}" already exists`);
|
|
136
|
+
}
|
|
137
|
+
// Generate API key
|
|
138
|
+
const apiKey = generateApiKey();
|
|
139
|
+
const apiKeyHash = hashApiKey(apiKey);
|
|
140
|
+
// Generate Nostr keypair for NIP-05 identity
|
|
141
|
+
const nostrKeys = generateNostrKeypair();
|
|
142
|
+
const nip05 = `${data.name}@clawdentials.com`;
|
|
143
|
+
const agent = {
|
|
144
|
+
...data,
|
|
145
|
+
createdAt: new Date(),
|
|
146
|
+
stats: defaultAgentStats(),
|
|
147
|
+
apiKeyHash,
|
|
148
|
+
balance: 0,
|
|
149
|
+
nostrPubkey: nostrKeys.publicKey,
|
|
150
|
+
nip05,
|
|
151
|
+
};
|
|
152
|
+
await docRef.set({
|
|
153
|
+
...agent,
|
|
154
|
+
createdAt: Timestamp.fromDate(agent.createdAt),
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
agent: { id: docRef.id, ...agent },
|
|
158
|
+
apiKey,
|
|
159
|
+
nostr: {
|
|
160
|
+
nsec: nostrKeys.nsec,
|
|
161
|
+
npub: nostrKeys.npub,
|
|
162
|
+
nip05,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export async function getAgent(agentId) {
|
|
167
|
+
const doc = await collections.agents().doc(agentId).get();
|
|
168
|
+
if (!doc.exists) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const data = doc.data();
|
|
172
|
+
return {
|
|
173
|
+
id: doc.id,
|
|
174
|
+
name: data.name,
|
|
175
|
+
description: data.description,
|
|
176
|
+
skills: data.skills || [],
|
|
177
|
+
createdAt: data.createdAt.toDate(),
|
|
178
|
+
verified: data.verified || false,
|
|
179
|
+
subscriptionTier: data.subscriptionTier || 'free',
|
|
180
|
+
stats: {
|
|
181
|
+
tasksCompleted: data.stats?.tasksCompleted || 0,
|
|
182
|
+
totalEarned: data.stats?.totalEarned || 0,
|
|
183
|
+
successRate: data.stats?.successRate || 100,
|
|
184
|
+
avgCompletionTime: data.stats?.avgCompletionTime || 0,
|
|
185
|
+
disputeCount: data.stats?.disputeCount || 0,
|
|
186
|
+
disputeRate: data.stats?.disputeRate || 0,
|
|
187
|
+
},
|
|
188
|
+
apiKeyHash: data.apiKeyHash || '',
|
|
189
|
+
balance: data.balance || 0,
|
|
190
|
+
nostrPubkey: data.nostrPubkey || undefined,
|
|
191
|
+
nip05: data.nip05 || undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Validate API key for an agent
|
|
195
|
+
export async function validateApiKey(agentId, apiKey) {
|
|
196
|
+
const agent = await getAgent(agentId);
|
|
197
|
+
if (!agent || !agent.apiKeyHash) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return agent.apiKeyHash === hashApiKey(apiKey);
|
|
201
|
+
}
|
|
202
|
+
export async function getOrCreateAgent(agentId) {
|
|
203
|
+
const existing = await getAgent(agentId);
|
|
204
|
+
if (existing) {
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
207
|
+
// Auto-create minimal agent record for tracking
|
|
208
|
+
const { agent } = await createAgent({
|
|
209
|
+
name: agentId,
|
|
210
|
+
description: 'Auto-registered agent',
|
|
211
|
+
skills: [],
|
|
212
|
+
verified: false,
|
|
213
|
+
subscriptionTier: 'free',
|
|
214
|
+
});
|
|
215
|
+
return agent;
|
|
216
|
+
}
|
|
217
|
+
export async function searchAgents(query) {
|
|
218
|
+
let ref = collections.agents().limit(query.limit || 20);
|
|
219
|
+
if (query.verified !== undefined) {
|
|
220
|
+
ref = ref.where('verified', '==', query.verified);
|
|
221
|
+
}
|
|
222
|
+
if (query.minTasksCompleted !== undefined) {
|
|
223
|
+
ref = ref.where('stats.tasksCompleted', '>=', query.minTasksCompleted);
|
|
224
|
+
}
|
|
225
|
+
const snapshot = await ref.get();
|
|
226
|
+
const agents = [];
|
|
227
|
+
for (const doc of snapshot.docs) {
|
|
228
|
+
const agent = await getAgent(doc.id);
|
|
229
|
+
if (agent) {
|
|
230
|
+
// Filter by skill in memory (Firestore array-contains limitation)
|
|
231
|
+
if (query.skill && !agent.skills.some(s => s.toLowerCase().includes(query.skill.toLowerCase()))) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
agents.push(agent);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return agents;
|
|
238
|
+
}
|
|
88
239
|
async function updateAgentStats(agentId, amount) {
|
|
240
|
+
// Ensure agent exists (auto-create if needed)
|
|
241
|
+
await getOrCreateAgent(agentId);
|
|
242
|
+
const agentRef = collections.agents().doc(agentId);
|
|
243
|
+
const doc = await agentRef.get();
|
|
244
|
+
const data = doc.data();
|
|
245
|
+
const currentStats = data.stats || defaultAgentStats();
|
|
246
|
+
const newTasksCompleted = currentStats.tasksCompleted + 1;
|
|
247
|
+
const newTotalEarned = currentStats.totalEarned + amount;
|
|
248
|
+
await agentRef.update({
|
|
249
|
+
'stats.tasksCompleted': newTasksCompleted,
|
|
250
|
+
'stats.totalEarned': newTotalEarned,
|
|
251
|
+
// Recalculate dispute rate
|
|
252
|
+
'stats.disputeRate': currentStats.disputeCount / newTasksCompleted * 100,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export async function incrementAgentDisputes(agentId) {
|
|
256
|
+
await getOrCreateAgent(agentId);
|
|
257
|
+
const agentRef = collections.agents().doc(agentId);
|
|
258
|
+
const doc = await agentRef.get();
|
|
259
|
+
const data = doc.data();
|
|
260
|
+
const currentStats = data.stats || defaultAgentStats();
|
|
261
|
+
const newDisputeCount = currentStats.disputeCount + 1;
|
|
262
|
+
const totalTasks = currentStats.tasksCompleted || 1;
|
|
263
|
+
await agentRef.update({
|
|
264
|
+
'stats.disputeCount': newDisputeCount,
|
|
265
|
+
'stats.disputeRate': (newDisputeCount / totalTasks) * 100,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export async function disputeEscrow(escrowId, reason) {
|
|
269
|
+
const escrowRef = collections.escrows().doc(escrowId);
|
|
270
|
+
const doc = await escrowRef.get();
|
|
271
|
+
if (!doc.exists) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const data = doc.data();
|
|
275
|
+
// Can only dispute pending or in_progress escrows
|
|
276
|
+
if (data.status === 'completed' || data.status === 'cancelled') {
|
|
277
|
+
throw new Error(`Cannot dispute escrow with status: ${data.status}`);
|
|
278
|
+
}
|
|
279
|
+
await escrowRef.update({
|
|
280
|
+
status: 'disputed',
|
|
281
|
+
disputeReason: reason,
|
|
282
|
+
});
|
|
283
|
+
// Increment dispute count for the provider agent
|
|
284
|
+
await incrementAgentDisputes(data.providerAgentId);
|
|
285
|
+
return getEscrow(escrowId);
|
|
286
|
+
}
|
|
287
|
+
// Reputation scoring algorithm (from ARCHITECTURE.md)
|
|
288
|
+
// score = (
|
|
289
|
+
// (tasks_completed * 2) +
|
|
290
|
+
// (success_rate * 30) +
|
|
291
|
+
// (log(total_earned + 1) * 10) +
|
|
292
|
+
// (speed_bonus * 10) +
|
|
293
|
+
// (account_age_days * 0.1)
|
|
294
|
+
// ) / max_possible * 100
|
|
295
|
+
export function calculateReputationScore(agent) {
|
|
296
|
+
const stats = agent.stats;
|
|
297
|
+
const accountAgeDays = Math.floor((Date.now() - agent.createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
298
|
+
// Calculate success rate (accounting for disputes)
|
|
299
|
+
const effectiveSuccessRate = stats.tasksCompleted > 0
|
|
300
|
+
? ((stats.tasksCompleted - stats.disputeCount) / stats.tasksCompleted) * 100
|
|
301
|
+
: 100;
|
|
302
|
+
// Speed bonus (placeholder - would need actual timing data)
|
|
303
|
+
const speedBonus = stats.avgCompletionTime > 0 ? Math.max(0, 10 - stats.avgCompletionTime / 60) : 5;
|
|
304
|
+
// Raw score components
|
|
305
|
+
const taskScore = stats.tasksCompleted * 2;
|
|
306
|
+
const successScore = effectiveSuccessRate * 0.3; // Normalized (max 30)
|
|
307
|
+
const earningsScore = Math.log10(stats.totalEarned + 1) * 10;
|
|
308
|
+
const speedScore = speedBonus;
|
|
309
|
+
const ageScore = accountAgeDays * 0.1;
|
|
310
|
+
// Max possible (for normalization)
|
|
311
|
+
// Assuming max: 10000 tasks, 100% success, $1M earned, max speed, 365 days
|
|
312
|
+
const maxPossible = (10000 * 2) + (100 * 0.3) + (Math.log10(1000001) * 10) + 10 + (365 * 0.1);
|
|
313
|
+
const rawScore = taskScore + successScore + earningsScore + speedScore + ageScore;
|
|
314
|
+
const normalizedScore = (rawScore / maxPossible) * 100;
|
|
315
|
+
// Clamp between 0 and 100
|
|
316
|
+
return Math.min(100, Math.max(0, Math.round(normalizedScore * 10) / 10));
|
|
317
|
+
}
|
|
318
|
+
// Balance operations
|
|
319
|
+
export async function getBalance(agentId) {
|
|
320
|
+
const agent = await getAgent(agentId);
|
|
321
|
+
return agent?.balance ?? 0;
|
|
322
|
+
}
|
|
323
|
+
export async function creditBalance(agentId, amount, notes) {
|
|
324
|
+
const agentRef = collections.agents().doc(agentId);
|
|
325
|
+
const doc = await agentRef.get();
|
|
326
|
+
if (!doc.exists) {
|
|
327
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
328
|
+
}
|
|
329
|
+
const currentBalance = doc.data().balance || 0;
|
|
330
|
+
const newBalance = currentBalance + amount;
|
|
331
|
+
await agentRef.update({ balance: newBalance });
|
|
332
|
+
return newBalance;
|
|
333
|
+
}
|
|
334
|
+
export async function debitBalance(agentId, amount) {
|
|
89
335
|
const agentRef = collections.agents().doc(agentId);
|
|
90
336
|
const doc = await agentRef.get();
|
|
91
|
-
if (doc.exists) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
337
|
+
if (!doc.exists) {
|
|
338
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
339
|
+
}
|
|
340
|
+
const currentBalance = doc.data().balance || 0;
|
|
341
|
+
if (currentBalance < amount) {
|
|
342
|
+
throw new Error(`Insufficient balance: have ${currentBalance}, need ${amount}`);
|
|
343
|
+
}
|
|
344
|
+
const newBalance = currentBalance - amount;
|
|
345
|
+
await agentRef.update({ balance: newBalance });
|
|
346
|
+
return newBalance;
|
|
347
|
+
}
|
|
348
|
+
// Escrow with balance (new flow)
|
|
349
|
+
export async function createEscrowWithBalance(data) {
|
|
350
|
+
// First, check and debit client balance
|
|
351
|
+
const clientBalance = await getBalance(data.clientAgentId);
|
|
352
|
+
if (clientBalance < data.amount) {
|
|
353
|
+
throw new Error(`Insufficient balance: have ${clientBalance} ${data.currency}, need ${data.amount} ${data.currency}`);
|
|
354
|
+
}
|
|
355
|
+
// Debit the full amount from client
|
|
356
|
+
await debitBalance(data.clientAgentId, data.amount);
|
|
357
|
+
// Create the escrow
|
|
358
|
+
const docRef = collections.escrows().doc();
|
|
359
|
+
const fee = data.amount * FEE_RATE;
|
|
360
|
+
const escrow = {
|
|
361
|
+
...data,
|
|
362
|
+
fee,
|
|
363
|
+
feeRate: FEE_RATE,
|
|
364
|
+
status: 'pending',
|
|
365
|
+
createdAt: new Date(),
|
|
366
|
+
completedAt: null,
|
|
367
|
+
proofOfWork: null,
|
|
368
|
+
disputeReason: null,
|
|
369
|
+
};
|
|
370
|
+
await docRef.set({
|
|
371
|
+
...escrow,
|
|
372
|
+
createdAt: Timestamp.fromDate(escrow.createdAt),
|
|
373
|
+
});
|
|
374
|
+
return { id: docRef.id, ...escrow };
|
|
375
|
+
}
|
|
376
|
+
export async function completeEscrowWithBalance(escrowId, proofOfWork) {
|
|
377
|
+
const escrowRef = collections.escrows().doc(escrowId);
|
|
378
|
+
const doc = await escrowRef.get();
|
|
379
|
+
if (!doc.exists) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const escrowData = doc.data();
|
|
383
|
+
// Credit provider with amount minus fee
|
|
384
|
+
const netAmount = escrowData.amount - (escrowData.fee || escrowData.amount * FEE_RATE);
|
|
385
|
+
await creditBalance(escrowData.providerAgentId, netAmount);
|
|
386
|
+
const completedAt = new Date();
|
|
387
|
+
await escrowRef.update({
|
|
388
|
+
status: 'completed',
|
|
389
|
+
completedAt: Timestamp.fromDate(completedAt),
|
|
390
|
+
proofOfWork,
|
|
391
|
+
});
|
|
392
|
+
const escrow = await getEscrow(escrowId);
|
|
393
|
+
// Update agent stats
|
|
394
|
+
if (escrow) {
|
|
395
|
+
await updateAgentStats(escrow.providerAgentId, netAmount);
|
|
396
|
+
}
|
|
397
|
+
return escrow;
|
|
398
|
+
}
|
|
399
|
+
export async function refundEscrow(escrowId) {
|
|
400
|
+
const escrow = await getEscrow(escrowId);
|
|
401
|
+
if (!escrow) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
if (escrow.status !== 'disputed') {
|
|
405
|
+
throw new Error('Can only refund disputed escrows');
|
|
406
|
+
}
|
|
407
|
+
// Refund full amount to client
|
|
408
|
+
await creditBalance(escrow.clientAgentId, escrow.amount);
|
|
409
|
+
// Update escrow status
|
|
410
|
+
const escrowRef = collections.escrows().doc(escrowId);
|
|
411
|
+
await escrowRef.update({ status: 'cancelled' });
|
|
412
|
+
return getEscrow(escrowId);
|
|
413
|
+
}
|
|
414
|
+
// Withdrawal operations
|
|
415
|
+
export async function createWithdrawal(agentId, amount, currency, paymentMethod) {
|
|
416
|
+
// Check balance
|
|
417
|
+
const balance = await getBalance(agentId);
|
|
418
|
+
if (balance < amount) {
|
|
419
|
+
throw new Error(`Insufficient balance: have ${balance}, need ${amount}`);
|
|
420
|
+
}
|
|
421
|
+
// Debit balance (hold funds)
|
|
422
|
+
await debitBalance(agentId, amount);
|
|
423
|
+
// Create withdrawal record
|
|
424
|
+
const docRef = collections.withdrawals().doc();
|
|
425
|
+
const withdrawal = {
|
|
426
|
+
agentId,
|
|
427
|
+
amount,
|
|
428
|
+
currency,
|
|
429
|
+
status: 'pending',
|
|
430
|
+
paymentMethod,
|
|
431
|
+
requestedAt: new Date(),
|
|
432
|
+
processedAt: null,
|
|
433
|
+
notes: null,
|
|
434
|
+
};
|
|
435
|
+
await docRef.set({
|
|
436
|
+
...withdrawal,
|
|
437
|
+
requestedAt: Timestamp.fromDate(withdrawal.requestedAt),
|
|
438
|
+
});
|
|
439
|
+
return { id: docRef.id, ...withdrawal };
|
|
440
|
+
}
|
|
441
|
+
export async function getWithdrawal(withdrawalId) {
|
|
442
|
+
const doc = await collections.withdrawals().doc(withdrawalId).get();
|
|
443
|
+
if (!doc.exists) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const data = doc.data();
|
|
447
|
+
return {
|
|
448
|
+
id: doc.id,
|
|
449
|
+
agentId: data.agentId,
|
|
450
|
+
amount: data.amount,
|
|
451
|
+
currency: data.currency,
|
|
452
|
+
status: data.status,
|
|
453
|
+
paymentMethod: data.paymentMethod,
|
|
454
|
+
requestedAt: data.requestedAt.toDate(),
|
|
455
|
+
processedAt: data.processedAt?.toDate() ?? null,
|
|
456
|
+
notes: data.notes ?? null,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
export async function listWithdrawals(status, limit = 50) {
|
|
460
|
+
// Simple query without compound index - filter in memory
|
|
461
|
+
const snapshot = await collections.withdrawals().limit(limit * 2).get();
|
|
462
|
+
let withdrawals = [];
|
|
463
|
+
for (const doc of snapshot.docs) {
|
|
464
|
+
const w = await getWithdrawal(doc.id);
|
|
465
|
+
if (w) {
|
|
466
|
+
if (!status || w.status === status) {
|
|
467
|
+
withdrawals.push(w);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Sort by requestedAt descending and limit
|
|
472
|
+
withdrawals = withdrawals
|
|
473
|
+
.sort((a, b) => b.requestedAt.getTime() - a.requestedAt.getTime())
|
|
474
|
+
.slice(0, limit);
|
|
475
|
+
return withdrawals;
|
|
476
|
+
}
|
|
477
|
+
export async function processWithdrawal(withdrawalId, action, notes) {
|
|
478
|
+
const withdrawal = await getWithdrawal(withdrawalId);
|
|
479
|
+
if (!withdrawal) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
if (withdrawal.status !== 'pending' && withdrawal.status !== 'processing') {
|
|
483
|
+
throw new Error(`Cannot process withdrawal with status: ${withdrawal.status}`);
|
|
484
|
+
}
|
|
485
|
+
const withdrawalRef = collections.withdrawals().doc(withdrawalId);
|
|
486
|
+
if (action === 'complete') {
|
|
487
|
+
await withdrawalRef.update({
|
|
488
|
+
status: 'completed',
|
|
489
|
+
processedAt: Timestamp.fromDate(new Date()),
|
|
490
|
+
notes: notes || 'Payment sent',
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Refund the held amount back to agent
|
|
495
|
+
await creditBalance(withdrawal.agentId, withdrawal.amount);
|
|
496
|
+
await withdrawalRef.update({
|
|
497
|
+
status: 'rejected',
|
|
498
|
+
processedAt: Timestamp.fromDate(new Date()),
|
|
499
|
+
notes: notes || 'Withdrawal rejected',
|
|
97
500
|
});
|
|
98
501
|
}
|
|
502
|
+
return getWithdrawal(withdrawalId);
|
|
99
503
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alby Payment Service - Bitcoin on Lightning
|
|
3
|
+
*
|
|
4
|
+
* Uses Alby API with Nostr Wallet Connect (NWC) for self-custodial Lightning payments.
|
|
5
|
+
* Docs: https://guides.getalby.com/developer-guide
|
|
6
|
+
* Fee: ~0% (just Lightning network fees)
|
|
7
|
+
*/
|
|
8
|
+
import type { Deposit } from '../../types/index.js';
|
|
9
|
+
export interface AlbyPaymentRequest {
|
|
10
|
+
amount: number;
|
|
11
|
+
agentId: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AlbyPaymentResponse {
|
|
15
|
+
success: boolean;
|
|
16
|
+
deposit?: Partial<Deposit>;
|
|
17
|
+
invoice?: {
|
|
18
|
+
paymentHash: string;
|
|
19
|
+
paymentRequest: string;
|
|
20
|
+
amountSats: number;
|
|
21
|
+
expiresAt: Date;
|
|
22
|
+
};
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convert USD to sats
|
|
27
|
+
*/
|
|
28
|
+
declare function usdToSats(usd: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Convert sats to USD
|
|
31
|
+
*/
|
|
32
|
+
declare function satsToUsd(sats: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Create a Lightning invoice for BTC deposit
|
|
35
|
+
*/
|
|
36
|
+
export declare function createAlbyDeposit(request: AlbyPaymentRequest): Promise<AlbyPaymentResponse>;
|
|
37
|
+
/**
|
|
38
|
+
* Check invoice status
|
|
39
|
+
*/
|
|
40
|
+
export declare function getInvoiceStatus(paymentHash: string): Promise<{
|
|
41
|
+
success: boolean;
|
|
42
|
+
status?: string;
|
|
43
|
+
paid?: boolean;
|
|
44
|
+
amountSats?: number;
|
|
45
|
+
amountUsd?: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Process webhook callback from Alby
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseWebhookPayload(body: Record<string, any>): {
|
|
52
|
+
paymentHash: string;
|
|
53
|
+
status: string;
|
|
54
|
+
amountSats: number;
|
|
55
|
+
amountUsd: number;
|
|
56
|
+
memo: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Send Lightning payment (for withdrawals)
|
|
60
|
+
*/
|
|
61
|
+
export declare function sendAlbyPayment(destination: string, // Lightning invoice or Lightning Address
|
|
62
|
+
amountUsd: number): Promise<{
|
|
63
|
+
success: boolean;
|
|
64
|
+
paymentHash?: string;
|
|
65
|
+
error?: string;
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Get wallet balance
|
|
69
|
+
*/
|
|
70
|
+
export declare function getWalletBalance(): Promise<{
|
|
71
|
+
success: boolean;
|
|
72
|
+
balanceSats?: number;
|
|
73
|
+
balanceUsd?: number;
|
|
74
|
+
error?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Decode a Lightning invoice to get amount and details
|
|
78
|
+
*/
|
|
79
|
+
export declare function decodeInvoice(invoice: string): Promise<{
|
|
80
|
+
success: boolean;
|
|
81
|
+
amountSats?: number;
|
|
82
|
+
amountUsd?: number;
|
|
83
|
+
description?: string;
|
|
84
|
+
paymentHash?: string;
|
|
85
|
+
expiresAt?: Date;
|
|
86
|
+
error?: string;
|
|
87
|
+
}>;
|
|
88
|
+
export declare const albyService: {
|
|
89
|
+
createDeposit: typeof createAlbyDeposit;
|
|
90
|
+
getInvoiceStatus: typeof getInvoiceStatus;
|
|
91
|
+
parseWebhookPayload: typeof parseWebhookPayload;
|
|
92
|
+
sendPayment: typeof sendAlbyPayment;
|
|
93
|
+
getWalletBalance: typeof getWalletBalance;
|
|
94
|
+
decodeInvoice: typeof decodeInvoice;
|
|
95
|
+
usdToSats: typeof usdToSats;
|
|
96
|
+
satsToUsd: typeof satsToUsd;
|
|
97
|
+
config: {
|
|
98
|
+
configured: boolean;
|
|
99
|
+
nwcConfigured: boolean;
|
|
100
|
+
webhookUrl: string;
|
|
101
|
+
satsPerUsd: number;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
export {};
|