2020117-agent 0.3.6 → 0.3.7
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/dist/agent.d.ts +1 -1
- package/dist/agent.js +86 -158
- package/dist/api.d.ts +0 -12
- package/dist/api.js +1 -31
- package/dist/cashu.d.ts +59 -0
- package/dist/cashu.js +91 -0
- package/dist/clink.d.ts +3 -36
- package/dist/clink.js +3 -69
- package/dist/p2p-customer.d.ts +2 -49
- package/dist/p2p-customer.js +2 -156
- package/dist/session.d.ts +2 -2
- package/dist/session.js +96 -41
- package/dist/swarm.d.ts +2 -2
- package/package.json +5 -4
package/dist/agent.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Unified Agent Runtime — runs as a long-lived daemon that handles:
|
|
4
4
|
* 1. Async platform tasks (inbox polling → accept → process → submit result)
|
|
5
|
-
* 2. P2P sessions (Hyperswarm +
|
|
5
|
+
* 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
package/dist/agent.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Unified Agent Runtime — runs as a long-lived daemon that handles:
|
|
4
4
|
* 1. Async platform tasks (inbox polling → accept → process → submit result)
|
|
5
|
-
* 2. P2P sessions (Hyperswarm +
|
|
5
|
+
* 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
|
@@ -69,10 +69,11 @@ import { randomBytes } from 'crypto';
|
|
|
69
69
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
70
70
|
import { createProcessor } from './processor.js';
|
|
71
71
|
import { hasApiKey, loadAgentName, registerService, startHeartbeatLoop, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, reportSession, } from './api.js';
|
|
72
|
-
import {
|
|
72
|
+
import { generateInvoice } from './clink.js';
|
|
73
|
+
import { receiveCashuToken } from './cashu.js';
|
|
73
74
|
import { readFileSync } from 'fs';
|
|
74
75
|
import WebSocket from 'ws';
|
|
75
|
-
// Polyfill global WebSocket for Node.js < 22 (needed by
|
|
76
|
+
// Polyfill global WebSocket for Node.js < 22 (needed by ws tunnel)
|
|
76
77
|
if (!globalThis.WebSocket)
|
|
77
78
|
globalThis.WebSocket = WebSocket;
|
|
78
79
|
// --- Config from env ---
|
|
@@ -81,7 +82,7 @@ const MAX_CONCURRENT = Number(process.env.MAX_JOBS) || 3;
|
|
|
81
82
|
const POLL_INTERVAL = Number(process.env.POLL_INTERVAL) || 30_000;
|
|
82
83
|
const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
|
|
83
84
|
const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
|
|
84
|
-
// ---
|
|
85
|
+
// --- Lightning payment config ---
|
|
85
86
|
let LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
|
|
86
87
|
// --- Sub-task delegation config ---
|
|
87
88
|
const SUB_KIND = process.env.SUB_KIND ? Number(process.env.SUB_KIND) : null;
|
|
@@ -161,12 +162,7 @@ async function main() {
|
|
|
161
162
|
console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
|
-
// 3.
|
|
165
|
-
if (LIGHTNING_ADDRESS) {
|
|
166
|
-
const { pubkey } = initClinkAgent();
|
|
167
|
-
console.log(`[${label}] CLINK: ${LIGHTNING_ADDRESS} (agent pubkey: ${pubkey.slice(0, 16)}...)`);
|
|
168
|
-
}
|
|
169
|
-
// 4. Platform registration + heartbeat
|
|
165
|
+
// 3. Platform registration + heartbeat
|
|
170
166
|
await setupPlatform(label);
|
|
171
167
|
// 5. Async inbox poller
|
|
172
168
|
startInboxPoller(label);
|
|
@@ -319,6 +315,37 @@ async function delegateAPI(kind, input, bidSats, provider) {
|
|
|
319
315
|
throw new Error(`Sub-task ${jobId} timed out after 120s`);
|
|
320
316
|
}
|
|
321
317
|
const activeSessions = new Map();
|
|
318
|
+
/** Send a billing tick to the customer — Cashu: request amount, Invoice: send bolt11 */
|
|
319
|
+
async function sendBillingTick(node, session, amount, label) {
|
|
320
|
+
const tickId = randomBytes(4).toString('hex');
|
|
321
|
+
if (session.paymentMethod === 'invoice') {
|
|
322
|
+
try {
|
|
323
|
+
const bolt11 = await generateInvoice(LIGHTNING_ADDRESS, amount);
|
|
324
|
+
node.send(session.socket, {
|
|
325
|
+
type: 'session_tick',
|
|
326
|
+
id: tickId,
|
|
327
|
+
session_id: session.sessionId,
|
|
328
|
+
bolt11,
|
|
329
|
+
amount,
|
|
330
|
+
});
|
|
331
|
+
console.log(`[${label}] Session ${session.sessionId}: sent invoice (${amount} sats)`);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
console.log(`[${label}] Session ${session.sessionId}: invoice error (${e.message}) — ending session`);
|
|
335
|
+
endSession(node, session, label);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Cashu: just request the amount, customer sends token
|
|
340
|
+
node.send(session.socket, {
|
|
341
|
+
type: 'session_tick',
|
|
342
|
+
id: tickId,
|
|
343
|
+
session_id: session.sessionId,
|
|
344
|
+
amount,
|
|
345
|
+
});
|
|
346
|
+
console.log(`[${label}] Session ${session.sessionId}: requested payment (${amount} sats)`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
322
349
|
// Backend WebSocket connections for WS tunnel (keyed by ws_id)
|
|
323
350
|
const backendWebSockets = new Map();
|
|
324
351
|
async function startSwarmListener(label) {
|
|
@@ -336,174 +363,75 @@ async function startSwarmListener(label) {
|
|
|
336
363
|
}
|
|
337
364
|
// --- Session protocol ---
|
|
338
365
|
if (msg.type === 'session_start') {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
// Negotiate payment method: customer requests "invoice" or "ndebit" (default)
|
|
345
|
-
const paymentMethod = msg.payment_method || (msg.ndebit ? 'ndebit' : 'invoice');
|
|
346
|
-
if (paymentMethod === 'ndebit' && !msg.ndebit) {
|
|
347
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'ndebit payment requires ndebit authorization' });
|
|
366
|
+
// Negotiate payment method: cashu (default) or invoice (requires Lightning Address)
|
|
367
|
+
const paymentMethod = msg.payment_method || 'cashu';
|
|
368
|
+
if (paymentMethod === 'invoice' && !LIGHTNING_ADDRESS) {
|
|
369
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'Invoice payment requires provider Lightning Address' });
|
|
348
370
|
return;
|
|
349
371
|
}
|
|
350
372
|
const satsPerMinute = state.skill?.pricing?.sats_per_minute
|
|
351
373
|
|| Number(process.env.SATS_PER_MINUTE)
|
|
352
374
|
|| msg.sats_per_minute
|
|
353
375
|
|| 10;
|
|
354
|
-
const BILLING_INTERVAL_MIN =
|
|
355
|
-
const
|
|
376
|
+
const BILLING_INTERVAL_MIN = 1;
|
|
377
|
+
const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
|
|
356
378
|
const sessionId = randomBytes(8).toString('hex');
|
|
357
|
-
console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${
|
|
379
|
+
console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${billingAmount} sats)`);
|
|
358
380
|
const session = {
|
|
359
381
|
socket,
|
|
360
382
|
peerId,
|
|
361
383
|
sessionId,
|
|
362
384
|
satsPerMinute,
|
|
363
385
|
paymentMethod,
|
|
364
|
-
ndebit: msg.ndebit || '',
|
|
365
386
|
totalEarned: 0,
|
|
366
387
|
startedAt: Date.now(),
|
|
367
|
-
|
|
368
|
-
|
|
388
|
+
lastPaidAt: Date.now(),
|
|
389
|
+
billingTimer: null,
|
|
369
390
|
timeoutTimer: null,
|
|
370
391
|
};
|
|
371
392
|
activeSessions.set(sessionId, session);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
node.send(socket, {
|
|
394
|
+
type: 'session_ack',
|
|
395
|
+
id: msg.id,
|
|
396
|
+
session_id: sessionId,
|
|
397
|
+
sats_per_minute: satsPerMinute,
|
|
398
|
+
payment_method: paymentMethod,
|
|
399
|
+
});
|
|
400
|
+
// Send first billing tick
|
|
401
|
+
await sendBillingTick(node, session, billingAmount, label);
|
|
402
|
+
// Recurring billing every 10 minutes
|
|
403
|
+
session.billingTimer = setInterval(() => {
|
|
404
|
+
sendBillingTick(node, session, billingAmount, label);
|
|
405
|
+
}, BILLING_INTERVAL_MIN * 60_000);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Customer sent payment (Cashu token or Lightning preimage)
|
|
409
|
+
if (msg.type === 'session_tick_ack') {
|
|
410
|
+
const session = activeSessions.get(msg.session_id || '');
|
|
411
|
+
if (!session)
|
|
412
|
+
return;
|
|
413
|
+
if (msg.cashu_token) {
|
|
414
|
+
// Cashu mode: verify token by swapping at mint
|
|
375
415
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
416
|
+
const { amount } = await receiveCashuToken(msg.cashu_token);
|
|
417
|
+
session.totalEarned += amount;
|
|
418
|
+
session.lastPaidAt = Date.now();
|
|
419
|
+
console.log(`[${label}] Session ${session.sessionId}: Cashu payment received (+${amount}, total: ${session.totalEarned} sats)`);
|
|
381
420
|
}
|
|
382
421
|
catch (e) {
|
|
383
|
-
console.warn(`[${label}] Session ${sessionId}:
|
|
384
|
-
node
|
|
385
|
-
activeSessions.delete(sessionId);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (!firstDebit.ok) {
|
|
389
|
-
console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
|
|
390
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
|
|
391
|
-
activeSessions.delete(sessionId);
|
|
392
|
-
return;
|
|
422
|
+
console.warn(`[${label}] Session ${session.sessionId}: Cashu token invalid: ${e.message} — ending session`);
|
|
423
|
+
endSession(node, session, label);
|
|
393
424
|
}
|
|
394
|
-
session.totalEarned += debitAmount;
|
|
395
|
-
session.lastDebitAt = Date.now();
|
|
396
|
-
console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
|
|
397
|
-
node.send(socket, {
|
|
398
|
-
type: 'session_ack',
|
|
399
|
-
id: msg.id,
|
|
400
|
-
session_id: sessionId,
|
|
401
|
-
sats_per_minute: satsPerMinute,
|
|
402
|
-
payment_method: 'ndebit',
|
|
403
|
-
});
|
|
404
|
-
node.send(socket, {
|
|
405
|
-
type: 'session_tick_ack',
|
|
406
|
-
id: sessionId,
|
|
407
|
-
session_id: sessionId,
|
|
408
|
-
amount: debitAmount,
|
|
409
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
410
|
-
});
|
|
411
|
-
// Recurring debit every 10 minutes
|
|
412
|
-
session.debitTimer = setInterval(async () => {
|
|
413
|
-
let debit;
|
|
414
|
-
try {
|
|
415
|
-
debit = await collectPayment({
|
|
416
|
-
ndebit: session.ndebit,
|
|
417
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
418
|
-
amountSats: debitAmount,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
catch (e) {
|
|
422
|
-
console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
|
|
423
|
-
endSession(node, session, label);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (!debit.ok) {
|
|
427
|
-
console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
|
|
428
|
-
endSession(node, session, label);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
session.totalEarned += debitAmount;
|
|
432
|
-
session.lastDebitAt = Date.now();
|
|
433
|
-
console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
|
|
434
|
-
node.send(socket, {
|
|
435
|
-
type: 'session_tick_ack',
|
|
436
|
-
id: sessionId,
|
|
437
|
-
session_id: sessionId,
|
|
438
|
-
amount: debitAmount,
|
|
439
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
440
|
-
});
|
|
441
|
-
}, BILLING_INTERVAL_MIN * 60_000);
|
|
442
425
|
}
|
|
443
|
-
else {
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
sats_per_minute: satsPerMinute,
|
|
450
|
-
payment_method: 'invoice',
|
|
451
|
-
});
|
|
452
|
-
// Generate and send first invoice
|
|
453
|
-
let firstInvoice;
|
|
454
|
-
try {
|
|
455
|
-
firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
456
|
-
}
|
|
457
|
-
catch (e) {
|
|
458
|
-
console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
|
|
459
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
|
|
460
|
-
activeSessions.delete(sessionId);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
const tickId = randomBytes(4).toString('hex');
|
|
464
|
-
node.send(socket, {
|
|
465
|
-
type: 'session_tick',
|
|
466
|
-
id: tickId,
|
|
467
|
-
session_id: sessionId,
|
|
468
|
-
bolt11: firstInvoice,
|
|
469
|
-
amount: debitAmount,
|
|
470
|
-
});
|
|
471
|
-
console.log(`[${label}] Session ${sessionId}: sent first invoice (${debitAmount} sats)`);
|
|
472
|
-
// Recurring invoices every 10 minutes
|
|
473
|
-
session.debitTimer = setInterval(async () => {
|
|
474
|
-
let invoice;
|
|
475
|
-
try {
|
|
476
|
-
invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
477
|
-
}
|
|
478
|
-
catch (e) {
|
|
479
|
-
console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
|
|
480
|
-
endSession(node, session, label);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
const tid = randomBytes(4).toString('hex');
|
|
484
|
-
node.send(socket, {
|
|
485
|
-
type: 'session_tick',
|
|
486
|
-
id: tid,
|
|
487
|
-
session_id: sessionId,
|
|
488
|
-
bolt11: invoice,
|
|
489
|
-
amount: debitAmount,
|
|
490
|
-
});
|
|
491
|
-
console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
|
|
492
|
-
}, BILLING_INTERVAL_MIN * 60_000);
|
|
426
|
+
else if (msg.preimage) {
|
|
427
|
+
// Invoice mode: preimage proves payment
|
|
428
|
+
const amount = msg.amount || 0;
|
|
429
|
+
session.totalEarned += amount;
|
|
430
|
+
session.lastPaidAt = Date.now();
|
|
431
|
+
console.log(`[${label}] Session ${session.sessionId}: invoice payment received (+${amount}, total: ${session.totalEarned} sats)`);
|
|
493
432
|
}
|
|
494
433
|
return;
|
|
495
434
|
}
|
|
496
|
-
// Handle invoice payment confirmation (invoice mode only)
|
|
497
|
-
if (msg.type === 'session_tick_ack' && msg.preimage) {
|
|
498
|
-
const session = activeSessions.get(msg.session_id || '');
|
|
499
|
-
if (!session || session.paymentMethod !== 'invoice')
|
|
500
|
-
return;
|
|
501
|
-
const amount = msg.amount || 0;
|
|
502
|
-
session.totalEarned += amount;
|
|
503
|
-
session.lastDebitAt = Date.now();
|
|
504
|
-
console.log(`[${label}] Session ${session.sessionId}: payment received (+${amount}, total: ${session.totalEarned} sats)`);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
435
|
if (msg.type === 'session_end') {
|
|
508
436
|
const session = activeSessions.get(msg.session_id || '');
|
|
509
437
|
if (!session)
|
|
@@ -688,7 +616,7 @@ async function startSwarmListener(label) {
|
|
|
688
616
|
return;
|
|
689
617
|
}
|
|
690
618
|
});
|
|
691
|
-
// Handle customer disconnect
|
|
619
|
+
// Handle customer disconnect
|
|
692
620
|
node.on('peer-leave', (peerId) => {
|
|
693
621
|
const tag = peerId.slice(0, 8);
|
|
694
622
|
// Find and end all sessions for this peer
|
|
@@ -720,10 +648,10 @@ function findSessionBySocket(socket) {
|
|
|
720
648
|
}
|
|
721
649
|
function endSession(node, session, label) {
|
|
722
650
|
const durationS = Math.round((Date.now() - session.startedAt) / 1000);
|
|
723
|
-
// Stop
|
|
724
|
-
if (session.
|
|
725
|
-
clearInterval(session.
|
|
726
|
-
session.
|
|
651
|
+
// Stop billing timer
|
|
652
|
+
if (session.billingTimer) {
|
|
653
|
+
clearInterval(session.billingTimer);
|
|
654
|
+
session.billingTimer = null;
|
|
727
655
|
}
|
|
728
656
|
if (session.timeoutTimer) {
|
|
729
657
|
clearTimeout(session.timeoutTimer);
|
|
@@ -752,7 +680,7 @@ function endSession(node, session, label) {
|
|
|
752
680
|
// Socket may already be closed (peer disconnect)
|
|
753
681
|
}
|
|
754
682
|
console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
|
|
755
|
-
// Update P2P lifetime counters
|
|
683
|
+
// Update P2P lifetime counters
|
|
756
684
|
state.p2pSessionsCompleted++;
|
|
757
685
|
state.p2pTotalEarnedSats += session.totalEarned;
|
|
758
686
|
// Report session to platform activity feed (best-effort, no content exposed)
|
package/dist/api.d.ts
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** Returns the resolved agent name from env or first key in file */
|
|
9
9
|
export declare function loadAgentName(): string | null;
|
|
10
|
-
/** Returns the ndebit from env or .2020117_keys file */
|
|
11
|
-
export declare function loadNdebit(): string | null;
|
|
12
10
|
export declare function hasApiKey(): boolean;
|
|
13
11
|
/** Fetch the authenticated agent's profile from the platform. */
|
|
14
12
|
export declare function getProfile(): Promise<{
|
|
@@ -99,16 +97,6 @@ export declare function walletPayInvoice(bolt11: string): Promise<{
|
|
|
99
97
|
error?: string;
|
|
100
98
|
}>;
|
|
101
99
|
export declare function walletGetBalance(): Promise<number>;
|
|
102
|
-
export interface ProxyDebitResult {
|
|
103
|
-
ok: boolean;
|
|
104
|
-
preimage?: string;
|
|
105
|
-
error?: string;
|
|
106
|
-
}
|
|
107
|
-
export declare function proxyDebit(opts: {
|
|
108
|
-
ndebit: string;
|
|
109
|
-
lightningAddress: string;
|
|
110
|
-
amountSats: number;
|
|
111
|
-
}): Promise<ProxyDebitResult | null>;
|
|
112
100
|
/**
|
|
113
101
|
* Report a completed P2P session to the platform for the activity feed.
|
|
114
102
|
* Content stays private — only metadata (kind, duration, sats) is sent.
|
package/dist/api.js
CHANGED
|
@@ -51,29 +51,6 @@ export function loadAgentName() {
|
|
|
51
51
|
}
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
|
-
/** Returns the ndebit from env or .2020117_keys file */
|
|
55
|
-
export function loadNdebit() {
|
|
56
|
-
if (process.env.CLINK_NDEBIT)
|
|
57
|
-
return process.env.CLINK_NDEBIT;
|
|
58
|
-
const agentName = process.env.AGENT_NAME || process.env.AGENT;
|
|
59
|
-
for (const dir of [process.cwd(), homedir()]) {
|
|
60
|
-
try {
|
|
61
|
-
const raw = readFileSync(join(dir, '.2020117_keys'), 'utf-8');
|
|
62
|
-
const keys = JSON.parse(raw);
|
|
63
|
-
if (agentName && keys[agentName]?.ndebit)
|
|
64
|
-
return keys[agentName].ndebit;
|
|
65
|
-
if (!agentName) {
|
|
66
|
-
const first = Object.values(keys)[0];
|
|
67
|
-
if (first?.ndebit)
|
|
68
|
-
return first.ndebit;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// try next
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
54
|
const API_KEY = loadApiKey();
|
|
78
55
|
// --- Helpers ---
|
|
79
56
|
async function apiGet(path, auth = true) {
|
|
@@ -303,7 +280,7 @@ export async function walletPayInvoice(bolt11) {
|
|
|
303
280
|
if (!API_KEY)
|
|
304
281
|
return { ok: false, error: 'No API key' };
|
|
305
282
|
try {
|
|
306
|
-
const resp = await fetch(`${BASE_URL}/api/wallet/
|
|
283
|
+
const resp = await fetch(`${BASE_URL}/api/wallet/pay`, {
|
|
307
284
|
method: 'POST',
|
|
308
285
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` },
|
|
309
286
|
body: JSON.stringify({ bolt11 }),
|
|
@@ -321,13 +298,6 @@ export async function walletGetBalance() {
|
|
|
321
298
|
const data = await apiGet('/api/wallet/balance');
|
|
322
299
|
return data?.balance_sats ?? 0;
|
|
323
300
|
}
|
|
324
|
-
export async function proxyDebit(opts) {
|
|
325
|
-
return apiPost('/api/dvm/proxy-debit', {
|
|
326
|
-
ndebit: opts.ndebit,
|
|
327
|
-
lightning_address: opts.lightningAddress,
|
|
328
|
-
amount_sats: opts.amountSats,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
301
|
/**
|
|
332
302
|
* Report a completed P2P session to the platform for the activity feed.
|
|
333
303
|
* Content stays private — only metadata (kind, duration, sats) is sent.
|
package/dist/cashu.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cashu eCash utilities — send and receive tokens over P2P
|
|
3
|
+
*
|
|
4
|
+
* Customer: pre-loads tokens, splits per billing tick, sends to Provider
|
|
5
|
+
* Provider: receives tokens, swaps at mint to verify, accumulates proofs
|
|
6
|
+
*/
|
|
7
|
+
export type Proof = {
|
|
8
|
+
id: string;
|
|
9
|
+
amount: number;
|
|
10
|
+
secret: string;
|
|
11
|
+
C: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Split proofs to create a Cashu token of the exact amount.
|
|
15
|
+
* Returns the encoded token string and remaining change proofs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sendCashuToken(mintUrl: string, proofs: Proof[], amount: number): Promise<{
|
|
18
|
+
token: string;
|
|
19
|
+
change: Proof[];
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Verify and claim a Cashu token — swaps proofs with the mint to prevent double-spend.
|
|
23
|
+
* Returns the claimed proofs (now owned by the receiver).
|
|
24
|
+
*/
|
|
25
|
+
export declare function receiveCashuToken(tokenStr: string): Promise<{
|
|
26
|
+
proofs: Proof[];
|
|
27
|
+
amount: number;
|
|
28
|
+
mintUrl: string;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Inspect a token's amount and mint without claiming it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function peekCashuToken(tokenStr: string): {
|
|
34
|
+
amount: number;
|
|
35
|
+
mint: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Decode a token string to get the raw proofs and mint URL.
|
|
39
|
+
*/
|
|
40
|
+
export declare function decodeCashuToken(tokenStr: string): {
|
|
41
|
+
mint: string;
|
|
42
|
+
proofs: Proof[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Encode proofs back into a portable token string.
|
|
46
|
+
*/
|
|
47
|
+
export declare function encodeCashuToken(mintUrl: string, proofs: Proof[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Request a mint quote — returns a Lightning invoice to pay for minting tokens.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createMintQuote(mintUrl: string, amountSats: number): Promise<{
|
|
52
|
+
quote: string;
|
|
53
|
+
invoice: string;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Claim a paid mint quote — polls for payment, then mints proofs and returns an encoded token.
|
|
57
|
+
* Throws if the quote is not paid within the timeout.
|
|
58
|
+
*/
|
|
59
|
+
export declare function claimMintQuote(mintUrl: string, amountSats: number, quoteId: string, timeoutMs?: number): Promise<string>;
|
package/dist/cashu.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cashu eCash utilities — send and receive tokens over P2P
|
|
3
|
+
*
|
|
4
|
+
* Customer: pre-loads tokens, splits per billing tick, sends to Provider
|
|
5
|
+
* Provider: receives tokens, swaps at mint to verify, accumulates proofs
|
|
6
|
+
*/
|
|
7
|
+
import { CashuMint, CashuWallet, getEncodedTokenV4, getDecodedToken } from '@cashu/cashu-ts';
|
|
8
|
+
// Cache wallet instances per mint URL
|
|
9
|
+
const walletCache = new Map();
|
|
10
|
+
async function getWallet(mintUrl) {
|
|
11
|
+
let wallet = walletCache.get(mintUrl);
|
|
12
|
+
if (!wallet) {
|
|
13
|
+
const mint = new CashuMint(mintUrl);
|
|
14
|
+
wallet = new CashuWallet(mint);
|
|
15
|
+
await wallet.loadMint();
|
|
16
|
+
walletCache.set(mintUrl, wallet);
|
|
17
|
+
}
|
|
18
|
+
return wallet;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Split proofs to create a Cashu token of the exact amount.
|
|
22
|
+
* Returns the encoded token string and remaining change proofs.
|
|
23
|
+
*/
|
|
24
|
+
export async function sendCashuToken(mintUrl, proofs, amount) {
|
|
25
|
+
const wallet = await getWallet(mintUrl);
|
|
26
|
+
const { send, keep } = await wallet.send(amount, proofs);
|
|
27
|
+
const token = getEncodedTokenV4({ mint: mintUrl, proofs: send });
|
|
28
|
+
return { token, change: keep };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Verify and claim a Cashu token — swaps proofs with the mint to prevent double-spend.
|
|
32
|
+
* Returns the claimed proofs (now owned by the receiver).
|
|
33
|
+
*/
|
|
34
|
+
export async function receiveCashuToken(tokenStr) {
|
|
35
|
+
const decoded = getDecodedToken(tokenStr);
|
|
36
|
+
const mintUrl = decoded.mint;
|
|
37
|
+
const wallet = await getWallet(mintUrl);
|
|
38
|
+
const proofs = await wallet.receive(tokenStr);
|
|
39
|
+
const amount = proofs.reduce((sum, p) => sum + p.amount, 0);
|
|
40
|
+
return { proofs, amount, mintUrl };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Inspect a token's amount and mint without claiming it.
|
|
44
|
+
*/
|
|
45
|
+
export function peekCashuToken(tokenStr) {
|
|
46
|
+
const decoded = getDecodedToken(tokenStr);
|
|
47
|
+
const amount = decoded.proofs.reduce((sum, p) => sum + p.amount, 0);
|
|
48
|
+
return { amount, mint: decoded.mint };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Decode a token string to get the raw proofs and mint URL.
|
|
52
|
+
*/
|
|
53
|
+
export function decodeCashuToken(tokenStr) {
|
|
54
|
+
const decoded = getDecodedToken(tokenStr);
|
|
55
|
+
return { mint: decoded.mint, proofs: decoded.proofs };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Encode proofs back into a portable token string.
|
|
59
|
+
*/
|
|
60
|
+
export function encodeCashuToken(mintUrl, proofs) {
|
|
61
|
+
return getEncodedTokenV4({ mint: mintUrl, proofs });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Request a mint quote — returns a Lightning invoice to pay for minting tokens.
|
|
65
|
+
*/
|
|
66
|
+
export async function createMintQuote(mintUrl, amountSats) {
|
|
67
|
+
const wallet = await getWallet(mintUrl);
|
|
68
|
+
const quoteRes = await wallet.createMintQuote(amountSats);
|
|
69
|
+
return { quote: quoteRes.quote, invoice: quoteRes.request };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Claim a paid mint quote — polls for payment, then mints proofs and returns an encoded token.
|
|
73
|
+
* Throws if the quote is not paid within the timeout.
|
|
74
|
+
*/
|
|
75
|
+
export async function claimMintQuote(mintUrl, amountSats, quoteId, timeoutMs = 60_000) {
|
|
76
|
+
const wallet = await getWallet(mintUrl);
|
|
77
|
+
// Poll until paid or timeout
|
|
78
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
const check = await wallet.checkMintQuote(quoteId);
|
|
81
|
+
if (check.state === 'PAID') {
|
|
82
|
+
const proofs = await wallet.mintProofs(amountSats, quoteId);
|
|
83
|
+
return getEncodedTokenV4({ mint: mintUrl, proofs });
|
|
84
|
+
}
|
|
85
|
+
if (check.state === 'ISSUED') {
|
|
86
|
+
throw new Error('Mint quote already issued');
|
|
87
|
+
}
|
|
88
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
89
|
+
}
|
|
90
|
+
throw new Error('Mint quote payment timeout');
|
|
91
|
+
}
|
package/dist/clink.d.ts
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lightning payment utilities — invoice generation via LNURL-pay
|
|
3
3
|
*
|
|
4
|
-
* Provider
|
|
5
|
-
*
|
|
4
|
+
* Provider generates invoices from their own Lightning Address.
|
|
5
|
+
* Customer pays invoices via built-in wallet (POST /api/wallet/send).
|
|
6
6
|
*/
|
|
7
|
-
export declare function initClinkAgent(): {
|
|
8
|
-
privateKey: Uint8Array;
|
|
9
|
-
pubkey: string;
|
|
10
|
-
};
|
|
11
|
-
export interface DebitResult {
|
|
12
|
-
ok: boolean;
|
|
13
|
-
preimage?: string;
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Provider calls this to debit customer's wallet via CLINK protocol.
|
|
18
|
-
* Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
|
|
19
|
-
*/
|
|
20
|
-
export declare function debitCustomer(opts: {
|
|
21
|
-
ndebit: string;
|
|
22
|
-
bolt11: string;
|
|
23
|
-
timeoutSeconds?: number;
|
|
24
|
-
}): Promise<DebitResult>;
|
|
25
7
|
/**
|
|
26
8
|
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
27
9
|
* The provider calls this on their OWN Lightning Address to generate
|
|
@@ -30,18 +12,3 @@ export declare function debitCustomer(opts: {
|
|
|
30
12
|
* Flow: address → .well-known/lnurlp → callback?amount= → bolt11
|
|
31
13
|
*/
|
|
32
14
|
export declare function generateInvoice(lightningAddress: string, amountSats: number): Promise<string>;
|
|
33
|
-
/**
|
|
34
|
-
* Full payment cycle: generate invoice from provider's Lightning Address,
|
|
35
|
-
* then debit customer's wallet via CLINK.
|
|
36
|
-
*
|
|
37
|
-
* Priority: platform proxy (if API key available) → direct debit (if agent key initialized).
|
|
38
|
-
* Proxy uses the platform's pre-authorized CLINK identity, so providers don't need
|
|
39
|
-
* individual DebitAccess on customer wallets. Direct debit works for power users
|
|
40
|
-
* who pre-authorize each other's keys.
|
|
41
|
-
*/
|
|
42
|
-
export declare function collectPayment(opts: {
|
|
43
|
-
ndebit: string;
|
|
44
|
-
lightningAddress: string;
|
|
45
|
-
amountSats: number;
|
|
46
|
-
timeoutSeconds?: number;
|
|
47
|
-
}): Promise<DebitResult>;
|
package/dist/clink.js
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lightning payment utilities — invoice generation via LNURL-pay
|
|
3
3
|
*
|
|
4
|
-
* Provider
|
|
5
|
-
*
|
|
4
|
+
* Provider generates invoices from their own Lightning Address.
|
|
5
|
+
* Customer pays invoices via built-in wallet (POST /api/wallet/send).
|
|
6
6
|
*/
|
|
7
|
-
import { ClinkSDK, decodeBech32, generateSecretKey, getPublicKey, newNdebitPaymentRequest } from '@shocknet/clink-sdk';
|
|
8
|
-
import { hasApiKey, proxyDebit } from './api.js';
|
|
9
|
-
// --- Agent identity ---
|
|
10
|
-
let agentKey = null;
|
|
11
|
-
let agentPubkey = null;
|
|
12
|
-
export function initClinkAgent() {
|
|
13
|
-
agentKey = generateSecretKey();
|
|
14
|
-
agentPubkey = getPublicKey(agentKey);
|
|
15
|
-
console.log(`[clink] Agent identity: ${agentPubkey.slice(0, 16)}...`);
|
|
16
|
-
return { privateKey: agentKey, pubkey: agentPubkey };
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Provider calls this to debit customer's wallet via CLINK protocol.
|
|
20
|
-
* Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
|
|
21
|
-
*/
|
|
22
|
-
export async function debitCustomer(opts) {
|
|
23
|
-
if (!agentKey)
|
|
24
|
-
throw new Error('CLINK agent not initialized — call initClinkAgent() first');
|
|
25
|
-
const decoded = decodeBech32(opts.ndebit);
|
|
26
|
-
if (decoded.type !== 'ndebit')
|
|
27
|
-
throw new Error(`Invalid ndebit string (got type: ${decoded.type})`);
|
|
28
|
-
const sdk = new ClinkSDK({
|
|
29
|
-
privateKey: agentKey,
|
|
30
|
-
relays: [decoded.data.relay],
|
|
31
|
-
toPubKey: decoded.data.pubkey,
|
|
32
|
-
defaultTimeoutSeconds: opts.timeoutSeconds ?? 30,
|
|
33
|
-
});
|
|
34
|
-
const result = await sdk.Ndebit(newNdebitPaymentRequest(opts.bolt11, undefined, decoded.data.pointer));
|
|
35
|
-
if (result.res === 'ok') {
|
|
36
|
-
return { ok: true, preimage: result.preimage };
|
|
37
|
-
}
|
|
38
|
-
return { ok: false, error: result.error || 'Debit rejected' };
|
|
39
|
-
}
|
|
40
|
-
// --- Invoice generation via LNURL-pay ---
|
|
41
7
|
/**
|
|
42
8
|
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
43
9
|
* The provider calls this on their OWN Lightning Address to generate
|
|
@@ -73,35 +39,3 @@ export async function generateInvoice(lightningAddress, amountSats) {
|
|
|
73
39
|
throw new Error(`No invoice returned: ${invoiceData.reason || 'unknown error'}`);
|
|
74
40
|
return invoiceData.pr;
|
|
75
41
|
}
|
|
76
|
-
// --- Combined: generate invoice + debit ---
|
|
77
|
-
/**
|
|
78
|
-
* Full payment cycle: generate invoice from provider's Lightning Address,
|
|
79
|
-
* then debit customer's wallet via CLINK.
|
|
80
|
-
*
|
|
81
|
-
* Priority: platform proxy (if API key available) → direct debit (if agent key initialized).
|
|
82
|
-
* Proxy uses the platform's pre-authorized CLINK identity, so providers don't need
|
|
83
|
-
* individual DebitAccess on customer wallets. Direct debit works for power users
|
|
84
|
-
* who pre-authorize each other's keys.
|
|
85
|
-
*/
|
|
86
|
-
export async function collectPayment(opts) {
|
|
87
|
-
// Try platform proxy first (provider doesn't need DebitAccess)
|
|
88
|
-
if (hasApiKey()) {
|
|
89
|
-
console.log(`[clink] Proxy debit: ${opts.amountSats} sats → ${opts.lightningAddress}`);
|
|
90
|
-
const result = await proxyDebit({
|
|
91
|
-
ndebit: opts.ndebit,
|
|
92
|
-
lightningAddress: opts.lightningAddress,
|
|
93
|
-
amountSats: opts.amountSats,
|
|
94
|
-
});
|
|
95
|
-
if (result) {
|
|
96
|
-
return { ok: result.ok, preimage: result.preimage, error: result.error };
|
|
97
|
-
}
|
|
98
|
-
console.warn('[clink] Proxy debit unavailable, trying direct...');
|
|
99
|
-
}
|
|
100
|
-
// Fallback: direct debit (requires provider's CLINK key to be authorized)
|
|
101
|
-
const bolt11 = await generateInvoice(opts.lightningAddress, opts.amountSats);
|
|
102
|
-
return debitCustomer({
|
|
103
|
-
ndebit: opts.ndebit,
|
|
104
|
-
bolt11,
|
|
105
|
-
timeoutSeconds: opts.timeoutSeconds,
|
|
106
|
-
});
|
|
107
|
-
}
|
package/dist/p2p-customer.d.ts
CHANGED
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared P2P customer protocol —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The customer sends an ndebit authorization with the request. The provider
|
|
6
|
-
* pulls payments directly from the customer's wallet via CLINK debit —
|
|
7
|
-
* the customer does not send payment messages.
|
|
2
|
+
* Shared P2P customer protocol — utility functions for connecting
|
|
3
|
+
* to providers via Hyperswarm.
|
|
8
4
|
*
|
|
9
5
|
* Exports:
|
|
10
|
-
* - P2PStreamOptions — config interface
|
|
11
|
-
* - streamFromProvider() — async generator that yields chunks from a provider
|
|
12
6
|
* - queryProviderSkill() — queries a provider's skill manifest via an existing connection
|
|
13
7
|
*/
|
|
14
8
|
import { SwarmNode } from './swarm.js';
|
|
15
|
-
/**
|
|
16
|
-
* Configuration for a P2P connection.
|
|
17
|
-
*/
|
|
18
|
-
export interface P2PStreamOptions {
|
|
19
|
-
/** DVM kind number (e.g. 5100 for text generation) */
|
|
20
|
-
kind: number;
|
|
21
|
-
/** The input/prompt to send to the provider */
|
|
22
|
-
input: string;
|
|
23
|
-
/** Total budget in sats for this session */
|
|
24
|
-
budgetSats: number;
|
|
25
|
-
/** Customer's ndebit1... authorization for CLINK debit payments */
|
|
26
|
-
ndebit: string;
|
|
27
|
-
/** Maximum acceptable price per chunk in sats (default: 5) */
|
|
28
|
-
maxSatsPerChunk?: number;
|
|
29
|
-
/** Overall timeout in milliseconds (default: 120_000) */
|
|
30
|
-
timeoutMs?: number;
|
|
31
|
-
/** Log prefix label (default: 'p2p') */
|
|
32
|
-
label?: string;
|
|
33
|
-
/** Additional job parameters passed in the request message */
|
|
34
|
-
params?: Record<string, unknown>;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Connect to a provider via Hyperswarm, authorize CLINK debit payments,
|
|
38
|
-
* and yield output chunks as they arrive.
|
|
39
|
-
*
|
|
40
|
-
* Creates and destroys its own temporary SwarmNode — callers do not need
|
|
41
|
-
* to manage any swarm state.
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```ts
|
|
45
|
-
* for await (const chunk of streamFromProvider({
|
|
46
|
-
* kind: 5100,
|
|
47
|
-
* input: 'Explain quantum computing',
|
|
48
|
-
* budgetSats: 50,
|
|
49
|
-
* ndebit: 'ndebit1...',
|
|
50
|
-
* })) {
|
|
51
|
-
* process.stdout.write(chunk)
|
|
52
|
-
* }
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export declare function streamFromProvider(opts: P2PStreamOptions): AsyncGenerator<string>;
|
|
56
9
|
/**
|
|
57
10
|
* Query a provider's skill manifest over an existing P2P connection.
|
|
58
11
|
*
|
package/dist/p2p-customer.js
CHANGED
|
@@ -1,165 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared P2P customer protocol —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The customer sends an ndebit authorization with the request. The provider
|
|
6
|
-
* pulls payments directly from the customer's wallet via CLINK debit —
|
|
7
|
-
* the customer does not send payment messages.
|
|
2
|
+
* Shared P2P customer protocol — utility functions for connecting
|
|
3
|
+
* to providers via Hyperswarm.
|
|
8
4
|
*
|
|
9
5
|
* Exports:
|
|
10
|
-
* - P2PStreamOptions — config interface
|
|
11
|
-
* - streamFromProvider() — async generator that yields chunks from a provider
|
|
12
6
|
* - queryProviderSkill() — queries a provider's skill manifest via an existing connection
|
|
13
7
|
*/
|
|
14
|
-
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
15
8
|
import { randomBytes } from 'crypto';
|
|
16
|
-
/**
|
|
17
|
-
* Connect to a provider via Hyperswarm, authorize CLINK debit payments,
|
|
18
|
-
* and yield output chunks as they arrive.
|
|
19
|
-
*
|
|
20
|
-
* Creates and destroys its own temporary SwarmNode — callers do not need
|
|
21
|
-
* to manage any swarm state.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* for await (const chunk of streamFromProvider({
|
|
26
|
-
* kind: 5100,
|
|
27
|
-
* input: 'Explain quantum computing',
|
|
28
|
-
* budgetSats: 50,
|
|
29
|
-
* ndebit: 'ndebit1...',
|
|
30
|
-
* })) {
|
|
31
|
-
* process.stdout.write(chunk)
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export async function* streamFromProvider(opts) {
|
|
36
|
-
const { kind, input, budgetSats, ndebit, maxSatsPerChunk = 5, timeoutMs = 120_000, label = 'p2p', params, } = opts;
|
|
37
|
-
if (!ndebit) {
|
|
38
|
-
throw new Error('ndebit authorization required for P2P connection');
|
|
39
|
-
}
|
|
40
|
-
const jobId = randomBytes(8).toString('hex');
|
|
41
|
-
const tag = `${label}-${jobId.slice(0, 8)}`;
|
|
42
|
-
const node = new SwarmNode();
|
|
43
|
-
const topic = topicFromKind(kind);
|
|
44
|
-
try {
|
|
45
|
-
console.log(`[${tag}] Looking for kind ${kind} provider...`);
|
|
46
|
-
await node.connect(topic);
|
|
47
|
-
const peer = await node.waitForPeer(30_000);
|
|
48
|
-
console.log(`[${tag}] Connected to provider: ${peer.peerId.slice(0, 12)}...`);
|
|
49
|
-
// Query provider's skill manifest for pricing before committing
|
|
50
|
-
const skill = await queryProviderSkill(node, peer.socket, kind);
|
|
51
|
-
if (skill) {
|
|
52
|
-
const pricing = skill.pricing;
|
|
53
|
-
const jobPrice = pricing?.sats_per_job ?? pricing?.[String(kind)];
|
|
54
|
-
if (jobPrice !== undefined) {
|
|
55
|
-
console.log(`[${tag}] Provider pricing: ${jobPrice} sats/job`);
|
|
56
|
-
if (typeof jobPrice === 'number' && jobPrice > budgetSats) {
|
|
57
|
-
throw new Error(`Provider price ${jobPrice} sats exceeds budget ${budgetSats} sats`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (skill.name)
|
|
61
|
-
console.log(`[${tag}] Provider: ${skill.name}`);
|
|
62
|
-
}
|
|
63
|
-
// Channel: message handler pushes chunks, generator consumes them
|
|
64
|
-
const chunks = [];
|
|
65
|
-
let finished = false;
|
|
66
|
-
let error = null;
|
|
67
|
-
let notify = null;
|
|
68
|
-
function wake() {
|
|
69
|
-
if (notify) {
|
|
70
|
-
notify();
|
|
71
|
-
notify = null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function waitForChunk() {
|
|
75
|
-
if (chunks.length > 0 || finished || error)
|
|
76
|
-
return Promise.resolve();
|
|
77
|
-
return new Promise(r => { notify = r; });
|
|
78
|
-
}
|
|
79
|
-
// Timeout
|
|
80
|
-
const timeout = setTimeout(() => {
|
|
81
|
-
error = new Error(`P2P delegation timed out after ${timeoutMs / 1000}s`);
|
|
82
|
-
wake();
|
|
83
|
-
}, timeoutMs);
|
|
84
|
-
node.on('message', async (msg) => {
|
|
85
|
-
if (msg.id !== jobId)
|
|
86
|
-
return;
|
|
87
|
-
switch (msg.type) {
|
|
88
|
-
case 'offer': {
|
|
89
|
-
const spc = msg.sats_per_chunk ?? 0;
|
|
90
|
-
if (spc > maxSatsPerChunk) {
|
|
91
|
-
node.send(peer.socket, { type: 'stop', id: jobId });
|
|
92
|
-
error = new Error(`Price too high: ${spc} sat/chunk > max ${maxSatsPerChunk}`);
|
|
93
|
-
wake();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
console.log(`[${tag}] Offer: ${spc} sat/chunk, ${msg.chunks_per_payment ?? 0} chunks/payment`);
|
|
97
|
-
// No action needed — provider will debit our wallet via CLINK
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
case 'payment_ack':
|
|
101
|
-
// Provider debited our wallet and is reporting the amount
|
|
102
|
-
console.log(`[${tag}] Provider debited: ${msg.amount} sats`);
|
|
103
|
-
break;
|
|
104
|
-
case 'accepted':
|
|
105
|
-
console.log(`[${tag}] Accepted, streaming...`);
|
|
106
|
-
break;
|
|
107
|
-
case 'chunk':
|
|
108
|
-
if (msg.data) {
|
|
109
|
-
chunks.push(msg.data);
|
|
110
|
-
wake();
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
case 'pay_required':
|
|
114
|
-
// With CLINK, provider handles payment collection directly
|
|
115
|
-
// This shouldn't normally fire, but log it if it does
|
|
116
|
-
console.log(`[${tag}] Provider requested payment (will be debited via CLINK)`);
|
|
117
|
-
break;
|
|
118
|
-
case 'result':
|
|
119
|
-
console.log(`[${tag}] Result: ${(msg.output || '').length} chars, ${msg.total_sats ?? '?'} sats`);
|
|
120
|
-
finished = true;
|
|
121
|
-
wake();
|
|
122
|
-
break;
|
|
123
|
-
case 'error':
|
|
124
|
-
error = new Error(`Provider error: ${msg.message}`);
|
|
125
|
-
wake();
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
// Send request with ndebit authorization (provider will use it to debit)
|
|
130
|
-
const requestMsg = {
|
|
131
|
-
type: 'request',
|
|
132
|
-
id: jobId,
|
|
133
|
-
kind,
|
|
134
|
-
input,
|
|
135
|
-
budget: budgetSats,
|
|
136
|
-
ndebit,
|
|
137
|
-
};
|
|
138
|
-
if (params) {
|
|
139
|
-
requestMsg.params = params;
|
|
140
|
-
}
|
|
141
|
-
node.send(peer.socket, requestMsg);
|
|
142
|
-
// Yield chunks as they arrive
|
|
143
|
-
while (true) {
|
|
144
|
-
await waitForChunk();
|
|
145
|
-
if (error) {
|
|
146
|
-
clearTimeout(timeout);
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
// Drain all available chunks
|
|
150
|
-
while (chunks.length > 0) {
|
|
151
|
-
yield chunks.shift();
|
|
152
|
-
}
|
|
153
|
-
if (finished) {
|
|
154
|
-
clearTimeout(timeout);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
await node.destroy();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
9
|
/**
|
|
164
10
|
* Query a provider's skill manifest over an existing P2P connection.
|
|
165
11
|
*
|
package/dist/session.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* P2P Session Client — "rent" a provider's service over Hyperswarm with
|
|
4
|
-
* per-minute
|
|
4
|
+
* per-minute Lightning invoice payments.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
7
|
+
* - Customer pays provider's invoices via built-in wallet
|
|
8
8
|
* - HTTP proxy server for browser-based access to provider APIs
|
|
9
9
|
* - Interactive CLI REPL (generate, status, skill, help, quit)
|
|
10
10
|
*
|
package/dist/session.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* P2P Session Client — "rent" a provider's service over Hyperswarm with
|
|
4
|
-
* per-minute
|
|
4
|
+
* per-minute Lightning invoice payments.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
7
|
+
* - Customer pays provider's invoices via built-in wallet
|
|
8
8
|
* - HTTP proxy server for browser-based access to provider APIs
|
|
9
9
|
* - Interactive CLI REPL (generate, status, skill, help, quit)
|
|
10
10
|
*
|
|
@@ -37,27 +37,31 @@ for (const arg of process.argv.slice(2)) {
|
|
|
37
37
|
case '--provider':
|
|
38
38
|
process.env.PROVIDER_PEER = val;
|
|
39
39
|
break;
|
|
40
|
-
case '--
|
|
41
|
-
process.env.
|
|
40
|
+
case '--cashu-token':
|
|
41
|
+
process.env.CASHU_TOKEN = val;
|
|
42
|
+
break;
|
|
43
|
+
case '--mint':
|
|
44
|
+
process.env.CASHU_MINT_URL = val;
|
|
42
45
|
break;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
46
49
|
import { queryProviderSkill } from './p2p-customer.js';
|
|
47
|
-
import {
|
|
50
|
+
import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
|
|
51
|
+
import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
|
|
48
52
|
import { randomBytes } from 'crypto';
|
|
49
53
|
import { createServer } from 'http';
|
|
50
54
|
import { createInterface } from 'readline';
|
|
51
55
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
52
56
|
import { WebSocketServer, WebSocket as WsWebSocket } from 'ws';
|
|
53
|
-
// Polyfill global WebSocket for Node.js < 22 (needed by @shocknet/clink-sdk)
|
|
54
|
-
if (!globalThis.WebSocket)
|
|
55
|
-
globalThis.WebSocket = WsWebSocket;
|
|
56
57
|
// --- Config ---
|
|
57
58
|
const KIND = Number(process.env.DVM_KIND) || 5200;
|
|
58
59
|
const BUDGET = Number(process.env.BUDGET_SATS) || 500;
|
|
59
60
|
const PORT = Number(process.env.SESSION_PORT) || 8080;
|
|
60
|
-
const
|
|
61
|
+
const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
|
|
62
|
+
const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
|
|
63
|
+
// Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
|
|
64
|
+
let cashuState = null;
|
|
61
65
|
const TICK_INTERVAL_MS = 60_000;
|
|
62
66
|
const HTTP_TIMEOUT_MS = 60_000;
|
|
63
67
|
const state = {
|
|
@@ -163,14 +167,14 @@ function setupMessageHandler() {
|
|
|
163
167
|
break;
|
|
164
168
|
}
|
|
165
169
|
case 'session_tick': {
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const amount = msg.amount || 0;
|
|
171
|
+
if (state.totalSpent + amount > BUDGET) {
|
|
172
|
+
log(`Budget exhausted (need ${amount}, remaining ${remainingSats()}) — ending session`);
|
|
173
|
+
endSession();
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (msg.bolt11) {
|
|
177
|
+
// Invoice mode: pay bolt11 via wallet
|
|
174
178
|
log(`Paying invoice: ${amount} sats...`);
|
|
175
179
|
const payResult = await walletPayInvoice(msg.bolt11);
|
|
176
180
|
if (payResult.ok) {
|
|
@@ -185,20 +189,39 @@ function setupMessageHandler() {
|
|
|
185
189
|
});
|
|
186
190
|
}
|
|
187
191
|
else {
|
|
188
|
-
warn(`
|
|
192
|
+
warn(`Invoice payment failed: ${payResult.error} — ending session`);
|
|
193
|
+
endSession();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (cashuState) {
|
|
197
|
+
// Cashu mode: split tokens and send
|
|
198
|
+
log(`Paying with Cashu: ${amount} sats...`);
|
|
199
|
+
try {
|
|
200
|
+
const { token, change } = await sendCashuToken(cashuState.mintUrl, cashuState.proofs, amount);
|
|
201
|
+
cashuState.proofs = change;
|
|
202
|
+
state.totalSpent += amount;
|
|
203
|
+
log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
|
|
204
|
+
state.node.send(state.socket, {
|
|
205
|
+
type: 'session_tick_ack',
|
|
206
|
+
id: msg.id,
|
|
207
|
+
session_id: state.sessionId,
|
|
208
|
+
cashu_token: token,
|
|
209
|
+
amount,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
warn(`Cashu payment failed: ${e.message} — ending session`);
|
|
189
214
|
endSession();
|
|
190
215
|
}
|
|
191
216
|
}
|
|
217
|
+
else {
|
|
218
|
+
warn('No payment method available — ending session');
|
|
219
|
+
endSession();
|
|
220
|
+
}
|
|
192
221
|
break;
|
|
193
222
|
}
|
|
194
223
|
case 'session_tick_ack': {
|
|
195
|
-
//
|
|
196
|
-
if (msg.amount !== undefined) {
|
|
197
|
-
state.totalSpent += msg.amount;
|
|
198
|
-
}
|
|
199
|
-
if (msg.balance !== undefined) {
|
|
200
|
-
log(`Debit: ${msg.amount ?? '?'} sats (balance: ${msg.balance} sats, ${estimatedMinutesLeft()} min left)`);
|
|
201
|
-
}
|
|
224
|
+
// Ignore — this is our own ack echoed back
|
|
202
225
|
break;
|
|
203
226
|
}
|
|
204
227
|
case 'error': {
|
|
@@ -256,10 +279,9 @@ function setupMessageHandler() {
|
|
|
256
279
|
}
|
|
257
280
|
});
|
|
258
281
|
}
|
|
259
|
-
// --- 3. Payment tracking
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// updated balance. No tick timer needed on the customer side.
|
|
282
|
+
// --- 3. Payment tracking ---
|
|
283
|
+
// Provider sends session_tick with bolt11 invoice every billing period.
|
|
284
|
+
// Customer pays via built-in wallet and sends session_tick_ack with preimage.
|
|
263
285
|
// --- 4. HTTP proxy ---
|
|
264
286
|
function startHttpProxy() {
|
|
265
287
|
return new Promise((resolve, reject) => {
|
|
@@ -667,25 +689,59 @@ async function main() {
|
|
|
667
689
|
state.satsPerMinute = satsPerMinute;
|
|
668
690
|
log(`Pricing: ${satsPerMinute} sats/min`);
|
|
669
691
|
log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
|
|
670
|
-
// 4.
|
|
692
|
+
// 4. Determine payment method: Cashu (default) or invoice (fallback)
|
|
671
693
|
let paymentMethod;
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
|
|
694
|
+
if (CASHU_TOKEN) {
|
|
695
|
+
// Load pre-existing Cashu token
|
|
696
|
+
const { mint, proofs } = decodeCashuToken(CASHU_TOKEN);
|
|
697
|
+
const tokenAmount = proofs.reduce((sum, p) => sum + p.amount, 0);
|
|
698
|
+
cashuState = { mintUrl: mint, proofs };
|
|
699
|
+
paymentMethod = 'cashu';
|
|
700
|
+
log(`Payment: Cashu (${tokenAmount} sats from ${mint})`);
|
|
701
|
+
if (tokenAmount < BUDGET) {
|
|
702
|
+
warn(`Cashu token (${tokenAmount} sats) is less than budget (${BUDGET} sats)`);
|
|
703
|
+
}
|
|
675
704
|
}
|
|
676
705
|
else if (hasApiKey()) {
|
|
677
|
-
|
|
706
|
+
// Auto-mint Cashu tokens via NWC wallet
|
|
678
707
|
const balance = await walletGetBalance();
|
|
679
|
-
if (balance
|
|
680
|
-
warn(
|
|
681
|
-
|
|
708
|
+
if (balance <= 0) {
|
|
709
|
+
warn('Wallet balance is 0. Cannot auto-mint Cashu tokens.');
|
|
710
|
+
await node.destroy();
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const mintAmount = Math.min(balance, BUDGET);
|
|
714
|
+
log(`Wallet balance: ${balance} sats — minting ${mintAmount} sats from ${MINT_URL}`);
|
|
715
|
+
try {
|
|
716
|
+
// 1. Request mint quote (Lightning invoice)
|
|
717
|
+
const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
|
|
718
|
+
log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
|
|
719
|
+
// 2. Pay the invoice via platform NWC wallet
|
|
720
|
+
log('Paying mint invoice via NWC wallet...');
|
|
721
|
+
const payResult = await walletPayInvoice(invoice);
|
|
722
|
+
if (!payResult.ok) {
|
|
723
|
+
throw new Error(`Payment failed: ${payResult.error}`);
|
|
724
|
+
}
|
|
725
|
+
log(`Invoice paid (preimage: ${payResult.preimage?.slice(0, 16)}...)`);
|
|
726
|
+
// 3. Claim minted proofs
|
|
727
|
+
log('Claiming minted tokens...');
|
|
728
|
+
const token = await claimMintQuote(MINT_URL, mintAmount, quote);
|
|
729
|
+
const { mint, proofs } = decodeCashuToken(token);
|
|
730
|
+
cashuState = { mintUrl: mint, proofs };
|
|
731
|
+
paymentMethod = 'cashu';
|
|
732
|
+
const totalMinted = proofs.reduce((s, p) => s + p.amount, 0);
|
|
733
|
+
log(`Minted ${totalMinted} sats Cashu token — using Cashu payment mode`);
|
|
734
|
+
}
|
|
735
|
+
catch (e) {
|
|
736
|
+
warn(`Auto-mint failed: ${e.message}`);
|
|
737
|
+
await node.destroy();
|
|
738
|
+
process.exit(1);
|
|
682
739
|
}
|
|
683
|
-
log(`Payment: invoice (wallet pays provider, balance: ${balance} sats)`);
|
|
684
740
|
}
|
|
685
741
|
else {
|
|
686
742
|
warn('No payment method available.');
|
|
687
|
-
warn(' Option 1: --
|
|
688
|
-
warn(' Option 2: --agent=NAME (
|
|
743
|
+
warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
|
|
744
|
+
warn(' Option 2: --agent=NAME (auto-mints Cashu via NWC wallet)');
|
|
689
745
|
await node.destroy();
|
|
690
746
|
process.exit(1);
|
|
691
747
|
}
|
|
@@ -697,7 +753,6 @@ async function main() {
|
|
|
697
753
|
budget: BUDGET,
|
|
698
754
|
sats_per_minute: satsPerMinute,
|
|
699
755
|
payment_method: paymentMethod,
|
|
700
|
-
...(paymentMethod === 'ndebit' ? { ndebit: NDEBIT } : {}),
|
|
701
756
|
}, 15_000);
|
|
702
757
|
if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
|
|
703
758
|
warn(`Unexpected response: ${ackResp.type}`);
|
package/dist/swarm.d.ts
CHANGED
|
@@ -45,10 +45,10 @@ export interface SwarmMessage {
|
|
|
45
45
|
sats_per_minute?: number;
|
|
46
46
|
balance?: number;
|
|
47
47
|
duration_s?: number;
|
|
48
|
-
|
|
49
|
-
payment_method?: 'ndebit' | 'invoice';
|
|
48
|
+
payment_method?: 'cashu' | 'invoice';
|
|
50
49
|
bolt11?: string;
|
|
51
50
|
preimage?: string;
|
|
51
|
+
cashu_token?: string;
|
|
52
52
|
method?: string;
|
|
53
53
|
path?: string;
|
|
54
54
|
headers?: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "2020117 agent runtime — API polling + Hyperswarm P2P +
|
|
3
|
+
"version": "0.3.7",
|
|
4
|
+
"description": "2020117 agent runtime — API polling + Hyperswarm P2P + Cashu/Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"2020117-agent": "./dist/agent.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
"./processor": "./dist/processor.js",
|
|
15
15
|
"./swarm": "./dist/swarm.js",
|
|
16
|
-
"./
|
|
16
|
+
"./cashu": "./dist/cashu.js",
|
|
17
|
+
"./lightning": "./dist/clink.js",
|
|
17
18
|
"./api": "./dist/api.js"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"dev:session": "npx tsx src/session.ts"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@
|
|
30
|
+
"@cashu/cashu-ts": "^2.5.3",
|
|
30
31
|
"hyperswarm": "^4.17.0",
|
|
31
32
|
"ws": "^8.19.0"
|
|
32
33
|
},
|