Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/model/types/jsonrpc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export type RpcSubKinds = 'bolt11_mint_quote' | 'bolt11_melt_quote' | 'proof_state';
export type RpcSubKinds = RpcSubKind | DeprecatedRpcSubKind;

export type RpcSubKind = 'mint_quote' | 'melt_quote' | 'proof_state';

/**
* @deprecated NUT-17 subscription kinds are method-independent now.
* Remove these aliases once old bolt11-specific subscription kinds are no longer needed.
*/
export type DeprecatedRpcSubKind = 'bolt11_mint_quote' | 'bolt11_melt_quote';
export type RpcSubId = string | number | null;

export type JsonRpcParams = {
Expand Down
6 changes: 4 additions & 2 deletions src/wallet/WalletEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ export class WalletEvents {
if (!ws) throw new Error('Failed to establish WebSocket connection.');

const uniq = Array.from(new Set(ids));
const subId = ws.createSubscription({ kind: 'bolt11_mint_quote', filters: uniq }, cb, err);
// TODO: Remove the deprecated bolt11_* aliases once old NUT-17 subscription kinds are gone.
const subId = ws.createSubscription({ kind: 'mint_quote', filters: uniq }, cb, err);
const cancel = () => ws.cancelSubscription(subId, cb);
return this.withAbort(opts?.signal, cancel);
}
Expand Down Expand Up @@ -292,7 +293,8 @@ export class WalletEvents {
if (!ws) throw new Error('Failed to establish WebSocket connection.');

const uniq = Array.from(new Set(ids));
const subId = ws.createSubscription({ kind: 'bolt11_melt_quote', filters: uniq }, cb, err);
// TODO: Remove the deprecated bolt11_* aliases once old NUT-17 subscription kinds are gone.
const subId = ws.createSubscription({ kind: 'melt_quote', filters: uniq }, cb, err);
const cancel = () => ws.cancelSubscription(subId, cb);
return this.withAbort(opts?.signal, cancel);
}
Expand Down
2 changes: 1 addition & 1 deletion test/consts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { KeyChain, KeyChainCache, MintKeys, MintKeyset } from '../src';

export const MINTINFORESP = JSON.parse(
'{"name":"Testnut mint","pubkey":"0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679","version":"Nutshell/0.16.3","description":"Mint for testing Cashu wallets","description_long":"This mint usually runs the latest main branch of the nutshell repository. It uses a FakeWallet, all your Lightning invoices will always be marked paid so that you can test minting and melting ecash via Lightning.","contact":[{"method":"email","info":"contact@me.com"},{"method":"twitter","info":"@me"},{"method":"nostr","info":"npub1337"}],"motd":"This is a message of the day field. You should display this field to your users if the content changes!","icon_url":"https://image.nostr.build/46ee47763c345d2cfa3317f042d332003f498ee281fb42808d47a7d3b9585911.png","time":1731684933,"nuts":{"4":{"methods":[{"method":"bolt11","unit":"sat","options":{"description":true}},{"method":"bolt11","unit":"usd","options":{"description":true}},{"method":"bolt11","unit":"eur","options":{"description":true}},{"method":"bolt12","unit":"sat","options":{"description":true}},{"method":"bolt12","unit":"usd","options":{"description":true}},{"method":"bolt12","unit":"eur","options":{"description":true}}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"},{"method":"bolt11","unit":"eur"},{"method":"bolt12","unit":"sat"},{"method":"bolt12","unit":"usd"},{"method":"bolt12","unit":"eur"}],"disabled":false},"7":{"supported":true},"8":{"supported":true},"9":{"supported":true},"10":{"supported":true},"11":{"supported":true},"12":{"supported":true},"14":{"supported":true},"17":{"supported":[{"method":"bolt11","unit":"sat","commands":["bolt11_melt_quote","proof_state","bolt11_mint_quote"]},{"method":"bolt11","unit":"usd","commands":["bolt11_melt_quote","proof_state","bolt11_mint_quote"]},{"method":"bolt11","unit":"eur","commands":["bolt11_melt_quote","proof_state","bolt11_mint_quote"]},{"method":"bolt12","unit":"sat","commands":["bolt12_melt_quote","proof_state","bolt12_mint_quote"]},{"method":"bolt12","unit":"usd","commands":["bolt12_melt_quote","proof_state","bolt12_mint_quote"]},{"method":"bolt12","unit":"eur","commands":["bolt12_melt_quote","proof_state","bolt12_mint_quote"]}]}}}',
'{"name":"Testnut mint","pubkey":"0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679","version":"Nutshell/0.16.3","description":"Mint for testing Cashu wallets","description_long":"This mint usually runs the latest main branch of the nutshell repository. It uses a FakeWallet, all your Lightning invoices will always be marked paid so that you can test minting and melting ecash via Lightning.","contact":[{"method":"email","info":"contact@me.com"},{"method":"twitter","info":"@me"},{"method":"nostr","info":"npub1337"}],"motd":"This is a message of the day field. You should display this field to your users if the content changes!","icon_url":"https://image.nostr.build/46ee47763c345d2cfa3317f042d332003f498ee281fb42808d47a7d3b9585911.png","time":1731684933,"nuts":{"4":{"methods":[{"method":"bolt11","unit":"sat","options":{"description":true}},{"method":"bolt11","unit":"usd","options":{"description":true}},{"method":"bolt11","unit":"eur","options":{"description":true}},{"method":"bolt12","unit":"sat","options":{"description":true}},{"method":"bolt12","unit":"usd","options":{"description":true}},{"method":"bolt12","unit":"eur","options":{"description":true}}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"},{"method":"bolt11","unit":"eur"},{"method":"bolt12","unit":"sat"},{"method":"bolt12","unit":"usd"},{"method":"bolt12","unit":"eur"}],"disabled":false},"7":{"supported":true},"8":{"supported":true},"9":{"supported":true},"10":{"supported":true},"11":{"supported":true},"12":{"supported":true},"14":{"supported":true},"17":{"supported":[{"method":"bolt11","unit":"sat","commands":["melt_quote","proof_state","mint_quote"]},{"method":"bolt11","unit":"usd","commands":["melt_quote","proof_state","mint_quote"]},{"method":"bolt11","unit":"eur","commands":["melt_quote","proof_state","mint_quote"]},{"method":"bolt12","unit":"sat","commands":["melt_quote","proof_state","mint_quote"]},{"method":"bolt12","unit":"usd","commands":["melt_quote","proof_state","mint_quote"]},{"method":"bolt12","unit":"eur","commands":["melt_quote","proof_state","mint_quote"]}]}}}',
);

/**
Expand Down
30 changes: 27 additions & 3 deletions test/transport/WSConnection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ describe('testing WSConnection', () => {
expect(connectionSpy).toHaveBeenCalled();
});
test('requesting subscription', async () => {
const message = (await new Promise(async (res) => {
server.on('connection', (socket) => {
socket.on('message', (m) => {
res(m.toString());
});
});
const conn = new WSConnection(fakeUrl);
await conn.connect();

const callback = vi.fn();
const errorCallback = vi.fn();
conn.createSubscription(
{ kind: 'mint_quote', filters: ['12345'] },
callback,
errorCallback,
);
})) as string;
expect(JSON.parse(message)).toMatchObject({
jsonrpc: '2.0',
method: 'subscribe',
params: { kind: 'mint_quote', filters: ['12345'] },
});
});
test('keeps deprecated bolt11 subscription kinds working', async () => {
const message = (await new Promise(async (res) => {
server.on('connection', (socket) => {
socket.on('message', (m) => {
Expand Down Expand Up @@ -45,7 +69,7 @@ describe('testing WSConnection', () => {
const errorCallback = vi.fn();
expect(() => {
conn.createSubscription(
{ kind: 'bolt11_mint_quote', filters: ['123'] },
{ kind: 'mint_quote', filters: ['123'] },
callback,
errorCallback,
);
Expand Down Expand Up @@ -75,7 +99,7 @@ describe('testing WSConnection', () => {
}
});
subId = conn.createSubscription(
{ kind: 'bolt11_mint_quote', filters: ['123'] },
{ kind: 'mint_quote', filters: ['123'] },
callback,
errorCallback,
);
Expand Down Expand Up @@ -117,7 +141,7 @@ describe('testing WSConnection', () => {
});
const errorCallback = vi.fn();
conn.createSubscription(
{ kind: 'bolt11_mint_quote', filters: ['123'] },
{ kind: 'mint_quote', filters: ['123'] },
callback,
errorCallback,
);
Expand Down
40 changes: 20 additions & 20 deletions test/wallet/WalletEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class MockWS {
* Deliver an update payload to matching subscribers for a kind.
*/
emit(
kind: 'bolt11_mint_quote' | 'bolt11_melt_quote',
kind: 'mint_quote' | 'melt_quote',
payload: { quote: string; [k: string]: any },
) {
for (const { kind: k, filters, cb } of this.subs.values()) {
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('WalletEvents', () => {
const canceller = await events.mintQuoteUpdates(['a', 'b'], cb, err);

const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_mint_quote', { quote: 'a', state: 'PAID' });
ws.emit('mint_quote', { quote: 'a', state: 'PAID' });

expect(cb).toHaveBeenCalledWith(expect.objectContaining({ quote: 'a' }));
expect(typeof canceller).toBe('function');
Expand All @@ -140,10 +140,10 @@ describe('WalletEvents', () => {
await events.mintQuotePaid('x', cb, err);

const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_mint_quote', { quote: 'x', state: 'UNPAID' });
ws.emit('mint_quote', { quote: 'x', state: 'UNPAID' });
expect(cb).not.toHaveBeenCalled();

ws.emit('bolt11_mint_quote', { quote: 'x', state: 'PAID' });
ws.emit('mint_quote', { quote: 'x', state: 'PAID' });
expect(cb).toHaveBeenCalledWith(expect.objectContaining({ quote: 'x' }));
});

Expand All @@ -153,7 +153,7 @@ describe('WalletEvents', () => {
await events.meltQuoteUpdates(['m1'], cb, err);

const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_melt_quote', { quote: 'm1', state: 'PAID' });
ws.emit('melt_quote', { quote: 'm1', state: 'PAID' });
expect(cb).toHaveBeenCalledWith(expect.objectContaining({ quote: 'm1' }));
});

Expand All @@ -163,10 +163,10 @@ describe('WalletEvents', () => {
await events.meltQuotePaid('m2', cb, err);

const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_melt_quote', { quote: 'm2', state: 'UNPAID' });
ws.emit('melt_quote', { quote: 'm2', state: 'UNPAID' });
expect(cb).not.toHaveBeenCalled();

ws.emit('bolt11_melt_quote', { quote: 'm2', state: 'PAID' });
ws.emit('melt_quote', { quote: 'm2', state: 'PAID' });
expect(cb).toHaveBeenCalledWith(expect.objectContaining({ quote: 'm2' }));
});

Expand All @@ -189,7 +189,7 @@ describe('WalletEvents', () => {
const p = events.onceMintPaid('q1');
await flushMicrotasks(); // wait for subscription to be created
const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_mint_quote', { quote: 'q1', state: 'PAID', amount: 123 });
ws.emit('mint_quote', { quote: 'q1', state: 'PAID', amount: 123 });
const res = await p;
expect(res).toMatchObject({ quote: 'q1', amount: 123 });
await flushMicrotasks();
Expand Down Expand Up @@ -221,7 +221,7 @@ describe('WalletEvents', () => {
const p = events.onceMintPaid('q4');
await flushMicrotasks(); // wait for subscription
const ws = mock.mint.webSocketConnection!;
ws.fail('bolt11_mint_quote', new Error('boom'));
ws.fail('mint_quote', new Error('boom'));
await expect(p).rejects.toThrow('boom');
await flushMicrotasks();
expect(ws.cancelSubscription).toHaveBeenCalled();
Expand All @@ -233,7 +233,7 @@ describe('WalletEvents', () => {
const p = events.onceAnyMintPaid(['a', 'b', 'c']);
await flushMicrotasks(); // subs ready
const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_mint_quote', { quote: 'b', state: 'PAID', amount: 42 });
ws.emit('mint_quote', { quote: 'b', state: 'PAID', amount: 42 });
const res = await p;
expect(res).toMatchObject({ id: 'b', quote: expect.objectContaining({ amount: 42 }) });
await flushMicrotasks();
Expand Down Expand Up @@ -269,7 +269,7 @@ describe('WalletEvents', () => {
const p = events.onceAnyMintPaid(['f1', 'f2', 'f3'], { failOnError: true });
await flushMicrotasks(); // subs ready
const ws = mock.mint.webSocketConnection!;
ws.fail('bolt11_mint_quote', new Error('bad'));
ws.fail('mint_quote', new Error('bad'));
await expect(p).rejects.toThrow(/bad/);
await flushMicrotasks();
expect(ws.cancelSubscription.mock.calls.length).toBeGreaterThanOrEqual(3);
Expand All @@ -279,8 +279,8 @@ describe('WalletEvents', () => {
const p = events.onceAnyMintPaid(['dup', 'dup', 'other']);
await flushMicrotasks(); // subs ready
const ws = mock.mint.webSocketConnection!;
expect(ws.count('bolt11_mint_quote')).toBe(2); // dup + other
ws.emit('bolt11_mint_quote', { quote: 'dup', state: 'PAID' });
expect(ws.count('mint_quote')).toBe(2); // dup + other
ws.emit('mint_quote', { quote: 'dup', state: 'PAID' });
const res = await p;
expect(res.id).toBe('dup');
});
Expand All @@ -292,8 +292,8 @@ describe('WalletEvents', () => {
const ws = mock.mint.webSocketConnection!;
// Our WS mock broadcasts the same error to all subs per call; the first call
// already empties the set. So assert we get *a* JSON-stringified object.
ws.fail('bolt11_mint_quote', { code: 1, msg: 'x' } as any);
ws.fail('bolt11_mint_quote', { code: 2, msg: 'y' } as any);
ws.fail('mint_quote', { code: 1, msg: 'x' } as any);
ws.fail('mint_quote', { code: 2, msg: 'y' } as any);

await expect(p).rejects.toThrow(/"code":\s*\d/);
});
Expand All @@ -302,7 +302,7 @@ describe('WalletEvents', () => {
const p = events.onceMintPaid('bigobj');
await flushMicrotasks(); // sub ready
const ws = mock.mint.webSocketConnection!;
ws.fail('bolt11_mint_quote', { n: 10n } as any);
ws.fail('mint_quote', { n: 10n } as any);
await expect(p).rejects.toThrow(/\[object Object\]/);
});

Expand All @@ -311,7 +311,7 @@ describe('WalletEvents', () => {
await flushMicrotasks(); // sub ready

const ws = mock.mint.webSocketConnection!;
ws.fail('bolt11_mint_quote', 10n);
ws.fail('mint_quote', 10n);

// With current normalizeError/safeStringify, primitives fall back to
// Object.prototype.toString => "[object BigInt]".
Expand All @@ -324,7 +324,7 @@ describe('WalletEvents', () => {
const p = events.onceMeltPaid('m1');
await flushMicrotasks(); // sub ready
const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_melt_quote', { quote: 'm1', state: 'PAID', amount: 7 });
ws.emit('melt_quote', { quote: 'm1', state: 'PAID', amount: 7 });
const res = await p;
expect(res).toMatchObject({ quote: 'm1', amount: 7 });
await flushMicrotasks();
Expand Down Expand Up @@ -360,7 +360,7 @@ describe('WalletEvents', () => {
const p = events.onceMeltPaid('m-error');
await flushMicrotasks(); // sub ready
const ws = mock.mint.webSocketConnection!;
ws.fail('bolt11_melt_quote', new Error('melt-boom'));
ws.fail('melt_quote', new Error('melt-boom'));
await expect(p).rejects.toThrow('melt-boom');
await flushMicrotasks();
expect(ws.cancelSubscription).toHaveBeenCalled();
Expand All @@ -372,7 +372,7 @@ describe('WalletEvents', () => {
const p = events.onceMeltPaid('m-ok', { signal: ac.signal });
await flushMicrotasks(); // sub ready
const ws = mock.mint.webSocketConnection!;
ws.emit('bolt11_melt_quote', { quote: 'm-ok', state: 'PAID', amount: 1 });
ws.emit('melt_quote', { quote: 'm-ok', state: 'PAID', amount: 1 });
await expect(p).resolves.toMatchObject({ quote: 'm-ok' });
await flushMicrotasks();
expect(ws.cancelSubscription).toHaveBeenCalled();
Expand Down
Loading
Loading