2020117-agent 0.3.5 → 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 +82 -88
- 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 +127 -28
- package/dist/swarm.d.ts +4 -1
- 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,106 +363,73 @@ async function startSwarmListener(label) {
|
|
|
336
363
|
}
|
|
337
364
|
// --- Session protocol ---
|
|
338
365
|
if (msg.type === 'session_start') {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
if (!msg.ndebit) {
|
|
345
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'session_start 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' });
|
|
346
370
|
return;
|
|
347
371
|
}
|
|
348
372
|
const satsPerMinute = state.skill?.pricing?.sats_per_minute
|
|
349
373
|
|| Number(process.env.SATS_PER_MINUTE)
|
|
350
374
|
|| msg.sats_per_minute
|
|
351
375
|
|| 10;
|
|
352
|
-
const BILLING_INTERVAL_MIN =
|
|
353
|
-
const
|
|
376
|
+
const BILLING_INTERVAL_MIN = 1;
|
|
377
|
+
const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
|
|
354
378
|
const sessionId = randomBytes(8).toString('hex');
|
|
355
|
-
console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, 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)`);
|
|
356
380
|
const session = {
|
|
357
381
|
socket,
|
|
358
382
|
peerId,
|
|
359
383
|
sessionId,
|
|
360
384
|
satsPerMinute,
|
|
361
|
-
|
|
385
|
+
paymentMethod,
|
|
362
386
|
totalEarned: 0,
|
|
363
387
|
startedAt: Date.now(),
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
lastPaidAt: Date.now(),
|
|
389
|
+
billingTimer: null,
|
|
366
390
|
timeoutTimer: null,
|
|
367
391
|
};
|
|
368
392
|
activeSessions.set(sessionId, session);
|
|
369
|
-
// Debit first 10 minutes immediately (prepaid model)
|
|
370
|
-
let firstDebit;
|
|
371
|
-
try {
|
|
372
|
-
firstDebit = await collectPayment({
|
|
373
|
-
ndebit: session.ndebit,
|
|
374
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
375
|
-
amountSats: debitAmount,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
catch (e) {
|
|
379
|
-
console.warn(`[${label}] Session ${sessionId}: first debit error: ${e.message}`);
|
|
380
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Payment error: ${e.message}` });
|
|
381
|
-
activeSessions.delete(sessionId);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (!firstDebit.ok) {
|
|
385
|
-
console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
|
|
386
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
|
|
387
|
-
activeSessions.delete(sessionId);
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
session.totalEarned += debitAmount;
|
|
391
|
-
session.lastDebitAt = Date.now();
|
|
392
|
-
console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
|
|
393
393
|
node.send(socket, {
|
|
394
394
|
type: 'session_ack',
|
|
395
395
|
id: msg.id,
|
|
396
396
|
session_id: sessionId,
|
|
397
397
|
sats_per_minute: satsPerMinute,
|
|
398
|
+
payment_method: paymentMethod,
|
|
398
399
|
});
|
|
399
|
-
//
|
|
400
|
-
node
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
410
415
|
try {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
});
|
|
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)`);
|
|
416
420
|
}
|
|
417
421
|
catch (e) {
|
|
418
|
-
console.
|
|
419
|
-
endSession(node, session, label);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
if (!debit.ok) {
|
|
423
|
-
console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
|
|
422
|
+
console.warn(`[${label}] Session ${session.sessionId}: Cashu token invalid: ${e.message} — ending session`);
|
|
424
423
|
endSession(node, session, label);
|
|
425
|
-
return;
|
|
426
424
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
amount: debitAmount,
|
|
436
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
437
|
-
});
|
|
438
|
-
}, BILLING_INTERVAL_MIN * 60_000);
|
|
425
|
+
}
|
|
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)`);
|
|
432
|
+
}
|
|
439
433
|
return;
|
|
440
434
|
}
|
|
441
435
|
if (msg.type === 'session_end') {
|
|
@@ -622,7 +616,7 @@ async function startSwarmListener(label) {
|
|
|
622
616
|
return;
|
|
623
617
|
}
|
|
624
618
|
});
|
|
625
|
-
// Handle customer disconnect
|
|
619
|
+
// Handle customer disconnect
|
|
626
620
|
node.on('peer-leave', (peerId) => {
|
|
627
621
|
const tag = peerId.slice(0, 8);
|
|
628
622
|
// Find and end all sessions for this peer
|
|
@@ -654,10 +648,10 @@ function findSessionBySocket(socket) {
|
|
|
654
648
|
}
|
|
655
649
|
function endSession(node, session, label) {
|
|
656
650
|
const durationS = Math.round((Date.now() - session.startedAt) / 1000);
|
|
657
|
-
// Stop
|
|
658
|
-
if (session.
|
|
659
|
-
clearInterval(session.
|
|
660
|
-
session.
|
|
651
|
+
// Stop billing timer
|
|
652
|
+
if (session.billingTimer) {
|
|
653
|
+
clearInterval(session.billingTimer);
|
|
654
|
+
session.billingTimer = null;
|
|
661
655
|
}
|
|
662
656
|
if (session.timeoutTimer) {
|
|
663
657
|
clearTimeout(session.timeoutTimer);
|
|
@@ -686,7 +680,7 @@ function endSession(node, session, label) {
|
|
|
686
680
|
// Socket may already be closed (peer disconnect)
|
|
687
681
|
}
|
|
688
682
|
console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
|
|
689
|
-
// Update P2P lifetime counters
|
|
683
|
+
// Update P2P lifetime counters
|
|
690
684
|
state.p2pSessionsCompleted++;
|
|
691
685
|
state.p2pTotalEarnedSats += session.totalEarned;
|
|
692
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 = {
|
|
@@ -112,7 +116,7 @@ function sendAndWait(msg, timeoutMs) {
|
|
|
112
116
|
}
|
|
113
117
|
// --- 6. Message handler ---
|
|
114
118
|
function setupMessageHandler() {
|
|
115
|
-
state.node.on('message', (msg) => {
|
|
119
|
+
state.node.on('message', async (msg) => {
|
|
116
120
|
// Handle chunked HTTP responses — reassemble before resolving
|
|
117
121
|
if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
|
|
118
122
|
const id = msg.id;
|
|
@@ -162,14 +166,62 @@ function setupMessageHandler() {
|
|
|
162
166
|
cleanup();
|
|
163
167
|
break;
|
|
164
168
|
}
|
|
165
|
-
case '
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
+
case 'session_tick': {
|
|
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;
|
|
169
175
|
}
|
|
170
|
-
if (msg.
|
|
171
|
-
|
|
176
|
+
if (msg.bolt11) {
|
|
177
|
+
// Invoice mode: pay bolt11 via wallet
|
|
178
|
+
log(`Paying invoice: ${amount} sats...`);
|
|
179
|
+
const payResult = await walletPayInvoice(msg.bolt11);
|
|
180
|
+
if (payResult.ok) {
|
|
181
|
+
state.totalSpent += amount;
|
|
182
|
+
log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
|
|
183
|
+
state.node.send(state.socket, {
|
|
184
|
+
type: 'session_tick_ack',
|
|
185
|
+
id: msg.id,
|
|
186
|
+
session_id: state.sessionId,
|
|
187
|
+
preimage: payResult.preimage,
|
|
188
|
+
amount,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
warn(`Invoice payment failed: ${payResult.error} — ending session`);
|
|
193
|
+
endSession();
|
|
194
|
+
}
|
|
172
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`);
|
|
214
|
+
endSession();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
warn('No payment method available — ending session');
|
|
219
|
+
endSession();
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'session_tick_ack': {
|
|
224
|
+
// Ignore — this is our own ack echoed back
|
|
173
225
|
break;
|
|
174
226
|
}
|
|
175
227
|
case 'error': {
|
|
@@ -227,10 +279,9 @@ function setupMessageHandler() {
|
|
|
227
279
|
}
|
|
228
280
|
});
|
|
229
281
|
}
|
|
230
|
-
// --- 3. Payment tracking
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// 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.
|
|
234
285
|
// --- 4. HTTP proxy ---
|
|
235
286
|
function startHttpProxy() {
|
|
236
287
|
return new Promise((resolve, reject) => {
|
|
@@ -638,22 +689,70 @@ async function main() {
|
|
|
638
689
|
state.satsPerMinute = satsPerMinute;
|
|
639
690
|
log(`Pricing: ${satsPerMinute} sats/min`);
|
|
640
691
|
log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
|
|
641
|
-
// 4.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
692
|
+
// 4. Determine payment method: Cashu (default) or invoice (fallback)
|
|
693
|
+
let paymentMethod;
|
|
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
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else if (hasApiKey()) {
|
|
706
|
+
// Auto-mint Cashu tokens via NWC wallet
|
|
707
|
+
const balance = await walletGetBalance();
|
|
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);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
warn('No payment method available.');
|
|
743
|
+
warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
|
|
744
|
+
warn(' Option 2: --agent=NAME (auto-mints Cashu via NWC wallet)');
|
|
645
745
|
await node.destroy();
|
|
646
746
|
process.exit(1);
|
|
647
747
|
}
|
|
648
|
-
|
|
649
|
-
// 5. Send session_start with ndebit, wait for session_ack
|
|
748
|
+
// 5. Send session_start, wait for session_ack
|
|
650
749
|
const startId = randomBytes(4).toString('hex');
|
|
651
750
|
const ackResp = await sendAndWait({
|
|
652
751
|
type: 'session_start',
|
|
653
752
|
id: startId,
|
|
654
753
|
budget: BUDGET,
|
|
655
754
|
sats_per_minute: satsPerMinute,
|
|
656
|
-
|
|
755
|
+
payment_method: paymentMethod,
|
|
657
756
|
}, 15_000);
|
|
658
757
|
if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
|
|
659
758
|
warn(`Unexpected response: ${ackResp.type}`);
|
|
@@ -668,7 +767,7 @@ async function main() {
|
|
|
668
767
|
log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
|
|
669
768
|
}
|
|
670
769
|
log(`Session started: ${state.sessionId}`);
|
|
671
|
-
log(`
|
|
770
|
+
log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
|
|
672
771
|
// 6. Start HTTP proxy
|
|
673
772
|
try {
|
|
674
773
|
await startHttpProxy();
|
package/dist/swarm.d.ts
CHANGED
|
@@ -45,7 +45,10 @@ export interface SwarmMessage {
|
|
|
45
45
|
sats_per_minute?: number;
|
|
46
46
|
balance?: number;
|
|
47
47
|
duration_s?: number;
|
|
48
|
-
|
|
48
|
+
payment_method?: 'cashu' | 'invoice';
|
|
49
|
+
bolt11?: string;
|
|
50
|
+
preimage?: string;
|
|
51
|
+
cashu_token?: string;
|
|
49
52
|
method?: string;
|
|
50
53
|
path?: string;
|
|
51
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
|
},
|