2020117-agent 0.4.4 → 0.4.6
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.js +28 -0
- package/dist/cashu.d.ts +16 -0
- package/dist/cashu.js +25 -0
- package/dist/session.js +99 -26
- package/dist/swarm.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -743,6 +743,7 @@ async function startSwarmListener(label) {
|
|
|
743
743
|
lastPaidAt: Date.now(),
|
|
744
744
|
billingTimer: null,
|
|
745
745
|
timeoutTimer: null,
|
|
746
|
+
customerPubkey: msg.pubkey || undefined,
|
|
746
747
|
};
|
|
747
748
|
activeSessions.set(sessionId, session);
|
|
748
749
|
node.send(socket, {
|
|
@@ -751,6 +752,7 @@ async function startSwarmListener(label) {
|
|
|
751
752
|
session_id: sessionId,
|
|
752
753
|
sats_per_minute: satsPerMinute,
|
|
753
754
|
payment_method: paymentMethod,
|
|
755
|
+
pubkey: state.sovereignKeys?.pubkey,
|
|
754
756
|
});
|
|
755
757
|
// Send first billing tick
|
|
756
758
|
await sendBillingTick(node, session, billingAmount, label);
|
|
@@ -1035,6 +1037,32 @@ function endSession(node, session, label) {
|
|
|
1035
1037
|
// Socket may already be closed (peer disconnect)
|
|
1036
1038
|
}
|
|
1037
1039
|
console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
|
|
1040
|
+
// Publish Kind 30311 endorsement for customer (best-effort)
|
|
1041
|
+
if (state.sovereignKeys && state.relayPool && session.customerPubkey) {
|
|
1042
|
+
try {
|
|
1043
|
+
const endorsement = signEvent({
|
|
1044
|
+
kind: 30311,
|
|
1045
|
+
tags: [
|
|
1046
|
+
['d', session.customerPubkey],
|
|
1047
|
+
['p', session.customerPubkey],
|
|
1048
|
+
['rating', '5'],
|
|
1049
|
+
['k', String(KIND)],
|
|
1050
|
+
],
|
|
1051
|
+
content: JSON.stringify({
|
|
1052
|
+
rating: 5,
|
|
1053
|
+
context: {
|
|
1054
|
+
session_duration_s: durationS,
|
|
1055
|
+
total_sats: session.totalEarned,
|
|
1056
|
+
kinds: [KIND],
|
|
1057
|
+
last_job_at: Math.floor(Date.now() / 1000),
|
|
1058
|
+
},
|
|
1059
|
+
}),
|
|
1060
|
+
}, state.sovereignKeys.privkey);
|
|
1061
|
+
state.relayPool.publish(endorsement).catch(() => { });
|
|
1062
|
+
console.log(`[${label}] Published endorsement for customer ${session.customerPubkey.slice(0, 8)}`);
|
|
1063
|
+
}
|
|
1064
|
+
catch { }
|
|
1065
|
+
}
|
|
1038
1066
|
// Update P2P lifetime counters
|
|
1039
1067
|
state.p2pSessionsCompleted++;
|
|
1040
1068
|
state.p2pTotalEarnedSats += session.totalEarned;
|
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
|
@@ -51,9 +51,9 @@ for (const arg of process.argv.slice(2)) {
|
|
|
51
51
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
52
52
|
import { queryProviderSkill } from './p2p-customer.js';
|
|
53
53
|
import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
|
|
54
|
-
import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
|
|
55
|
-
import { parseNwcUri, nwcGetBalance, nwcPayInvoice } from './nwc.js';
|
|
56
|
-
import { loadSovereignKeys } from './nostr.js';
|
|
54
|
+
import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote, meltProofs, estimateMeltFee } from './cashu.js';
|
|
55
|
+
import { parseNwcUri, nwcGetBalance, nwcPayInvoice, nwcMakeInvoice } from './nwc.js';
|
|
56
|
+
import { loadSovereignKeys, signEvent, RelayPool } from './nostr.js';
|
|
57
57
|
import { randomBytes } from 'crypto';
|
|
58
58
|
import { createServer } from 'http';
|
|
59
59
|
import { createInterface } from 'readline';
|
|
@@ -65,11 +65,15 @@ const BUDGET = Number(process.env.BUDGET_SATS) || 500;
|
|
|
65
65
|
const PORT = Number(process.env.SESSION_PORT) || 8080;
|
|
66
66
|
const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
|
|
67
67
|
const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
|
|
68
|
+
// Load sovereign keys (for pubkey exchange + endorsement publishing)
|
|
69
|
+
const sovereignKeys = loadSovereignKeys(process.env.AGENT);
|
|
68
70
|
// Load NWC URI: --nwc flag > .2020117_keys nwc_uri > none
|
|
69
71
|
let nwcParsed = null;
|
|
70
72
|
const nwcUri = process.env.NWC_URI
|
|
71
|
-
||
|
|
73
|
+
|| sovereignKeys?.nwc_uri
|
|
72
74
|
|| null;
|
|
75
|
+
// Track provider's Nostr pubkey (received in session_ack)
|
|
76
|
+
let providerPubkey = null;
|
|
73
77
|
// Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
|
|
74
78
|
let cashuState = null;
|
|
75
79
|
const TICK_INTERVAL_MS = 60_000;
|
|
@@ -694,6 +698,62 @@ async function endSession() {
|
|
|
694
698
|
}
|
|
695
699
|
log(`Session ended. Total: ${state.totalSpent} sats for ${duration}s.`);
|
|
696
700
|
}
|
|
701
|
+
// Publish Kind 30311 endorsement for provider (best-effort)
|
|
702
|
+
if (sovereignKeys?.privkey && providerPubkey) {
|
|
703
|
+
try {
|
|
704
|
+
const endorsement = signEvent({
|
|
705
|
+
kind: 30311,
|
|
706
|
+
tags: [
|
|
707
|
+
['d', providerPubkey],
|
|
708
|
+
['p', providerPubkey],
|
|
709
|
+
['rating', '5'],
|
|
710
|
+
['k', String(KIND)],
|
|
711
|
+
],
|
|
712
|
+
content: JSON.stringify({
|
|
713
|
+
rating: 5,
|
|
714
|
+
context: {
|
|
715
|
+
session_duration_s: elapsedSeconds(),
|
|
716
|
+
total_sats: state.totalSpent,
|
|
717
|
+
kinds: [KIND],
|
|
718
|
+
last_job_at: Math.floor(Date.now() / 1000),
|
|
719
|
+
},
|
|
720
|
+
}),
|
|
721
|
+
}, sovereignKeys.privkey);
|
|
722
|
+
const relayUrls = sovereignKeys.relays?.length ? sovereignKeys.relays : ['wss://relay.2020117.xyz'];
|
|
723
|
+
const relay = new RelayPool(relayUrls);
|
|
724
|
+
await relay.connect();
|
|
725
|
+
await relay.publish(endorsement);
|
|
726
|
+
await relay.close();
|
|
727
|
+
log(`Published endorsement for provider ${providerPubkey.slice(0, 8)}`);
|
|
728
|
+
}
|
|
729
|
+
catch { }
|
|
730
|
+
}
|
|
731
|
+
// Refund remaining Cashu proofs back to NWC wallet
|
|
732
|
+
if (cashuState && cashuState.proofs.length > 0 && nwcParsed) {
|
|
733
|
+
const remaining = cashuState.proofs.reduce((s, p) => s + p.amount, 0);
|
|
734
|
+
if (remaining >= 3) {
|
|
735
|
+
log(`Refunding ${remaining} sats Cashu → NWC wallet...`);
|
|
736
|
+
try {
|
|
737
|
+
// Probe: create 1-sat invoice to discover melt fee_reserve + swap fee
|
|
738
|
+
const probe = await nwcMakeInvoice(nwcParsed, 1000, 'fee-probe');
|
|
739
|
+
const { fee } = await estimateMeltFee(cashuState.mintUrl, probe.bolt11);
|
|
740
|
+
// fee = melt fee_reserve; +1 for Cashu internal swap fee
|
|
741
|
+
const refundAmount = remaining - fee - 1;
|
|
742
|
+
if (refundAmount < 1) {
|
|
743
|
+
warn(`Remaining ${remaining} sats < melt fee (${fee} sats), cannot refund`);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const { bolt11 } = await nwcMakeInvoice(nwcParsed, refundAmount * 1000, '2020117-session refund');
|
|
747
|
+
const { preimage } = await meltProofs(cashuState.mintUrl, cashuState.proofs, bolt11);
|
|
748
|
+
cashuState.proofs = [];
|
|
749
|
+
log(`Refunded ${refundAmount} sats (fee: ${fee}, preimage: ${preimage?.slice(0, 16)}...)`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
warn(`Refund failed: ${e.message} — ${remaining} sats lost`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
697
757
|
// Close HTTP proxy
|
|
698
758
|
if (state.httpServer) {
|
|
699
759
|
state.httpServer.close();
|
|
@@ -774,36 +834,20 @@ async function main() {
|
|
|
774
834
|
}
|
|
775
835
|
}
|
|
776
836
|
else if (nwcUri) {
|
|
777
|
-
// NWC direct:
|
|
837
|
+
// NWC direct: pay provider invoices via Lightning — no Cashu needed
|
|
778
838
|
nwcParsed = parseNwcUri(nwcUri);
|
|
779
839
|
const { balance_msats } = await nwcGetBalance(nwcParsed);
|
|
780
840
|
const balance = Math.floor(balance_msats / 1000);
|
|
781
841
|
if (balance <= 0) {
|
|
782
|
-
warn('NWC wallet balance is 0.
|
|
842
|
+
warn('NWC wallet balance is 0.');
|
|
783
843
|
await node.destroy();
|
|
784
844
|
process.exit(1);
|
|
785
845
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
try {
|
|
789
|
-
const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
|
|
790
|
-
log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
|
|
791
|
-
log('Paying mint invoice via NWC...');
|
|
792
|
-
const { preimage } = await nwcPayInvoice(nwcParsed, invoice);
|
|
793
|
-
log(`Invoice paid (preimage: ${preimage?.slice(0, 16)}...)`);
|
|
794
|
-
log('Claiming minted tokens...');
|
|
795
|
-
const token = await claimMintQuote(MINT_URL, mintAmount, quote);
|
|
796
|
-
const { mint, proofs } = decodeCashuToken(token);
|
|
797
|
-
cashuState = { mintUrl: mint, proofs };
|
|
798
|
-
paymentMethod = 'cashu';
|
|
799
|
-
const totalMinted = proofs.reduce((s, p) => s + p.amount, 0);
|
|
800
|
-
log(`Minted ${totalMinted} sats Cashu token — using Cashu payment mode`);
|
|
801
|
-
}
|
|
802
|
-
catch (e) {
|
|
803
|
-
warn(`NWC auto-mint failed: ${e.message}`);
|
|
804
|
-
await node.destroy();
|
|
805
|
-
process.exit(1);
|
|
846
|
+
if (balance < BUDGET) {
|
|
847
|
+
warn(`NWC wallet balance (${balance} sats) < budget (${BUDGET} sats)`);
|
|
806
848
|
}
|
|
849
|
+
paymentMethod = 'invoice';
|
|
850
|
+
log(`Payment: NWC direct (balance: ${balance} sats, pay-per-tick via Lightning)`);
|
|
807
851
|
}
|
|
808
852
|
else if (hasApiKey()) {
|
|
809
853
|
// Platform API fallback: auto-mint Cashu tokens via platform wallet proxy
|
|
@@ -854,6 +898,7 @@ async function main() {
|
|
|
854
898
|
budget: BUDGET,
|
|
855
899
|
sats_per_minute: satsPerMinute,
|
|
856
900
|
payment_method: paymentMethod,
|
|
901
|
+
pubkey: sovereignKeys?.pubkey,
|
|
857
902
|
}, 15_000);
|
|
858
903
|
if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
|
|
859
904
|
warn(`Unexpected response: ${ackResp.type}`);
|
|
@@ -862,11 +907,39 @@ async function main() {
|
|
|
862
907
|
}
|
|
863
908
|
state.sessionId = ackResp.session_id;
|
|
864
909
|
state.startedAt = Date.now();
|
|
910
|
+
providerPubkey = ackResp.pubkey || null;
|
|
865
911
|
// If the provider dictated a different rate, use it
|
|
866
912
|
if (ackResp.sats_per_minute && ackResp.sats_per_minute !== satsPerMinute) {
|
|
867
913
|
state.satsPerMinute = ackResp.sats_per_minute;
|
|
868
914
|
log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
|
|
869
915
|
}
|
|
916
|
+
// Provider may override payment method (e.g. no Lightning Address → force cashu)
|
|
917
|
+
if (ackResp.payment_method && ackResp.payment_method !== paymentMethod) {
|
|
918
|
+
if (ackResp.payment_method === 'cashu' && paymentMethod === 'invoice' && nwcParsed) {
|
|
919
|
+
// Provider doesn't support invoice — fall back to NWC-minted Cashu
|
|
920
|
+
log(`Provider requires Cashu — minting via NWC...`);
|
|
921
|
+
try {
|
|
922
|
+
const { balance_msats } = await nwcGetBalance(nwcParsed);
|
|
923
|
+
const balance = Math.floor(balance_msats / 1000);
|
|
924
|
+
const mintAmount = Math.min(balance, BUDGET);
|
|
925
|
+
const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
|
|
926
|
+
const { preimage } = await nwcPayInvoice(nwcParsed, invoice);
|
|
927
|
+
const token = await claimMintQuote(MINT_URL, mintAmount, quote);
|
|
928
|
+
const { mint, proofs } = decodeCashuToken(token);
|
|
929
|
+
cashuState = { mintUrl: mint, proofs };
|
|
930
|
+
paymentMethod = 'cashu';
|
|
931
|
+
log(`Minted ${proofs.reduce((s, p) => s + p.amount, 0)} sats Cashu — fallback ready`);
|
|
932
|
+
}
|
|
933
|
+
catch (e) {
|
|
934
|
+
warn(`Cashu fallback mint failed: ${e.message}`);
|
|
935
|
+
await node.destroy();
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
paymentMethod = ackResp.payment_method;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
870
943
|
log(`Session started: ${state.sessionId}`);
|
|
871
944
|
log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
|
|
872
945
|
// 6. Start HTTP proxy
|
package/dist/swarm.d.ts
CHANGED