-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnip61-nutzap.test.ts
More file actions
173 lines (157 loc) · 6.3 KB
/
nip61-nutzap.test.ts
File metadata and controls
173 lines (157 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* E2E: NIP-61 nutzap end-to-end on regtest Cashu + Nostr relay.
*
* 1. Sender mints proofs from regtest Lightning.
* 2. sendNutzap locks `amountSats` to recipient's Nostr pubkey via
* NUT-11 P2PK and publishes a kind:9321 event tagged with the
* recipient. Sender keeps change.
* 3. fetchIncomingNutzaps on the recipient side queries the relay for
* kind:9321 events tagged `#p` self.
* 4. redeemNutzap calls wallet.receive with the recipient's nsec —
* cashu-ts signs each proof's secret, mint accepts the n_sigs=1
* lock, returns plain proofs.
* 5. Asserts recipient's plain-proof balance grew by ~amount minus
* mint swap fee.
*
* No mocks, no fallbacks. Real mint, real relay, real signature path.
*
* Run:
* docker compose up -d && ./scripts/init-regtest.sh && docker compose restart cashu-mint
* CASHU_MINT_URL=http://localhost:3338 NOSTR_RELAY_URL=ws://localhost:7777 \
* deno test e2e/nip61-nutzap.test.ts --allow-all
*/
import { describe, test } from "@std/testing/bdd";
import { expect } from "@std/expect";
import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
import { SimplePool } from "nostr-tools/pool";
import {
fetchIncomingNutzaps,
redeemNutzap,
sendNutzap,
} from "../example/two-party-binary-bet/src/nip61.ts";
import {
checkInfraReady,
createWallet,
throttledMintProofs,
} from "./helpers/regtest.ts";
import process from "node:process";
const MINT_URL = process.env.CASHU_MINT_URL ?? "http://localhost:3338";
const RELAY_URL = process.env.NOSTR_RELAY_URL ?? "ws://localhost:7777";
async function isRelayReachable(): Promise<boolean> {
try {
const ws = new WebSocket(RELAY_URL);
const ok = await new Promise<boolean>((resolve) => {
const timer = setTimeout(() => { ws.close(); resolve(false); }, 2000);
ws.onopen = () => { clearTimeout(timer); ws.close(); resolve(true); };
ws.onerror = () => { clearTimeout(timer); resolve(false); };
});
return ok;
} catch {
return false;
}
}
const INFRA_READY = await checkInfraReady(MINT_URL) && await isRelayReachable();
const suite = INFRA_READY ? describe : describe.ignore;
suite("e2e: NIP-61 nutzap (regtest Cashu + Nostr relay)", () => {
test("sender → recipient: nutzap delivered, locked, swapped, banked", async () => {
const senderSk = generateSecretKey();
const recipientSk = generateSecretKey();
const recipientPk = getPublicKey(recipientSk);
const senderWallet = await createWallet(MINT_URL);
const recipientWallet = await createWallet(MINT_URL);
const FUND = 256;
const ZAP = 200;
const senderProofs = await throttledMintProofs(senderWallet, FUND);
expect(senderProofs.reduce((s, p) => s + p.amount, 0)).toBe(FUND);
const pool = new SimplePool();
try {
// Sender publishes the nutzap.
const sendResult = await sendNutzap({
senderSecret: senderSk,
recipientPubkey: recipientPk,
mintUrl: MINT_URL,
senderWallet,
senderProofs,
amountSats: ZAP,
relays: [RELAY_URL],
comment: "for the two-party binary bet faucet",
pool,
});
expect(sendResult.eventId).toMatch(/^[0-9a-f]{64}$/);
const senderChange = sendResult.keepProofs.reduce((s, p) => s + p.amount, 0);
// Sender keeps `FUND - ZAP - fee`. Nutshell input_fee_ppk=100 ≈ 1 sat.
expect(senderChange).toBeLessThanOrEqual(FUND - ZAP);
expect(senderChange).toBeGreaterThanOrEqual(FUND - ZAP - 5);
// Recipient queries the relay for nutzaps targeting them.
// Allow a beat for the relay to flush the event.
await new Promise((r) => setTimeout(r, 200));
const incoming = await fetchIncomingNutzaps(pool, [RELAY_URL], recipientPk);
expect(incoming.length).toBeGreaterThanOrEqual(1);
const nz = incoming.find((n) => n.eventId === sendResult.eventId);
expect(nz).toBeTruthy();
expect(nz!.amountSats).toBe(ZAP);
expect(nz!.mintUrl).toBe(MINT_URL);
expect(nz!.comment).toBe("for the two-party binary bet faucet");
// Recipient swaps the locked nutzap at the mint. The proofs are
// P2PK-locked to recipientPk; redeemNutzap passes the nsec to
// wallet.receive which signs each proof's secret.
const fresh = await redeemNutzap({
recipientWallet,
recipientSecret: recipientSk,
nutzap: nz!,
});
const got = fresh.reduce((s, p) => s + p.amount, 0);
expect(got).toBeGreaterThan(0);
// Mint takes one swap-fee chunk on the receive too.
expect(got).toBeLessThanOrEqual(ZAP);
expect(got).toBeGreaterThanOrEqual(ZAP - 5);
} finally {
pool.close([RELAY_URL]);
await new Promise((r) => setTimeout(r, 100));
}
});
test("third party cannot redeem a nutzap addressed to someone else", async () => {
const senderSk = generateSecretKey();
const aliceSk = generateSecretKey();
const evePk = getPublicKey(generateSecretKey()); // an unrelated key
const aliceXOnly = getPublicKey(aliceSk);
const senderWallet = await createWallet(MINT_URL);
const eveWallet = await createWallet(MINT_URL);
const senderProofs = await throttledMintProofs(senderWallet, 256);
const pool = new SimplePool();
try {
const sent = await sendNutzap({
senderSecret: senderSk,
recipientPubkey: aliceXOnly,
mintUrl: MINT_URL,
senderWallet,
senderProofs,
amountSats: 100,
relays: [RELAY_URL],
pool,
});
void sent;
void evePk;
await new Promise((r) => setTimeout(r, 200));
const incoming = await fetchIncomingNutzaps(pool, [RELAY_URL], aliceXOnly);
expect(incoming.length).toBeGreaterThanOrEqual(1);
// Eve gets her hands on alice's nutzap event somehow (the relay is
// public). She tries to redeem with HER nsec — the mint must reject.
const eveSk = generateSecretKey();
let redeemError: string | null = null;
try {
await redeemNutzap({
recipientWallet: eveWallet,
recipientSecret: eveSk,
nutzap: incoming[0]!,
});
} catch (err) {
redeemError = err instanceof Error ? err.message : String(err);
}
expect(redeemError).toBeTruthy();
} finally {
pool.close([RELAY_URL]);
await new Promise((r) => setTimeout(r, 100));
}
});
});