2020117-agent 0.4.3 → 0.4.5
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/cashu.d.ts +16 -0
- package/dist/cashu.js +25 -0
- package/dist/session.js +118 -21
- package/package.json +1 -1
package/dist/cashu.d.ts
CHANGED
|
@@ -45,6 +45,22 @@ export declare function decodeCashuToken(tokenStr: string): {
|
|
|
45
45
|
* Encode proofs back into a portable token string.
|
|
46
46
|
*/
|
|
47
47
|
export declare function encodeCashuToken(mintUrl: string, proofs: Proof[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Melt proofs back into a Lightning invoice — converts Cashu back to Lightning.
|
|
50
|
+
* Returns the payment preimage and any change proofs.
|
|
51
|
+
*/
|
|
52
|
+
export declare function meltProofs(mintUrl: string, proofs: Proof[], invoice: string): Promise<{
|
|
53
|
+
preimage: string;
|
|
54
|
+
change: Proof[];
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Estimate total cost to melt (invoice amount + fee_reserve) for a given invoice.
|
|
58
|
+
*/
|
|
59
|
+
export declare function estimateMeltFee(mintUrl: string, invoice: string): Promise<{
|
|
60
|
+
amount: number;
|
|
61
|
+
fee: number;
|
|
62
|
+
total: number;
|
|
63
|
+
}>;
|
|
48
64
|
/**
|
|
49
65
|
* Request a mint quote — returns a Lightning invoice to pay for minting tokens.
|
|
50
66
|
*/
|
package/dist/cashu.js
CHANGED
|
@@ -60,6 +60,31 @@ export function decodeCashuToken(tokenStr) {
|
|
|
60
60
|
export function encodeCashuToken(mintUrl, proofs) {
|
|
61
61
|
return getEncodedTokenV4({ mint: mintUrl, proofs });
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Melt proofs back into a Lightning invoice — converts Cashu back to Lightning.
|
|
65
|
+
* Returns the payment preimage and any change proofs.
|
|
66
|
+
*/
|
|
67
|
+
export async function meltProofs(mintUrl, proofs, invoice) {
|
|
68
|
+
const wallet = await getWallet(mintUrl);
|
|
69
|
+
const meltQuote = await wallet.createMeltQuote(invoice);
|
|
70
|
+
const amountNeeded = meltQuote.amount + meltQuote.fee_reserve;
|
|
71
|
+
const total = proofs.reduce((s, p) => s + p.amount, 0);
|
|
72
|
+
if (total < amountNeeded) {
|
|
73
|
+
throw new Error(`Need ${amountNeeded} sats (invoice ${meltQuote.amount} + fee ${meltQuote.fee_reserve}) but only have ${total}`);
|
|
74
|
+
}
|
|
75
|
+
const { send, keep } = await wallet.send(amountNeeded, proofs, { includeFees: true });
|
|
76
|
+
const result = await wallet.meltProofs(meltQuote, send);
|
|
77
|
+
const change = [...keep, ...(result.change || [])];
|
|
78
|
+
return { preimage: result.quote.payment_preimage || '', change };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Estimate total cost to melt (invoice amount + fee_reserve) for a given invoice.
|
|
82
|
+
*/
|
|
83
|
+
export async function estimateMeltFee(mintUrl, invoice) {
|
|
84
|
+
const wallet = await getWallet(mintUrl);
|
|
85
|
+
const quote = await wallet.createMeltQuote(invoice);
|
|
86
|
+
return { amount: quote.amount, fee: quote.fee_reserve, total: quote.amount + quote.fee_reserve };
|
|
87
|
+
}
|
|
63
88
|
/**
|
|
64
89
|
* Request a mint quote — returns a Lightning invoice to pay for minting tokens.
|
|
65
90
|
*/
|
package/dist/session.js
CHANGED
|
@@ -43,12 +43,17 @@ for (const arg of process.argv.slice(2)) {
|
|
|
43
43
|
case '--mint':
|
|
44
44
|
process.env.CASHU_MINT_URL = val;
|
|
45
45
|
break;
|
|
46
|
+
case '--nwc':
|
|
47
|
+
process.env.NWC_URI = val;
|
|
48
|
+
break;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
49
52
|
import { queryProviderSkill } from './p2p-customer.js';
|
|
50
53
|
import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
|
|
51
|
-
import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
|
|
54
|
+
import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote, meltProofs, estimateMeltFee } from './cashu.js';
|
|
55
|
+
import { parseNwcUri, nwcGetBalance, nwcPayInvoice, nwcMakeInvoice } from './nwc.js';
|
|
56
|
+
import { loadSovereignKeys } from './nostr.js';
|
|
52
57
|
import { randomBytes } from 'crypto';
|
|
53
58
|
import { createServer } from 'http';
|
|
54
59
|
import { createInterface } from 'readline';
|
|
@@ -60,6 +65,11 @@ const BUDGET = Number(process.env.BUDGET_SATS) || 500;
|
|
|
60
65
|
const PORT = Number(process.env.SESSION_PORT) || 8080;
|
|
61
66
|
const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
|
|
62
67
|
const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
|
|
68
|
+
// Load NWC URI: --nwc flag > .2020117_keys nwc_uri > none
|
|
69
|
+
let nwcParsed = null;
|
|
70
|
+
const nwcUri = process.env.NWC_URI
|
|
71
|
+
|| loadSovereignKeys(process.env.AGENT)?.nwc_uri
|
|
72
|
+
|| null;
|
|
63
73
|
// Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
|
|
64
74
|
let cashuState = null;
|
|
65
75
|
const TICK_INTERVAL_MS = 60_000;
|
|
@@ -184,23 +194,43 @@ function setupMessageHandler() {
|
|
|
184
194
|
break;
|
|
185
195
|
}
|
|
186
196
|
if (msg.bolt11) {
|
|
187
|
-
// Invoice mode: pay bolt11 via
|
|
197
|
+
// Invoice mode: pay bolt11 via NWC direct or platform API
|
|
188
198
|
log(`Paying invoice: ${amount} sats...`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
if (nwcParsed) {
|
|
200
|
+
try {
|
|
201
|
+
const { preimage } = await nwcPayInvoice(nwcParsed, msg.bolt11);
|
|
202
|
+
state.totalSpent += amount;
|
|
203
|
+
log(`Paid ${amount} sats via NWC (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
|
+
preimage,
|
|
209
|
+
amount,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
warn(`NWC invoice payment failed: ${e.message} — ending session`);
|
|
214
|
+
endSession();
|
|
215
|
+
}
|
|
200
216
|
}
|
|
201
217
|
else {
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
const payResult = await walletPayInvoice(msg.bolt11);
|
|
219
|
+
if (payResult.ok) {
|
|
220
|
+
state.totalSpent += amount;
|
|
221
|
+
log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
|
|
222
|
+
state.node.send(state.socket, {
|
|
223
|
+
type: 'session_tick_ack',
|
|
224
|
+
id: msg.id,
|
|
225
|
+
session_id: state.sessionId,
|
|
226
|
+
preimage: payResult.preimage,
|
|
227
|
+
amount,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
warn(`Invoice payment failed: ${payResult.error} — ending session`);
|
|
232
|
+
endSession();
|
|
233
|
+
}
|
|
204
234
|
}
|
|
205
235
|
}
|
|
206
236
|
else if (cashuState) {
|
|
@@ -664,6 +694,32 @@ async function endSession() {
|
|
|
664
694
|
}
|
|
665
695
|
log(`Session ended. Total: ${state.totalSpent} sats for ${duration}s.`);
|
|
666
696
|
}
|
|
697
|
+
// Refund remaining Cashu proofs back to NWC wallet
|
|
698
|
+
if (cashuState && cashuState.proofs.length > 0 && nwcParsed) {
|
|
699
|
+
const remaining = cashuState.proofs.reduce((s, p) => s + p.amount, 0);
|
|
700
|
+
if (remaining >= 3) {
|
|
701
|
+
log(`Refunding ${remaining} sats Cashu → NWC wallet...`);
|
|
702
|
+
try {
|
|
703
|
+
// Probe: create 1-sat invoice to discover melt fee_reserve + swap fee
|
|
704
|
+
const probe = await nwcMakeInvoice(nwcParsed, 1000, 'fee-probe');
|
|
705
|
+
const { fee } = await estimateMeltFee(cashuState.mintUrl, probe.bolt11);
|
|
706
|
+
// fee = melt fee_reserve; +1 for Cashu internal swap fee
|
|
707
|
+
const refundAmount = remaining - fee - 1;
|
|
708
|
+
if (refundAmount < 1) {
|
|
709
|
+
warn(`Remaining ${remaining} sats < melt fee (${fee} sats), cannot refund`);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
const { bolt11 } = await nwcMakeInvoice(nwcParsed, refundAmount * 1000, '2020117-session refund');
|
|
713
|
+
const { preimage } = await meltProofs(cashuState.mintUrl, cashuState.proofs, bolt11);
|
|
714
|
+
cashuState.proofs = [];
|
|
715
|
+
log(`Refunded ${refundAmount} sats (fee: ${fee}, preimage: ${preimage?.slice(0, 16)}...)`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch (e) {
|
|
719
|
+
warn(`Refund failed: ${e.message} — ${remaining} sats lost`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
667
723
|
// Close HTTP proxy
|
|
668
724
|
if (state.httpServer) {
|
|
669
725
|
state.httpServer.close();
|
|
@@ -743,8 +799,24 @@ async function main() {
|
|
|
743
799
|
warn(`Cashu token (${tokenAmount} sats) is less than budget (${BUDGET} sats)`);
|
|
744
800
|
}
|
|
745
801
|
}
|
|
802
|
+
else if (nwcUri) {
|
|
803
|
+
// NWC direct: pay provider invoices via Lightning — no Cashu needed
|
|
804
|
+
nwcParsed = parseNwcUri(nwcUri);
|
|
805
|
+
const { balance_msats } = await nwcGetBalance(nwcParsed);
|
|
806
|
+
const balance = Math.floor(balance_msats / 1000);
|
|
807
|
+
if (balance <= 0) {
|
|
808
|
+
warn('NWC wallet balance is 0.');
|
|
809
|
+
await node.destroy();
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
if (balance < BUDGET) {
|
|
813
|
+
warn(`NWC wallet balance (${balance} sats) < budget (${BUDGET} sats)`);
|
|
814
|
+
}
|
|
815
|
+
paymentMethod = 'invoice';
|
|
816
|
+
log(`Payment: NWC direct (balance: ${balance} sats, pay-per-tick via Lightning)`);
|
|
817
|
+
}
|
|
746
818
|
else if (hasApiKey()) {
|
|
747
|
-
//
|
|
819
|
+
// Platform API fallback: auto-mint Cashu tokens via platform wallet proxy
|
|
748
820
|
const balance = await walletGetBalance();
|
|
749
821
|
if (balance <= 0) {
|
|
750
822
|
warn('Wallet balance is 0. Cannot auto-mint Cashu tokens.');
|
|
@@ -754,17 +826,14 @@ async function main() {
|
|
|
754
826
|
const mintAmount = Math.min(balance, BUDGET);
|
|
755
827
|
log(`Wallet balance: ${balance} sats — minting ${mintAmount} sats from ${MINT_URL}`);
|
|
756
828
|
try {
|
|
757
|
-
// 1. Request mint quote (Lightning invoice)
|
|
758
829
|
const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
|
|
759
830
|
log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
|
|
760
|
-
|
|
761
|
-
log('Paying mint invoice via NWC wallet...');
|
|
831
|
+
log('Paying mint invoice via platform wallet...');
|
|
762
832
|
const payResult = await walletPayInvoice(invoice);
|
|
763
833
|
if (!payResult.ok) {
|
|
764
834
|
throw new Error(`Payment failed: ${payResult.error}`);
|
|
765
835
|
}
|
|
766
836
|
log(`Invoice paid (preimage: ${payResult.preimage?.slice(0, 16)}...)`);
|
|
767
|
-
// 3. Claim minted proofs
|
|
768
837
|
log('Claiming minted tokens...');
|
|
769
838
|
const token = await claimMintQuote(MINT_URL, mintAmount, quote);
|
|
770
839
|
const { mint, proofs } = decodeCashuToken(token);
|
|
@@ -782,7 +851,8 @@ async function main() {
|
|
|
782
851
|
else {
|
|
783
852
|
warn('No payment method available.');
|
|
784
853
|
warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
|
|
785
|
-
warn(' Option 2: --
|
|
854
|
+
warn(' Option 2: --nwc="nostr+walletconnect://..." (NWC direct wallet)');
|
|
855
|
+
warn(' Option 3: --agent=NAME (auto-mints via platform API)');
|
|
786
856
|
await node.destroy();
|
|
787
857
|
process.exit(1);
|
|
788
858
|
}
|
|
@@ -807,6 +877,33 @@ async function main() {
|
|
|
807
877
|
state.satsPerMinute = ackResp.sats_per_minute;
|
|
808
878
|
log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
|
|
809
879
|
}
|
|
880
|
+
// Provider may override payment method (e.g. no Lightning Address → force cashu)
|
|
881
|
+
if (ackResp.payment_method && ackResp.payment_method !== paymentMethod) {
|
|
882
|
+
if (ackResp.payment_method === 'cashu' && paymentMethod === 'invoice' && nwcParsed) {
|
|
883
|
+
// Provider doesn't support invoice — fall back to NWC-minted Cashu
|
|
884
|
+
log(`Provider requires Cashu — minting via NWC...`);
|
|
885
|
+
try {
|
|
886
|
+
const { balance_msats } = await nwcGetBalance(nwcParsed);
|
|
887
|
+
const balance = Math.floor(balance_msats / 1000);
|
|
888
|
+
const mintAmount = Math.min(balance, BUDGET);
|
|
889
|
+
const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
|
|
890
|
+
const { preimage } = await nwcPayInvoice(nwcParsed, invoice);
|
|
891
|
+
const token = await claimMintQuote(MINT_URL, mintAmount, quote);
|
|
892
|
+
const { mint, proofs } = decodeCashuToken(token);
|
|
893
|
+
cashuState = { mintUrl: mint, proofs };
|
|
894
|
+
paymentMethod = 'cashu';
|
|
895
|
+
log(`Minted ${proofs.reduce((s, p) => s + p.amount, 0)} sats Cashu — fallback ready`);
|
|
896
|
+
}
|
|
897
|
+
catch (e) {
|
|
898
|
+
warn(`Cashu fallback mint failed: ${e.message}`);
|
|
899
|
+
await node.destroy();
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
paymentMethod = ackResp.payment_method;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
810
907
|
log(`Session started: ${state.sessionId}`);
|
|
811
908
|
log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
|
|
812
909
|
// 6. Start HTTP proxy
|