From ec76cc3848908b58a6131addf47be8a087a0663f Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 12:44:49 +0200 Subject: [PATCH 001/246] getPreferences checks against keyset amounts, not necessarily powers of 2 --- src/CashuWallet.ts | 17 ++++++++--------- src/utils.ts | 31 +++++++++++++++++++------------ test/utils.test.ts | 34 ++++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1f9d1dca4..3bb42230d 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -174,10 +174,10 @@ class CashuWallet { try { const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); let preference = options?.preference; + const keys = await this.getKeys(options?.keysetId); if (!preference) { - preference = getDefaultAmountPreference(amount); + preference = getDefaultAmountPreference(amount, keys); } - const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSplitPayload( amount, tokenEntry.proofs, @@ -226,7 +226,6 @@ class CashuWallet { counter?: number; pubkey?: string; privkey?: string; - keysetId?: string; } ): Promise { if (options?.preference) { @@ -376,7 +375,7 @@ class CashuWallet { const keyset = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, - options?.keysetId ?? keyset.id, + keyset, options?.amountPreference, options?.counter, options?.pubkey @@ -520,7 +519,7 @@ class CashuWallet { const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, - keyset.id, + keyset, undefined, counter ); @@ -529,7 +528,7 @@ class CashuWallet { } const sendBlindedMessages = this.createRandomBlindedMessages( amount, - keyset.id, + keyset, preference, counter, pubkey @@ -604,13 +603,13 @@ class CashuWallet { */ private createRandomBlindedMessages( amount: number, - keysetId: string, + keyset: MintKeys, amountPreference?: Array, counter?: number, pubkey?: string ): BlindedMessageData & { amounts: Array } { - const amounts = splitAmount(amount, amountPreference); - return this.createBlindedMessages(amounts, keysetId, counter, pubkey); + const amounts = splitAmount(amount, keyset.keys, amountPreference); + return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } /** diff --git a/src/utils.ts b/src/utils.ts index 1fcbd11da..dfb0482e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,15 +4,18 @@ import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; -function splitAmount(value: number, amountPreference?: Array): Array { +function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { const chunks: Array = []; if (amountPreference) { - chunks.push(...getPreference(value, amountPreference)); - value = - value - - chunks.reduce((curr, acc) => { - return curr + acc; - }, 0); + if (amountPreference.length > 0) { + chunks.push(...getPreference(value, keyset, amountPreference)); + value = + value - + chunks.reduce((curr, acc) => { + return curr + acc; + }, 0); + return chunks; + } } for (let i = 0; i < 32; i++) { const mask: number = 1 << i; @@ -27,13 +30,17 @@ function isPowerOfTwo(number: number) { return number && !(number & (number - 1)); } -function getPreference(amount: number, preferredAmounts: Array): Array { +function hasCorrespondingKey(amount: number, keyset: Keys) { + return amount in keyset; +} + +function getPreference(amount: number, keyset: Keys, preferredAmounts: Array): Array { const chunks: Array = []; let accumulator = 0; preferredAmounts.forEach((pa) => { - if (!isPowerOfTwo(pa.amount)) { + if (!hasCorrespondingKey(pa.amount, keyset)) { throw new Error( - 'Provided amount preferences contain non-power-of-2 numbers. Use only ^2 numbers' + 'Provided amount preferences do not match the amounts of the mint keyset.' ); } for (let i = 1; i <= pa.count; i++) { @@ -47,8 +54,8 @@ function getPreference(amount: number, preferredAmounts: Array return chunks; } -function getDefaultAmountPreference(amount: number): Array { - const amounts = splitAmount(amount); +function getDefaultAmountPreference(amount: number, keyset: Keys): Array { + const amounts = splitAmount(amount, keyset); return amounts.map((a) => { return { amount: a, count: 1 }; }); diff --git a/test/utils.test.ts b/test/utils.test.ts index ccc341607..4b7e02739 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,14 +1,19 @@ -import { AmountPreference } from '../src/model/types/index.js'; +import { AmountPreference, Keys } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; +const keys: Keys = {}; +for (let i=1; i<2048; i *= 2) { + keys[i] = "deadbeef"; +} + describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561); + const chunks = utils.splitAmount(2561, keys); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { - const chunks = utils.splitAmount(0); + const chunks = utils.splitAmount(0, keys); expect(chunks).toStrictEqual([]); }); }); @@ -16,7 +21,7 @@ describe('test split amounts ', () => { describe('test split custom amounts ', () => { const fiveToOne: AmountPreference = { amount: 1, count: 5 }; test('testing amount 5', async () => { - const chunks = utils.splitAmount(5, [fiveToOne]); + const chunks = utils.splitAmount(5, keys, [fiveToOne]); expect(chunks).toStrictEqual([1, 1, 1, 1, 1]); }); const tenToOneAndTwo: Array = [ @@ -24,21 +29,30 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, tenToOneAndTwo); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve: Array = [{ amount: 512, count: 2 }]; - test('testing amount 516', async () => { - const chunks = utils.splitAmount(518, fiveTwelve); + const fiveTwelve: Array = [ + { amount: 512, count: 1 }, + { amount: 2, count: 1 }, + { amount: 4, count: 1} + ]; + test('testing amount 518', async () => { + const chunks = utils.splitAmount(518, keys, fiveTwelve); expect(chunks).toStrictEqual([512, 2, 4]); }); const illegal: Array = [{ amount: 3, count: 2 }]; test('testing non pow2', async () => { - expect(() => utils.splitAmount(6, illegal)).toThrowError(); + expect(() => utils.splitAmount(6, keys, illegal)).toThrowError(); }); const empty: Array = []; test('testing empty', async () => { - const chunks = utils.splitAmount(5, empty); + const chunks = utils.splitAmount(5, keys, empty); + expect(chunks).toStrictEqual([1, 4]); + }); + const undef = undefined; + test('testing undefined', async () => { + const chunks = utils.splitAmount(5, keys, undef); expect(chunks).toStrictEqual([1, 4]); }); }); From a5e4cd217604cbd6842b84bf7e548e3915a725e1 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 13:01:15 +0200 Subject: [PATCH 002/246] remove splitAmount fix for "filling up the rest" behaviour --- src/utils.ts | 15 ++++++--------- test/utils.test.ts | 6 +----- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index dfb0482e1..5e6a899d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,15 +7,12 @@ import { sha256 } from '@noble/hashes/sha256'; function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { const chunks: Array = []; if (amountPreference) { - if (amountPreference.length > 0) { - chunks.push(...getPreference(value, keyset, amountPreference)); - value = - value - - chunks.reduce((curr, acc) => { - return curr + acc; - }, 0); - return chunks; - } + chunks.push(...getPreference(value, keyset, amountPreference)); + value = + value - + chunks.reduce((curr, acc) => { + return curr + acc; + }, 0); } for (let i = 0; i < 32; i++) { const mask: number = 1 << i; diff --git a/test/utils.test.ts b/test/utils.test.ts index 4b7e02739..3a39605e6 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -32,11 +32,7 @@ describe('test split custom amounts ', () => { const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve: Array = [ - { amount: 512, count: 1 }, - { amount: 2, count: 1 }, - { amount: 4, count: 1} - ]; + const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve); expect(chunks).toStrictEqual([512, 2, 4]); From 4d51bf96c824304e12c2663ea74e4cd985c5eced Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 16:57:12 +0200 Subject: [PATCH 003/246] sendPreference, keepPreference --- src/CashuWallet.ts | 16 +++++++++------- src/model/types/index.ts | 5 +++++ test/wallet.test.ts | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 3bb42230d..039aaf236 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,7 +20,8 @@ import { type SplitPayload, type Token, type TokenEntry, - CheckStateEnum + CheckStateEnum, + Preferences } from './model/types/index.js'; import { bytesToNumber, @@ -178,11 +179,12 @@ class CashuWallet { if (!preference) { preference = getDefaultAmountPreference(amount, keys); } + let pref: Preferences = {sendPreference: preference}; const { payload, blindedMessages } = this.createSplitPayload( amount, tokenEntry.proofs, keys, - preference, + pref, options?.counter, options?.pubkey, options?.privkey @@ -222,14 +224,14 @@ class CashuWallet { proofs: Array, options?: { keysetId?: string; - preference?: Array; + preference?: Preferences; counter?: number; pubkey?: string; privkey?: string; } ): Promise { if (options?.preference) { - amount = options?.preference?.reduce((acc, curr) => acc + curr.amount * curr.count, 0); + amount = options?.preference?.sendPreference.reduce((acc, curr) => acc + curr.amount * curr.count, 0); } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; @@ -508,7 +510,7 @@ class CashuWallet { amount: number, proofsToSend: Array, keyset: MintKeys, - preference?: Array, + preference?: Preferences, counter?: number, pubkey?: string, privkey?: string @@ -520,7 +522,7 @@ class CashuWallet { const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, keyset, - undefined, + preference?.keepPreference, counter ); if (this._seed && counter) { @@ -529,7 +531,7 @@ class CashuWallet { const sendBlindedMessages = this.createRandomBlindedMessages( amount, keyset, - preference, + preference?.sendPreference, counter, pubkey ); diff --git a/src/model/types/index.ts b/src/model/types/index.ts index da3ac7224..e861504a8 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -530,6 +530,11 @@ export type AmountPreference = { count: number; }; +export type Preferences = { + sendPreference: Array; + keepPreference?: Array; +} + export type InvoiceData = { paymentRequest: string; amountInSats?: number; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 27fcb7224..da613d386 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -465,7 +465,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: [{ amount: 1, count: 4 }] }); + const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 4 }] }}); expect(result.send).toHaveLength(4); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); @@ -520,7 +520,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: [{ amount: 1, count: 3 }] }); + const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 3 }]} }); expect(result.send).toHaveLength(3); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); From bd00ff66b716268b463467c6f2a4de36afa54e29 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 21:38:38 +0200 Subject: [PATCH 004/246] splitAmount: split is now based on key amounts, not powers of 2. --- src/utils.ts | 24 ++++++++++++++++-------- test/utils.test.ts | 43 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 5e6a899d9..2f4067eff 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,12 @@ import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; -function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { +function splitAmount( + value: number, + keyset: Keys, + amountPreference?: Array, + order?: string +): Array { const chunks: Array = []; if (amountPreference) { chunks.push(...getPreference(value, keyset, amountPreference)); @@ -14,13 +19,16 @@ function splitAmount(value: number, keyset: Keys, amountPreference?: Array = Object.keys(keyset) + .map(k => parseInt(k)) + .sort((a, b) => b-a); + sortedKeyAmounts.forEach(amt => { + let q = Math.floor(value / amt); + for (let i=0; i (order === "asc") ? (a-b) : (b-a)); } function isPowerOfTwo(number: number) { diff --git a/test/utils.test.ts b/test/utils.test.ts index 3a39605e6..7520a1386 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,13 +3,23 @@ import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; const keys: Keys = {}; -for (let i=1; i<2048; i *= 2) { +for (let i=1; i<=2048; i *= 2) { keys[i] = "deadbeef"; } +const keys_base10: Keys = {}; +for (let i=1; i<=10000; i *= 10) { + keys_base10[i] = "deadbeef"; +} + +const keys_base16: Keys = {}; +for (let i=1; i<=0x10000; i *= 16) { + keys_base16[i] = "deadbeef"; +} + describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys); + const chunks = utils.splitAmount(2561, keys, undefined, "asc"); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -29,13 +39,13 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, "asc"); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve); - expect(chunks).toStrictEqual([512, 2, 4]); + expect(chunks).toStrictEqual([512, 4, 2]); }); const illegal: Array = [{ amount: 3, count: 2 }]; test('testing non pow2', async () => { @@ -44,15 +54,36 @@ describe('test split custom amounts ', () => { const empty: Array = []; test('testing empty', async () => { const chunks = utils.splitAmount(5, keys, empty); - expect(chunks).toStrictEqual([1, 4]); + expect(chunks).toStrictEqual([4, 1]); }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef); + const chunks = utils.splitAmount(5, keys, undef, "asc"); expect(chunks).toStrictEqual([1, 4]); }); }); +describe('test split different key amount', () => { + test('testing amount 68251', async () => { + const chunks = utils.splitAmount(68251, keys_base10); + expect(chunks).toStrictEqual([ + 10000, 10000, 10000, 10000, 10000, 10000, + 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, + 100, 100, + 10, 10, 10, 10, 10, + 1 + ]); + }); + test('testing amount 1917', async () => { + const chunks = utils.splitAmount(1917, keys_base16); + expect(chunks).toStrictEqual([ + 256, 256, 256, 256, 256, 256, 256, + 16, 16, 16, 16, 16, 16, 16, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]); + }); +}); + describe('test decode token', () => { test('testing v1 Token', () => { const token = From f7f37a606df2ae9cd4dfdcd706c7a2dfc924018d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 19 Jun 2024 16:12:10 +0200 Subject: [PATCH 005/246] fix for integration test --- src/CashuWallet.ts | 4 +++- src/model/types/index.ts | 2 +- src/utils.ts | 23 ++++++++++++----------- test/utils.test.ts | 30 +++++++++++++----------------- test/wallet.test.ts | 8 ++++++-- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 039aaf236..b0a77ff02 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -237,7 +237,9 @@ class CashuWallet { let amountAvailable = 0; const proofsToSend: Array = []; const proofsToKeep: Array = []; - proofs.forEach((proof) => { + // Start from smallest amounts and work your way up. + // This limits small change. + proofs.reverse().forEach((proof) => { if (amountAvailable >= amount) { proofsToKeep.push(proof); return; diff --git a/src/model/types/index.ts b/src/model/types/index.ts index e861504a8..af1d26711 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -533,7 +533,7 @@ export type AmountPreference = { export type Preferences = { sendPreference: Array; keepPreference?: Array; -} +}; export type InvoiceData = { paymentRequest: string; diff --git a/src/utils.ts b/src/utils.ts index 2f4067eff..b85161113 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; function splitAmount( - value: number, + value: number, keyset: Keys, amountPreference?: Array, order?: string @@ -20,15 +20,14 @@ function splitAmount( }, 0); } const sortedKeyAmounts: Array = Object.keys(keyset) - .map(k => parseInt(k)) - .sort((a, b) => b-a); - sortedKeyAmounts.forEach(amt => { + .map((k) => parseInt(k)) + .sort((a, b) => b - a); + sortedKeyAmounts.forEach((amt) => { let q = Math.floor(value / amt); - for (let i=0; i (order === "asc") ? (a-b) : (b-a)); + return chunks.sort((a, b) => (order === 'asc' ? a - b : b - a)); } function isPowerOfTwo(number: number) { @@ -39,14 +38,16 @@ function hasCorrespondingKey(amount: number, keyset: Keys) { return amount in keyset; } -function getPreference(amount: number, keyset: Keys, preferredAmounts: Array): Array { +function getPreference( + amount: number, + keyset: Keys, + preferredAmounts: Array +): Array { const chunks: Array = []; let accumulator = 0; preferredAmounts.forEach((pa) => { if (!hasCorrespondingKey(pa.amount, keyset)) { - throw new Error( - 'Provided amount preferences do not match the amounts of the mint keyset.' - ); + throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } for (let i = 1; i <= pa.count; i++) { accumulator += pa.amount; diff --git a/test/utils.test.ts b/test/utils.test.ts index 7520a1386..9c27873d6 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,23 +3,23 @@ import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; const keys: Keys = {}; -for (let i=1; i<=2048; i *= 2) { - keys[i] = "deadbeef"; +for (let i = 1; i <= 2048; i *= 2) { + keys[i] = 'deadbeef'; } const keys_base10: Keys = {}; -for (let i=1; i<=10000; i *= 10) { - keys_base10[i] = "deadbeef"; +for (let i = 1; i <= 10000; i *= 10) { + keys_base10[i] = 'deadbeef'; } const keys_base16: Keys = {}; -for (let i=1; i<=0x10000; i *= 16) { - keys_base16[i] = "deadbeef"; +for (let i = 1; i <= 0x10000; i *= 16) { + keys_base16[i] = 'deadbeef'; } describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys, undefined, "asc"); + const chunks = utils.splitAmount(2561, keys, undefined, 'asc'); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -39,7 +39,7 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, "asc"); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, 'asc'); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; @@ -58,7 +58,7 @@ describe('test split custom amounts ', () => { }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef, "asc"); + const chunks = utils.splitAmount(5, keys, undef, 'asc'); expect(chunks).toStrictEqual([1, 4]); }); }); @@ -67,19 +67,15 @@ describe('test split different key amount', () => { test('testing amount 68251', async () => { const chunks = utils.splitAmount(68251, keys_base10); expect(chunks).toStrictEqual([ - 10000, 10000, 10000, 10000, 10000, 10000, - 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, - 100, 100, - 10, 10, 10, 10, 10, - 1 + 10000, 10000, 10000, 10000, 10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 100, + 100, 10, 10, 10, 10, 10, 1 ]); }); test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 256, 256, 256, 256, 256, 256, 256, - 16, 16, 16, 16, 16, 16, 16, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + 256, 256, 256, 256, 256, 256, 256, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1 ]); }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index da613d386..ef7efc7d6 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -465,7 +465,9 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 4 }] }}); + const result = await wallet.send(4, overpayProofs, { + preference: { sendPreference: [{ amount: 1, count: 4 }] } + }); expect(result.send).toHaveLength(4); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); @@ -520,7 +522,9 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 3 }]} }); + const result = await wallet.send(4, overpayProofs, { + preference: { sendPreference: [{ amount: 1, count: 3 }] } + }); expect(result.send).toHaveLength(3); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); From 63b2c1ecd78ee73c796724990cd3a0cd17c2128e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 20 Jun 2024 15:36:40 +0200 Subject: [PATCH 006/246] primary order is ascending amounts --- src/CashuWallet.ts | 4 +--- src/utils.ts | 2 +- test/utils.test.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index b0a77ff02..039aaf236 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -237,9 +237,7 @@ class CashuWallet { let amountAvailable = 0; const proofsToSend: Array = []; const proofsToKeep: Array = []; - // Start from smallest amounts and work your way up. - // This limits small change. - proofs.reverse().forEach((proof) => { + proofs.forEach((proof) => { if (amountAvailable >= amount) { proofsToKeep.push(proof); return; diff --git a/src/utils.ts b/src/utils.ts index b85161113..8b1b1ed6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,7 +27,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => (order === 'asc' ? a - b : b - a)); + return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); } function isPowerOfTwo(number: number) { diff --git a/test/utils.test.ts b/test/utils.test.ts index 9c27873d6..72284e6f4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -19,7 +19,7 @@ for (let i = 1; i <= 0x10000; i *= 16) { describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys, undefined, 'asc'); + const chunks = utils.splitAmount(2561, keys); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -39,12 +39,12 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, 'asc'); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { - const chunks = utils.splitAmount(518, keys, fiveTwelve); + const chunks = utils.splitAmount(518, keys, fiveTwelve, 'desc'); expect(chunks).toStrictEqual([512, 4, 2]); }); const illegal: Array = [{ amount: 3, count: 2 }]; @@ -53,19 +53,19 @@ describe('test split custom amounts ', () => { }); const empty: Array = []; test('testing empty', async () => { - const chunks = utils.splitAmount(5, keys, empty); + const chunks = utils.splitAmount(5, keys, empty, 'desc'); expect(chunks).toStrictEqual([4, 1]); }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef, 'asc'); + const chunks = utils.splitAmount(5, keys, undef); expect(chunks).toStrictEqual([1, 4]); }); }); describe('test split different key amount', () => { test('testing amount 68251', async () => { - const chunks = utils.splitAmount(68251, keys_base10); + const chunks = utils.splitAmount(68251, keys_base10, undefined, 'desc'); expect(chunks).toStrictEqual([ 10000, 10000, 10000, 10000, 10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 100, 100, 10, 10, 10, 10, 10, 1 @@ -74,8 +74,8 @@ describe('test split different key amount', () => { test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 256, 256, 256, 256, 256, 256, 256, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, 256, 256 ]); }); }); From 4766e90fe5f1b9babab69b8c38676df913713687 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 20 Jun 2024 15:42:58 +0200 Subject: [PATCH 007/246] npm run format --- src/CashuWallet.ts | 7 +++++-- test/utils.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 039aaf236..2bb25dae7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -179,7 +179,7 @@ class CashuWallet { if (!preference) { preference = getDefaultAmountPreference(amount, keys); } - let pref: Preferences = {sendPreference: preference}; + let pref: Preferences = { sendPreference: preference }; const { payload, blindedMessages } = this.createSplitPayload( amount, tokenEntry.proofs, @@ -231,7 +231,10 @@ class CashuWallet { } ): Promise { if (options?.preference) { - amount = options?.preference?.sendPreference.reduce((acc, curr) => acc + curr.amount * curr.count, 0); + amount = options?.preference?.sendPreference.reduce( + (acc, curr) => acc + curr.amount * curr.count, + 0 + ); } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; diff --git a/test/utils.test.ts b/test/utils.test.ts index 72284e6f4..86727a3e7 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -74,8 +74,8 @@ describe('test split different key amount', () => { test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, 256, 256 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, + 256, 256 ]); }); }); From 9a0e589701d397f4e4276045e156b65fa4e1d565 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 28 Jun 2024 11:22:23 +0200 Subject: [PATCH 008/246] updated tests --- test/utils.test.ts | 138 +++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 79 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index c43b36fe9..15fa721aa 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,4 +1,4 @@ -import { AmountPreference } from '../src/model/types/index.js'; +import { AmountPreference, Token } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; @@ -47,87 +47,20 @@ describe('test decode token', () => { test('testing v1 Token', () => { const token = 'W3siaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjIsInNlY3JldCI6Ild6ZC9vNUVHdmVKb3hTQVlGcjZ1U3lnUmFWSUFrOFc4MXNLTlRxdVd4UjQ9IiwiQyI6IjAzNWNiZmQwOTNiOWZlMWRjNjU2MGEwNDM3YzQyNDQxZjA0ZDIyYzk4MDY2NGMyNGExMGZlZGFiNTlmZWY0YmZjOSJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjQsInNlY3JldCI6InU0N2lWUkhneUNuUFhCNWxOdFpGaTBOeHpPZ1lyRk1WODV2aFpyRThIbWM9IiwiQyI6IjAyNThiYmZkZWJmZGQzYjk0OTljZDk1YzFkMWZiYTVjZTQ1MWFjOGNlZTE0NzM1Yzk2MGFiMDc1ZmI2ZTQ4ZjBkYyJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjY0LCJzZWNyZXQiOiJ1YTFaT0hjeVB3T0M0UUxPaWthQVV1MThJM2pEUDJCSVNYREFGcW91N1VNPSIsIkMiOiIwMjU2MWNhNjcyNTdlNzdhNjNjN2U3NWQ4MGVkYTI3ZDlhMmEyYzUxZTA0NGM4ZjhmODVlNzc0OTZlMGRlM2U2NWIifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJ5ZTlNRCtaQ25VUHlHOTBscmYyZ2tudnA3N2I4V05wNUxRT2ZtcERjRGNFPSIsIkMiOiIwM2UwN2M1NjExNzcwMmNmODg3MDFlYjAyOTM2YjA5MDNhZmEyMTQwZDcwNTY1N2ZkODVkM2YxZWI5MzRiYTBjYzMifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoyLCJzZWNyZXQiOiJIUHpzRmZPUDFWRU1BMW8vTnFHVXFhRXdaV2RiN3VERzM4T1grLzlZTURzPSIsIkMiOiIwMmQ3ZDE1YTBhZmIyNThjMjlhZDdmOWY4N2ZmMzIxZWRmNTgyOTM0ZWI0NWExNTE2MjhiNTJjMDExZjQ2MWZkOGEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJnMVR1YXdha1RVQkJBTW9tZGpDVHkrRENNTnBaUmd3dWluNXB5V2xoTVVNPSIsIkMiOiIwMzU4Y2IxMGE5NWEzY2E1YmE5MTc5MTllMWNhODA1NjZmMTg5NTI4Njk1MTJjYWFjMDlmYmQ5MGYxN2QyZTZlYmEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoyLCJzZWNyZXQiOiJRMTFyamNXWk55Q2dkRmxqRThaNkdwNFhDYllKcndzRGhncXVQOTU1VWU0PSIsIkMiOiIwMjAxNjBmODIwNGU4MGIxNDg4NmFlMzZjMzRiMjI3ODllMzMxZmM5MjVhNGMwOGE3ZWYxZDZjYzMyYTIwNjZjZWUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50Ijo4LCJzZWNyZXQiOiI1MVZrUXFYT2kwM0k2a0pzM0tlSEI0OVVCQTFSRktrWnMyMFljZEtOSW1JPSIsIkMiOiIwMjZiYWU2YTgzOWE3OTdjNmU5NGZlNGM5MWZlNTIwOGU4MDE3MTg2Y2NkMDk0ZmI4ZTNkZjYyNjAyZWJmMjczMjUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxNiwic2VjcmV0IjoiVk4ySlMwUENKdGQ3MjJUTXUxdGFxNUZSMXg0dDlXM28xNndWRGVweXBxYz0iLCJDIjoiMDIxMmM4ZGE5NWE4NDEyYjgyMDE4MTgxNzQxZWY1YWQ0ZjYzMTU1NjBhMWFmODM5ZjMxOTU4NTcwZTVlYzI2ZDQyIn1d'; - - const result = utils.getDecodedToken(token); - expect(result.token[0].proofs.reduce((c, p) => c + p.amount, 0)).toEqual(100); - expect(result.token[0].mint).toStrictEqual(''); - }); - test('test corrupt v1 token', () => { - const token = - 'W3siaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjIsInNlY3JldCI6Ild6ZC9vNUVHdmVKb3hTQVlGcjZ1U3lnUmFWSUFrOFc4MXNLTlRxdVd4UjQ9IiwiQyI6IjAzNWNiZmQwOTNiOWZlMWRjNjU2MGEwNDM3YzQyNDQxZjA0ZDIyYzk4MDY2NGMyNGExMGZlZGFiNTlmZWY0YmZjOSJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjQsInNlY3JldCI6InU0N2lWUkhneUNuUFhCNWxOdFpGaTBOIkMiOiIwMmQ3ZDE1YTBhZmIyNThjMjlhZDdmOWY4N2ZmMzIxZWRmNTgyOTM0ZWI0NWExNTE2MjhiNTJjMDExZjQ2MWZkOGEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJnMVR1YXdha1RVQkJBTW9tZGpDVHkrRENNTnBaUmd3dWluNXB5V2xoTVVNPSIsIkMiOiIwMzU4Y2IxMGE5NWEzY2E1YmE5MTc5MTllMWNhODA1NjZmMTg5NTI4Njk1MTJjYWFjMDlmYmQ5MGYxN2QyZTZlYmEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoyLCJzZWNyZXQiOiJRMTFyamNXWk55Q2dkRmxqRThaNkdwNFhDYllKcndzRGhncXVQOTU1VWU0PSIsIkMiOiIwMjAxNjBmODIwNGU4MGIxNDg4NmFlMzZjMzRiMjI3ODllMzMxZmM5MjVhNGMwOGE3ZWYxZDZjYzMyYTIwNjZjZWUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50Ijo4LCJzZWNyZXQiOiI1MVZrUXFYT2kwM0k2a0pzM0tlSEI0OVVCQTFSRktrWnMyMFljZEtOSW1JPSIsIkMiOiIwMjZiYWU2YTgzOWE3OTdjNmU5NGZlNGM5MWZlNTIwOGU4MDE3MTg2Y2NkMDk0ZmI4ZTNkZjYyNjAyZWJmMjczMjUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxNiwic2VjcmV0IjoiVk4ySlMwUENKdGQ3MjJUTXUxdGFxNUZSMXg0dDlXM28xNndWRGVweXBxYz0iLCJDIjoiMDIxMmM4ZGE5NWE4NDEyYjgyMDE4MTgxNzQxZWY1YWQ0ZjYzMTU1NjBhMWFmODM5ZjMxOTU4NTcwZTVlYzI2ZDQyIn1d'; - expect(() => utils.getDecodedToken(token)).toThrowError(); + let result: Token | undefined; + expect(() => { + result = utils.getDecodedToken(token); + }).toThrow(); + expect(result).toBe(undefined); }); test('testing v2 Token', async () => { const token = 'eyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IkkyeU4raVJZZmt6VCIsImFtb3VudCI6MSwic2VjcmV0IjoiOTd6Zm1tYUdmNWs4TWcwZ2FqcG5ibXBlcnZUdEVlRTh3d0tyaTdyV3BVcz0iLCJDIjoiMDIxOTUwODFlNjIyZjk4YmZjMTlhMDVlYmUyMzQxZDk1NWMwZDEyNTg4YzU5NDhjODU4ZDA3YWRlYzAwN2JjMWU0In1dLCJtaW50IjoiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ'; - - const result = utils.getDecodedToken(token); - expect(result).toStrictEqual({ - token: [ - { - proofs: [ - { - id: 'I2yN+iRYfkzT', - amount: 1, - secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=', - C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4' - } - ], - mint: 'http://localhost:3338' - } - ] - }); - }); - test('testing v2 Token 2', () => { - const token = - 'eyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IjBOSTNUVUFzMVNmeSIsImFtb3VudCI6MSwic2VjcmV0Ijoia3hxWFlwYkNWb0l1eDRsbVdJRFh4M29NMi83S1ZzaHFqSklJZXh0cU1hVT0iLCJDIjoiMDJhNDIxNDBkMWJiZDU5Y2E0YzViYTllYjczZDAyMGYzMWY2OGY0ZTMwNzhmZDNhZjFlZTY0ZWJlY2U1MmI2ZWRhIn1dLCJtaW50IjoiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ'; - - const result = utils.getDecodedToken(token); - expect(result).toStrictEqual({ - token: [ - { - proofs: [ - { - id: '0NI3TUAs1Sfy', - amount: 1, - secret: 'kxqXYpbCVoIux4lmWIDXx3oM2/7KVshqjJIIextqMaU=', - C: '02a42140d1bbd59ca4c5ba9eb73d020f31f68f4e3078fd3af1ee64ebece52b6eda' - } - ], - mint: 'http://localhost:3338' - } - ] - }); - }); - test('test corrupt v2 token', () => { - const token = - 'W3siaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjIsInNlY3JldCI6Ild6ZC9vNUVHdmVKb3hTQVlGcjZ1U3lnUmFWSUFrOFc4MXNLTlRxdVd4UjQ9IiwiQyI6IjAzNWNiZmQwOTNiOWZlMWRjNjU2MGEwNDM3YzQyNDQxZjA0ZDIyYzk4MDY2NGMyNGExMGZlZGFiNTlmZWY0YmZjOSJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjQsInNlY3JldCI6InU0N2lWUkhneUNuUFhCNWxOdFpGaTBOIkMiOiIwMmQ3ZDE1YTBhZmIyNThjMjlhZDdmOWY4N2ZmMzIxZWRmNTgyOTM0ZWI0NWExNTE2MjhiNTJjMDExZjQ2MWZkOGEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJnMVR1YXdha1RVQkJBTW9tZGpDkMiOiIwMjZiYWU2YTgzOWE3OTdjNmU5NGZlNGM5MWZlNTIwOGU4MDE3MTg2Y2NkMDk0ZmI4ZTNkZjYyNjAyZWJmMjczMjUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxNiwic2VjcmV0IjoiVk4ySlMwUENKdGQ3MjJUTXUxdGFxNUZSMXg0dDlXM28xNndWRGVweXBxYz0iLCJDIjoiMDIxMmM4ZGE5NWE4NDEyYjgyMDE4MTgxNzQxZWY1YWQ0ZjYzMTU1NjBhMWFmODM5ZjMxOTU4NTcwZTVlYzI2ZDQyIn1d'; - - expect(() => utils.getDecodedToken(token)).toThrowError(); - }); -}); - -describe('test encode token', () => { - test('testing v3 Token', async () => { - const token = - 'cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCIsInByb29mcyI6W3siaWQiOiJJMnlOK2lSWWZrelQiLCJhbW91bnQiOjEsInNlY3JldCI6Ijk3emZtbWFHZjVrOE1nMGdhanBuYm1wZXJ2VHRFZUU4d3dLcmk3cldwVXM9IiwiQyI6IjAyMTk1MDgxZTYyMmY5OGJmYzE5YTA1ZWJlMjM0MWQ5NTVjMGQxMjU4OGM1OTQ4Yzg1OGQwN2FkZWMwMDdiYzFlNCJ9XX1dfQ'; - - const obj = { - proofs: [ - { - id: 'I2yN+iRYfkzT', - amount: 1, - secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=', - C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4' - } - ], - mints: [{ url: 'http://localhost:3338', ids: ['L3zxxRB/I8uE', 'I2yN+iRYfkzT'] }] - }; - - const result = utils.getEncodedToken({ - token: [{ mint: obj.mints[0].url, proofs: obj.proofs }] - }); - expect(result).toEqual(token); + let result: Token | undefined; + expect(() => { + result = utils.getDecodedToken(token); + }).toThrow(); + expect(result).toBe(undefined); }); }); @@ -176,10 +109,57 @@ describe('test decode token', () => { }; const token = - 'eyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCIsInByb29mcyI6W3siaWQiOiJJMnlOK2lSWWZrelQiLCJhbW91bnQiOjEsInNlY3JldCI6Ijk3emZtbWFHZjVrOE1nMGdhanBuYm1wZXJ2VHRFZUU4d3dLcmk3cldwVXM9IiwiQyI6IjAyMTk1MDgxZTYyMmY5OGJmYzE5YTA1ZWJlMjM0MWQ5NTVjMGQxMjU4OGM1OTQ4Yzg1OGQwN2FkZWMwMDdiYzFlNCJ9XX1dfQ'; + 'AeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCIsInByb29mcyI6W3siaWQiOiJJMnlOK2lSWWZrelQiLCJhbW91bnQiOjEsInNlY3JldCI6Ijk3emZtbWFHZjVrOE1nMGdhanBuYm1wZXJ2VHRFZUU4d3dLcmk3cldwVXM9IiwiQyI6IjAyMTk1MDgxZTYyMmY5OGJmYzE5YTA1ZWJlMjM0MWQ5NTVjMGQxMjU4OGM1OTQ4Yzg1OGQwN2FkZWMwMDdiYzFlNCJ9XX1dfQ'; const result = utils.getDecodedToken(token); expect(result).toStrictEqual(obj); }); + test('testing v4 Token', () => { + const v3Token = { + memo: '', + token: [ + { + mint: 'https://mint.minibits.cash/Bitcoin', + proofs: [ + { + secret: '7e98535c6f8cd7a5eff150963a2743613a91e9498150fd5af8d2bfcfd5babe68', + C: '03022a28d163cf63792c1533e6660112f2b75db2fe46aa840e7f5d0f979a2c6cfd', + id: '00500550f0494146', + amount: 16 + }, + { + amount: 4, + secret: '96bd8480717673311bc70e92818b5babcb665edee39b639defad5584d8d18b1f', + C: '030936759e03235867f9cea58f047c043acdd7455f604c92c75839e5e08a91e198', + id: '00500550f0494146' + }, + { + secret: 'e145fa7fba21a9cd3c8743c9de5e4de33e0095abc50b262f1b3831b69b8f63df', + id: '00500550f0494146', + C: '03eba391a31e101e1ba1853db1e4bbb6a166d4fbbb1e181e82892c3301e4e02015', + amount: 1 + } + ] + } + ] + }; + + const token = + 'cashuBuQACYXSBuQACYXCDuQADYWEQYXN4QDdlOTg1MzVjNmY4Y2Q3YTVlZmYxNTA5NjNhMjc0MzYxM2E5MWU5NDk4MTUwZmQ1YWY4ZDJiZmNmZDViYWJlNjhhY3hCMDMwMjJhMjhkMTYzY2Y2Mzc5MmMxNTMzZTY2NjAxMTJmMmI3NWRiMmZlNDZhYTg0MGU3ZjVkMGY5NzlhMmM2Y2ZkuQADYWEEYXN4QDk2YmQ4NDgwNzE3NjczMzExYmM3MGU5MjgxOGI1YmFiY2I2NjVlZGVlMzliNjM5ZGVmYWQ1NTg0ZDhkMThiMWZhY3hCMDMwOTM2NzU5ZTAzMjM1ODY3ZjljZWE1OGYwNDdjMDQzYWNkZDc0NTVmNjA0YzkyYzc1ODM5ZTVlMDhhOTFlMTk4uQADYWEBYXN4QGUxNDVmYTdmYmEyMWE5Y2QzYzg3NDNjOWRlNWU0ZGUzM2UwMDk1YWJjNTBiMjYyZjFiMzgzMWI2OWI4ZjYzZGZhY3hCMDNlYmEzOTFhMzFlMTAxZTFiYTE4NTNkYjFlNGJiYjZhMTY2ZDRmYmJiMWUxODFlODI4OTJjMzMwMWU0ZTAyMDE1YWlwMDA1MDA1NTBmMDQ5NDE0NmFteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2lu'; + + const result = utils.getDecodedToken(token); + console.log(JSON.stringify(result)); + expect(result).toStrictEqual(v3Token); + }); + test('testing joining urls', () => { + const mint_url = 'http://localhost:3338'; + const info_url = utils.joinUrls(mint_url, 'info'); + + expect(info_url).toBe('http://localhost:3338/info'); + + const mint_url_trailing_slash = 'http://localhost:3338/'; + const mint_info_url = utils.joinUrls(mint_url_trailing_slash, 'info'); + expect(mint_info_url).toBe('http://localhost:3338/info'); + }); }); describe('test keyset derivation', () => { From 6ed41a99073257ece044947acb9c4e327cb81b4d Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 28 Jun 2024 11:22:49 +0200 Subject: [PATCH 009/246] added cbor-x --- package-lock.json | 2747 +++++++++++++++++++++++---------------------- package.json | 3 +- 2 files changed, 1433 insertions(+), 1317 deletions(-) diff --git a/package-lock.json b/package-lock.json index 973669c62..f4b4f45e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", "@scure/bip39": "^1.2.2", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "cbor-x": "^1.5.9" }, "devDependencies": { "@types/jest": "^29.5.1", @@ -51,119 +52,48 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.20.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", - "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz", - "integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.20.7", - "@babel/helpers": "^7.20.7", - "@babel/parser": "^7.20.7", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -173,21 +103,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -195,186 +119,188 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.20.2" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.7.tgz", - "integrity": "sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -452,9 +378,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -524,12 +450,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -626,12 +552,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", - "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -641,33 +567,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", - "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -676,13 +602,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -707,6 +633,78 @@ "buffer": "^6.0.3" } }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -889,16 +887,16 @@ } }, "node_modules/@jest/console": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", - "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -906,37 +904,37 @@ } }, "node_modules/@jest/core": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", - "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -953,89 +951,89 @@ } }, "node_modules/@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", - "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -1043,13 +1041,13 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -1067,25 +1065,53 @@ } } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", - "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, @@ -1094,13 +1120,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", - "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -1109,14 +1135,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", - "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -1124,22 +1150,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1150,12 +1176,12 @@ } }, "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -1189,9 +1215,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1204,13 +1230,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@noble/curves": { @@ -1304,27 +1330,27 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "dependencies": { - "@sinonjs/commons": "^2.0.0" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/@tsconfig/node10": { @@ -1352,9 +1378,9 @@ "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -1365,18 +1391,18 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -1384,12 +1410,12 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", - "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "dependencies": { - "@babel/types": "^7.3.0" + "@babel/types": "^7.20.7" } }, "node_modules/@types/graceful-fs": { @@ -1463,12 +1489,6 @@ "form-data": "^3.0.0" } }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -1973,15 +1993,15 @@ "dev": true }, "node_modules/babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -2010,9 +2030,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", - "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -2048,12 +2068,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", - "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -2112,9 +2132,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, "funding": [ { @@ -2124,13 +2144,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -2263,9 +2287,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001441", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", - "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true, "funding": [ { @@ -2275,9 +2299,42 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", + "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2313,9 +2370,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "node_modules/cliui": { @@ -2343,9 +2400,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, "node_modules/color-convert": { @@ -2390,6 +2447,27 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2428,10 +2506,18 @@ } }, "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/deep-is": { "version": "0.1.4", @@ -2473,6 +2559,15 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2492,9 +2587,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2525,9 +2620,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "version": "1.4.814", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.814.tgz", + "integrity": "sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==", "dev": true }, "node_modules/emittery": { @@ -2623,9 +2718,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -3253,16 +3348,16 @@ } }, "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4077,17 +4172,17 @@ } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-lib-source-maps": { @@ -4105,9 +4200,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4118,15 +4213,15 @@ } }, "node_modules/jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", - "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -4144,12 +4239,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", - "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -4157,28 +4253,28 @@ } }, "node_modules/jest-circus": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", - "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -4188,22 +4284,21 @@ } }, "node_modules/jest-cli": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", - "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -4222,31 +4317,31 @@ } }, "node_modules/jest-config": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", - "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -4267,24 +4362,24 @@ } }, "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", - "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -4294,62 +4389,62 @@ } }, "node_modules/jest-each": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", - "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", - "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -4361,46 +4456,46 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", - "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -4409,14 +4504,14 @@ } }, "node_modules/jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.5.0" + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4440,26 +4535,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", - "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -4469,43 +4564,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", - "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", - "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4514,31 +4609,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", - "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4547,59 +4642,41 @@ } }, "node_modules/jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -4607,19 +4684,13 @@ "node": ">=10" } }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -4631,17 +4702,17 @@ } }, "node_modules/jest-validate": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", - "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4660,18 +4731,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", - "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -4679,13 +4750,13 @@ } }, "node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -4874,20 +4945,32 @@ "dev": true }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -5047,6 +5130,20 @@ } } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5054,9 +5151,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -5298,9 +5395,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -5361,12 +5458,12 @@ } }, "node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -5418,9 +5515,9 @@ } }, "node_modules/pure-rand": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", - "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, "funding": [ { @@ -6184,9 +6281,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "funding": [ { @@ -6196,14 +6293,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -6225,25 +6326,19 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -6373,9 +6468,9 @@ "dev": true }, "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -6433,260 +6528,196 @@ } }, "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { - "version": "7.20.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", - "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true }, "@babel/core": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz", - "integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.20.7", - "@babel/helpers": "^7.20.7", - "@babel/parser": "^7.20.7", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - } + "json5": "^2.2.3", + "semver": "^6.3.1" } }, "@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, "requires": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } } } }, "@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" } }, "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } }, "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" } }, "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true }, "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "requires": { - "@babel/types": "^7.20.2" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true }, "@babel/helpers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.7.tgz", - "integrity": "sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "dependencies": { "ansi-styles": { @@ -6748,9 +6779,9 @@ } }, "@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -6799,12 +6830,12 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-syntax-logical-assignment-operators": { @@ -6871,51 +6902,51 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", - "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/traverse": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", - "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -6937,6 +6968,42 @@ "buffer": "^6.0.3" } }, + "@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "optional": true + }, + "@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "optional": true + }, + "@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "optional": true + }, + "@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "optional": true + }, + "@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "optional": true + }, + "@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "optional": true + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -7071,124 +7138,124 @@ "dev": true }, "@jest/console": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", - "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "@jest/core": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", - "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" } }, "@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "requires": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.7.0" } }, "@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "requires": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" } }, "@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "requires": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" } }, "@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" } }, "@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" } }, "@jest/reporters": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", - "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -7196,80 +7263,101 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } } }, "@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "requires": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" } }, "@jest/source-map": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", - "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "@jest/test-result": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", - "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", - "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "requires": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" } }, "@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -7277,12 +7365,12 @@ } }, "@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "requires": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -7307,9 +7395,9 @@ "dev": true }, "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, "@jridgewell/sourcemap-codec": { @@ -7319,13 +7407,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@noble/curves": { @@ -7392,27 +7480,27 @@ } }, "@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "requires": { "type-detect": "4.0.8" } }, "@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "requires": { - "@sinonjs/commons": "^2.0.0" + "@sinonjs/commons": "^3.0.0" } }, "@tsconfig/node10": { @@ -7440,9 +7528,9 @@ "dev": true }, "@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "requires": { "@babel/parser": "^7.20.7", @@ -7453,18 +7541,18 @@ } }, "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "requires": { "@babel/types": "^7.0.0" } }, "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -7472,12 +7560,12 @@ } }, "@types/babel__traverse": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", - "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "requires": { - "@babel/types": "^7.3.0" + "@babel/types": "^7.20.7" } }, "@types/graceful-fs": { @@ -7551,12 +7639,6 @@ "form-data": "^3.0.0" } }, - "@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -7897,15 +7979,15 @@ "dev": true }, "babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "requires": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -7925,9 +8007,9 @@ } }, "babel-plugin-jest-hoist": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", - "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "requires": { "@babel/template": "^7.3.3", @@ -7957,12 +8039,12 @@ } }, "babel-preset-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", - "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -7997,15 +8079,15 @@ } }, "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" } }, "bs-logger": { @@ -8099,11 +8181,34 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001441", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", - "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true }, + "cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "optional": true, + "requires": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0", + "node-gyp-build-optional-packages": "5.1.1" + } + }, + "cbor-x": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", + "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", + "requires": { + "cbor-extract": "^2.2.0" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8127,9 +8232,9 @@ "dev": true }, "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "cliui": { @@ -8150,9 +8255,9 @@ "dev": true }, "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, "color-convert": { @@ -8191,6 +8296,21 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -8218,10 +8338,11 @@ } }, "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "requires": {} }, "deep-is": { "version": "0.1.4", @@ -8251,6 +8372,12 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8264,9 +8391,9 @@ "dev": true }, "diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true }, "dir-glob": { @@ -8288,9 +8415,9 @@ } }, "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "version": "1.4.814", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.814.tgz", + "integrity": "sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==", "dev": true }, "emittery": { @@ -8368,9 +8495,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true }, "escape-string-regexp": { @@ -8807,16 +8934,16 @@ "dev": true }, "expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "requires": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" } }, "fast-deep-equal": { @@ -9390,13 +9517,13 @@ } }, "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "requires": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, @@ -9412,9 +9539,9 @@ } }, "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -9422,227 +9549,227 @@ } }, "jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", - "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "requires": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.7.0" } }, "jest-changed-files": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", - "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "requires": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "jest-circus": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", - "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-cli": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", - "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "requires": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" } }, "jest-config": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", - "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" } }, "jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, "jest-docblock": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", - "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", - "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" } }, "jest-environment-node": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", - "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" } }, "jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true }, "jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "jest-leak-detector": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", - "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "requires": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, "jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" } }, "jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.5.0" + "jest-util": "^29.7.0" } }, "jest-pnp-resolver": { @@ -9653,161 +9780,140 @@ "requires": {} }, "jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true }, "jest-resolve": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", - "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "requires": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "jest-resolve-dependencies": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", - "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "requires": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" } }, "jest-runner": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", - "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "jest-runtime": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", - "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "requires": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true } } }, "jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -9816,17 +9922,17 @@ } }, "jest-validate": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", - "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.7.0" }, "dependencies": { "camelcase": { @@ -9838,29 +9944,29 @@ } }, "jest-watcher": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", - "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "requires": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -10011,12 +10117,20 @@ "dev": true }, "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "semver": "^6.0.0" + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } } }, "make-error": { @@ -10137,6 +10251,15 @@ "whatwg-url": "^5.0.0" } }, + "node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "optional": true, + "requires": { + "detect-libc": "^2.0.1" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10144,9 +10267,9 @@ "dev": true }, "node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "normalize-path": { @@ -10318,9 +10441,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "picomatch": { @@ -10357,12 +10480,12 @@ "dev": true }, "pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -10398,9 +10521,9 @@ "dev": true }, "pure-rand": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz", - "integrity": "sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true }, "queue-microtask": { @@ -10915,13 +11038,13 @@ } }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" } }, "uri-js": { @@ -10940,22 +11063,14 @@ "dev": true }, "v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "dependencies": { - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - } + "convert-source-map": "^2.0.0" } }, "vscode-oniguruma": { @@ -11063,9 +11178,9 @@ "dev": true }, "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { "cliui": "^8.0.1", diff --git a/package.json b/package.json index f7b58aa05..41cbb2a3d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", "@scure/bip39": "^1.2.2", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "cbor-x": "^1.5.9" } } From 1060428e7cf99948fa7d3713100fcdb51823ad4c Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 28 Jun 2024 11:23:08 +0200 Subject: [PATCH 010/246] added tokenv4 parsing / removed depracated token --- src/utils.ts | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 48a2a00cd..87dcf43b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -81,9 +81,9 @@ function getEncodedToken(token: Token): string { * @param token an encoded cashu token (cashuAey...) * @returns cashu token object */ -function getDecodedToken(token: string): Token { +function getDecodedToken(token: string) { // remove prefixes - const uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:', 'cashuA']; + const uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:', 'cashu']; uriPrefixes.forEach((prefix) => { if (!token.startsWith(prefix)) { return; @@ -97,21 +97,29 @@ function getDecodedToken(token: string): Token { * @param token * @returns */ -function handleTokens(token: string): Token { - const obj = encodeBase64ToJson | Token>(token); - - // check if v3 - if ('token' in obj) { - return obj; - } - - // check if v1 - if (Array.isArray(obj)) { - return { token: [{ proofs: obj, mint: '' }] }; +function handleTokens(token: string): Token | undefined { + const version = token.slice(0, 1); + const encodedToken = token.slice(1); + if (version === 'A') { + return encodeBase64ToJson(encodedToken); + } else if (version === 'B') { + const uInt8Token = encodeBase64toUint8(encodedToken); + const tokenData = decode(uInt8Token) as { + t: { p: { a: number; s: string; c: string }[]; i: string }[]; + m: string; + }; + const tokenEntries = tokenData.t.map( + (tokenEntry): TokenEntry => ({ + mint: tokenData.m, + proofs: tokenEntry.p.map( + (p): Proof => ({ secret: p.s, C: p.c, amount: p.a, id: tokenEntry.i }) + ) + }) + ); + return { token: tokenEntries, memo: '' }; + } else { + throw new Error('Token version is not supported'); } - - // if v2 token return v3 format - return { token: [{ proofs: obj.proofs, mint: obj?.mints[0]?.url ?? '' }] }; } /** * Returns the keyset id of a set of keys From b5ec29b41c1aa5b11a1285da4ecdf9292bb1e10c Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 30 Jun 2024 06:33:31 +0200 Subject: [PATCH 011/246] remove cbor dependency --- package-lock.json | 201 +----------------------------------------- package.json | 3 +- src/cbor.ts | 216 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 7 +- 4 files changed, 222 insertions(+), 205 deletions(-) create mode 100644 src/cbor.ts diff --git a/package-lock.json b/package-lock.json index f4b4f45e7..a3c6104f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,7 @@ "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", "@scure/bip39": "^1.2.2", - "buffer": "^6.0.3", - "cbor-x": "^1.5.9" + "buffer": "^6.0.3" }, "devDependencies": { "@types/jest": "^29.5.1", @@ -633,78 +632,6 @@ "buffer": "^6.0.3" } }, - "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", - "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-darwin-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", - "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", - "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", - "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", - "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2306,35 +2233,6 @@ } ] }, - "node_modules/cbor-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", - "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.1.1" - }, - "bin": { - "download-cbor-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", - "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", - "@cbor-extract/cbor-extract-linux-x64": "2.2.0", - "@cbor-extract/cbor-extract-win32-x64": "2.2.0" - } - }, - "node_modules/cbor-x": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", - "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", - "optionalDependencies": { - "cbor-extract": "^2.2.0" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2559,15 +2457,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5130,20 +5019,6 @@ } } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", - "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6968,42 +6843,6 @@ "buffer": "^6.0.3" } }, - "@cbor-extract/cbor-extract-darwin-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", - "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", - "optional": true - }, - "@cbor-extract/cbor-extract-darwin-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", - "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", - "optional": true - }, - "@cbor-extract/cbor-extract-linux-arm": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", - "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", - "optional": true - }, - "@cbor-extract/cbor-extract-linux-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", - "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", - "optional": true - }, - "@cbor-extract/cbor-extract-linux-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", - "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", - "optional": true - }, - "@cbor-extract/cbor-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", - "optional": true - }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -8186,29 +8025,6 @@ "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true }, - "cbor-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", - "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", - "optional": true, - "requires": { - "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", - "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", - "@cbor-extract/cbor-extract-linux-x64": "2.2.0", - "@cbor-extract/cbor-extract-win32-x64": "2.2.0", - "node-gyp-build-optional-packages": "5.1.1" - } - }, - "cbor-x": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", - "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", - "requires": { - "cbor-extract": "^2.2.0" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8372,12 +8188,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, - "detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true - }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -10251,15 +10061,6 @@ "whatwg-url": "^5.0.0" } }, - "node-gyp-build-optional-packages": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", - "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", - "optional": true, - "requires": { - "detect-libc": "^2.0.1" - } - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 41cbb2a3d..f7b58aa05 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", "@scure/bip39": "^1.2.2", - "buffer": "^6.0.3", - "cbor-x": "^1.5.9" + "buffer": "^6.0.3" } } diff --git a/src/cbor.ts b/src/cbor.ts new file mode 100644 index 000000000..a998caf2f --- /dev/null +++ b/src/cbor.ts @@ -0,0 +1,216 @@ +export function encodeCBOR(value: any) { + const buffer: Array = []; + encodeItem(value, buffer); + return new Uint8Array(buffer); +} + +function encodeItem(value: any, buffer: Array) { + if (value === null) { + buffer.push(0xf6); + } else if (value === undefined) { + buffer.push(0xf7); + } else if (typeof value === 'boolean') { + buffer.push(value ? 0xf5 : 0xf4); + } else if (typeof value === 'number') { + encodeUnsigned(value, buffer); + } else if (typeof value === 'string') { + encodeString(value, buffer); + } else if (Array.isArray(value)) { + encodeArray(value, buffer); + } else if (typeof value === 'object') { + encodeObject(value, buffer); + } else { + throw new Error('Unsupported type'); + } +} + +function encodeUnsigned(value: number, buffer: Array) { + if (value < 24) { + buffer.push(value); + } else if (value < 256) { + buffer.push(0x18, value); + } else if (value < 65536) { + buffer.push(0x19, value >> 8, value & 0xff); + } else if (value < 4294967296) { + buffer.push(0x1a, value >> 24, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff); + } else { + throw new Error('Unsupported integer size'); + } +} + +function encodeString(value: string, buffer: Array) { + const utf8 = new TextEncoder().encode(value); + encodeUnsigned(utf8.length, buffer); + buffer[buffer.length - 1] |= 0x60; + utf8.forEach((b) => buffer.push(b)); +} + +function encodeArray(value: Array, buffer: Array) { + encodeUnsigned(value.length, buffer); + buffer[buffer.length - 1] |= 0x80; + for (const item of value) { + encodeItem(item, buffer); + } +} + +function encodeObject(value: { [key: string]: any }, buffer: Array) { + const keys = Object.keys(value); + encodeUnsigned(keys.length, buffer); + buffer[buffer.length - 1] |= 0xa0; + for (const key of keys) { + encodeString(key, buffer); + encodeItem(value[key], buffer); + } +} +type DecodeResult = { + value: any; + offset: number; +}; + +export function decodeCBOR(data: Uint8Array): any { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const result = decodeItem(view, 0); + return result.value; +} + +function decodeItem(view: DataView, offset: number): DecodeResult { + if (offset >= view.byteLength) { + throw new Error('Unexpected end of data'); + } + const initialByte = view.getUint8(offset++); + const majorType = initialByte >> 5; + const additionalInfo = initialByte & 0x1f; + + switch (majorType) { + case 0: + return decodeUnsigned(view, offset, additionalInfo); + case 1: + return decodeSigned(view, offset, additionalInfo); + case 2: + return decodeByteString(view, offset, additionalInfo); + case 3: + return decodeString(view, offset, additionalInfo); + case 4: + return decodeArray(view, offset, additionalInfo); + case 5: + return decodeMap(view, offset, additionalInfo); + case 7: + return decodeSimpleAndFloat(view, offset, additionalInfo); + default: + throw new Error(`Unsupported major type: ${majorType}`); + } +} + +function decodeLength(view: DataView, offset: number, additionalInfo: number): DecodeResult { + if (additionalInfo < 24) return { value: additionalInfo, offset }; + if (additionalInfo === 24) return { value: view.getUint8(offset++), offset }; + if (additionalInfo === 25) { + const value = view.getUint16(offset, false); + offset += 2; + return { value, offset }; + } + if (additionalInfo === 26) { + const value = view.getUint32(offset, false); + offset += 4; + return { value, offset }; + } + if (additionalInfo === 27) { + const hi = view.getUint32(offset, false); + const lo = view.getUint32(offset + 4, false); + offset += 8; + return { value: hi * 2 ** 32 + lo, offset }; + } + throw new Error(`Unsupported length: ${additionalInfo}`); +} + +function decodeUnsigned(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value, offset: newOffset } = decodeLength(view, offset, additionalInfo); + return { value, offset: newOffset }; +} + +function decodeSigned(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value, offset: newOffset } = decodeLength(view, offset, additionalInfo); + return { value: -1 - value, offset: newOffset }; +} + +function decodeByteString(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value: length, offset: newOffset } = decodeLength(view, offset, additionalInfo); + if (newOffset + length > view.byteLength) { + throw new Error('Byte string length exceeds data length'); + } + const value = new Uint8Array(view.buffer, view.byteOffset + newOffset, length); + return { value, offset: newOffset + length }; +} + +function decodeString(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value: length, offset: newOffset } = decodeLength(view, offset, additionalInfo); + if (newOffset + length > view.byteLength) { + throw new Error('String length exceeds data length'); + } + const bytes = new Uint8Array(view.buffer, view.byteOffset + newOffset, length); + const value = new TextDecoder().decode(bytes); + return { value, offset: newOffset + length }; +} + +function decodeArray(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value: length, offset: newOffset } = decodeLength(view, offset, additionalInfo); + const array = []; + let currentOffset = newOffset; + for (let i = 0; i < length; i++) { + const result = decodeItem(view, currentOffset); + array.push(result.value); + currentOffset = result.offset; + } + return { value: array, offset: currentOffset }; +} + +function decodeMap(view: DataView, offset: number, additionalInfo: number): DecodeResult { + const { value: length, offset: newOffset } = decodeLength(view, offset, additionalInfo); + const map: { [key: string]: any } = {}; + let currentOffset = newOffset; + for (let i = 0; i < length; i++) { + const keyResult = decodeItem(view, currentOffset); + const valueResult = decodeItem(view, keyResult.offset); + map[keyResult.value] = valueResult.value; + currentOffset = valueResult.offset; + } + return { value: map, offset: currentOffset }; +} + +function decodeSimpleAndFloat( + view: DataView, + offset: number, + additionalInfo: number +): DecodeResult { + if (additionalInfo < 24) { + switch (additionalInfo) { + case 20: + return { value: false, offset }; + case 21: + return { value: true, offset }; + case 22: + return { value: null, offset }; + case 23: + return { value: undefined, offset }; + default: + throw new Error(`Unknown simple value: ${additionalInfo}`); + } + } + if (additionalInfo === 24) return { value: view.getUint8(offset++), offset }; + if (additionalInfo === 25) { + const value = view.getUint16(offset, false); + offset += 2; + return { value, offset }; + } + if (additionalInfo === 26) { + const value = view.getFloat32(offset, false); + offset += 4; + return { value, offset }; + } + if (additionalInfo === 27) { + const value = view.getFloat64(offset, false); + offset += 8; + return { value, offset }; + } + throw new Error(`Unknown simple or float value: ${additionalInfo}`); +} diff --git a/src/utils.ts b/src/utils.ts index 87dcf43b3..5ffc70d81 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,9 @@ -import { encodeBase64ToJson, encodeJsonToBase64 } from './base64.js'; -import { AmountPreference, Keys, Proof, Token, TokenV2 } from './model/types/index.js'; +import { encodeBase64ToJson, encodeBase64toUint8, encodeJsonToBase64 } from './base64.js'; +import { AmountPreference, Keys, Proof, Token, TokenEntry, TokenV2 } from './model/types/index.js'; import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; +import { decodeCBOR } from './cbor.js'; function splitAmount(value: number, amountPreference?: Array): Array { const chunks: Array = []; @@ -104,7 +105,7 @@ function handleTokens(token: string): Token | undefined { return encodeBase64ToJson(encodedToken); } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); - const tokenData = decode(uInt8Token) as { + const tokenData = decodeCBOR(uInt8Token) as { t: { p: { a: number; s: string; c: string }[]; i: string }[]; m: string; }; From a34c439b9f9fae6e35f3c38865b919032b25deda Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 1 Jul 2024 10:16:35 +0200 Subject: [PATCH 012/246] added byte id and C --- src/utils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 5ffc70d81..dfcd3479f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -106,14 +106,19 @@ function handleTokens(token: string): Token | undefined { } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); const tokenData = decodeCBOR(uInt8Token) as { - t: { p: { a: number; s: string; c: string }[]; i: string }[]; + t: { p: { a: number; s: string; c: Uint8Array }[]; i: Uint8Array }[]; m: string; }; const tokenEntries = tokenData.t.map( (tokenEntry): TokenEntry => ({ mint: tokenData.m, proofs: tokenEntry.p.map( - (p): Proof => ({ secret: p.s, C: p.c, amount: p.a, id: tokenEntry.i }) + (p): Proof => ({ + secret: p.s, + C: bytesToHex(p.c), + amount: p.a, + id: bytesToHex(tokenEntry.i) + }) ) }) ); From 7f23bf208b9fb5e3664d60040e1f52b8e58c8996 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 1 Jul 2024 15:28:02 +0200 Subject: [PATCH 013/246] added testcases and multi token --- src/utils.ts | 23 ++++++++++----------- test/utils.test.ts | 51 +++++++++++++++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index dfcd3479f..842853028 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -108,21 +108,20 @@ function handleTokens(token: string): Token | undefined { const tokenData = decodeCBOR(uInt8Token) as { t: { p: { a: number; s: string; c: Uint8Array }[]; i: Uint8Array }[]; m: string; + d: string; }; - const tokenEntries = tokenData.t.map( - (tokenEntry): TokenEntry => ({ - mint: tokenData.m, - proofs: tokenEntry.p.map( - (p): Proof => ({ - secret: p.s, - C: bytesToHex(p.c), - amount: p.a, - id: bytesToHex(tokenEntry.i) - }) - ) + const mergedTokenEntry: TokenEntry = { mint: tokenData.m, proofs: [] }; + tokenData.t.forEach((tokenEntry) => + tokenEntry.p.forEach((p) => { + mergedTokenEntry.proofs.push({ + secret: p.s, + C: bytesToHex(p.c), + amount: p.a, + id: bytesToHex(tokenEntry.i) + }); }) ); - return { token: tokenEntries, memo: '' }; + return { token: [mergedTokenEntry], memo: tokenData.d || '' }; } else { throw new Error('Token version is not supported'); } diff --git a/test/utils.test.ts b/test/utils.test.ts index 15fa721aa..8e2117d5e 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -114,28 +114,52 @@ describe('test decode token', () => { expect(result).toStrictEqual(obj); }); test('testing v4 Token', () => { + const v3Token = { + memo: 'Thank you', + token: [ + { + mint: 'http://localhost:3338', + proofs: [ + { + secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', + C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', + id: '00ad268c4d1f5826', + amount: 1 + } + ] + } + ] + }; + + const token = + 'cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ='; + + const result = utils.getDecodedToken(token); + expect(result).toStrictEqual(v3Token); + }); + test('testing v4 Token with multi keyset', () => { const v3Token = { memo: '', token: [ { - mint: 'https://mint.minibits.cash/Bitcoin', + mint: 'http://localhost:3338', proofs: [ { - secret: '7e98535c6f8cd7a5eff150963a2743613a91e9498150fd5af8d2bfcfd5babe68', - C: '03022a28d163cf63792c1533e6660112f2b75db2fe46aa840e7f5d0f979a2c6cfd', - id: '00500550f0494146', - amount: 16 + secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', + C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', + id: '00ffd48b8f5ecf80', + amount: 1 }, { - amount: 4, - secret: '96bd8480717673311bc70e92818b5babcb665edee39b639defad5584d8d18b1f', - C: '030936759e03235867f9cea58f047c043acdd7455f604c92c75839e5e08a91e198', - id: '00500550f0494146' + secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', + C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', + id: '00ad268c4d1f5826', + amount: 2 }, { - secret: 'e145fa7fba21a9cd3c8743c9de5e4de33e0095abc50b262f1b3831b69b8f63df', - id: '00500550f0494146', - C: '03eba391a31e101e1ba1853db1e4bbb6a166d4fbbb1e181e82892c3301e4e02015', + secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', + C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', + id: '00ad268c4d1f5826', amount: 1 } ] @@ -144,10 +168,9 @@ describe('test decode token', () => { }; const token = - 'cashuBuQACYXSBuQACYXCDuQADYWEQYXN4QDdlOTg1MzVjNmY4Y2Q3YTVlZmYxNTA5NjNhMjc0MzYxM2E5MWU5NDk4MTUwZmQ1YWY4ZDJiZmNmZDViYWJlNjhhY3hCMDMwMjJhMjhkMTYzY2Y2Mzc5MmMxNTMzZTY2NjAxMTJmMmI3NWRiMmZlNDZhYTg0MGU3ZjVkMGY5NzlhMmM2Y2ZkuQADYWEEYXN4QDk2YmQ4NDgwNzE3NjczMzExYmM3MGU5MjgxOGI1YmFiY2I2NjVlZGVlMzliNjM5ZGVmYWQ1NTg0ZDhkMThiMWZhY3hCMDMwOTM2NzU5ZTAzMjM1ODY3ZjljZWE1OGYwNDdjMDQzYWNkZDc0NTVmNjA0YzkyYzc1ODM5ZTVlMDhhOTFlMTk4uQADYWEBYXN4QGUxNDVmYTdmYmEyMWE5Y2QzYzg3NDNjOWRlNWU0ZGUzM2UwMDk1YWJjNTBiMjYyZjFiMzgzMWI2OWI4ZjYzZGZhY3hCMDNlYmEzOTFhMzFlMTAxZTFiYTE4NTNkYjFlNGJiYjZhMTY2ZDRmYmJiMWUxODFlODI4OTJjMzMwMWU0ZTAyMDE1YWlwMDA1MDA1NTBmMDQ5NDE0NmFteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2lu'; + 'cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA=='; const result = utils.getDecodedToken(token); - console.log(JSON.stringify(result)); expect(result).toStrictEqual(v3Token); }); test('testing joining urls', () => { From da1c60354e1a9a5230f14c6dfa3d55d2d853bc23 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 1 Jul 2024 18:40:22 +0200 Subject: [PATCH 014/246] specified return type --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 842853028..1ace779b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -98,7 +98,7 @@ function getDecodedToken(token: string) { * @param token * @returns */ -function handleTokens(token: string): Token | undefined { +function handleTokens(token: string): Token { const version = token.slice(0, 1); const encodedToken = token.slice(1); if (version === 'A') { From c68de1af53825da6a2ff5eff028f7d57274161a2 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 4 Jul 2024 10:57:57 +0200 Subject: [PATCH 015/246] cleanup rebase --- test/utils.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index 8e2117d5e..1569cd782 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -173,16 +173,6 @@ describe('test decode token', () => { const result = utils.getDecodedToken(token); expect(result).toStrictEqual(v3Token); }); - test('testing joining urls', () => { - const mint_url = 'http://localhost:3338'; - const info_url = utils.joinUrls(mint_url, 'info'); - - expect(info_url).toBe('http://localhost:3338/info'); - - const mint_url_trailing_slash = 'http://localhost:3338/'; - const mint_info_url = utils.joinUrls(mint_url_trailing_slash, 'info'); - expect(mint_info_url).toBe('http://localhost:3338/info'); - }); }); describe('test keyset derivation', () => { From c91c27e9a54fab41a9dc6f9d734d36a01df577f7 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 18 Jul 2024 16:08:52 +0200 Subject: [PATCH 016/246] added cbor test cases --- test/cbor.test.ts | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 test/cbor.test.ts diff --git a/test/cbor.test.ts b/test/cbor.test.ts new file mode 100644 index 000000000..e9df53b1c --- /dev/null +++ b/test/cbor.test.ts @@ -0,0 +1,294 @@ +import { decodeCBOR } from '../src/cbor'; + +const tests = [ + { + cbor: 'AA==', + hex: '00', + roundtrip: true, + decoded: 0 + }, + { + cbor: 'AQ==', + hex: '01', + roundtrip: true, + decoded: 1 + }, + { + cbor: 'Cg==', + hex: '0a', + roundtrip: true, + decoded: 10 + }, + { + cbor: 'Fw==', + hex: '17', + roundtrip: true, + decoded: 23 + }, + { + cbor: 'GBg=', + hex: '1818', + roundtrip: true, + decoded: 24 + }, + { + cbor: 'GBk=', + hex: '1819', + roundtrip: true, + decoded: 25 + }, + { + cbor: 'GGQ=', + hex: '1864', + roundtrip: true, + decoded: 100 + }, + { + cbor: 'GQPo', + hex: '1903e8', + roundtrip: true, + decoded: 1000 + }, + { + cbor: 'GgAPQkA=', + hex: '1a000f4240', + roundtrip: true, + decoded: 1000000 + }, + { + cbor: 'GwAAAOjUpRAA', + hex: '1b000000e8d4a51000', + roundtrip: true, + decoded: 1000000000000 + }, + { + cbor: 'IA==', + hex: '20', + roundtrip: true, + decoded: -1 + }, + { + cbor: 'KQ==', + hex: '29', + roundtrip: true, + decoded: -10 + }, + { + cbor: 'OGM=', + hex: '3863', + roundtrip: true, + decoded: -100 + }, + { + cbor: 'OQPn', + hex: '3903e7', + roundtrip: true, + decoded: -1000 + }, + { + cbor: '+QAA', + hex: 'f90000', + roundtrip: true, + decoded: 0.0 + }, + { + cbor: '+TwA', + hex: 'f93c00', + roundtrip: true, + decoded: 1.0 + }, + { + cbor: '+z/xmZmZmZma', + hex: 'fb3ff199999999999a', + roundtrip: true, + decoded: 1.1 + }, + { + cbor: '+T4A', + hex: 'f93e00', + roundtrip: true, + decoded: 1.5 + }, + { + cbor: '+Xv/', + hex: 'f97bff', + roundtrip: true, + decoded: 65504.0 + }, + { + cbor: '+kfDUAA=', + hex: 'fa47c35000', + roundtrip: true, + decoded: 100000.0 + }, + { + cbor: '+n9///8=', + hex: 'fa7f7fffff', + roundtrip: true, + decoded: 3.4028234663852886e38 + }, + { + cbor: '+3435DyIAHWc', + hex: 'fb7e37e43c8800759c', + roundtrip: true, + decoded: 1.0e300 + }, + { + cbor: '+QAB', + hex: 'f90001', + roundtrip: true, + decoded: 5.960464477539063e-8 + }, + { + cbor: '+QQA', + hex: 'f90400', + roundtrip: true, + decoded: 6.103515625e-5 + }, + { + cbor: '+cQA', + hex: 'f9c400', + roundtrip: true, + decoded: -4.0 + }, + { + cbor: '+8AQZmZmZmZm', + hex: 'fbc010666666666666', + roundtrip: true, + decoded: -4.1 + }, + { + cbor: '9A==', + hex: 'f4', + roundtrip: true, + decoded: false + }, + { + cbor: '9Q==', + hex: 'f5', + roundtrip: true, + decoded: true + }, + { + cbor: '9g==', + hex: 'f6', + roundtrip: true, + decoded: null + }, + { + cbor: 'YA==', + hex: '60', + roundtrip: true, + decoded: '' + }, + { + cbor: 'YWE=', + hex: '6161', + roundtrip: true, + decoded: 'a' + }, + { + cbor: 'ZElFVEY=', + hex: '6449455446', + roundtrip: true, + decoded: 'IETF' + }, + { + cbor: 'YiJc', + hex: '62225c', + roundtrip: true, + decoded: '"\\' + }, + { + cbor: 'YsO8', + hex: '62c3bc', + roundtrip: true, + decoded: 'ü' + }, + { + cbor: 'Y+awtA==', + hex: '63e6b0b4', + roundtrip: true, + decoded: '水' + }, + { + cbor: 'ZPCQhZE=', + hex: '64f0908591', + roundtrip: true, + decoded: '𐅑' + }, + { + cbor: 'gA==', + hex: '80', + roundtrip: true, + decoded: [] + }, + { + cbor: 'gwECAw==', + hex: '83010203', + roundtrip: true, + decoded: [1, 2, 3] + }, + { + cbor: 'gwGCAgOCBAU=', + hex: '8301820203820405', + roundtrip: true, + decoded: [1, [2, 3], [4, 5]] + }, + { + cbor: 'mBkBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgYGBk=', + hex: '98190102030405060708090a0b0c0d0e0f101112131415161718181819', + roundtrip: true, + decoded: [ + 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 + ] + }, + { + cbor: 'oA==', + hex: 'a0', + roundtrip: true, + decoded: {} + }, + { + cbor: 'omFhAWFiggID', + hex: 'a26161016162820203', + roundtrip: true, + decoded: { + a: 1, + b: [2, 3] + } + }, + { + cbor: 'gmFhoWFiYWM=', + hex: '826161a161626163', + roundtrip: true, + decoded: [ + 'a', + { + b: 'c' + } + ] + }, + { + cbor: 'pWFhYUFhYmFCYWNhQ2FkYURhZWFF', + hex: 'a56161614161626142616361436164614461656145', + roundtrip: true, + decoded: { + a: 'A', + b: 'B', + c: 'C', + d: 'D', + e: 'E' + } + } +]; + +describe('cbor decoder', () => { + test.each(tests)('given $hex as arguments, returns $decoded', ({ hex, decoded }) => { + //@ts-ignore + const res = decodeCBOR(Buffer.from(hex, 'hex')); + console.log(decoded); + console.log(res); + expect(res).toEqual(decoded); + }); +}); From 79cefe540cf70cda1095727f2fe8ae898572a397 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 18 Jul 2024 16:09:05 +0200 Subject: [PATCH 017/246] fixed 16 bit float parsing --- src/cbor.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/cbor.ts b/src/cbor.ts index a998caf2f..4a78889c5 100644 --- a/src/cbor.ts +++ b/src/cbor.ts @@ -177,6 +177,19 @@ function decodeMap(view: DataView, offset: number, additionalInfo: number): Deco return { value: map, offset: currentOffset }; } +function decodeFloat16(uint16: number): number { + const exponent = (uint16 & 0x7c00) >> 10; + const fraction = uint16 & 0x03ff; + const sign = uint16 & 0x8000 ? -1 : 1; + + if (exponent === 0) { + return sign * 2 ** -14 * (fraction / 1024); + } else if (exponent === 0x1f) { + return fraction ? NaN : sign * Infinity; + } + return sign * 2 ** (exponent - 15) * (1 + fraction / 1024); +} + function decodeSimpleAndFloat( view: DataView, offset: number, @@ -198,7 +211,7 @@ function decodeSimpleAndFloat( } if (additionalInfo === 24) return { value: view.getUint8(offset++), offset }; if (additionalInfo === 25) { - const value = view.getUint16(offset, false); + const value = decodeFloat16(view.getUint16(offset, false)); offset += 2; return { value, offset }; } From b2f70a57467c4aec3b7de364406a79e2af408ea7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:49:20 +0200 Subject: [PATCH 018/246] wip --- src/CashuWallet.ts | 144 ++++++++++++++++++++++++++++----------- src/legacy/nut-06.ts | 38 +++++------ src/model/types/index.ts | 4 ++ 3 files changed, 126 insertions(+), 60 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 23a31fae7..8b33c14e0 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -8,6 +8,7 @@ import { type MeltPayload, type MeltQuoteResponse, type MintKeys, + type MintKeyset, type MeltTokensResponse, type MintPayload, type Proof, @@ -49,7 +50,9 @@ import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; * This class should act as the entry point for this library */ class CashuWallet { - private _keys: MintKeys | undefined; + private _keys: Map = new Map(); + private _keyset_id: string | undefined; + private _keysets: Array = []; private _seed: Uint8Array | undefined; private _unit = 'sat'; mint: CashuMint; @@ -65,27 +68,26 @@ class CashuWallet { mint: CashuMint, options?: { unit?: string; - keys?: MintKeys; + keys?: Array; + keysets?: Array; mnemonicOrSeed?: string | Uint8Array; } ) { this.mint = mint; if (options?.unit) this._unit = options?.unit; - if (options?.keys) { - this._keys = options.keys; - this._unit = options.keys.unit; - } + if (options?.keys) options.keys.forEach((key) => this._keys.set(key.id, key)); + if (options?.keysets) this._keysets = options.keysets; + if (!options?.mnemonicOrSeed) { return; - } - if (options?.mnemonicOrSeed instanceof Uint8Array) { + } else if (options?.mnemonicOrSeed instanceof Uint8Array) { this._seed = options.mnemonicOrSeed; - return; - } - if (!validateMnemonic(options.mnemonicOrSeed, wordlist)) { - throw new Error('Tried to instantiate with mnemonic, but mnemonic was invalid'); + } else { + if (!validateMnemonic(options.mnemonicOrSeed, wordlist)) { + throw new Error('Tried to instantiate with mnemonic, but mnemonic was invalid'); + } + this._seed = deriveSeedFromMnemonic(options.mnemonicOrSeed); } - this._seed = deriveSeedFromMnemonic(options.mnemonicOrSeed); } get unit(): string { @@ -93,14 +95,23 @@ class CashuWallet { } get keys(): MintKeys { - if (!this._keys) { + if (!this._keyset_id || !this._keys.get(this._keyset_id)) { throw new Error('Keys are not set'); } - return this._keys; + return this._keys.get(this._keyset_id) as MintKeys; } set keys(keys: MintKeys) { - this._keys = keys; - this._unit = keys.unit; + if (keys.unit !== this._unit) { + throw new Error('Unit of keyset does not match the unit of the wallet'); + } + this._keys.set(keys.id, keys); + this._keyset_id = keys.id; + } + get keysets(): Array { + return this._keysets; + } + set keysets(keysets: Array) { + this._keysets = keysets; } /** @@ -111,6 +122,81 @@ class CashuWallet { return this.mint.getInfo(); } + /** + * Load mint information, keysets and keys. This function can be called if no keysets are passed in the constructor + */ + async loadMint() { + await this.getMintInfo(); + if (!this._keys.size) { + await this.getKeys() + } else { + await this.getKeySets(); + // get all keysets from this._keysets which are not already in this._keys + this._keysets.forEach(async (keyset) => { + if (!this._keys.get(keyset.id)) { + await this.getKeys(keyset.id); + } + }); + } + } + + /** + * Get keysets from the mint with the unit of the wallet + * @returns keysets + */ + async getKeySets(): Promise> { + const allKeysets = await this.mint.getKeySets(); + const unitKeysets = allKeysets.keysets.filter((k) => k.unit === this._unit); + this._keysets = unitKeysets; + return this._keysets; + } + + /** + * Get public keys from the mint. + * If a keysetId is set, it will fetch and return that speficic keyset. + * Otherwise, we select an active keyset with the unit of the wallet. + * + * @param keysetId optional keysetId to get keys for + * @param unit optional unit to get keys for + * @returns keyset + */ + async getKeys(keysetId?: string): Promise { + if (keysetId) { + if (this._keys.get(keysetId)) { + this._keyset_id = keysetId; + return this._keys.get(keysetId) as MintKeys; + } + const allKeysets = await this.mint.getKeys(keysetId) + const keyset = allKeysets.keysets[0]; + if (!keyset) { + throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); + } + this._keys.set(keysetId, keyset); + this._keyset_id = keysetId; + return keyset; + } + + // no keysetId was set, so we get the active keyset with the unit of the wallet with the lowest fees + const allKeysets = await this.mint.getKeySets(); + const keysetToActivate = allKeysets.keysets + .filter((k) => k.unit === this._unit && k.active) + .sort((a, b) => (a.input_fees_ppk ?? 0) - (b.input_fees_ppk ?? 0))[0]; + if (!keysetToActivate) { + throw new Error(`could not initialize keys. No active keyset with unit '${this._unit}' found`); + } + + if (!this._keys.get(keysetToActivate.id)) { + const keysetGet = await this.mint.getKeys(keysetToActivate.id); + const keys = keysetGet.keysets.find((k) => k.id === keysetToActivate.id); + if (!keys) { + throw new Error(`could not initialize keys. No keyset with id '${keysetToActivate.id}' found`); + } + this._keys.set(keys.id, keys); + } + this._keyset_id = keysetToActivate.id; + return this._keys.get(keysetToActivate.id) as MintKeys; + } + /** * Receive an encoded or raw Cashu token (only supports single tokens. It will only process the first token in the token array) * @param {(string|Token)} token - Cashu token @@ -312,30 +398,6 @@ class CashuWallet { }; } - /** - * Initialize the wallet with the mints public keys - */ - private async getKeys(keysetId?: string, unit?: string): Promise { - if (!this._keys || (keysetId !== undefined && this._keys.id !== keysetId)) { - const allKeys = await this.mint.getKeys(keysetId); - let keys; - if (keysetId) { - keys = allKeys.keysets.find((k) => k.id === keysetId); - } else { - keys = allKeys.keysets.find((k) => (unit ? k.unit === unit : k.unit === 'sat')); - } - if (!keys) { - throw new Error( - `could not initialize keys. No keyset with unit '${unit ? unit : 'sat'}' found` - ); - } - if (!this._keys) { - this._keys = keys; - } - } - return this._keys; - } - /** * Requests a mint quote form the mint. Response returns a Lightning payment request for the requested given amount and unit. * @param amount Amount requesting for mint. diff --git a/src/legacy/nut-06.ts b/src/legacy/nut-06.ts index ded610490..eff911bc1 100644 --- a/src/legacy/nut-06.ts +++ b/src/legacy/nut-06.ts @@ -1,23 +1,23 @@ import type { MintContactInfo, GetInfoResponse } from '../model/types/index.js'; export function handleMintInfoContactFieldDeprecated(data: GetInfoResponse) { - // Monkey patch old contact field ["email", "me@mail.com"] Array<[string, string]>; to new contact field [{method: "email", info: "me@mail.com"}] Array - // This is to maintain backwards compatibility with older versions of the mint - if (Array.isArray(data?.contact) && data?.contact.length > 0) { - data.contact = data.contact.map((contact: MintContactInfo) => { - if ( - Array.isArray(contact) && - contact.length === 2 && - typeof contact[0] === 'string' && - typeof contact[1] === 'string' - ) { - return { method: contact[0], info: contact[1] } as MintContactInfo; - } - console.warn( - "Mint returned deprecated 'contact' field. Update NUT-06: https://github.com/cashubtc/nuts/pull/117" - ); - return contact; - }); - } - return data; + // Monkey patch old contact field ["email", "me@mail.com"] Array<[string, string]>; to new contact field [{method: "email", info: "me@mail.com"}] Array + // This is to maintain backwards compatibility with older versions of the mint + if (Array.isArray(data?.contact) && data?.contact.length > 0) { + data.contact = data.contact.map((contact: MintContactInfo) => { + if ( + Array.isArray(contact) && + contact.length === 2 && + typeof contact[0] === 'string' && + typeof contact[1] === 'string' + ) { + console.warn( + `Mint returned deprecated 'contact' field: Update NUT-06: https://github.com/cashubtc/nuts/pull/117` + ); + return { method: contact[0], info: contact[1] } as MintContactInfo; + } + return contact; + }); + } + return data; } diff --git a/src/model/types/index.ts b/src/model/types/index.ts index e4cc4e44a..07fd331f0 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -79,6 +79,10 @@ export type MintKeyset = { * Whether the keyset is active or not. */ active: boolean; + /** + * Input fees for keyset (in ppk) + */ + input_fees_ppk?: number; }; /** From 0197df4b6819cb6d0ed425ab0d289de406b87fbe Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:01:18 +0200 Subject: [PATCH 019/246] tests working again --- src/CashuWallet.ts | 4 ++-- test/wallet.test.ts | 34 +++++++++++++++------------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8b33c14e0..196cd58a9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -53,7 +53,7 @@ class CashuWallet { private _keys: Map = new Map(); private _keyset_id: string | undefined; private _keysets: Array = []; - private _seed: Uint8Array | undefined; + private _seed: Uint8Array | undefined = undefined; private _unit = 'sat'; mint: CashuMint; @@ -230,7 +230,7 @@ class CashuWallet { }); return proofs; } catch (error) { - throw new Error('Error when receiving'); + throw new Error(`Error receiving token: ${error}`); } } diff --git a/test/wallet.test.ts b/test/wallet.test.ts index b8bd97d86..6e8544c00 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -11,7 +11,17 @@ const dummyKeysResp = { { id: '009a1f293253e41e', unit: 'sat', - keys: { 1: '02f970b6ee058705c0dddc4313721cffb7efd3d142d96ea8e01d31c2b2ff09f181' } + keys: { 1: '02f970b6ee058705c0dddc4313721cffb7efd3d142d96ea8e01d31c2b2ff09f181', 2: '03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5' } + } + ] +}; +const dummyKeysetResp = { + keysets: [ + { + id: '009a1f293253e41e', + unit: 'sat', + active: true, + input_fees_ppk: 0, } ] }; @@ -31,6 +41,7 @@ beforeEach(() => { nock.cleanAll(); nock(mintUrl).get('/v1/keys').reply(200, dummyKeysResp); nock(mintUrl).get('/v1/keys/009a1f293253e41e').reply(200, dummyKeysResp); + nock(mintUrl).get('/v1/keysets').reply(200, dummyKeysetResp); }); describe('test info', () => { @@ -180,14 +191,14 @@ describe('receive', () => { nock(mintUrl).post('/v1/swap').reply(400, { detail: msg }); const wallet = new CashuWallet(mint, { unit }); const result = await wallet.receive(tokenInput).catch((e) => e); - expect(result).toEqual(new Error('Error when receiving')); + expect(result).toEqual(new Error('Error receiving token: Error: Error receiving token entry')); }); test('test receive could not verify proofs', async () => { nock(mintUrl).post('/v1/swap').reply(400, { code: 0, error: 'could not verify proofs.' }); const wallet = new CashuWallet(mint, { unit }); const result = await wallet.receive(tokenInput).catch((e) => e); - expect(result).toEqual(new Error('Error when receiving')); + expect(result).toEqual(new Error('Error receiving token: Error: Error receiving token entry')); }); }); @@ -249,21 +260,6 @@ describe('payLnInvoice', () => { expect(result).toEqual({ isPaid: true, preimage: null, change: [] }); }); test('test payLnInvoice change', async () => { - nock.cleanAll(); - nock(mintUrl) - .get('/v1/keys') - .reply(200, { - keysets: [ - { - id: '009a1f293253e41e', - unit: 'sat', - keys: { - 1: '02f970b6ee058705c0dddc4313721cffb7efd3d142d96ea8e01d31c2b2ff09f181', - 2: '03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5' - } - } - ] - }); nock(mintUrl) .get('/v1/melt/quote/bolt11/test') .reply(200, { @@ -352,7 +348,7 @@ describe('send', () => { ]; test('test send base case', async () => { nock(mintUrl) - .post('/split') + .post('/v1/swap') .reply(200, { signatures: [ { From 64236c3e7b62ca35b3cd6f55e476f75aff9243a1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:24:27 +0200 Subject: [PATCH 020/246] wip: working --- src/CashuMint.ts | 8 +- src/CashuWallet.ts | 297 ++++++++++++++++++++++++++------------- src/legacy/nut-06.ts | 4 +- src/model/types/index.ts | 8 +- src/utils.ts | 6 +- test/wallet.test.ts | 4 +- 6 files changed, 215 insertions(+), 112 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 3729ae028..a13ff4eea 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -1,7 +1,7 @@ import type { CheckStatePayload, CheckStateResponse, - GetInfoResponse, + MintInfo, MeltPayload, MintActiveKeys, MintAllKeysets, @@ -55,9 +55,9 @@ class CashuMint { public static async getInfo( mintUrl: string, customRequest?: typeof request - ): Promise { + ): Promise { const requestInstance = customRequest || request; - const response = await requestInstance({ + const response = await requestInstance({ endpoint: joinUrls(mintUrl, '/v1/info') }); const data = handleMintInfoContactFieldDeprecated(response); @@ -66,7 +66,7 @@ class CashuMint { /** * fetches mints info at the /info endpoint */ - async getInfo(): Promise { + async getInfo(): Promise { return CashuMint.getInfo(this._mintUrl, this._customRequest); } diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 196cd58a9..098b264b7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,13 +21,15 @@ import { type TokenEntry, CheckStateEnum, SerializedBlindedSignature, - MeltQuoteState + MeltQuoteState, + MintInfo } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, getDefaultAmountPreference, - splitAmount + splitAmount, + sumProofs } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; @@ -51,16 +53,20 @@ import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; */ class CashuWallet { private _keys: Map = new Map(); - private _keyset_id: string | undefined; + private _keysetId: string | undefined; private _keysets: Array = []; private _seed: Uint8Array | undefined = undefined; private _unit = 'sat'; + private _mintInfo: MintInfo | undefined = undefined; + mint: CashuMint; /** - * @param unit optionally set unit - * @param keys public keys from the mint. If set, it will override the unit with the keysets unit * @param mint Cashu mint instance is used to make api calls + * @param unit optionally set unit (default is 'sat') + * @param keys public keys from the mint (will be fetched from mint if not provided) + * @param keysets keysets from the mint (will be fetched from mint if not provided) + * @param mintInfo mint info from the mint (will be fetched from mint if not provided) * @param mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided */ @@ -68,14 +74,21 @@ class CashuWallet { mint: CashuMint, options?: { unit?: string; - keys?: Array; + keys?: Array | MintKeys; keysets?: Array; + mintInfo?: MintInfo; mnemonicOrSeed?: string | Uint8Array; } ) { + let keys: Array = []; + if (options?.keys && !Array.isArray(options.keys)) { + keys = [options.keys]; + } else if (options?.keys && Array.isArray(options?.keys)) { + keys = options?.keys; + } this.mint = mint; if (options?.unit) this._unit = options?.unit; - if (options?.keys) options.keys.forEach((key) => this._keys.set(key.id, key)); + if (keys) keys.forEach((key) => this._keys.set(key.id, key)); if (options?.keysets) this._keysets = options.keysets; if (!options?.mnemonicOrSeed) { @@ -93,33 +106,35 @@ class CashuWallet { get unit(): string { return this._unit; } - - get keys(): MintKeys { - if (!this._keyset_id || !this._keys.get(this._keyset_id)) { - throw new Error('Keys are not set'); - } - return this._keys.get(this._keyset_id) as MintKeys; + get keys(): Map { + return this._keys } - set keys(keys: MintKeys) { - if (keys.unit !== this._unit) { - throw new Error('Unit of keyset does not match the unit of the wallet'); + get keysetId(): string { + if (!this._keysetId) { + throw new Error('No keysetId set'); } - this._keys.set(keys.id, keys); - this._keyset_id = keys.id; + return this._keysetId; + } + set keysetId(keysetId: string) { + this._keysetId = keysetId; } get keysets(): Array { return this._keysets; } - set keysets(keysets: Array) { - this._keysets = keysets; + get mintInfo(): MintInfo { + if (!this._mintInfo) { + throw new Error('Mint info not loaded'); + } + return this._mintInfo; } /** * Get information about the mint * @returns mint info */ - async getMintInfo() { - return this.mint.getInfo(); + async getMintInfo(): Promise { + this._mintInfo = await this.mint.getInfo(); + return this._mintInfo; } /** @@ -127,17 +142,8 @@ class CashuWallet { */ async loadMint() { await this.getMintInfo(); - if (!this._keys.size) { - await this.getKeys() - } else { - await this.getKeySets(); - // get all keysets from this._keysets which are not already in this._keys - this._keysets.forEach(async (keyset) => { - if (!this._keys.get(keyset.id)) { - await this.getKeys(keyset.id); - } - }); - } + await this.getKeySets(); + await this.getAllKeys(); } /** @@ -151,9 +157,16 @@ class CashuWallet { return this._keysets; } + async getAllKeys(): Promise> { + const keysets = await this.mint.getKeys(); + this._keys = new Map(keysets.keysets.map((k) => [k.id, k])); + return keysets.keysets; + } + /** - * Get public keys from the mint. - * If a keysetId is set, it will fetch and return that speficic keyset. + * Get public keys from the mint. If keys were already fetched, it will return those. + * + * If `keysetId` is set, it will fetch and return that specific keyset. * Otherwise, we select an active keyset with the unit of the wallet. * * @param keysetId optional keysetId to get keys for @@ -163,7 +176,7 @@ class CashuWallet { async getKeys(keysetId?: string): Promise { if (keysetId) { if (this._keys.get(keysetId)) { - this._keyset_id = keysetId; + this.keysetId = keysetId; return this._keys.get(keysetId) as MintKeys; } const allKeysets = await this.mint.getKeys(keysetId) @@ -172,15 +185,15 @@ class CashuWallet { throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); } this._keys.set(keysetId, keyset); - this._keyset_id = keysetId; + this.keysetId = keysetId; return keyset; } - // no keysetId was set, so we get the active keyset with the unit of the wallet with the lowest fees + // no keysetId was set, so we select an active keyset with the unit of the wallet with the lowest fees and use that const allKeysets = await this.mint.getKeySets(); const keysetToActivate = allKeysets.keysets .filter((k) => k.unit === this._unit && k.active) - .sort((a, b) => (a.input_fees_ppk ?? 0) - (b.input_fees_ppk ?? 0))[0]; + .sort((a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0]; if (!keysetToActivate) { throw new Error(`could not initialize keys. No active keyset with unit '${this._unit}' found`); } @@ -193,7 +206,7 @@ class CashuWallet { } this._keys.set(keys.id, keys); } - this._keyset_id = keysetToActivate.id; + this.keysetId = keysetToActivate.id; return this._keys.get(keysetToActivate.id) as MintKeys; } @@ -284,6 +297,111 @@ class CashuWallet { return proofs; } + async send( + amount: number, + proofs: Array, + options?: { + preference?: Array; + counter?: number; + pubkey?: string; + privkey?: string; + keysetId?: string; + offline?: boolean, + } + ): Promise { + if (sumProofs(proofs) < amount) { + throw new Error('Not enough funds available to send'); + } + // try to select the exact amount of proofs to send (without fees) + console.log("calling selectProofsToSend"); + console.log(`proofs: ${sumProofs(proofs)}, amount: ${amount}`); + const { returnChange: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend(proofs, amount); + console.log(`keepProofsOffline: ${sumProofs(keepProofsOffline)}, sendProofOffline: ${sumProofs(sendProofOffline)}, amount: ${amount}`); + const fees = this.getFeesForProofs(sendProofOffline); + console.log(`fees: ${fees}`); + + if ( + sumProofs(sendProofOffline) != amount || // if the exact amount cannot be selected + options?.preference || options?.pubkey || options?.privkey || options?.keysetId // these options require a swap + ) { + console.log(`>>>>>>> must do swap with ${sumProofs(proofs)} sat`); + const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend(proofs, amount, true); + console.log(`keepProofsSelect: ${sumProofs(keepProofsSelect)}, sendProofs: ${sumProofs(sendProofs)}, amount: ${amount}`); + const { returnChange, send } = await this.swap(amount, sendProofs, options); + console.log(`returnChange: ${sumProofs(returnChange)}, send: ${sumProofs(send)}`); + const returnChangeProofs = keepProofsSelect.concat(returnChange); + console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); + return { returnChange: returnChangeProofs, send }; + } + + return { returnChange: keepProofsOffline, send: sendProofOffline }; + + } + + + private selectProofsToSend( + proofs: Array, + amountToSend: number, + includeFees = false + ): SendResponse { + const sortedProofs = proofs.sort((a, b) => a.amount - b.amount); + const smallerProofs = sortedProofs.filter((p) => p.amount <= amountToSend).sort((a, b) => b.amount - a.amount); + const biggerProofs = sortedProofs.filter((p) => p.amount > amountToSend).sort((a, b) => a.amount - b.amount); + const nextBigger = biggerProofs[0]; + + console.log(`> enter select with proofs: ${sumProofs(proofs)}, amountToSend: ${amountToSend}, smallerProofs: ${sumProofs(smallerProofs)}, biggerProofs: ${sumProofs(biggerProofs)}, nextBigger: ${nextBigger?.amount}`); + + if (!smallerProofs.length && nextBigger) { + console.log(`! no smallerProofs and nextBigger: ${nextBigger.amount}`); + console.log(`< 0 select ${nextBigger.amount}, return: ${sumProofs(proofs.filter((p) => p.id !== nextBigger.id))}`); + return { + returnChange: proofs.filter((p) => p.id !== nextBigger.id), + send: [nextBigger] + }; + } + + if (!smallerProofs.length && !nextBigger) { + console.log(`! no smallerProofs and no nextBigger, change: ${sumProofs(proofs)}`); + console.log(`< 1 select: 0, return: ${sumProofs(proofs)}`); + return { + returnChange: proofs, + send: [] + }; + } + + let remainder = amountToSend; + let selectedProofs = [smallerProofs[0]]; + console.log(`>> select ${smallerProofs[0].amount} – rest: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)}`); + const returnedProofs = [] + const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; + remainder -= smallerProofs[0].amount - feePPK / 1000; + if (remainder > 0) { + const { returnChange, send } = this.selectProofsToSend(smallerProofs.slice(1), remainder, includeFees); + selectedProofs.push(...send); + returnedProofs.push(...returnChange); + } + console.log(`>> EXIT select ${smallerProofs[0].amount} – rest: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)} - returnedProofs: ${sumProofs(returnedProofs)}`); + + if (sumProofs(selectedProofs) < amountToSend && nextBigger) { + console.log(`! selectedProofs (${sumProofs(selectedProofs)}) < amountToSend (${amountToSend}) and nextBigger: ${nextBigger.amount}`); + console.log(`< 3 select ${nextBigger.amount}, return: ${sumProofs(proofs.filter((p) => p.id !== nextBigger.id))} – total: ${sumProofs(proofs)}`); + selectedProofs = [nextBigger] + } + console.log(`< 4 select: ${sumProofs(selectedProofs)}, return: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)}`); + return { + returnChange: proofs.filter((p) => !selectedProofs.includes(p)), + send: selectedProofs + }; + } + + getFeesForProofs(proofs: Array): number { + const fees = Math.floor(Math.max( + (proofs.reduce((total, curr) => total + (this._keysets.find((k) => k.id === curr.id)?.input_fee_ppk || 0), 0) + 999) / 1000, + 0 + )) + return fees; + } + /** * Splits and creates sendable tokens * if no amount is specified, the amount is implied by the cumulative amount of all proofs @@ -296,7 +414,7 @@ class CashuWallet { * @param privkey? will create a signature on the @param proofs secrets if set * @returns promise of the change- and send-proofs */ - async send( + async swap( amount: number, proofs: Array, options?: { @@ -311,57 +429,46 @@ class CashuWallet { amount = options?.preference?.reduce((acc, curr) => acc + curr.amount * curr.count, 0); } const keyset = await this.getKeys(options?.keysetId); - let amountAvailable = 0; - const proofsToSend: Array = []; - const proofsToKeep: Array = []; - proofs.forEach((proof) => { - if (amountAvailable >= amount) { - proofsToKeep.push(proof); + const proofsToSend = proofs; + const amountAvailable = sumProofs(proofs); + if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { + throw new Error('Not enough funds available'); + } + const amountToSend = amount + this.getFeesForProofs(proofsToSend) + const amountToKeep = sumProofs(proofsToSend) - amountToSend + console.log(`amountToKeep: ${amountToKeep}, amountToSend: ${amountToSend}`); + const { payload, blindedMessages } = this.createSwapPayload( + amountToSend, + proofsToSend, + keyset, + options?.preference, + options?.counter, + options?.pubkey, + options?.privkey + ); + const { signatures } = await this.mint.split(payload); + const swapProofs = this.constructProofs( + signatures, + blindedMessages.rs, + blindedMessages.secrets, + keyset + ); + + const splitProofsToKeep: Array = []; + const splitProofsToSend: Array = []; + let amountToKeepCounter = 0; + swapProofs.forEach((proof) => { + if (amountToKeepCounter < amountToKeep) { + amountToKeepCounter += proof.amount; + splitProofsToKeep.push(proof); return; } - amountAvailable = amountAvailable + proof.amount; - proofsToSend.push(proof); + splitProofsToSend.push(proof); }); - - if (amount > amountAvailable) { - throw new Error('Not enough funds available'); - } - if (amount < amountAvailable || options?.preference || options?.pubkey) { - const { amountKeep, amountSend } = this.splitReceive(amount, amountAvailable); - const { payload, blindedMessages } = this.createSwapPayload( - amountSend, - proofsToSend, - keyset, - options?.preference, - options?.counter, - options?.pubkey, - options?.privkey - ); - const { signatures } = await this.mint.split(payload); - const proofs = this.constructProofs( - signatures, - blindedMessages.rs, - blindedMessages.secrets, - keyset - ); - // sum up proofs until amount2 is reached - const splitProofsToKeep: Array = []; - const splitProofsToSend: Array = []; - let amountKeepCounter = 0; - proofs.forEach((proof) => { - if (amountKeepCounter < amountKeep) { - amountKeepCounter += proof.amount; - splitProofsToKeep.push(proof); - return; - } - splitProofsToSend.push(proof); - }); - return { - returnChange: [...splitProofsToKeep, ...proofsToKeep], - send: splitProofsToSend - }; - } - return { returnChange: proofsToKeep, send: proofsToSend }; + return { + returnChange: splitProofsToKeep, + send: splitProofsToSend + }; } /** @@ -496,7 +603,6 @@ class CashuWallet { } ): Promise { const keys = await this.getKeys(options?.keysetId); - const { blindedMessages, secrets, rs } = this.createBlankOutputs( meltQuote.fee_reserve, keys.id, @@ -597,7 +703,7 @@ class CashuWallet { } { const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); const keepBlindedMessages = this.createRandomBlindedMessages( - totalAmount - amount, + totalAmount - amount - this.getFeesForProofs(proofsToSend), keyset.id, undefined, counter @@ -662,14 +768,6 @@ class CashuWallet { return state && state.state === CheckStateEnum.SPENT; }); } - private splitReceive( - amount: number, - amountAvailable: number - ): { amountKeep: number; amountSend: number } { - const amountKeep: number = amountAvailable - amount; - const amountSend: number = amount; - return { amountKeep, amountSend }; - } /** * Creates blinded messages for a given amount @@ -785,6 +883,7 @@ class CashuWallet { }) .map((p) => serializeProof(p) as Proof); } + } export { CashuWallet }; diff --git a/src/legacy/nut-06.ts b/src/legacy/nut-06.ts index eff911bc1..f412032ce 100644 --- a/src/legacy/nut-06.ts +++ b/src/legacy/nut-06.ts @@ -1,6 +1,6 @@ -import type { MintContactInfo, GetInfoResponse } from '../model/types/index.js'; +import type { MintContactInfo, MintInfo } from '../model/types/index.js'; -export function handleMintInfoContactFieldDeprecated(data: GetInfoResponse) { +export function handleMintInfoContactFieldDeprecated(data: MintInfo) { // Monkey patch old contact field ["email", "me@mail.com"] Array<[string, string]>; to new contact field [{method: "email", info: "me@mail.com"}] Array // This is to maintain backwards compatibility with older versions of the mint if (Array.isArray(data?.contact) && data?.contact.length > 0) { diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 07fd331f0..610bda7ff 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -80,9 +80,9 @@ export type MintKeyset = { */ active: boolean; /** - * Input fees for keyset (in ppk) + * Input fee for keyset (in ppk) */ - input_fees_ppk?: number; + input_fee_ppk?: number; }; /** @@ -488,7 +488,7 @@ export type MintContactInfo = { /** * Response from mint at /info endpoint */ -export type GetInfoResponse = { +export type MintInfo = { name: string; pubkey: string; version: string; @@ -530,7 +530,7 @@ export type GetInfoResponse = { }; /** - * Ecash to other MoE swap method, displayed in @type {GetInfoResponse} + * Ecash to other MoE swap method, displayed in @type {MintInfo} */ export type SwapMethod = { method: string; diff --git a/src/utils.ts b/src/utils.ts index 48a2a00cd..f6b3b186e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -162,6 +162,10 @@ export function sanitizeUrl(url: string): string { return url.replace(/\/$/, ''); } +export function sumProofs(proofs: Array) { + return proofs.reduce((acc, proof) => acc + proof.amount, 0); +} + export { bigIntStringify, bytesToNumber, @@ -169,5 +173,5 @@ export { getEncodedToken, hexToNumber, splitAmount, - getDefaultAmountPreference + getDefaultAmountPreference, }; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 6e8544c00..cff855f1b 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -21,7 +21,7 @@ const dummyKeysetResp = { id: '009a1f293253e41e', unit: 'sat', active: true, - input_fees_ppk: 0, + input_fee_ppk: 0, } ] }; @@ -570,7 +570,7 @@ describe('send', () => { const result = await wallet.send(2, proofs).catch((e) => e); - expect(result).toEqual(new Error('Not enough funds available')); + expect(result).toEqual(new Error('Not enough funds available to send')); }); test('test send bad response', async () => { nock(mintUrl).post('/v1/swap').reply(200, {}); From 003c5fddb4a4f716766346264cf71aa4247301ae Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:26:45 +0200 Subject: [PATCH 021/246] cleanup --- src/CashuWallet.ts | 43 ++++++------------------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 098b264b7..4805c2cdc 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -312,25 +312,14 @@ class CashuWallet { if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } - // try to select the exact amount of proofs to send (without fees) - console.log("calling selectProofsToSend"); - console.log(`proofs: ${sumProofs(proofs)}, amount: ${amount}`); const { returnChange: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend(proofs, amount); - console.log(`keepProofsOffline: ${sumProofs(keepProofsOffline)}, sendProofOffline: ${sumProofs(sendProofOffline)}, amount: ${amount}`); - const fees = this.getFeesForProofs(sendProofOffline); - console.log(`fees: ${fees}`); - if ( sumProofs(sendProofOffline) != amount || // if the exact amount cannot be selected options?.preference || options?.pubkey || options?.privkey || options?.keysetId // these options require a swap ) { - console.log(`>>>>>>> must do swap with ${sumProofs(proofs)} sat`); const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend(proofs, amount, true); - console.log(`keepProofsSelect: ${sumProofs(keepProofsSelect)}, sendProofs: ${sumProofs(sendProofs)}, amount: ${amount}`); const { returnChange, send } = await this.swap(amount, sendProofs, options); - console.log(`returnChange: ${sumProofs(returnChange)}, send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); - console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); return { returnChange: returnChangeProofs, send }; } @@ -349,29 +338,14 @@ class CashuWallet { const biggerProofs = sortedProofs.filter((p) => p.amount > amountToSend).sort((a, b) => a.amount - b.amount); const nextBigger = biggerProofs[0]; - console.log(`> enter select with proofs: ${sumProofs(proofs)}, amountToSend: ${amountToSend}, smallerProofs: ${sumProofs(smallerProofs)}, biggerProofs: ${sumProofs(biggerProofs)}, nextBigger: ${nextBigger?.amount}`); - - if (!smallerProofs.length && nextBigger) { - console.log(`! no smallerProofs and nextBigger: ${nextBigger.amount}`); - console.log(`< 0 select ${nextBigger.amount}, return: ${sumProofs(proofs.filter((p) => p.id !== nextBigger.id))}`); - return { - returnChange: proofs.filter((p) => p.id !== nextBigger.id), - send: [nextBigger] - }; - } + if (!smallerProofs.length && nextBigger) + return { returnChange: proofs.filter((p) => p.id !== nextBigger.id), send: [nextBigger] }; - if (!smallerProofs.length && !nextBigger) { - console.log(`! no smallerProofs and no nextBigger, change: ${sumProofs(proofs)}`); - console.log(`< 1 select: 0, return: ${sumProofs(proofs)}`); - return { - returnChange: proofs, - send: [] - }; - } + if (!smallerProofs.length && !nextBigger) + return { returnChange: proofs, send: [] }; let remainder = amountToSend; let selectedProofs = [smallerProofs[0]]; - console.log(`>> select ${smallerProofs[0].amount} – rest: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)}`); const returnedProofs = [] const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= smallerProofs[0].amount - feePPK / 1000; @@ -380,14 +354,10 @@ class CashuWallet { selectedProofs.push(...send); returnedProofs.push(...returnChange); } - console.log(`>> EXIT select ${smallerProofs[0].amount} – rest: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)} - returnedProofs: ${sumProofs(returnedProofs)}`); - if (sumProofs(selectedProofs) < amountToSend && nextBigger) { - console.log(`! selectedProofs (${sumProofs(selectedProofs)}) < amountToSend (${amountToSend}) and nextBigger: ${nextBigger.amount}`); - console.log(`< 3 select ${nextBigger.amount}, return: ${sumProofs(proofs.filter((p) => p.id !== nextBigger.id))} – total: ${sumProofs(proofs)}`); + if (sumProofs(selectedProofs) < amountToSend && nextBigger) selectedProofs = [nextBigger] - } - console.log(`< 4 select: ${sumProofs(selectedProofs)}, return: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))} – total: ${sumProofs(proofs)}`); + return { returnChange: proofs.filter((p) => !selectedProofs.includes(p)), send: selectedProofs @@ -436,7 +406,6 @@ class CashuWallet { } const amountToSend = amount + this.getFeesForProofs(proofsToSend) const amountToKeep = sumProofs(proofsToSend) - amountToSend - console.log(`amountToKeep: ${amountToKeep}, amountToSend: ${amountToSend}`); const { payload, blindedMessages } = this.createSwapPayload( amountToSend, proofsToSend, From 59417ad9ce85694b0c2b3732041be46e6f1df4b5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:07:24 +0200 Subject: [PATCH 022/246] refactor --- src/CashuWallet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 4805c2cdc..100847c92 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -80,17 +80,17 @@ class CashuWallet { mnemonicOrSeed?: string | Uint8Array; } ) { + + this.mint = mint; let keys: Array = []; if (options?.keys && !Array.isArray(options.keys)) { keys = [options.keys]; } else if (options?.keys && Array.isArray(options?.keys)) { keys = options?.keys; } - this.mint = mint; - if (options?.unit) this._unit = options?.unit; if (keys) keys.forEach((key) => this._keys.set(key.id, key)); + if (options?.unit) this._unit = options?.unit; if (options?.keysets) this._keysets = options.keysets; - if (!options?.mnemonicOrSeed) { return; } else if (options?.mnemonicOrSeed instanceof Uint8Array) { From 65f5c8c73db21ae723dc87ee1bdcf36e73c515dd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:38:44 +0200 Subject: [PATCH 023/246] wip --- src/CashuWallet.ts | 57 +++++++++++++++++++++------------------- src/model/types/index.ts | 6 ++--- src/utils.ts | 35 ++++++++++-------------- test/utils.test.ts | 15 +++++------ test/wallet.test.ts | 8 +++--- 5 files changed, 58 insertions(+), 63 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 7eb18435f..bd0a069c9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,12 +21,11 @@ import { CheckStateEnum, SerializedBlindedSignature, MeltQuoteState, - Preferences + OutputAmounts } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, - getDefaultAmountPreference, splitAmount } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; @@ -115,7 +114,8 @@ class CashuWallet { /** * Receive an encoded or raw Cashu token (only supports single tokens. It will only process the first token in the token array) * @param {(string|Token)} token - Cashu token - * @param preference optional preference for splitting proofs into specific amounts + * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. + * @param outputAmounts? optionally specify the output's amounts to keep and to send. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param privkey? will create a signature on the @param token secrets if set @@ -126,6 +126,7 @@ class CashuWallet { options?: { keysetId?: string; preference?: Array; + outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; privkey?: string; @@ -138,7 +139,7 @@ class CashuWallet { const tokenEntries: Array = token.token; const proofs = await this.receiveTokenEntry(tokenEntries[0], { keysetId: options?.keysetId, - preference: options?.preference, + outputAmounts: options?.outputAmounts, counter: options?.counter, pubkey: options?.pubkey, privkey: options?.privkey @@ -163,6 +164,7 @@ class CashuWallet { options?: { keysetId?: string; preference?: Array; + outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; privkey?: string; @@ -171,17 +173,12 @@ class CashuWallet { const proofs: Array = []; try { const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); - let preference = options?.preference; const keys = await this.getKeys(options?.keysetId); - if (!preference) { - preference = getDefaultAmountPreference(amount, keys); - } - let pref: Preferences = { sendPreference: preference }; const { payload, blindedMessages } = this.createSwapPayload( amount, tokenEntry.proofs, keys, - pref, + options?.outputAmounts, options?.counter, options?.pubkey, options?.privkey @@ -206,7 +203,8 @@ class CashuWallet { * if both amount and preference are set, but the preference cannot fulfill the amount, then we use the default split * @param amount amount to send while performing the optimal split (least proofs possible). can be set to undefined if preference is set * @param proofs proofs matching that amount - * @param preference optional preference for splitting proofs into specific amounts. overrides amount param + * @param preference Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. + * @param outputAmounts? optionally specify the output's amounts to keep and to send. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param privkey? will create a signature on the @param proofs secrets if set @@ -216,19 +214,14 @@ class CashuWallet { amount: number, proofs: Array, options?: { - preference?: Preferences; + preference?: Array; + outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; privkey?: string; keysetId?: string; } ): Promise { - if (options?.preference) { - amount = options?.preference?.sendPreference.reduce( - (acc, curr) => acc + curr.amount * curr.count, - 0 - ); - } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; const proofsToSend: Array = []; @@ -245,13 +238,13 @@ class CashuWallet { if (amount > amountAvailable) { throw new Error('Not enough funds available'); } - if (amount < amountAvailable || options?.preference || options?.pubkey) { + if (amount < amountAvailable || options?.outputAmounts || options?.pubkey) { const { amountKeep, amountSend } = this.splitReceive(amount, amountAvailable); const { payload, blindedMessages } = this.createSwapPayload( amountSend, proofsToSend, keyset, - options?.preference, + options?.outputAmounts, options?.counter, options?.pubkey, options?.privkey @@ -367,6 +360,12 @@ class CashuWallet { * Mint tokens for a given mint quote * @param amount amount to request * @param quote ID of mint quote + * @param options.keysetId? optionally set keysetId for blank outputs for returned change. + * @deprecated + * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. + * @param outputAmounts? optionally specify the output's amounts to keep and to send. + * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @returns proofs */ async mintTokens( @@ -375,6 +374,7 @@ class CashuWallet { options?: { keysetId?: string; preference?: Array; + outputAmounts?: OutputAmounts, counter?: number; pubkey?: string; } @@ -383,7 +383,7 @@ class CashuWallet { const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, keyset, - options?.preference, + options?.outputAmounts?.keepAmounts, options?.counter, options?.pubkey ); @@ -520,7 +520,7 @@ class CashuWallet { * Creates a split payload * @param amount amount to send * @param proofsToSend proofs to split* - * @param preference optional preference for splitting proofs into specific amounts. overrides amount param + * @param outputAmounts? optionally specify the output's amounts to keep and to send. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param privkey? will create a signature on the @param proofsToSend secrets if set @@ -530,7 +530,7 @@ class CashuWallet { amount: number, proofsToSend: Array, keyset: MintKeys, - preference?: Preferences, + outputAmounts?: OutputAmounts, counter?: number, pubkey?: string, privkey?: string @@ -539,10 +539,13 @@ class CashuWallet { blindedMessages: BlindedTransaction; } { const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); + if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts?.length) { + outputAmounts.keepAmounts = splitAmount(totalAmount - amount, keyset.keys); + } const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, keyset, - preference?.keepPreference, + outputAmounts?.keepAmounts, counter ); if (this._seed && counter) { @@ -551,7 +554,7 @@ class CashuWallet { const sendBlindedMessages = this.createRandomBlindedMessages( amount, keyset, - preference?.sendPreference, + outputAmounts?.sendAmounts, counter, pubkey ); @@ -626,11 +629,11 @@ class CashuWallet { private createRandomBlindedMessages( amount: number, keyset: MintKeys, - amountPreference?: Array, + split?: Array, counter?: number, pubkey?: string ): BlindedMessageData & { amounts: Array } { - const amounts = splitAmount(amount, keyset.keys, amountPreference); + const amounts = splitAmount(amount, keyset.keys, split); return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 39bd42e96..7ebb6f8ba 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -556,9 +556,9 @@ export type AmountPreference = { count: number; }; -export type Preferences = { - sendPreference: Array; - keepPreference?: Array; +export type OutputAmounts = { + sendAmounts: Array; + keepAmounts?: Array; }; export type InvoiceData = { diff --git a/src/utils.ts b/src/utils.ts index 2ea94e27d..f5b664790 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,12 +8,12 @@ import { decodeCBOR } from './cbor.js'; function splitAmount( value: number, keyset: Keys, - amountPreference?: Array, + split?: Array, order?: string ): Array { const chunks: Array = []; - if (amountPreference) { - chunks.push(...getPreference(value, keyset, amountPreference)); + if (split) { + chunks.push(...getPreference(value, keyset, split)); value = value - chunks.reduce((curr, acc) => { @@ -24,7 +24,7 @@ function splitAmount( .map((k) => parseInt(k)) .sort((a, b) => b - a); sortedKeyAmounts.forEach((amt) => { - let q = Math.floor(value / amt); + const q = Math.floor(value / amt); for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); @@ -42,32 +42,26 @@ function hasCorrespondingKey(amount: number, keyset: Keys) { function getPreference( amount: number, keyset: Keys, - preferredAmounts: Array + split: Array ): Array { const chunks: Array = []; let accumulator = 0; - preferredAmounts.forEach((pa) => { - if (!hasCorrespondingKey(pa.amount, keyset)) { + split.forEach((splitAmount) => { + if (!hasCorrespondingKey(splitAmount, keyset)) { throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } - for (let i = 1; i <= pa.count; i++) { - accumulator += pa.amount; + const count = split.filter(value => value === splitAmount).length; + for (let i = 1; i <= count; i++) { + accumulator += splitAmount; if (accumulator > amount) { return; } - chunks.push(pa.amount); + chunks.push(splitAmount); } }); return chunks; } -function getDefaultAmountPreference(amount: number, keyset: Keys): Array { - const amounts = splitAmount(amount, keyset); - return amounts.map((a) => { - return { amount: a, count: 1 }; - }); -} - function bytesToNumber(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(bytes)); } @@ -119,7 +113,7 @@ function handleTokens(token: string): Token { } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); const tokenData = decodeCBOR(uInt8Token) as { - t: { p: { a: number; s: string; c: Uint8Array }[]; i: Uint8Array }[]; + t: Array<{ p: Array<{ a: number; s: string; c: Uint8Array }>; i: Uint8Array }>; m: string; d: string; }; @@ -135,9 +129,9 @@ function handleTokens(token: string): Token { }) ); return { token: [mergedTokenEntry], memo: tokenData.d || '' }; - } else { - throw new Error('Token version is not supported'); } + throw new Error('Token version is not supported'); + } /** * Returns the keyset id of a set of keys @@ -195,5 +189,4 @@ export { getEncodedToken, hexToNumber, splitAmount, - getDefaultAmountPreference }; diff --git a/test/utils.test.ts b/test/utils.test.ts index cdb75d1b0..238124c26 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -29,29 +29,26 @@ describe('test split amounts ', () => { }); describe('test split custom amounts ', () => { - const fiveToOne: AmountPreference = { amount: 1, count: 5 }; + const fiveToOne = [1, 1, 1, 1, 1]; test('testing amount 5', async () => { - const chunks = utils.splitAmount(5, keys, [fiveToOne]); + const chunks = utils.splitAmount(5, keys, fiveToOne); expect(chunks).toStrictEqual([1, 1, 1, 1, 1]); }); - const tenToOneAndTwo: Array = [ - { amount: 1, count: 2 }, - { amount: 2, count: 4 } - ]; + const tenToOneAndTwo = [1, 1, 2, 2, 2, 2]; test('testing amount 10', async () => { const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve: Array = [{ amount: 512, count: 2 }]; + const fiveTwelve = [512, 512]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve, 'desc'); expect(chunks).toStrictEqual([512, 4, 2]); }); - const illegal: Array = [{ amount: 3, count: 2 }]; + const illegal = [3, 3]; test('testing non pow2', async () => { expect(() => utils.splitAmount(6, keys, illegal)).toThrowError(); }); - const empty: Array = []; + const empty: Array = []; test('testing empty', async () => { const chunks = utils.splitAmount(5, keys, empty, 'desc'); expect(chunks).toStrictEqual([4, 1]); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 93ca7c777..b32b48296 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -492,7 +492,8 @@ describe('send', () => { } ]; const result = await wallet.send(4, overpayProofs, { - preference: { sendPreference: [{ amount: 1, count: 4 }] } + // preference: { sendPreference: [{ amount: 1, count: 4 }] } + outputAmounts: { "sendAmounts": [1, 1, 1, 1], "keepAmounts": [] } }); expect(result.send).toHaveLength(4); @@ -548,8 +549,9 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { - preference: { sendPreference: [{ amount: 1, count: 3 }] } + const result = await wallet.send(3, overpayProofs, { + // preference: { sendPreference: [{ amount: 1, count: 3 }] } + outputAmounts: { "sendAmounts": [1, 1, 1], "keepAmounts": [] } }); expect(result.send).toHaveLength(3); From 07db2ef4570d190af917dbee49c95f927d1da665 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:06:57 +0200 Subject: [PATCH 024/246] owrks --- src/CashuWallet.ts | 8 ++++++-- src/utils.ts | 25 +++++++++++++++---------- test/utils.test.ts | 10 +++++++++- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bd0a069c9..fb9c735f9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -26,7 +26,8 @@ import { import { bytesToNumber, getDecodedToken, - splitAmount + splitAmount, + deprecatedPreferenceToOutputAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; @@ -132,6 +133,7 @@ class CashuWallet { privkey?: string; } ): Promise> { + if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); try { if (typeof token === 'string') { token = getDecodedToken(token); @@ -170,6 +172,7 @@ class CashuWallet { privkey?: string; } ): Promise> { + if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const proofs: Array = []; try { const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); @@ -222,6 +225,7 @@ class CashuWallet { keysetId?: string; } ): Promise { + if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; const proofsToSend: Array = []; @@ -361,7 +365,6 @@ class CashuWallet { * @param amount amount to request * @param quote ID of mint quote * @param options.keysetId? optionally set keysetId for blank outputs for returned change. - * @deprecated * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. * @param outputAmounts? optionally specify the output's amounts to keep and to send. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect @@ -379,6 +382,7 @@ class CashuWallet { pubkey?: string; } ): Promise<{ proofs: Array }> { + if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const keyset = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, diff --git a/src/utils.ts b/src/utils.ts index f5b664790..56362f4fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { encodeBase64ToJson, encodeBase64toUint8, encodeJsonToBase64 } from './base64.js'; -import { AmountPreference, Keys, Proof, Token, TokenEntry, TokenV2 } from './model/types/index.js'; +import { AmountPreference, Keys, OutputAmounts, Proof, Token, TokenEntry, TokenV2 } from './model/types/index.js'; import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; @@ -13,6 +13,9 @@ function splitAmount( ): Array { const chunks: Array = []; if (split) { + if (split.reduce((a, b) => a + b, 0) > value) { + throw new Error('Split amount is greater than the value'); + } chunks.push(...getPreference(value, keyset, split)); value = value - @@ -45,23 +48,24 @@ function getPreference( split: Array ): Array { const chunks: Array = []; - let accumulator = 0; split.forEach((splitAmount) => { if (!hasCorrespondingKey(splitAmount, keyset)) { throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } - const count = split.filter(value => value === splitAmount).length; - for (let i = 1; i <= count; i++) { - accumulator += splitAmount; - if (accumulator > amount) { - return; - } - chunks.push(splitAmount); - } + chunks.push(splitAmount); }); return chunks; } +function deprecatedPreferenceToOutputAmounts(preference?: Array): OutputAmounts { + const sendAmounts: Array = []; + preference?.forEach(({ count, amount }) => { + for (let i = 0; i < count; i++) { + sendAmounts.push(amount); + } + }); + return { sendAmounts }; +} function bytesToNumber(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(bytes)); } @@ -189,4 +193,5 @@ export { getEncodedToken, hexToNumber, splitAmount, + deprecatedPreferenceToOutputAmounts, }; diff --git a/test/utils.test.ts b/test/utils.test.ts index 238124c26..8538064c0 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -39,11 +39,19 @@ describe('test split custom amounts ', () => { const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve = [512, 512]; + test('testing amount 12', async () => { + const chunks = utils.splitAmount(12, keys, tenToOneAndTwo); + expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2, 2]); + }); + const fiveTwelve = [512]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve, 'desc'); expect(chunks).toStrictEqual([512, 4, 2]); }); + const tooMuch = [512, 512]; + test('testing amount 512 but split too much', async () => { + expect(() => utils.splitAmount(512, keys, tooMuch)).toThrowError(); + }); const illegal = [3, 3]; test('testing non pow2', async () => { expect(() => utils.splitAmount(6, keys, illegal)).toThrowError(); From 865d93cea7f8c80c126130770034b14d6fd262f0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:08:12 +0200 Subject: [PATCH 025/246] npm run format --- src/CashuWallet.ts | 16 ++++++++++------ src/utils.ts | 19 +++++++++++-------- test/wallet.test.ts | 4 ++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index fb9c735f9..94b6e974b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -133,7 +133,8 @@ class CashuWallet { privkey?: string; } ): Promise> { - if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + if (options?.preference) + options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); try { if (typeof token === 'string') { token = getDecodedToken(token); @@ -172,7 +173,8 @@ class CashuWallet { privkey?: string; } ): Promise> { - if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + if (options?.preference) + options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const proofs: Array = []; try { const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); @@ -225,7 +227,8 @@ class CashuWallet { keysetId?: string; } ): Promise { - if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + if (options?.preference) + options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; const proofsToSend: Array = []; @@ -377,12 +380,13 @@ class CashuWallet { options?: { keysetId?: string; preference?: Array; - outputAmounts?: OutputAmounts, + outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; } ): Promise<{ proofs: Array }> { - if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + if (options?.preference) + options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const keyset = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, @@ -524,7 +528,7 @@ class CashuWallet { * Creates a split payload * @param amount amount to send * @param proofsToSend proofs to split* - * @param outputAmounts? optionally specify the output's amounts to keep and to send. + * @param outputAmounts? optionally specify the output's amounts to keep and to send. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param privkey? will create a signature on the @param proofsToSend secrets if set diff --git a/src/utils.ts b/src/utils.ts index 56362f4fb..bc6919a34 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,13 @@ import { encodeBase64ToJson, encodeBase64toUint8, encodeJsonToBase64 } from './base64.js'; -import { AmountPreference, Keys, OutputAmounts, Proof, Token, TokenEntry, TokenV2 } from './model/types/index.js'; +import { + AmountPreference, + Keys, + OutputAmounts, + Proof, + Token, + TokenEntry, + TokenV2 +} from './model/types/index.js'; import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; @@ -42,11 +50,7 @@ function hasCorrespondingKey(amount: number, keyset: Keys) { return amount in keyset; } -function getPreference( - amount: number, - keyset: Keys, - split: Array -): Array { +function getPreference(amount: number, keyset: Keys, split: Array): Array { const chunks: Array = []; split.forEach((splitAmount) => { if (!hasCorrespondingKey(splitAmount, keyset)) { @@ -135,7 +139,6 @@ function handleTokens(token: string): Token { return { token: [mergedTokenEntry], memo: tokenData.d || '' }; } throw new Error('Token version is not supported'); - } /** * Returns the keyset id of a set of keys @@ -193,5 +196,5 @@ export { getEncodedToken, hexToNumber, splitAmount, - deprecatedPreferenceToOutputAmounts, + deprecatedPreferenceToOutputAmounts }; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index b32b48296..0e2bcb9e7 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -493,7 +493,7 @@ describe('send', () => { ]; const result = await wallet.send(4, overpayProofs, { // preference: { sendPreference: [{ amount: 1, count: 4 }] } - outputAmounts: { "sendAmounts": [1, 1, 1, 1], "keepAmounts": [] } + outputAmounts: { sendAmounts: [1, 1, 1, 1], keepAmounts: [] } }); expect(result.send).toHaveLength(4); @@ -551,7 +551,7 @@ describe('send', () => { ]; const result = await wallet.send(3, overpayProofs, { // preference: { sendPreference: [{ amount: 1, count: 3 }] } - outputAmounts: { "sendAmounts": [1, 1, 1], "keepAmounts": [] } + outputAmounts: { sendAmounts: [1, 1, 1], keepAmounts: [] } }); expect(result.send).toHaveLength(3); From 69e1fcbf954a520786fedc4b1afcc4ba452f3504 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:16:29 +0200 Subject: [PATCH 026/246] merge with preferences branch --- src/CashuMint.ts | 5 +- src/CashuWallet.ts | 157 ++++++++++++++++++++++++++++++++++--------- src/legacy/nut-06.ts | 38 +++++------ src/utils.ts | 35 ++++++++-- test/wallet.test.ts | 7 +- 5 files changed, 179 insertions(+), 63 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index a13ff4eea..73d20662b 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -52,10 +52,7 @@ class CashuMint { * @param mintUrl * @param customRequest */ - public static async getInfo( - mintUrl: string, - customRequest?: typeof request - ): Promise { + public static async getInfo(mintUrl: string, customRequest?: typeof request): Promise { const requestInstance = customRequest || request; const response = await requestInstance({ endpoint: joinUrls(mintUrl, '/v1/info') diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 247cc6443..295a456ab 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -30,7 +30,8 @@ import { getDecodedToken, splitAmount, sumProofs, - deprecatedPreferenceToOutputAmounts + deprecatedPreferenceToOutputAmounts, + getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; @@ -81,7 +82,6 @@ class CashuWallet { mnemonicOrSeed?: string | Uint8Array; } ) { - this.mint = mint; let keys: Array = []; if (options?.keys && !Array.isArray(options.keys)) { @@ -108,7 +108,7 @@ class CashuWallet { return this._unit; } get keys(): Map { - return this._keys + return this._keys; } get keysetId(): string { if (!this._keysetId) { @@ -166,10 +166,10 @@ class CashuWallet { /** * Get public keys from the mint. If keys were already fetched, it will return those. - * + * * If `keysetId` is set, it will fetch and return that specific keyset. * Otherwise, we select an active keyset with the unit of the wallet. - * + * * @param keysetId optional keysetId to get keys for * @param unit optional unit to get keys for * @returns keyset @@ -180,7 +180,7 @@ class CashuWallet { this.keysetId = keysetId; return this._keys.get(keysetId) as MintKeys; } - const allKeysets = await this.mint.getKeys(keysetId) + const allKeysets = await this.mint.getKeys(keysetId); const keyset = allKeysets.keysets[0]; if (!keyset) { throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); @@ -196,14 +196,18 @@ class CashuWallet { .filter((k) => k.unit === this._unit && k.active) .sort((a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0]; if (!keysetToActivate) { - throw new Error(`could not initialize keys. No active keyset with unit '${this._unit}' found`); + throw new Error( + `could not initialize keys. No active keyset with unit '${this._unit}' found` + ); } if (!this._keys.get(keysetToActivate.id)) { const keysetGet = await this.mint.getKeys(keysetToActivate.id); const keys = keysetGet.keysets.find((k) => k.id === keysetToActivate.id); if (!keys) { - throw new Error(`could not initialize keys. No keyset with id '${keysetToActivate.id}' found`); + throw new Error( + `could not initialize keys. No keyset with id '${keysetToActivate.id}' found` + ); } this._keys.set(keys.id, keys); } @@ -306,62 +310,130 @@ class CashuWallet { proofs: Array, options?: { preference?: Array; + outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; privkey?: string; keysetId?: string; - offline?: boolean, + offline?: boolean; } ): Promise { + if (options?.preference) + options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } - const { returnChange: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend(proofs, amount); + const { returnChange: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( + proofs, + amount + ); if ( sumProofs(sendProofOffline) != amount || // if the exact amount cannot be selected - options?.preference || options?.pubkey || options?.privkey || options?.keysetId // these options require a swap + options?.outputAmounts || + options?.pubkey || + options?.privkey || + options?.keysetId // these options require a swap ) { - const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend(proofs, amount, true); + console.log('>> yes swap'); + const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( + proofs, + amount, + true + ); + console.log( + `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)}` + ); + // if (options && !options?.outputAmounts?.keepAmounts) { + // options.outputAmounts = { + // keepAmounts: getKeepAmounts(keepProofsSelect, sumProofs(keepProofsSelect), this._keys.get(options?.keysetId || this.keysetId) as MintKeys, 3), + // sendAmounts: options?.outputAmounts?.sendAmounts || [] + // } + // } const { returnChange, send } = await this.swap(amount, sendProofs, options); + console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); + console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); return { returnChange: returnChangeProofs, send }; } - + console.log('>> no swap'); + // console.log(`keepProofsOffline: ${sumProofs(keepProofsOffline)} | sendProofOffline: ${sumProofs(sendProofOffline)}`); return { returnChange: keepProofsOffline, send: sendProofOffline }; - } - private selectProofsToSend( proofs: Array, amountToSend: number, includeFees = false ): SendResponse { + // heavy logging in this function const sortedProofs = proofs.sort((a, b) => a.amount - b.amount); - const smallerProofs = sortedProofs.filter((p) => p.amount <= amountToSend).sort((a, b) => b.amount - a.amount); - const biggerProofs = sortedProofs.filter((p) => p.amount > amountToSend).sort((a, b) => a.amount - b.amount); + const smallerProofs = sortedProofs + .filter((p) => p.amount <= amountToSend) + .sort((a, b) => b.amount - a.amount); + const biggerProofs = sortedProofs + .filter((p) => p.amount > amountToSend) + .sort((a, b) => a.amount - b.amount); const nextBigger = biggerProofs[0]; + console.log( + `> enter | amountToSend: ${amountToSend} | proofs: ${sumProofs( + proofs + )} | smallerProofs: ${sumProofs(smallerProofs)} | nextBigger: ${nextBigger?.amount}` + ); + if (!smallerProofs.length && nextBigger) { + console.log( + `< [0] exit: no smallerProofs, nextBigger: ${nextBigger.amount} | keep: ${sumProofs( + proofs.filter((p) => p.secret !== nextBigger.secret) + )} | proofs: ${sumProofs(proofs)}` + ); + return { + returnChange: proofs.filter((p) => p.secret !== nextBigger.secret), + send: [nextBigger] + }; + } - if (!smallerProofs.length && nextBigger) - return { returnChange: proofs.filter((p) => p.id !== nextBigger.id), send: [nextBigger] }; - - if (!smallerProofs.length && !nextBigger) + if (!smallerProofs.length && !nextBigger) { + console.log( + `< [1] exit: no smallerProofs, no nextBigger | amountToSend: ${amountToSend} | keep: ${sumProofs( + proofs + )}` + ); return { returnChange: proofs, send: [] }; + } let remainder = amountToSend; let selectedProofs = [smallerProofs[0]]; - const returnedProofs = [] + console.log(`Selected proof: ${smallerProofs[0].amount}`); + const returnedProofs = []; const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= smallerProofs[0].amount - feePPK / 1000; if (remainder > 0) { - const { returnChange, send } = this.selectProofsToSend(smallerProofs.slice(1), remainder, includeFees); + const { returnChange, send } = this.selectProofsToSend( + smallerProofs.slice(1), + remainder, + includeFees + ); + // if (!send.length) { + // send = [nextBigger]; + // console.log(`< exit [2.1] no send | send: ${sumProofs(send)} | returnChange: ${sumProofs(returnChange)}`); + // } selectedProofs.push(...send); returnedProofs.push(...returnChange); } - if (sumProofs(selectedProofs) < amountToSend && nextBigger) - selectedProofs = [nextBigger] + if (sumProofs(selectedProofs) < amountToSend && nextBigger) { + selectedProofs = [nextBigger]; + console.log( + `< exit [2.2] selecting nextBigger | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( + selectedProofs + )} | returnedProofs: ${sumProofs(returnedProofs)}` + ); + } + console.log( + `< exit [2] selectProofsToSend | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( + selectedProofs + )} | returnedProofs: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))}` + ); return { returnChange: proofs.filter((p) => !selectedProofs.includes(p)), send: selectedProofs @@ -369,10 +441,18 @@ class CashuWallet { } getFeesForProofs(proofs: Array): number { - const fees = Math.floor(Math.max( - (proofs.reduce((total, curr) => total + (this._keysets.find((k) => k.id === curr.id)?.input_fee_ppk || 0), 0) + 999) / 1000, - 0 - )) + const fees = Math.floor( + Math.max( + (proofs.reduce( + (total, curr) => + total + (this._keysets.find((k) => k.id === curr.id)?.input_fee_ppk || 0), + 0 + ) + + 999) / + 1000, + 0 + ) + ); return fees; } @@ -407,10 +487,15 @@ class CashuWallet { const proofsToSend = proofs; const amountAvailable = sumProofs(proofs); if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { + console.log( + `amount: ${amount} | fees: ${this.getFeesForProofs( + proofsToSend + )} | amountAvailable: ${amountAvailable}` + ); throw new Error('Not enough funds available'); } - const amountToSend = amount + this.getFeesForProofs(proofsToSend) - const amountToKeep = sumProofs(proofsToSend) - amountToSend + const amountToSend = amount + this.getFeesForProofs(proofsToSend); + const amountToKeep = sumProofs(proofsToSend) - amountToSend; const { payload, blindedMessages } = this.createSwapPayload( amountToSend, proofsToSend, @@ -525,11 +610,18 @@ class CashuWallet { ): Promise<{ proofs: Array }> { if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + console.log( + `outputAmounts: ${ + options?.outputAmounts?.keepAmounts + } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${ + options?.outputAmounts?.sendAmounts + } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a, b) => a + b, 0)})` + ); const keyset = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, keyset, - options?.outputAmounts?.keepAmounts, + options?.outputAmounts?.sendAmounts, options?.counter, options?.pubkey ); @@ -868,7 +960,6 @@ class CashuWallet { }) .map((p) => serializeProof(p) as Proof); } - } export { CashuWallet }; diff --git a/src/legacy/nut-06.ts b/src/legacy/nut-06.ts index f412032ce..fc3600ae2 100644 --- a/src/legacy/nut-06.ts +++ b/src/legacy/nut-06.ts @@ -1,23 +1,23 @@ import type { MintContactInfo, MintInfo } from '../model/types/index.js'; export function handleMintInfoContactFieldDeprecated(data: MintInfo) { - // Monkey patch old contact field ["email", "me@mail.com"] Array<[string, string]>; to new contact field [{method: "email", info: "me@mail.com"}] Array - // This is to maintain backwards compatibility with older versions of the mint - if (Array.isArray(data?.contact) && data?.contact.length > 0) { - data.contact = data.contact.map((contact: MintContactInfo) => { - if ( - Array.isArray(contact) && - contact.length === 2 && - typeof contact[0] === 'string' && - typeof contact[1] === 'string' - ) { - console.warn( - `Mint returned deprecated 'contact' field: Update NUT-06: https://github.com/cashubtc/nuts/pull/117` - ); - return { method: contact[0], info: contact[1] } as MintContactInfo; - } - return contact; - }); - } - return data; + // Monkey patch old contact field ["email", "me@mail.com"] Array<[string, string]>; to new contact field [{method: "email", info: "me@mail.com"}] Array + // This is to maintain backwards compatibility with older versions of the mint + if (Array.isArray(data?.contact) && data?.contact.length > 0) { + data.contact = data.contact.map((contact: MintContactInfo) => { + if ( + Array.isArray(contact) && + contact.length === 2 && + typeof contact[0] === 'string' && + typeof contact[1] === 'string' + ) { + console.warn( + `Mint returned deprecated 'contact' field: Update NUT-06: https://github.com/cashubtc/nuts/pull/117` + ); + return { method: contact[0], info: contact[1] } as MintContactInfo; + } + return contact; + }); + } + return data; } diff --git a/src/utils.ts b/src/utils.ts index 60feef10b..5e9e662e8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,7 +22,9 @@ function splitAmount( const chunks: Array = []; if (split) { if (split.reduce((a, b) => a + b, 0) > value) { - throw new Error('Split amount is greater than the value'); + throw new Error( + `Split is greater than total amount: ${split.reduce((a, b) => a + b, 0)} > ${value}` + ); } chunks.push(...getPreference(value, keyset, split)); value = @@ -31,9 +33,7 @@ function splitAmount( return curr + acc; }, 0); } - const sortedKeyAmounts: Array = Object.keys(keyset) - .map((k) => parseInt(k)) - .sort((a, b) => b - a); + const sortedKeyAmounts = getKeysetAmounts(keyset); sortedKeyAmounts.forEach((amt) => { const q = Math.floor(value / amt); for (let i = 0; i < q; ++i) chunks.push(amt); @@ -42,9 +42,33 @@ function splitAmount( return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); } +function getKeepAmounts( + proofsWeHave: Array, + amountToKeep: number, + keyset: Keys, + targetCount: number +): Array { + // determines amounts we need to reach the targetCount for each amount based on the amounts of the proofs we have + // it tries to select amounts so that the proofs we have and the proofs we want reach the targetCount + const amountWeWant: Array = []; + const amountsWeHave = proofsWeHave.map((p) => p.amount); + const sortedKeyAmounts = getKeysetAmounts(keyset); + sortedKeyAmounts.forEach((amt) => { + const count = amountsWeHave.filter((a) => a === amt).length; + const q = Math.floor(targetCount - count); + for (let i = 0; i < q; ++i) amountWeWant.push(amt); + }); + return amountWeWant; +} + function isPowerOfTwo(number: number) { return number && !(number & (number - 1)); } +function getKeysetAmounts(keyset: Keys): Array { + return Object.keys(keyset) + .map((k) => parseInt(k)) + .sort((a, b) => b - a); +} function hasCorrespondingKey(amount: number, keyset: Keys) { return amount in keyset; @@ -200,5 +224,6 @@ export { getEncodedToken, hexToNumber, splitAmount, - deprecatedPreferenceToOutputAmounts + deprecatedPreferenceToOutputAmounts, + getKeepAmounts }; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 9987b24a7..33331c32a 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -11,7 +11,10 @@ const dummyKeysResp = { { id: '009a1f293253e41e', unit: 'sat', - keys: { 1: '02f970b6ee058705c0dddc4313721cffb7efd3d142d96ea8e01d31c2b2ff09f181', 2: '03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5' } + keys: { + 1: '02f970b6ee058705c0dddc4313721cffb7efd3d142d96ea8e01d31c2b2ff09f181', + 2: '03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5' + } } ] }; @@ -21,7 +24,7 @@ const dummyKeysetResp = { id: '009a1f293253e41e', unit: 'sat', active: true, - input_fee_ppk: 0, + input_fee_ppk: 0 } ] }; From b8e90a3bd95c8e6298516dafb575c9d75b2021c5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:18:10 +0200 Subject: [PATCH 027/246] use latest nutshell docker image for integration tests --- .github/workflows/nutshell-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nutshell-integration.yml b/.github/workflows/nutshell-integration.yml index 8dafd40be..5925eba7a 100644 --- a/.github/workflows/nutshell-integration.yml +++ b/.github/workflows/nutshell-integration.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Pull and start mint run: | - docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.15.2 poetry run mint + docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:latest poetry run mint - name: Check running containers run: docker ps From 8414ae269b3659b91b2eda8c2e2263d7731886c5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:19:05 +0200 Subject: [PATCH 028/246] npm run format --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 79071cb99..75244c600 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ const tokens = await wallet.mintTokens(64, mintQuote.quote); Contributions are very welcome. -If you want to contribute, please open an Issue or a PR. -If you open a PR, please do so from the `development` branch as the base branch. +If you want to contribute, please open an Issue or a PR. +If you open a PR, please do so from the `development` branch as the base branch. ### Version @@ -80,17 +80,17 @@ If you open a PR, please do so from the `development` branch as the base branch. | | * `hotfix` | | | * `staging` -| |\ +| |\ | |\ \ | | | * `bugfix` | | | -| | * `development` -| | |\ +| | * `development` +| | |\ | | | * `feature1` | | | | | | |/ | | * -| | |\ +| | |\ | | | * `feature2` | | |/ | |/ From 6aa0553e29a141d213d20094fc9843b67db5d99c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:52:13 +0200 Subject: [PATCH 029/246] fix test --- package.json | 2 +- test/integration.test.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f7b58aa05..38cf20ac9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "compile": "rm -rf dist/lib && tsc && tsc --build tsconfig.es5.json", - "test": "jest --coverage --testPathIgnorePatterns ./test/integration.test.ts", + "test": "jest --coverage", "test-integration": "jest --coverage --testPathPattern ./test/integration.test.ts", "dev": "tsc --watch", "lint": "eslint --ext .js,.ts . --fix", diff --git a/test/integration.test.ts b/test/integration.test.ts index bb43f253d..baa0f2eed 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,7 +2,7 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken } from '../src/utils.js'; +import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; dns.setDefaultResultOrder('ipv4first'); @@ -144,6 +144,7 @@ describe('mint api', () => { expect(sendResponse.returnChange).toBeDefined(); expect(sendResponse.send.length).toBe(1); expect(sendResponse.returnChange.length).toBe(0); + expect(sumProofs(sendResponse.send)).toBe(64); }); test('test send tokens with change', async () => { const mint = new CashuMint(mintUrl); @@ -156,8 +157,10 @@ describe('mint api', () => { expect(sendResponse.send).toBeDefined(); expect(sendResponse.returnChange).toBeDefined(); expect(sendResponse.send.length).toBe(2); - expect(sendResponse.returnChange.length).toBe(4); - }); + expect(sendResponse.returnChange.length).toBe(5); + expect(sumProofs(sendResponse.send)).toBe(10); + expect(sumProofs(sendResponse.returnChange)).toBe(90); + }, 10000000); test('receive tokens with previous split', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); @@ -203,7 +206,7 @@ describe('mint api', () => { const result = await wallet .receive(encoded, { privkey: bytesToHex(privKeyAlice) }) .catch((e) => e); - expect(result).toEqual(new Error('Error when receiving')); + expect(result).toEqual(new Error('Error receiving token: Error: Error receiving token entry')); const proofs = await wallet.receive(encoded, { privkey: bytesToHex(privKeyBob) }); From 29a2915755d0a5991fe1d9ee2ca03272a2ba4cd9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:55:25 +0200 Subject: [PATCH 030/246] fix test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38cf20ac9..f7b58aa05 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "compile": "rm -rf dist/lib && tsc && tsc --build tsconfig.es5.json", - "test": "jest --coverage", + "test": "jest --coverage --testPathIgnorePatterns ./test/integration.test.ts", "test-integration": "jest --coverage --testPathPattern ./test/integration.test.ts", "dev": "tsc --watch", "lint": "eslint --ext .js,.ts . --fix", From 5be771574f5d42f6c5b4cfae015b3c47a0d6b10b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:05:20 +0200 Subject: [PATCH 031/246] wip --- src/CashuWallet.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 295a456ab..e70bd0722 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -449,7 +449,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -611,10 +611,8 @@ class CashuWallet { if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); console.log( - `outputAmounts: ${ - options?.outputAmounts?.keepAmounts - } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${ - options?.outputAmounts?.sendAmounts + `outputAmounts: ${options?.outputAmounts?.keepAmounts + } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a, b) => a + b, 0)})` ); const keyset = await this.getKeys(options?.keysetId); From 175373aef24d224857274d1772ea6d8c8daaa8d9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:17:23 +0200 Subject: [PATCH 032/246] wip: output selection --- .github/workflows/nutshell-integration.yml | 2 +- src/CashuWallet.ts | 134 ++++++++++++--------- src/utils.ts | 30 +++-- 3 files changed, 101 insertions(+), 65 deletions(-) diff --git a/.github/workflows/nutshell-integration.yml b/.github/workflows/nutshell-integration.yml index 5925eba7a..94ccf849e 100644 --- a/.github/workflows/nutshell-integration.yml +++ b/.github/workflows/nutshell-integration.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Pull and start mint run: | - docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:latest poetry run mint + docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint - name: Check running containers run: docker ps diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e70bd0722..aad990bff 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -145,8 +145,31 @@ class CashuWallet { await this.getMintInfo(); await this.getKeySets(); await this.getAllKeys(); + this.keysetId = this.getActiveKeyset(this._keysets).id; } + /** + * Choose a keyset to activate based on the lowest input fee + * + * Note: this function will filter out deprecated base64 keysets + * + * @param keysets keysets to choose from + * @returns active keyset + */ + getActiveKeyset(keysets: Array): MintKeyset { + let activeKeysets = keysets.filter((k) => k.active) + // begin deprecated: if there are keyset IDs that are not hex strings, we need to filter them out + const hexKeysets = activeKeysets.filter((k) => /^[0-9a-fA-F]+$/.test(k.id)); + if (hexKeysets.length > 0) { + activeKeysets = hexKeysets; + } + // end deprecated + const activeKeyset = activeKeysets.sort((a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0]; + if (!activeKeyset) { + throw new Error('No active keyset found'); + } + return activeKeyset; + } /** * Get keysets from the mint with the unit of the wallet * @returns keysets @@ -192,27 +215,9 @@ class CashuWallet { // no keysetId was set, so we select an active keyset with the unit of the wallet with the lowest fees and use that const allKeysets = await this.mint.getKeySets(); - const keysetToActivate = allKeysets.keysets - .filter((k) => k.unit === this._unit && k.active) - .sort((a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0]; - if (!keysetToActivate) { - throw new Error( - `could not initialize keys. No active keyset with unit '${this._unit}' found` - ); - } - - if (!this._keys.get(keysetToActivate.id)) { - const keysetGet = await this.mint.getKeys(keysetToActivate.id); - const keys = keysetGet.keysets.find((k) => k.id === keysetToActivate.id); - if (!keys) { - throw new Error( - `could not initialize keys. No keyset with id '${keysetToActivate.id}' found` - ); - } - this._keys.set(keys.id, keys); - } - this.keysetId = keysetToActivate.id; - return this._keys.get(keysetToActivate.id) as MintKeys; + const keysetToActivate = this.getActiveKeyset(allKeysets.keysets) + const keyset = await this.getKeys(keysetToActivate.id); + return keyset } /** @@ -311,6 +316,7 @@ class CashuWallet { options?: { preference?: Array; outputAmounts?: OutputAmounts; + proofsWeHave?: Array; counter?: number; pubkey?: string; privkey?: string; @@ -343,12 +349,12 @@ class CashuWallet { console.log( `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)}` ); - // if (options && !options?.outputAmounts?.keepAmounts) { - // options.outputAmounts = { - // keepAmounts: getKeepAmounts(keepProofsSelect, sumProofs(keepProofsSelect), this._keys.get(options?.keysetId || this.keysetId) as MintKeys, 3), - // sendAmounts: options?.outputAmounts?.sendAmounts || [] - // } - // } + if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { + options.outputAmounts = { + keepAmounts: getKeepAmounts(options.proofsWeHave, sumProofs(keepProofsSelect), this._keys.get(options?.keysetId || this.keysetId) as MintKeys, 3), + sendAmounts: options?.outputAmounts?.sendAmounts || [] + } + } const { returnChange, send } = await this.swap(amount, sendProofs, options); console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); @@ -374,17 +380,17 @@ class CashuWallet { .filter((p) => p.amount > amountToSend) .sort((a, b) => a.amount - b.amount); const nextBigger = biggerProofs[0]; - console.log( - `> enter | amountToSend: ${amountToSend} | proofs: ${sumProofs( - proofs - )} | smallerProofs: ${sumProofs(smallerProofs)} | nextBigger: ${nextBigger?.amount}` - ); + // console.log( + // `> enter | amountToSend: ${amountToSend} | proofs: ${sumProofs( + // proofs + // )} | smallerProofs: ${sumProofs(smallerProofs)} | nextBigger: ${nextBigger?.amount}` + // ); if (!smallerProofs.length && nextBigger) { - console.log( - `< [0] exit: no smallerProofs, nextBigger: ${nextBigger.amount} | keep: ${sumProofs( - proofs.filter((p) => p.secret !== nextBigger.secret) - )} | proofs: ${sumProofs(proofs)}` - ); + // console.log( + // `< [0] exit: no smallerProofs, nextBigger: ${nextBigger.amount} | keep: ${sumProofs( + // proofs.filter((p) => p.secret !== nextBigger.secret) + // )} | proofs: ${sumProofs(proofs)}` + // ); return { returnChange: proofs.filter((p) => p.secret !== nextBigger.secret), send: [nextBigger] @@ -392,17 +398,17 @@ class CashuWallet { } if (!smallerProofs.length && !nextBigger) { - console.log( - `< [1] exit: no smallerProofs, no nextBigger | amountToSend: ${amountToSend} | keep: ${sumProofs( - proofs - )}` - ); + // console.log( + // `< [1] exit: no smallerProofs, no nextBigger | amountToSend: ${amountToSend} | keep: ${sumProofs( + // proofs + // )}` + // ); return { returnChange: proofs, send: [] }; } let remainder = amountToSend; let selectedProofs = [smallerProofs[0]]; - console.log(`Selected proof: ${smallerProofs[0].amount}`); + // console.log(`Selected proof: ${smallerProofs[0].amount}`); const returnedProofs = []; const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= smallerProofs[0].amount - feePPK / 1000; @@ -422,18 +428,18 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend && nextBigger) { selectedProofs = [nextBigger]; - console.log( - `< exit [2.2] selecting nextBigger | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( - selectedProofs - )} | returnedProofs: ${sumProofs(returnedProofs)}` - ); - } - - console.log( - `< exit [2] selectProofsToSend | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( - selectedProofs - )} | returnedProofs: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))}` - ); + // console.log( + // `< exit [2.2] selecting nextBigger | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( + // selectedProofs + // )} | returnedProofs: ${sumProofs(returnedProofs)}` + // ); + } + + // console.log( + // `< exit [2] selectProofsToSend | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( + // selectedProofs + // )} | returnedProofs: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))}` + // ); return { returnChange: proofs.filter((p) => !selectedProofs.includes(p)), send: selectedProofs @@ -604,18 +610,34 @@ class CashuWallet { keysetId?: string; preference?: Array; outputAmounts?: OutputAmounts; + proofsWeHave?: Array; counter?: number; pubkey?: string; } ): Promise<{ proofs: Array }> { + const keyset = await this.getKeys(options?.keysetId); + if (options?.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); + + if (!options?.outputAmounts && options?.proofsWeHave) { + options.outputAmounts = { + keepAmounts: getKeepAmounts( + options.proofsWeHave, + amount, + keyset.keys, + 3 + ), + sendAmounts: [] + }; + } console.log( `outputAmounts: ${options?.outputAmounts?.keepAmounts } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a, b) => a + b, 0)})` ); - const keyset = await this.getKeys(options?.keysetId); + console.log(JSON.stringify(options?.outputAmounts)); + const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, keyset, diff --git a/src/utils.ts b/src/utils.ts index 5e9e662e8..ca518c4f3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,20 +45,34 @@ function splitAmount( function getKeepAmounts( proofsWeHave: Array, amountToKeep: number, - keyset: Keys, + keys: Keys, targetCount: number ): Array { // determines amounts we need to reach the targetCount for each amount based on the amounts of the proofs we have // it tries to select amounts so that the proofs we have and the proofs we want reach the targetCount - const amountWeWant: Array = []; + const amountsWeWant: Array = []; const amountsWeHave = proofsWeHave.map((p) => p.amount); - const sortedKeyAmounts = getKeysetAmounts(keyset); + const sortedKeyAmounts = getKeysetAmounts(keys); sortedKeyAmounts.forEach((amt) => { - const count = amountsWeHave.filter((a) => a === amt).length; - const q = Math.floor(targetCount - count); - for (let i = 0; i < q; ++i) amountWeWant.push(amt); + const countWeHave = amountsWeHave.filter((a) => a === amt).length; + const countWeWant = Math.floor(targetCount - countWeHave); + for (let i = 0; i < countWeWant; ++i) { + if (amountsWeWant.reduce((a, b) => a + b, 0) + amt > amountToKeep) { + break; + } + amountsWeWant.push(amt) + } }); - return amountWeWant; + // use splitAmount to fill the rest between the sum of amountsWeHave and amountToKeep + const amountDiff = amountToKeep - amountsWeWant.reduce((a, b) => a + b, 0) + if (amountDiff) { + const remainingAmounts = splitAmount(amountDiff, keys) + remainingAmounts.forEach((amt) => { amountsWeWant.push(amt) }) + } + const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b) + console.log(`amountsWeHave: ${amountsWeHave}`) + console.log(`amountsWeWant: ${sortedAmountsWeWant}`); + return sortedAmountsWeWant; } function isPowerOfTwo(number: number) { @@ -67,7 +81,7 @@ function isPowerOfTwo(number: number) { function getKeysetAmounts(keyset: Keys): Array { return Object.keys(keyset) .map((k) => parseInt(k)) - .sort((a, b) => b - a); + .sort((a, b) => a - b); } function hasCorrespondingKey(amount: number, keyset: Keys) { From d32e69790a76b6cae1fe6517909da538f973c508 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:11:23 +0200 Subject: [PATCH 033/246] tmp --- src/CashuWallet.ts | 6 ++++-- src/utils.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index aad990bff..163f404e3 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -350,11 +350,13 @@ class CashuWallet { `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)}` ); if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { + const keyset = await this.getKeys(options?.keysetId); options.outputAmounts = { - keepAmounts: getKeepAmounts(options.proofsWeHave, sumProofs(keepProofsSelect), this._keys.get(options?.keysetId || this.keysetId) as MintKeys, 3), + keepAmounts: getKeepAmounts(options.proofsWeHave, amount, keyset.keys, 3), sendAmounts: options?.outputAmounts?.sendAmounts || [] } } + console.log(`keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}`) const { returnChange, send } = await this.swap(amount, sendProofs, options); console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); @@ -641,7 +643,7 @@ class CashuWallet { const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, keyset, - options?.outputAmounts?.sendAmounts, + // options?.outputAmounts?.keepAmounts, options?.counter, options?.pubkey ); diff --git a/src/utils.ts b/src/utils.ts index ca518c4f3..0a8234377 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,7 +69,7 @@ function getKeepAmounts( const remainingAmounts = splitAmount(amountDiff, keys) remainingAmounts.forEach((amt) => { amountsWeWant.push(amt) }) } - const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b) + const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b); console.log(`amountsWeHave: ${amountsWeHave}`) console.log(`amountsWeWant: ${sortedAmountsWeWant}`); return sortedAmountsWeWant; From c0c2cd61620981ab1b004bf99e65d18d9cb9c793 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 30 Aug 2024 08:40:48 +0200 Subject: [PATCH 034/246] pipe: add prerelease pipeline --- .github/workflows/prerelease.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/prerelease.yml diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 000000000..a0ce67a4f --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,21 @@ +name: Publish Package to npmjs +permissions: + contents: read + id-token: write +on: + release: + types: [prereleased] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npm run compile + - run: npm publish --tag next --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 3e5622283e4c793c4487902a357acbae00a1b1e0 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 30 Aug 2024 07:20:27 +0200 Subject: [PATCH 035/246] linter: enforce param types --- .eslintrc.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 35123f100..0b96bd440 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,3 @@ -// .eslintrc { "parser": "@typescript-eslint/parser", "parserOptions": { @@ -11,7 +10,7 @@ "rules": { "@typescript-eslint/no-unused-vars": "warn", - // to enforce using type for object type definitions, can be type or interface + "@typescript-eslint/typedef": ["error", { "parameter": true }], "@typescript-eslint/consistent-type-definitions": ["warn", "type"], "@typescript-eslint/array-type": ["warn", { "default": "generic" }], "require-await": "off", From 63847736fcc40896a3006b3227588585ad41f504 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 30 Aug 2024 09:44:16 +0200 Subject: [PATCH 036/246] enforced linter rules --- .eslintrc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0b96bd440..b74d1793e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,12 +9,12 @@ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "rules": { - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/typedef": ["error", { "parameter": true }], + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/typedef": ["error", { "parameter": true, "arrowParameter": true }], "@typescript-eslint/consistent-type-definitions": ["warn", "type"], - "@typescript-eslint/array-type": ["warn", { "default": "generic" }], + "@typescript-eslint/array-type": ["error", { "default": "generic" }], "require-await": "off", - "@typescript-eslint/require-await": "warn", + "@typescript-eslint/require-await": "error", "@typescript-eslint/await-thenable": "warn", "@typescript-eslint/consistent-type-exports": "warn", "no-else-return": "warn" From eff6dcb2a9d675c9a0934eb3752cca9ec23da523 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 30 Aug 2024 10:00:33 +0200 Subject: [PATCH 037/246] add type annotations --- src/CashuMint.ts | 3 +-- src/CashuWallet.ts | 42 ++++++++++++++++++++++++------------------ src/utils.ts | 30 +++++++++++++++--------------- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 3729ae028..8df76b83d 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -15,8 +15,7 @@ import type { MintResponse, PostRestorePayload, MeltQuotePayload, - MeltQuoteResponse, - MintContactInfo + MeltQuoteResponse } from './model/types/index.js'; import { MeltQuoteState } from './model/types/index.js'; import request from './request.js'; diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 0442bb04e..46722ab59 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,7 +20,8 @@ import { type TokenEntry, CheckStateEnum, SerializedBlindedSignature, - MeltQuoteState + MeltQuoteState, + CheckStateEntry } from './model/types/index.js'; import { bytesToNumber, @@ -164,7 +165,7 @@ class CashuWallet { } ): Promise> { const proofs: Array = []; - const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); + const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0); let preference = options?.preference; if (!preference) { preference = getDefaultAmountPreference(amount); @@ -214,13 +215,16 @@ class CashuWallet { } ): Promise { if (options?.preference) { - amount = options?.preference?.reduce((acc, curr) => acc + curr.amount * curr.count, 0); + amount = options?.preference?.reduce( + (acc: number, curr: AmountPreference) => acc + curr.amount * curr.count, + 0 + ); } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; const proofsToSend: Array = []; const proofsToKeep: Array = []; - proofs.forEach((proof) => { + proofs.forEach((proof: Proof) => { if (amountAvailable >= amount) { proofsToKeep.push(proof); return; @@ -254,7 +258,7 @@ class CashuWallet { const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; let amountKeepCounter = 0; - proofs.forEach((proof) => { + proofs.forEach((proof: Proof) => { if (amountKeepCounter < amountKeep) { amountKeepCounter += proof.amount; splitProofsToKeep.push(proof); @@ -294,9 +298,11 @@ class CashuWallet { const { outputs, promises } = await this.mint.restore({ outputs: blindedMessages }); // Collect and map the secrets and blinding factors with the blinded messages that were returned from the mint - const validRs = rs.filter((r, i) => outputs.map((o) => o.B_).includes(blindedMessages[i].B_)); - const validSecrets = secrets.filter((s, i) => - outputs.map((o) => o.B_).includes(blindedMessages[i].B_) + const validRs = rs.filter((_: bigint, i: number) => + outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) + ); + const validSecrets = secrets.filter((_: Uint8Array, i: number) => + outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); return { @@ -312,9 +318,9 @@ class CashuWallet { const allKeys = await this.mint.getKeys(keysetId); let keys; if (keysetId) { - keys = allKeys.keysets.find((k) => k.id === keysetId); + keys = allKeys.keysets.find((k: MintKeys) => k.id === keysetId); } else { - keys = allKeys.keysets.find((k) => (unit ? k.unit === unit : k.unit === 'sat')); + keys = allKeys.keysets.find((k: MintKeys) => (unit ? k.unit === unit : k.unit === 'sat')); } if (!keys) { throw new Error( @@ -495,8 +501,8 @@ class CashuWallet { ): Promise { const decodedToken = getDecodedToken(token); const proofs = decodedToken.token - .filter((x) => x.mint === this.mint.mintUrl) - .flatMap((t) => t.proofs); + .filter((x: TokenEntry) => x.mint === this.mint.mintUrl) + .flatMap((t: TokenEntry) => t.proofs); return this.payLnInvoice(invoice, proofs, meltQuote, { keysetId: options?.keysetId, counter: options?.counter @@ -525,7 +531,7 @@ class CashuWallet { payload: SwapPayload; blindedMessages: BlindedTransaction; } { - const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); + const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, keyset.id, @@ -544,7 +550,7 @@ class CashuWallet { ); if (privkey) { proofsToSend = getSignedProofs( - proofsToSend.map((p) => { + proofsToSend.map((p: Proof) => { return { amount: p.amount, C: pointFromHex(p.C), @@ -580,15 +586,15 @@ class CashuWallet { */ async checkProofsSpent(proofs: Array): Promise> { const enc = new TextEncoder(); - const Ys = proofs.map((p) => hashToCurve(enc.encode(p.secret)).toHex(true)); + const Ys = proofs.map((p: T) => hashToCurve(enc.encode(p.secret)).toHex(true)); const payload = { // array of Ys of proofs to check Ys: Ys }; const { states } = await this.mint.check(payload); - return proofs.filter((_, i) => { - const state = states.find((state) => state.Y === Ys[i]); + return proofs.filter((_: T, i: number) => { + const state = states.find((state: CheckStateEntry) => state.Y === Ys[i]); return state && state.state === CheckStateEnum.SPENT; }); } @@ -713,7 +719,7 @@ class CashuWallet { const A = pointFromHex(keyset.keys[p.amount]); return constructProofFromPromise(blindSignature, r, secret, A); }) - .map((p) => serializeProof(p) as Proof); + .map((p: NUT11Proof) => serializeProof(p) as Proof); } } diff --git a/src/utils.ts b/src/utils.ts index 00d6ec012..9c48047b2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,6 @@ import { encodeBase64ToJson, encodeBase64toUint8, encodeJsonToBase64, - encodeUint8toBase64, encodeUint8toBase64Url } from './base64.js'; import { @@ -11,7 +10,6 @@ import { Proof, Token, TokenEntry, - TokenV2, TokenV4Template, V4InnerToken, V4ProofTemplate @@ -27,7 +25,7 @@ function splitAmount(value: number, amountPreference?: Array): chunks.push(...getPreference(value, amountPreference)); value = value - - chunks.reduce((curr, acc) => { + chunks.reduce((curr: number, acc: number) => { return curr + acc; }, 0); } @@ -47,7 +45,7 @@ function isPowerOfTwo(number: number) { function getPreference(amount: number, preferredAmounts: Array): Array { const chunks: Array = []; let accumulator = 0; - preferredAmounts.forEach((pa) => { + preferredAmounts.forEach((pa: AmountPreference) => { if (!isPowerOfTwo(pa.amount)) { throw new Error( 'Provided amount preferences contain non-power-of-2 numbers. Use only ^2 numbers' @@ -66,7 +64,7 @@ function getPreference(amount: number, preferredAmounts: Array function getDefaultAmountPreference(amount: number): Array { const amounts = splitAmount(amount); - return amounts.map((a) => { + return amounts.map((a: number) => { return { amount: a, count: 1 }; }); } @@ -117,9 +115,11 @@ function getEncodedTokenV4(token: Token): string { m: mint, u: token.unit || 'sat', t: Object.keys(idMap).map( - (id): V4InnerToken => ({ + (id: string): V4InnerToken => ({ i: hexToBytes(id), - p: idMap[id].map((p): V4ProofTemplate => ({ a: p.amount, s: p.secret, c: hexToBytes(p.C) })) + p: idMap[id].map( + (p: Proof): V4ProofTemplate => ({ a: p.amount, s: p.secret, c: hexToBytes(p.C) }) + ) }) ) } as TokenV4Template; @@ -143,7 +143,7 @@ function getEncodedTokenV4(token: Token): string { function getDecodedToken(token: string) { // remove prefixes const uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:', 'cashu']; - uriPrefixes.forEach((prefix) => { + uriPrefixes.forEach((prefix: string) => { if (!token.startsWith(prefix)) { return; } @@ -170,8 +170,8 @@ function handleTokens(token: string): Token { u: string; }; const mergedTokenEntry: TokenEntry = { mint: tokenData.m, proofs: [] }; - tokenData.t.forEach((tokenEntry) => - tokenEntry.p.forEach((p) => { + tokenData.t.forEach((tokenEntry: V4InnerToken) => + tokenEntry.p.forEach((p: V4ProofTemplate) => { mergedTokenEntry.proofs.push({ secret: p.s, C: bytesToHex(p.c), @@ -191,9 +191,9 @@ function handleTokens(token: string): Token { */ export function deriveKeysetId(keys: Keys) { const pubkeysConcat = Object.entries(keys) - .sort((a, b) => +a[0] - +b[0]) - .map(([, pubKey]) => hexToBytes(pubKey)) - .reduce((prev, curr) => mergeUInt8Arrays(prev, curr), new Uint8Array()); + .sort((a: [string, string], b: [string, string]) => +a[0] - +b[0]) + .map(([, pubKey]: [unknown, string]) => hexToBytes(pubKey)) + .reduce((prev: Uint8Array, curr: Uint8Array) => mergeUInt8Arrays(prev, curr), new Uint8Array()); const hash = sha256(pubkeysConcat); const hashHex = Buffer.from(hash).toString('hex').slice(0, 14); return '00' + hashHex; @@ -208,7 +208,7 @@ function mergeUInt8Arrays(a1: Uint8Array, a2: Uint8Array): Uint8Array { } export function sortProofsById(proofs: Array) { - return proofs.sort((a, b) => a.id.localeCompare(b.id)); + return proofs.sort((a: Proof, b: Proof) => a.id.localeCompare(b.id)); } export function isObj(v: unknown): v is object { @@ -226,7 +226,7 @@ export function checkResponse(data: { error?: string; detail?: string }) { } export function joinUrls(...parts: Array): string { - return parts.map((part) => part.replace(/(^\/+|\/+$)/g, '')).join('/'); + return parts.map((part: string) => part.replace(/(^\/+|\/+$)/g, '')).join('/'); } export function sanitizeUrl(url: string): string { From 0357e9c6685739bdd3b2b3a4fd4204df8c360408 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 5 Aug 2024 11:39:28 +0200 Subject: [PATCH 038/246] rebase --- src/CashuWallet.ts | 16 +++++++++++++++- test/integration.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 0442bb04e..cbb960f94 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -432,6 +432,19 @@ class CashuWallet { keys.id, options?.counter ); + if (options?.privkey != undefined) { + proofsToSend = getSignedProofs( + proofsToSend.map((p) => { + return { + amount: p.amount, + C: pointFromHex(p.C), + id: p.id, + secret: new TextEncoder().encode(p.secret) + }; + }), + options.privkey + ).map((p: NUT11Proof) => serializeProof(p)); + } const meltPayload: MeltPayload = { quote: meltQuote.quote, inputs: proofsToSend, @@ -472,7 +485,8 @@ class CashuWallet { } return await this.meltTokens(meltQuote, proofsToSend, { keysetId: options?.keysetId, - counter: options?.counter + counter: options?.counter, + privkey: options?.privkey }); } diff --git a/test/integration.test.ts b/test/integration.test.ts index 2d069392d..64577585c 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -213,4 +213,27 @@ describe('mint api', () => { }, 0) ).toBe(64); }); + + test('mint and melt p2pk', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const privKeyBob = secp256k1.utils.randomPrivateKey(); + const pubKeyBob = secp256k1.getPublicKey(privKeyBob); + + const mintRequest = await wallet.createMintQuote(3000); + + const proofs = await wallet.mintTokens(3000, mintRequest.quote, { + pubkey: bytesToHex(pubKeyBob) + }); + + const meltRequest = await wallet.createMeltQuote(externalInvoice); + const fee = meltRequest.fee_reserve; + expect(fee).toBeGreaterThan(0); + const response = await wallet.meltTokens(meltRequest, proofs.proofs, { + privkey: bytesToHex(privKeyBob) + }); + expect(response).toBeDefined(); + expect(response.isPaid).toBe(true); + }); }); From f31f0ca8171a7db74b64ecb1b3224fce0cd50d32 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 5 Sep 2024 12:13:06 +0200 Subject: [PATCH 039/246] update docs annotations to reflect change + update options to include privkey (lost in the rebase somehow?) --- src/CashuWallet.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index cbb960f94..3c684eb3b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -415,6 +415,7 @@ class CashuWallet { * @param proofsToSend proofs to melt * @param options.keysetId? optionally set keysetId for blank outputs for returned change. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.privkey? optionally set a private key to unlock P2PK locked secrets * @returns */ async meltTokens( @@ -423,6 +424,7 @@ class CashuWallet { options?: { keysetId?: string; counter?: number; + privkey?: string; } ): Promise { const keys = await this.getKeys(options?.keysetId); @@ -469,6 +471,7 @@ class CashuWallet { * @param meltQuote melt quote for the invoice * @param options.keysetId? optionally set keysetId for blank outputs for returned change. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.privkey? optionally set a private key to unlock P2PK locked secrets * @returns */ async payLnInvoice( @@ -478,6 +481,7 @@ class CashuWallet { options?: { keysetId?: string; counter?: number; + privkey?: string; } ): Promise { if (!meltQuote) { From 69690c3a79dddb3395e1a50354c56f30a344a41e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:32:25 +0200 Subject: [PATCH 040/246] NUT-05: description for invoice, integration test without bolt11 decode --- src/CashuWallet.ts | 9 ++++++--- src/model/types/wallet/payloads.ts | 4 ++++ test/integration.test.ts | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 0442bb04e..5e9cf89ef 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,7 +20,8 @@ import { type TokenEntry, CheckStateEnum, SerializedBlindedSignature, - MeltQuoteState + MeltQuoteState, + MintQuoteResponse } from './model/types/index.js'; import { bytesToNumber, @@ -331,12 +332,14 @@ class CashuWallet { /** * Requests a mint quote form the mint. Response returns a Lightning payment request for the requested given amount and unit. * @param amount Amount requesting for mint. + * @param description optional description for the mint quote * @returns the mint will return a mint quote with a Lightning invoice for minting tokens of the specified amount and unit */ - async createMintQuote(amount: number) { + async createMintQuote(amount: number, description?: string) { const mintQuotePayload: MintQuotePayload = { unit: this._unit, - amount: amount + amount: amount, + description: description || null, }; return await this.mint.createMintQuote(mintQuotePayload); } diff --git a/src/model/types/wallet/payloads.ts b/src/model/types/wallet/payloads.ts index 43539a91f..70855dea5 100644 --- a/src/model/types/wallet/payloads.ts +++ b/src/model/types/wallet/payloads.ts @@ -98,6 +98,10 @@ export type MintQuotePayload = { * Amount to be minted */ amount: number; + /** + * Description for the invoice + */ + description: string | null; }; /** diff --git a/test/integration.test.ts b/test/integration.test.ts index 2d069392d..936c82453 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -61,6 +61,13 @@ describe('mint api', () => { // because local invoice, fee should be 0 expect(fee).toBe(0); }); + test('invoice with description', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint, { unit }); + const quote = await wallet.createMintQuote(100, 'test description'); + expect(quote).toBeDefined(); + console.log(`invoice with description: ${quote.request}`); + }); test('get fee for external invoice', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); From 87d91b028105eda705edfac399b0f03557629e1a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:34:05 +0200 Subject: [PATCH 041/246] npm run format --- src/CashuWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 5e9cf89ef..4e783bf5e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -339,7 +339,7 @@ class CashuWallet { const mintQuotePayload: MintQuotePayload = { unit: this._unit, amount: amount, - description: description || null, + description: description || null }; return await this.mint.createMintQuote(mintQuotePayload); } From 56ee3ddb1e1b5d0ffbb8a56fff1c1329992e84f0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:14:54 +0200 Subject: [PATCH 042/246] use description? --- src/CashuWallet.ts | 2 +- src/model/types/wallet/payloads.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 4e783bf5e..2518664a9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -339,7 +339,7 @@ class CashuWallet { const mintQuotePayload: MintQuotePayload = { unit: this._unit, amount: amount, - description: description || null + description: description }; return await this.mint.createMintQuote(mintQuotePayload); } diff --git a/src/model/types/wallet/payloads.ts b/src/model/types/wallet/payloads.ts index 70855dea5..7e8b67662 100644 --- a/src/model/types/wallet/payloads.ts +++ b/src/model/types/wallet/payloads.ts @@ -101,7 +101,7 @@ export type MintQuotePayload = { /** * Description for the invoice */ - description: string | null; + description?: string; }; /** From e2247884773004eca9f5bf4548707e0281e719b3 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 12:44:49 +0200 Subject: [PATCH 043/246] getPreferences checks against keyset amounts, not necessarily powers of 2 --- src/CashuWallet.ts | 16 ++++++++-------- src/utils.ts | 31 +++++++++++++++++++------------ test/utils.test.ts | 34 ++++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 08e0a8476..8b15e6cb1 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -167,10 +167,10 @@ class CashuWallet { const proofs: Array = []; const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); let preference = options?.preference; + const keys = await this.getKeys(options?.keysetId); if (!preference) { - preference = getDefaultAmountPreference(amount); + preference = getDefaultAmountPreference(amount, keys); } - const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSwapPayload( amount, tokenEntry.proofs, @@ -372,7 +372,7 @@ class CashuWallet { const keyset = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, - options?.keysetId ?? keyset.id, + keyset, options?.preference, options?.counter, options?.pubkey @@ -549,7 +549,7 @@ class CashuWallet { const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, - keyset.id, + keyset, undefined, counter ); @@ -558,7 +558,7 @@ class CashuWallet { } const sendBlindedMessages = this.createRandomBlindedMessages( amount, - keyset.id, + keyset, preference, counter, pubkey @@ -633,13 +633,13 @@ class CashuWallet { */ private createRandomBlindedMessages( amount: number, - keysetId: string, + keyset: MintKeys, amountPreference?: Array, counter?: number, pubkey?: string ): BlindedMessageData & { amounts: Array } { - const amounts = splitAmount(amount, amountPreference); - return this.createBlindedMessages(amounts, keysetId, counter, pubkey); + const amounts = splitAmount(amount, keyset.keys, amountPreference); + return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } /** diff --git a/src/utils.ts b/src/utils.ts index 00d6ec012..0c233afcc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,15 +21,18 @@ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; -function splitAmount(value: number, amountPreference?: Array): Array { +function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { const chunks: Array = []; if (amountPreference) { - chunks.push(...getPreference(value, amountPreference)); - value = - value - - chunks.reduce((curr, acc) => { - return curr + acc; - }, 0); + if (amountPreference.length > 0) { + chunks.push(...getPreference(value, keyset, amountPreference)); + value = + value - + chunks.reduce((curr, acc) => { + return curr + acc; + }, 0); + return chunks; + } } for (let i = 0; i < 32; i++) { const mask: number = 1 << i; @@ -44,13 +47,17 @@ function isPowerOfTwo(number: number) { return number && !(number & (number - 1)); } -function getPreference(amount: number, preferredAmounts: Array): Array { +function hasCorrespondingKey(amount: number, keyset: Keys) { + return amount in keyset; +} + +function getPreference(amount: number, keyset: Keys, preferredAmounts: Array): Array { const chunks: Array = []; let accumulator = 0; preferredAmounts.forEach((pa) => { - if (!isPowerOfTwo(pa.amount)) { + if (!hasCorrespondingKey(pa.amount, keyset)) { throw new Error( - 'Provided amount preferences contain non-power-of-2 numbers. Use only ^2 numbers' + 'Provided amount preferences do not match the amounts of the mint keyset.' ); } for (let i = 1; i <= pa.count; i++) { @@ -64,8 +71,8 @@ function getPreference(amount: number, preferredAmounts: Array return chunks; } -function getDefaultAmountPreference(amount: number): Array { - const amounts = splitAmount(amount); +function getDefaultAmountPreference(amount: number, keyset: Keys): Array { + const amounts = splitAmount(amount, keyset); return amounts.map((a) => { return { amount: a, count: 1 }; }); diff --git a/test/utils.test.ts b/test/utils.test.ts index c686b8992..ab138d6c4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,14 +1,19 @@ -import { AmountPreference, Token } from '../src/model/types/index.js'; +import { AmountPreference, Token, Keys } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; +const keys: Keys = {}; +for (let i=1; i<2048; i *= 2) { + keys[i] = "deadbeef"; +} + describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561); + const chunks = utils.splitAmount(2561, keys); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { - const chunks = utils.splitAmount(0); + const chunks = utils.splitAmount(0, keys); expect(chunks).toStrictEqual([]); }); }); @@ -16,7 +21,7 @@ describe('test split amounts ', () => { describe('test split custom amounts ', () => { const fiveToOne: AmountPreference = { amount: 1, count: 5 }; test('testing amount 5', async () => { - const chunks = utils.splitAmount(5, [fiveToOne]); + const chunks = utils.splitAmount(5, keys, [fiveToOne]); expect(chunks).toStrictEqual([1, 1, 1, 1, 1]); }); const tenToOneAndTwo: Array = [ @@ -24,21 +29,30 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, tenToOneAndTwo); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve: Array = [{ amount: 512, count: 2 }]; - test('testing amount 516', async () => { - const chunks = utils.splitAmount(518, fiveTwelve); + const fiveTwelve: Array = [ + { amount: 512, count: 1 }, + { amount: 2, count: 1 }, + { amount: 4, count: 1} + ]; + test('testing amount 518', async () => { + const chunks = utils.splitAmount(518, keys, fiveTwelve); expect(chunks).toStrictEqual([512, 2, 4]); }); const illegal: Array = [{ amount: 3, count: 2 }]; test('testing non pow2', async () => { - expect(() => utils.splitAmount(6, illegal)).toThrowError(); + expect(() => utils.splitAmount(6, keys, illegal)).toThrowError(); }); const empty: Array = []; test('testing empty', async () => { - const chunks = utils.splitAmount(5, empty); + const chunks = utils.splitAmount(5, keys, empty); + expect(chunks).toStrictEqual([1, 4]); + }); + const undef = undefined; + test('testing undefined', async () => { + const chunks = utils.splitAmount(5, keys, undef); expect(chunks).toStrictEqual([1, 4]); }); }); From c867377f93fd869b3c258d1c4cce0cc1dd0a0002 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 13:01:15 +0200 Subject: [PATCH 044/246] remove splitAmount fix for "filling up the rest" behaviour --- src/utils.ts | 15 ++++++--------- test/utils.test.ts | 6 +----- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 0c233afcc..6d2e9a066 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,15 +24,12 @@ import { decodeCBOR, encodeCBOR } from './cbor.js'; function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { const chunks: Array = []; if (amountPreference) { - if (amountPreference.length > 0) { - chunks.push(...getPreference(value, keyset, amountPreference)); - value = - value - - chunks.reduce((curr, acc) => { - return curr + acc; - }, 0); - return chunks; - } + chunks.push(...getPreference(value, keyset, amountPreference)); + value = + value - + chunks.reduce((curr, acc) => { + return curr + acc; + }, 0); } for (let i = 0; i < 32; i++) { const mask: number = 1 << i; diff --git a/test/utils.test.ts b/test/utils.test.ts index ab138d6c4..a2ad0d9e2 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -32,11 +32,7 @@ describe('test split custom amounts ', () => { const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); - const fiveTwelve: Array = [ - { amount: 512, count: 1 }, - { amount: 2, count: 1 }, - { amount: 4, count: 1} - ]; + const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve); expect(chunks).toStrictEqual([512, 2, 4]); From 17b1963ea5521fef3145ecf1e8c501dedef80923 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 16:57:12 +0200 Subject: [PATCH 045/246] sendPreference, keepPreference --- src/CashuWallet.ts | 16 +++++++++------- src/model/types/index.ts | 5 +++++ test/wallet.test.ts | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8b15e6cb1..b078e2027 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,7 +21,8 @@ import { CheckStateEnum, SerializedBlindedSignature, MeltQuoteState, - MintQuoteResponse + MintQuoteResponse, + Preferences } from './model/types/index.js'; import { bytesToNumber, @@ -171,11 +172,12 @@ class CashuWallet { if (!preference) { preference = getDefaultAmountPreference(amount, keys); } + const pref: Preferences = {sendPreference: preference}; const { payload, blindedMessages } = this.createSwapPayload( amount, tokenEntry.proofs, keys, - preference, + pref, options?.counter, options?.pubkey, options?.privkey @@ -207,7 +209,7 @@ class CashuWallet { amount: number, proofs: Array, options?: { - preference?: Array; + preference?: Preferences; counter?: number; pubkey?: string; privkey?: string; @@ -215,7 +217,7 @@ class CashuWallet { } ): Promise { if (options?.preference) { - amount = options?.preference?.reduce((acc, curr) => acc + curr.amount * curr.count, 0); + amount = options?.preference?.sendPreference.reduce((acc, curr) => acc + curr.amount * curr.count, 0); } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; @@ -538,7 +540,7 @@ class CashuWallet { amount: number, proofsToSend: Array, keyset: MintKeys, - preference?: Array, + preference?: Preferences, counter?: number, pubkey?: string, privkey?: string @@ -550,7 +552,7 @@ class CashuWallet { const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, keyset, - undefined, + preference?.keepPreference, counter ); if (this._seed && counter) { @@ -559,7 +561,7 @@ class CashuWallet { const sendBlindedMessages = this.createRandomBlindedMessages( amount, keyset, - preference, + preference?.sendPreference, counter, pubkey ); diff --git a/src/model/types/index.ts b/src/model/types/index.ts index d0032a879..de99ab119 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -1,6 +1,11 @@ export * from './mint/index'; export * from './wallet/index'; +export type Preferences = { + sendPreference: Array; + keepPreference?: Array; +} + export type InvoiceData = { paymentRequest: string; amountInSats?: number; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 913c02c4a..51e050315 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -491,7 +491,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: [{ amount: 1, count: 4 }] }); + const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 4 }] }}); expect(result.send).toHaveLength(4); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); @@ -546,7 +546,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: [{ amount: 1, count: 3 }] }); + const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 3 }]} }); expect(result.send).toHaveLength(3); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); From 8068d7f85e941e9efee657c0f6625e485bf56eba Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 18 Jun 2024 21:38:38 +0200 Subject: [PATCH 046/246] splitAmount: split is now based on key amounts, not powers of 2. --- src/utils.ts | 24 ++++++++++++++++-------- test/utils.test.ts | 43 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 6d2e9a066..f7cd5a647 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,7 +21,12 @@ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; -function splitAmount(value: number, keyset: Keys, amountPreference?: Array): Array { +function splitAmount( + value: number, + keyset: Keys, + amountPreference?: Array, + order?: string +): Array { const chunks: Array = []; if (amountPreference) { chunks.push(...getPreference(value, keyset, amountPreference)); @@ -31,13 +36,16 @@ function splitAmount(value: number, keyset: Keys, amountPreference?: Array = Object.keys(keyset) + .map(k => parseInt(k)) + .sort((a, b) => b-a); + sortedKeyAmounts.forEach(amt => { + let q = Math.floor(value / amt); + for (let i=0; i (order === "asc") ? (a-b) : (b-a)); } function isPowerOfTwo(number: number) { diff --git a/test/utils.test.ts b/test/utils.test.ts index a2ad0d9e2..f0b16a296 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,13 +3,23 @@ import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; const keys: Keys = {}; -for (let i=1; i<2048; i *= 2) { +for (let i=1; i<=2048; i *= 2) { keys[i] = "deadbeef"; } +const keys_base10: Keys = {}; +for (let i=1; i<=10000; i *= 10) { + keys_base10[i] = "deadbeef"; +} + +const keys_base16: Keys = {}; +for (let i=1; i<=0x10000; i *= 16) { + keys_base16[i] = "deadbeef"; +} + describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys); + const chunks = utils.splitAmount(2561, keys, undefined, "asc"); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -29,13 +39,13 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, "asc"); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { const chunks = utils.splitAmount(518, keys, fiveTwelve); - expect(chunks).toStrictEqual([512, 2, 4]); + expect(chunks).toStrictEqual([512, 4, 2]); }); const illegal: Array = [{ amount: 3, count: 2 }]; test('testing non pow2', async () => { @@ -44,15 +54,36 @@ describe('test split custom amounts ', () => { const empty: Array = []; test('testing empty', async () => { const chunks = utils.splitAmount(5, keys, empty); - expect(chunks).toStrictEqual([1, 4]); + expect(chunks).toStrictEqual([4, 1]); }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef); + const chunks = utils.splitAmount(5, keys, undef, "asc"); expect(chunks).toStrictEqual([1, 4]); }); }); +describe('test split different key amount', () => { + test('testing amount 68251', async () => { + const chunks = utils.splitAmount(68251, keys_base10); + expect(chunks).toStrictEqual([ + 10000, 10000, 10000, 10000, 10000, 10000, + 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, + 100, 100, + 10, 10, 10, 10, 10, + 1 + ]); + }); + test('testing amount 1917', async () => { + const chunks = utils.splitAmount(1917, keys_base16); + expect(chunks).toStrictEqual([ + 256, 256, 256, 256, 256, 256, 256, + 16, 16, 16, 16, 16, 16, 16, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]); + }); +}); + describe('test decode token', () => { test('testing v1 Token', () => { const token = From f4cb88004ff414f50750b98bd5afcf29ee7b6661 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 19 Jun 2024 16:12:10 +0200 Subject: [PATCH 047/246] fix for integration test --- src/CashuWallet.ts | 4 +++- src/model/types/index.ts | 2 +- src/utils.ts | 23 ++++++++++++----------- test/utils.test.ts | 30 +++++++++++++----------------- test/wallet.test.ts | 8 ++++++-- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index b078e2027..dc7a7d5e1 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -223,7 +223,9 @@ class CashuWallet { let amountAvailable = 0; const proofsToSend: Array = []; const proofsToKeep: Array = []; - proofs.forEach((proof) => { + // Start from smallest amounts and work your way up. + // This limits small change. + proofs.reverse().forEach((proof) => { if (amountAvailable >= amount) { proofsToKeep.push(proof); return; diff --git a/src/model/types/index.ts b/src/model/types/index.ts index de99ab119..d9f4ed3fc 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -4,7 +4,7 @@ export * from './wallet/index'; export type Preferences = { sendPreference: Array; keepPreference?: Array; -} +}; export type InvoiceData = { paymentRequest: string; diff --git a/src/utils.ts b/src/utils.ts index f7cd5a647..2259edd13 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,7 +22,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; function splitAmount( - value: number, + value: number, keyset: Keys, amountPreference?: Array, order?: string @@ -37,15 +37,14 @@ function splitAmount( }, 0); } const sortedKeyAmounts: Array = Object.keys(keyset) - .map(k => parseInt(k)) - .sort((a, b) => b-a); - sortedKeyAmounts.forEach(amt => { + .map((k) => parseInt(k)) + .sort((a, b) => b - a); + sortedKeyAmounts.forEach((amt) => { let q = Math.floor(value / amt); - for (let i=0; i (order === "asc") ? (a-b) : (b-a)); + return chunks.sort((a, b) => (order === 'asc' ? a - b : b - a)); } function isPowerOfTwo(number: number) { @@ -56,14 +55,16 @@ function hasCorrespondingKey(amount: number, keyset: Keys) { return amount in keyset; } -function getPreference(amount: number, keyset: Keys, preferredAmounts: Array): Array { +function getPreference( + amount: number, + keyset: Keys, + preferredAmounts: Array +): Array { const chunks: Array = []; let accumulator = 0; preferredAmounts.forEach((pa) => { if (!hasCorrespondingKey(pa.amount, keyset)) { - throw new Error( - 'Provided amount preferences do not match the amounts of the mint keyset.' - ); + throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } for (let i = 1; i <= pa.count; i++) { accumulator += pa.amount; diff --git a/test/utils.test.ts b/test/utils.test.ts index f0b16a296..f1d64c795 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,23 +3,23 @@ import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; const keys: Keys = {}; -for (let i=1; i<=2048; i *= 2) { - keys[i] = "deadbeef"; +for (let i = 1; i <= 2048; i *= 2) { + keys[i] = 'deadbeef'; } const keys_base10: Keys = {}; -for (let i=1; i<=10000; i *= 10) { - keys_base10[i] = "deadbeef"; +for (let i = 1; i <= 10000; i *= 10) { + keys_base10[i] = 'deadbeef'; } const keys_base16: Keys = {}; -for (let i=1; i<=0x10000; i *= 16) { - keys_base16[i] = "deadbeef"; +for (let i = 1; i <= 0x10000; i *= 16) { + keys_base16[i] = 'deadbeef'; } describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys, undefined, "asc"); + const chunks = utils.splitAmount(2561, keys, undefined, 'asc'); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -39,7 +39,7 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, "asc"); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, 'asc'); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; @@ -58,7 +58,7 @@ describe('test split custom amounts ', () => { }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef, "asc"); + const chunks = utils.splitAmount(5, keys, undef, 'asc'); expect(chunks).toStrictEqual([1, 4]); }); }); @@ -67,19 +67,15 @@ describe('test split different key amount', () => { test('testing amount 68251', async () => { const chunks = utils.splitAmount(68251, keys_base10); expect(chunks).toStrictEqual([ - 10000, 10000, 10000, 10000, 10000, 10000, - 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, - 100, 100, - 10, 10, 10, 10, 10, - 1 + 10000, 10000, 10000, 10000, 10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 100, + 100, 10, 10, 10, 10, 10, 1 ]); }); test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 256, 256, 256, 256, 256, 256, 256, - 16, 16, 16, 16, 16, 16, 16, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + 256, 256, 256, 256, 256, 256, 256, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1 ]); }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 51e050315..85608d170 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -491,7 +491,9 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 4 }] }}); + const result = await wallet.send(4, overpayProofs, { + preference: { sendPreference: [{ amount: 1, count: 4 }] } + }); expect(result.send).toHaveLength(4); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); @@ -546,7 +548,9 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; - const result = await wallet.send(4, overpayProofs, { preference: { sendPreference: [{ amount: 1, count: 3 }]} }); + const result = await wallet.send(4, overpayProofs, { + preference: { sendPreference: [{ amount: 1, count: 3 }] } + }); expect(result.send).toHaveLength(3); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); From 53be0ef123674ed06fdfd6fe207790543e7d2d62 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 20 Jun 2024 15:36:40 +0200 Subject: [PATCH 048/246] primary order is ascending amounts --- src/CashuWallet.ts | 4 +--- src/utils.ts | 2 +- test/utils.test.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index dc7a7d5e1..b078e2027 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -223,9 +223,7 @@ class CashuWallet { let amountAvailable = 0; const proofsToSend: Array = []; const proofsToKeep: Array = []; - // Start from smallest amounts and work your way up. - // This limits small change. - proofs.reverse().forEach((proof) => { + proofs.forEach((proof) => { if (amountAvailable >= amount) { proofsToKeep.push(proof); return; diff --git a/src/utils.ts b/src/utils.ts index 2259edd13..ae9f4b943 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,7 +44,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => (order === 'asc' ? a - b : b - a)); + return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); } function isPowerOfTwo(number: number) { diff --git a/test/utils.test.ts b/test/utils.test.ts index f1d64c795..4c39d5848 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -19,7 +19,7 @@ for (let i = 1; i <= 0x10000; i *= 16) { describe('test split amounts ', () => { test('testing amount 2561', async () => { - const chunks = utils.splitAmount(2561, keys, undefined, 'asc'); + const chunks = utils.splitAmount(2561, keys); expect(chunks).toStrictEqual([1, 512, 2048]); }); test('testing amount 0', async () => { @@ -39,12 +39,12 @@ describe('test split custom amounts ', () => { { amount: 2, count: 4 } ]; test('testing amount 10', async () => { - const chunks = utils.splitAmount(10, keys, tenToOneAndTwo, 'asc'); + const chunks = utils.splitAmount(10, keys, tenToOneAndTwo); expect(chunks).toStrictEqual([1, 1, 2, 2, 2, 2]); }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { - const chunks = utils.splitAmount(518, keys, fiveTwelve); + const chunks = utils.splitAmount(518, keys, fiveTwelve, 'desc'); expect(chunks).toStrictEqual([512, 4, 2]); }); const illegal: Array = [{ amount: 3, count: 2 }]; @@ -53,19 +53,19 @@ describe('test split custom amounts ', () => { }); const empty: Array = []; test('testing empty', async () => { - const chunks = utils.splitAmount(5, keys, empty); + const chunks = utils.splitAmount(5, keys, empty, 'desc'); expect(chunks).toStrictEqual([4, 1]); }); const undef = undefined; test('testing undefined', async () => { - const chunks = utils.splitAmount(5, keys, undef, 'asc'); + const chunks = utils.splitAmount(5, keys, undef); expect(chunks).toStrictEqual([1, 4]); }); }); describe('test split different key amount', () => { test('testing amount 68251', async () => { - const chunks = utils.splitAmount(68251, keys_base10); + const chunks = utils.splitAmount(68251, keys_base10, undefined, 'desc'); expect(chunks).toStrictEqual([ 10000, 10000, 10000, 10000, 10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 100, 100, 10, 10, 10, 10, 10, 1 @@ -74,8 +74,8 @@ describe('test split different key amount', () => { test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 256, 256, 256, 256, 256, 256, 256, 16, 16, 16, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, 256, 256 ]); }); }); From 4895641f3f5624f3905e9cc62e7799d39834e5b7 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 20 Jun 2024 15:42:58 +0200 Subject: [PATCH 049/246] npm run format --- src/CashuWallet.ts | 7 +++++-- test/utils.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index b078e2027..89b91f16c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -172,7 +172,7 @@ class CashuWallet { if (!preference) { preference = getDefaultAmountPreference(amount, keys); } - const pref: Preferences = {sendPreference: preference}; + const pref: Preferences = { sendPreference: preference }; const { payload, blindedMessages } = this.createSwapPayload( amount, tokenEntry.proofs, @@ -217,7 +217,10 @@ class CashuWallet { } ): Promise { if (options?.preference) { - amount = options?.preference?.sendPreference.reduce((acc, curr) => acc + curr.amount * curr.count, 0); + amount = options?.preference?.sendPreference.reduce( + (acc, curr) => acc + curr.amount * curr.count, + 0 + ); } const keyset = await this.getKeys(options?.keysetId); let amountAvailable = 0; diff --git a/test/utils.test.ts b/test/utils.test.ts index 4c39d5848..f54407ab6 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -74,8 +74,8 @@ describe('test split different key amount', () => { test('testing amount 1917', async () => { const chunks = utils.splitAmount(1917, keys_base16); expect(chunks).toStrictEqual([ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, 256, 256 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16, 16, 16, 16, 16, 16, 16, 256, 256, 256, 256, 256, + 256, 256 ]); }); }); From 322cc01cef83f8c2c2f6aa5e980cb7995d3e0d0d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 19 Sep 2024 11:42:51 +0200 Subject: [PATCH 050/246] fix import resolution for type `Preferences` --- src/model/types/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/model/types/index.ts b/src/model/types/index.ts index d9f4ed3fc..5fc2e3b70 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -1,3 +1,5 @@ +import { AmountPreference } from './wallet/index'; + export * from './mint/index'; export * from './wallet/index'; From fb06ee5dc91da07966bf50cea5e6ec50d11a38ce Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 19 Sep 2024 12:22:20 +0200 Subject: [PATCH 051/246] `Proof` annonation on meltTokens' map lambda --- src/CashuWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index aa657b9ae..04d27f4cd 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -446,7 +446,7 @@ class CashuWallet { ); if (options?.privkey != undefined) { proofsToSend = getSignedProofs( - proofsToSend.map((p) => { + proofsToSend.map((p: Proof) => { return { amount: p.amount, C: pointFromHex(p.C), From 60e64e39780d1b3aac7ee302d407b5c0fcddb3e8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 19 Sep 2024 13:26:30 +0200 Subject: [PATCH 052/246] make `send` and `createSwapPayload` interfaces retroactively compatible, adding a deprecation warn layer. --- src/CashuWallet.ts | 11 +++++++++-- src/legacy/cashu-ts.ts | 21 +++++++++++++++++++++ src/utils.ts | 6 ++++-- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/legacy/cashu-ts.ts diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 04d27f4cd..6f8280ce7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -30,6 +30,7 @@ import { getDefaultAmountPreference, splitAmount } from './utils.js'; +import { isAmountPreferenceArray, deprecatedAmountPreferences } from './legacy/cashu-ts'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -209,7 +210,7 @@ class CashuWallet { amount: number, proofs: Array, options?: { - preference?: Preferences; + preference?: Preferences | Array; counter?: number; pubkey?: string; privkey?: string; @@ -217,6 +218,9 @@ class CashuWallet { } ): Promise { if (options?.preference) { + if (isAmountPreferenceArray(options.preference)) { + options.preference = deprecatedAmountPreferences(options.preference); + } amount = options?.preference?.sendPreference.reduce( (acc: number, curr: AmountPreference) => acc + curr.amount * curr.count, 0 @@ -545,7 +549,7 @@ class CashuWallet { amount: number, proofsToSend: Array, keyset: MintKeys, - preference?: Preferences, + preference?: Preferences | Array, counter?: number, pubkey?: string, privkey?: string @@ -553,6 +557,9 @@ class CashuWallet { payload: SwapPayload; blindedMessages: BlindedTransaction; } { + if (isAmountPreferenceArray(preference)) { + preference = deprecatedAmountPreferences(preference); + } const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount, diff --git a/src/legacy/cashu-ts.ts b/src/legacy/cashu-ts.ts new file mode 100644 index 000000000..0a6c16b5e --- /dev/null +++ b/src/legacy/cashu-ts.ts @@ -0,0 +1,21 @@ +import { AmountPreference, Preferences } from '../model/types/index'; + +export const deprecatedAmountPreferences = function (pref: Array): Preferences { + console.warn("[DEPRECATION] Use `Preferences` instead of `Array`"); + return { sendPreference: pref }; +} + +export const isAmountPreference = function (obj: any): obj is AmountPreference { + return ( + typeof obj === 'object' && + obj !== null && + 'amount' in obj && + 'count' in obj && + typeof obj.amount === 'number' && + typeof obj.count === 'number' + ); +} + +export const isAmountPreferenceArray = function (preference?: any): preference is Array { + return Array.isArray(preference) && preference.every((item) => isAmountPreference(item)); +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 345e8456a..b65dbaa9c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,11 +45,13 @@ function splitAmount( return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); } +/* function isPowerOfTwo(number: number) { return number && !(number & (number - 1)); } +*/ -function hasCorrespondingKey(amount: number, keyset: Keys) { +function hasCorrespondingKey(amount: number, keyset: Keys): boolean { return amount in keyset; } @@ -60,7 +62,7 @@ function getPreference( ): Array { const chunks: Array = []; let accumulator = 0; - preferredAmounts.forEach((pa) => { + preferredAmounts.forEach((pa: AmountPreference) => { if (!hasCorrespondingKey(pa.amount, keyset)) { throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } From 85a1ea7cd1919ec9c81abbf04aa43f2d44da3df6 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 19 Sep 2024 13:34:16 +0200 Subject: [PATCH 053/246] npm run format --- src/legacy/cashu-ts.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/legacy/cashu-ts.ts b/src/legacy/cashu-ts.ts index 0a6c16b5e..056091456 100644 --- a/src/legacy/cashu-ts.ts +++ b/src/legacy/cashu-ts.ts @@ -1,21 +1,23 @@ import { AmountPreference, Preferences } from '../model/types/index'; export const deprecatedAmountPreferences = function (pref: Array): Preferences { - console.warn("[DEPRECATION] Use `Preferences` instead of `Array`"); - return { sendPreference: pref }; -} + console.warn('[DEPRECATION] Use `Preferences` instead of `Array`'); + return { sendPreference: pref }; +}; export const isAmountPreference = function (obj: any): obj is AmountPreference { - return ( - typeof obj === 'object' && - obj !== null && - 'amount' in obj && - 'count' in obj && - typeof obj.amount === 'number' && - typeof obj.count === 'number' - ); -} + return ( + typeof obj === 'object' && + obj !== null && + 'amount' in obj && + 'count' in obj && + typeof obj.amount === 'number' && + typeof obj.count === 'number' + ); +}; -export const isAmountPreferenceArray = function (preference?: any): preference is Array { - return Array.isArray(preference) && preference.every((item) => isAmountPreference(item)); -} \ No newline at end of file +export const isAmountPreferenceArray = function ( + preference?: any +): preference is Array { + return Array.isArray(preference) && preference.every((item) => isAmountPreference(item)); +}; From 6c0409a1ce858e499f60ceee319b440347317f8b Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:41:02 +0200 Subject: [PATCH 054/246] Apply suggestions from code review Co-authored-by: gandlafbtc <123852829+gandlafbtc@users.noreply.github.com> --- src/utils.ts | 4 ++-- test/utils.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index b65dbaa9c..bf0d0d313 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,7 @@ function splitAmount( value: number, keyset: Keys, amountPreference?: Array, - order?: string + isDesc?: boolean ): Array { const chunks: Array = []; if (amountPreference) { @@ -42,7 +42,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); + return chunks.sort((a, b) => ( isDesc ? b - a : a - b)); } /* diff --git a/test/utils.test.ts b/test/utils.test.ts index f54407ab6..8422f44d1 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -44,7 +44,7 @@ describe('test split custom amounts ', () => { }); const fiveTwelve: Array = [{ amount: 512, count: 2 }]; test('testing amount 518', async () => { - const chunks = utils.splitAmount(518, keys, fiveTwelve, 'desc'); + const chunks = utils.splitAmount(518, keys, fiveTwelve, true); expect(chunks).toStrictEqual([512, 4, 2]); }); const illegal: Array = [{ amount: 3, count: 2 }]; @@ -53,7 +53,7 @@ describe('test split custom amounts ', () => { }); const empty: Array = []; test('testing empty', async () => { - const chunks = utils.splitAmount(5, keys, empty, 'desc'); + const chunks = utils.splitAmount(5, keys, empty, true); expect(chunks).toStrictEqual([4, 1]); }); const undef = undefined; @@ -65,7 +65,7 @@ describe('test split custom amounts ', () => { describe('test split different key amount', () => { test('testing amount 68251', async () => { - const chunks = utils.splitAmount(68251, keys_base10, undefined, 'desc'); + const chunks = utils.splitAmount(68251, keys_base10, undefined, true); expect(chunks).toStrictEqual([ 10000, 10000, 10000, 10000, 10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 100, 100, 10, 10, 10, 10, 10, 1 From 4e4dc12a20f665ff489f6e0ca83fcdd205362f19 Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Sat, 21 Sep 2024 21:44:01 +0900 Subject: [PATCH 055/246] add pipeline to build RC from staging branch --- .github/workflows/nextVersion.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/nextVersion.yml diff --git a/.github/workflows/nextVersion.yml b/.github/workflows/nextVersion.yml new file mode 100644 index 000000000..964ce5baa --- /dev/null +++ b/.github/workflows/nextVersion.yml @@ -0,0 +1,24 @@ +name: Publish Package to npmjs +permissions: + contents: write + id-token: write +on: + push: + branches: + - staging +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + - run: npm i + - run: npm run compile + - run: npm version prerelease --preid=rc --no-git-tag-version + - run: git push + - run: npm publish --provenance --access public --tag next + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 013590caa397166bc52ba0114989f8370749ca8d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:38:16 +0200 Subject: [PATCH 056/246] working agian --- src/CashuWallet.ts | 13 ++++++++----- src/utils.ts | 15 +++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 163f404e3..8b5442d31 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -340,20 +340,23 @@ class CashuWallet { options?.privkey || options?.keysetId // these options require a swap ) { - console.log('>> yes swap'); + console.log(`>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}`); const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, true ); + const returnAmount = sumProofs(sendProofs) - amount; console.log( - `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)}` + `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)} | sendProofs amounts: ${sendProofs.map( + (p) => p.amount + )}` ); if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { const keyset = await this.getKeys(options?.keysetId); options.outputAmounts = { - keepAmounts: getKeepAmounts(options.proofsWeHave, amount, keyset.keys, 3), - sendAmounts: options?.outputAmounts?.sendAmounts || [] + keepAmounts: getKeepAmounts(keepProofsSelect, returnAmount, keyset.keys, 3), + sendAmounts: options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys) } } console.log(`keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}`) @@ -643,7 +646,7 @@ class CashuWallet { const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, keyset, - // options?.outputAmounts?.keepAmounts, + options?.outputAmounts?.keepAmounts, options?.counter, options?.pubkey ); diff --git a/src/utils.ts b/src/utils.ts index 0a8234377..2ed8c6234 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -52,7 +52,7 @@ function getKeepAmounts( // it tries to select amounts so that the proofs we have and the proofs we want reach the targetCount const amountsWeWant: Array = []; const amountsWeHave = proofsWeHave.map((p) => p.amount); - const sortedKeyAmounts = getKeysetAmounts(keys); + const sortedKeyAmounts = getKeysetAmounts(keys, 'asc'); sortedKeyAmounts.forEach((amt) => { const countWeHave = amountsWeHave.filter((a) => a === amt).length; const countWeWant = Math.floor(targetCount - countWeHave); @@ -70,18 +70,25 @@ function getKeepAmounts( remainingAmounts.forEach((amt) => { amountsWeWant.push(amt) }) } const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b); - console.log(`amountsWeHave: ${amountsWeHave}`) - console.log(`amountsWeWant: ${sortedAmountsWeWant}`); + console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`) + console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`) + console.log(`# getKeepAmounts: amountsWeWant: ${sortedAmountsWeWant}`); return sortedAmountsWeWant; } function isPowerOfTwo(number: number) { return number && !(number & (number - 1)); } -function getKeysetAmounts(keyset: Keys): Array { +function getKeysetAmounts(keyset: Keys, order = 'desc'): Array { + if (order == 'desc') { + return Object.keys(keyset) + .map((k) => parseInt(k)) + .sort((a, b) => b - a); + } return Object.keys(keyset) .map((k) => parseInt(k)) .sort((a, b) => a - b); + } function hasCorrespondingKey(amount: number, keyset: Keys) { From 2a5d834e353adcca8cc8ea8b43f7f3792b910e7c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:27:16 +0200 Subject: [PATCH 057/246] npm run format --- src/CashuMint.ts | 5 ++++- src/CashuWallet.ts | 45 +++++++++++++++++++++------------------- src/utils.ts | 16 +++++++------- test/integration.test.ts | 4 ---- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 6a65e2398..8df76b83d 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -51,7 +51,10 @@ class CashuMint { * @param mintUrl * @param customRequest */ - public static async getInfo(mintUrl: string, customRequest?: typeof request): Promise { + public static async getInfo( + mintUrl: string, + customRequest?: typeof request + ): Promise { const requestInstance = customRequest || request; const response = await requestInstance({ endpoint: joinUrls(mintUrl, '/v1/info') diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2f7712d5c..edd9291b6 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -151,21 +151,23 @@ class CashuWallet { /** * Choose a keyset to activate based on the lowest input fee - * + * * Note: this function will filter out deprecated base64 keysets - * + * * @param keysets keysets to choose from * @returns active keyset */ getActiveKeyset(keysets: Array): MintKeyset { - let activeKeysets = keysets.filter((k) => k.active) + let activeKeysets = keysets.filter((k) => k.active); // begin deprecated: if there are keyset IDs that are not hex strings, we need to filter them out const hexKeysets = activeKeysets.filter((k) => /^[0-9a-fA-F]+$/.test(k.id)); if (hexKeysets.length > 0) { activeKeysets = hexKeysets; } // end deprecated - const activeKeyset = activeKeysets.sort((a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0]; + const activeKeyset = activeKeysets.sort( + (a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0) + )[0]; if (!activeKeyset) { throw new Error('No active keyset found'); } @@ -216,9 +218,9 @@ class CashuWallet { // no keysetId was set, so we select an active keyset with the unit of the wallet with the lowest fees and use that const allKeysets = await this.mint.getKeySets(); - const keysetToActivate = this.getActiveKeyset(allKeysets.keysets) + const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); const keyset = await this.getKeys(keysetToActivate.id); - return keyset + return keyset; } /** @@ -333,7 +335,9 @@ class CashuWallet { options?.privkey || options?.keysetId // these options require a swap ) { - console.log(`>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}`); + console.log( + `>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}` + ); const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, @@ -341,18 +345,20 @@ class CashuWallet { ); const returnAmount = sumProofs(sendProofs) - amount; console.log( - `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs(sendProofs)} | sendProofs amounts: ${sendProofs.map( - (p) => p.amount - )}` + `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs( + sendProofs + )} | sendProofs amounts: ${sendProofs.map((p) => p.amount)}` ); if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { const keyset = await this.getKeys(options?.keysetId); options.outputAmounts = { keepAmounts: getKeepAmounts(keepProofsSelect, returnAmount, keyset.keys, 3), sendAmounts: options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys) - } + }; } - console.log(`keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}`) + console.log( + `keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` + ); const { returnChange, send } = await this.swap(amount, sendProofs, options); console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); @@ -453,7 +459,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -624,18 +630,15 @@ class CashuWallet { if (!options?.outputAmounts && options?.proofsWeHave) { options.outputAmounts = { - keepAmounts: getKeepAmounts( - options.proofsWeHave, - amount, - keyset.keys, - 3 - ), + keepAmounts: getKeepAmounts(options.proofsWeHave, amount, keyset.keys, 3), sendAmounts: [] }; } console.log( - `outputAmounts: ${options?.outputAmounts?.keepAmounts - } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts + `outputAmounts: ${ + options?.outputAmounts?.keepAmounts + } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${ + options?.outputAmounts?.sendAmounts } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a, b) => a + b, 0)})` ); console.log(JSON.stringify(options?.outputAmounts)); diff --git a/src/utils.ts b/src/utils.ts index a7805f8a5..291785001 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -67,23 +67,24 @@ function getKeepAmounts( if (amountsWeWant.reduce((a, b) => a + b, 0) + amt > amountToKeep) { break; } - amountsWeWant.push(amt) + amountsWeWant.push(amt); } }); // use splitAmount to fill the rest between the sum of amountsWeHave and amountToKeep - const amountDiff = amountToKeep - amountsWeWant.reduce((a, b) => a + b, 0) + const amountDiff = amountToKeep - amountsWeWant.reduce((a, b) => a + b, 0); if (amountDiff) { - const remainingAmounts = splitAmount(amountDiff, keys) - remainingAmounts.forEach((amt) => { amountsWeWant.push(amt) }) + const remainingAmounts = splitAmount(amountDiff, keys); + remainingAmounts.forEach((amt) => { + amountsWeWant.push(amt); + }); } const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b); - console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`) - console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`) + console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`); + console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`); console.log(`# getKeepAmounts: amountsWeWant: ${sortedAmountsWeWant}`); return sortedAmountsWeWant; } - // function isPowerOfTwo(number: number) { // return number && !(number & (number - 1)); // } @@ -96,7 +97,6 @@ function getKeysetAmounts(keyset: Keys, order = 'desc'): Array { return Object.keys(keyset) .map((k: string) => parseInt(k)) .sort((a: number, b: number) => a - b); - } function hasCorrespondingKey(amount: number, keyset: Keys) { diff --git a/test/integration.test.ts b/test/integration.test.ts index 1dd9f161d..0c060f176 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -213,11 +213,7 @@ describe('mint api', () => { const result = await wallet .receive(encoded, { privkey: bytesToHex(privKeyAlice) }) .catch((e) => e); -<<<<<<< HEAD - expect(result).toEqual(new Error('Error receiving token: Error: Error receiving token entry')); -======= expect(result).toEqual(new Error('no valid signature provided for input.')); ->>>>>>> development const proofs = await wallet.receive(encoded, { privkey: bytesToHex(privKeyBob) }); From 7e72950e01da1718df6185b5ce474c69274195ba Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:30:32 +0200 Subject: [PATCH 058/246] remove empty file --- src/legacy/cashu-ts.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/legacy/cashu-ts.ts diff --git a/src/legacy/cashu-ts.ts b/src/legacy/cashu-ts.ts deleted file mode 100644 index e69de29bb..000000000 From cbca9b0b3a63f2acf5298f74caeb07670b18fccf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:47:59 +0200 Subject: [PATCH 059/246] send works with fees --- src/CashuWallet.ts | 159 +++++++++++++++++++++++---------------------- src/utils.ts | 8 +-- 2 files changed, 84 insertions(+), 83 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index edd9291b6..c32b96420 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -90,7 +90,7 @@ class CashuWallet { } else if (options?.keys && Array.isArray(options?.keys)) { keys = options?.keys; } - if (keys) keys.forEach((key) => this._keys.set(key.id, key)); + if (keys) keys.forEach((key: MintKeys) => this._keys.set(key.id, key)); if (options?.unit) this._unit = options?.unit; if (options?.keysets) this._keysets = options.keysets; if (!options?.mnemonicOrSeed) { @@ -158,15 +158,15 @@ class CashuWallet { * @returns active keyset */ getActiveKeyset(keysets: Array): MintKeyset { - let activeKeysets = keysets.filter((k) => k.active); + let activeKeysets = keysets.filter((k: MintKeyset) => k.active); // begin deprecated: if there are keyset IDs that are not hex strings, we need to filter them out - const hexKeysets = activeKeysets.filter((k) => /^[0-9a-fA-F]+$/.test(k.id)); + const hexKeysets = activeKeysets.filter((k: MintKeyset) => /^[0-9a-fA-F]+$/.test(k.id)); if (hexKeysets.length > 0) { activeKeysets = hexKeysets; } // end deprecated const activeKeyset = activeKeysets.sort( - (a, b) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0) + (a: MintKeyset, b: MintKeyset) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0) )[0]; if (!activeKeyset) { throw new Error('No active keyset found'); @@ -179,14 +179,14 @@ class CashuWallet { */ async getKeySets(): Promise> { const allKeysets = await this.mint.getKeySets(); - const unitKeysets = allKeysets.keysets.filter((k) => k.unit === this._unit); + const unitKeysets = allKeysets.keysets.filter((k: MintKeyset) => k.unit === this._unit); this._keysets = unitKeysets; return this._keysets; } async getAllKeys(): Promise> { const keysets = await this.mint.getKeys(); - this._keys = new Map(keysets.keysets.map((k) => [k.id, k])); + this._keys = new Map(keysets.keysets.map((k: MintKeys) => [k.id, k])); return keysets.keysets; } @@ -245,6 +245,7 @@ class CashuWallet { } ): Promise> { if (options?.preference) + // preference is only kept for backwards compatibility options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); if (typeof token === 'string') { token = getDecodedToken(token); @@ -263,7 +264,8 @@ class CashuWallet { /** * Receive a single cashu token entry * @param tokenEntry a single entry of a cashu token - * @param preference optional preference for splitting proofs into specific amounts. + * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. + * @param outputAmounts? optionally specify the output's amounts to keep. * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param privkey? will create a signature on the @param tokenEntry secrets if set @@ -281,9 +283,10 @@ class CashuWallet { } ): Promise> { if (options?.preference) + // preference is only kept for backwards compatibility options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const proofs: Array = []; - const amount = tokenEntry.proofs.reduce((total, curr) => total + curr.amount, 0); + const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0); const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSwapPayload( amount, @@ -319,8 +322,6 @@ class CashuWallet { offline?: boolean; } ): Promise { - if (options?.preference) - options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } @@ -338,27 +339,33 @@ class CashuWallet { console.log( `>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}` ); + // input selection const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, true ); - const returnAmount = sumProofs(sendProofs) - amount; - console.log( - `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs( - sendProofs - )} | sendProofs amounts: ${sendProofs.map((p) => p.amount)}` - ); - if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { - const keyset = await this.getKeys(options?.keysetId); - options.outputAmounts = { - keepAmounts: getKeepAmounts(keepProofsSelect, returnAmount, keyset.keys, 3), - sendAmounts: options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys) - }; - } - console.log( - `keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` - ); + // add keepProofsSelect to options?.proofsWeHave: + options?.proofsWeHave?.push(...keepProofsSelect); + + + + // const returnAmount = sumProofs(sendProofs) - amount; + // console.log( + // `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs( + // sendProofs + // )} | sendProofs amounts: ${sendProofs.map((p: Proof) => p.amount)}` + // ); + // if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { + // const keyset = await this.getKeys(options?.keysetId); + // options.outputAmounts = { + // keepAmounts: getKeepAmounts(keepProofsSelect, returnAmount, keyset.keys, 3), + // sendAmounts: options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys) + // }; + // } + // console.log( + // `keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` + // ); const { returnChange, send } = await this.swap(amount, sendProofs, options); console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); @@ -376,43 +383,27 @@ class CashuWallet { includeFees = false ): SendResponse { // heavy logging in this function - const sortedProofs = proofs.sort((a, b) => a.amount - b.amount); + const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs - .filter((p) => p.amount <= amountToSend) - .sort((a, b) => b.amount - a.amount); + .filter((p: Proof) => p.amount <= amountToSend) + .sort((a: Proof, b: Proof) => b.amount - a.amount); const biggerProofs = sortedProofs - .filter((p) => p.amount > amountToSend) - .sort((a, b) => a.amount - b.amount); + .filter((p: Proof) => p.amount > amountToSend) + .sort((a: Proof, b: Proof) => a.amount - b.amount); const nextBigger = biggerProofs[0]; - // console.log( - // `> enter | amountToSend: ${amountToSend} | proofs: ${sumProofs( - // proofs - // )} | smallerProofs: ${sumProofs(smallerProofs)} | nextBigger: ${nextBigger?.amount}` - // ); if (!smallerProofs.length && nextBigger) { - // console.log( - // `< [0] exit: no smallerProofs, nextBigger: ${nextBigger.amount} | keep: ${sumProofs( - // proofs.filter((p) => p.secret !== nextBigger.secret) - // )} | proofs: ${sumProofs(proofs)}` - // ); return { - returnChange: proofs.filter((p) => p.secret !== nextBigger.secret), + returnChange: proofs.filter((p: Proof) => p.secret !== nextBigger.secret), send: [nextBigger] }; } if (!smallerProofs.length && !nextBigger) { - // console.log( - // `< [1] exit: no smallerProofs, no nextBigger | amountToSend: ${amountToSend} | keep: ${sumProofs( - // proofs - // )}` - // ); return { returnChange: proofs, send: [] }; } let remainder = amountToSend; let selectedProofs = [smallerProofs[0]]; - // console.log(`Selected proof: ${smallerProofs[0].amount}`); const returnedProofs = []; const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= smallerProofs[0].amount - feePPK / 1000; @@ -422,30 +413,15 @@ class CashuWallet { remainder, includeFees ); - // if (!send.length) { - // send = [nextBigger]; - // console.log(`< exit [2.1] no send | send: ${sumProofs(send)} | returnChange: ${sumProofs(returnChange)}`); - // } selectedProofs.push(...send); returnedProofs.push(...returnChange); } if (sumProofs(selectedProofs) < amountToSend && nextBigger) { selectedProofs = [nextBigger]; - // console.log( - // `< exit [2.2] selecting nextBigger | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( - // selectedProofs - // )} | returnedProofs: ${sumProofs(returnedProofs)}` - // ); } - - // console.log( - // `< exit [2] selectProofsToSend | amountToSend: ${amountToSend} | selectedProofs: ${sumProofs( - // selectedProofs - // )} | returnedProofs: ${sumProofs(proofs.filter((p) => !selectedProofs.includes(p)))}` - // ); return { - returnChange: proofs.filter((p) => !selectedProofs.includes(p)), + returnChange: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs }; } @@ -454,12 +430,12 @@ class CashuWallet { const fees = Math.floor( Math.max( (proofs.reduce( - (total, curr) => - total + (this._keysets.find((k) => k.id === curr.id)?.input_fee_ppk || 0), + (total: number, curr: Proof) => + total + (this._keysets.find((k: MintKeyset) => k.id === curr.id)?.input_fee_ppk || 0), 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -485,17 +461,24 @@ class CashuWallet { options?: { preference?: Array; outputAmounts?: OutputAmounts; + proofsWeHave?: Array; counter?: number; pubkey?: string; privkey?: string; keysetId?: string; } ): Promise { - if (options?.preference) + if (!options) { + options = {}; + } + if (options.preference) options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); - const keyset = await this.getKeys(options?.keysetId); + const keyset = await this.getKeys(options.keysetId); const proofsToSend = proofs; + const amountToSend = amount const amountAvailable = sumProofs(proofs); + const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); + if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { console.log( `amount: ${amount} | fees: ${this.getFeesForProofs( @@ -504,8 +487,29 @@ class CashuWallet { ); throw new Error('Not enough funds available'); } - const amountToSend = amount + this.getFeesForProofs(proofsToSend); - const amountToKeep = sumProofs(proofsToSend) - amountToSend; + // output selection + if (options.proofsWeHave) { + console.log( + `proofsWeHave: ${sumProofs(options.proofsWeHave)} | sendProofs: ${sumProofs( + proofsToSend + )} | sendProofs amounts: ${proofsToSend.map((p: Proof) => p.amount)}` + ); + } + let keepAmounts; + if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { + keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3) + } else if (options.outputAmounts) { + keepAmounts = options.outputAmounts.keepAmounts; + } + const sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys); + options.outputAmounts = { + keepAmounts: keepAmounts, + sendAmounts: sendAmounts + }; + console.log( + `keepAmounts: ${keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` + ); + console.log(`>> amountToSend: ${amountToSend}`); const { payload, blindedMessages } = this.createSwapPayload( amountToSend, proofsToSend, @@ -522,11 +526,10 @@ class CashuWallet { blindedMessages.secrets, keyset ); - const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; let amountToKeepCounter = 0; - swapProofs.forEach((proof) => { + swapProofs.forEach((proof: Proof) => { if (amountToKeepCounter < amountToKeep) { amountToKeepCounter += proof.amount; splitProofsToKeep.push(proof); @@ -635,11 +638,9 @@ class CashuWallet { }; } console.log( - `outputAmounts: ${ - options?.outputAmounts?.keepAmounts - } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a, b) => a + b, 0)}) | ${ - options?.outputAmounts?.sendAmounts - } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a, b) => a + b, 0)})` + `outputAmounts: ${options?.outputAmounts?.keepAmounts + } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a: number, b: number) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts + } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a: number, b: number) => a + b, 0)})` ); console.log(JSON.stringify(options?.outputAmounts)); @@ -818,7 +819,7 @@ class CashuWallet { payload: SwapPayload; blindedMessages: BlindedTransaction; } { - const totalAmount = proofsToSend.reduce((total, curr) => total + curr.amount, 0); + const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts?.length) { outputAmounts.keepAmounts = splitAmount(totalAmount - amount, keyset.keys); } diff --git a/src/utils.ts b/src/utils.ts index 291785001..9710a8a4c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,9 +28,9 @@ function splitAmount( ): Array { const chunks: Array = []; if (split) { - if (split.reduce((a, b) => a + b, 0) > value) { + if (split.reduce((a: number, b: number) => a + b, 0) > value) { throw new Error( - `Split is greater than total amount: ${split.reduce((a, b) => a + b, 0)} > ${value}` + `Split is greater than total amount: ${split.reduce((a: number, b: number) => a + b, 0)} > ${value}` ); } chunks.push(...getPreference(value, keyset, split)); @@ -74,7 +74,7 @@ function getKeepAmounts( const amountDiff = amountToKeep - amountsWeWant.reduce((a, b) => a + b, 0); if (amountDiff) { const remainingAmounts = splitAmount(amountDiff, keys); - remainingAmounts.forEach((amt) => { + remainingAmounts.forEach((amt: number) => { amountsWeWant.push(amt); }); } @@ -288,7 +288,7 @@ export function sanitizeUrl(url: string): string { } export function sumProofs(proofs: Array) { - return proofs.reduce((acc, proof) => acc + proof.amount, 0); + return proofs.reduce((acc: number, proof: Proof) => acc + proof.amount, 0); } export { From 5d3563d848680fa856f0a468eba1e795da86deca Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:58:00 +0200 Subject: [PATCH 060/246] receive works with fees --- src/CashuWallet.ts | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index c32b96420..bf38811e4 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -239,6 +239,7 @@ class CashuWallet { keysetId?: string; preference?: Array; outputAmounts?: OutputAmounts; + proofsWeHave?: Array; counter?: number; pubkey?: string; privkey?: string; @@ -286,7 +287,7 @@ class CashuWallet { // preference is only kept for backwards compatibility options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const proofs: Array = []; - const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0); + const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSwapPayload( amount, @@ -297,7 +298,7 @@ class CashuWallet { options?.pubkey, options?.privkey ); - const { signatures } = await CashuMint.split(tokenEntry.mint, payload); + const { signatures } = await this.mint.split(payload); const newProofs = this.constructProofs( signatures, blindedMessages.rs, @@ -345,27 +346,7 @@ class CashuWallet { amount, true ); - // add keepProofsSelect to options?.proofsWeHave: options?.proofsWeHave?.push(...keepProofsSelect); - - - - // const returnAmount = sumProofs(sendProofs) - amount; - // console.log( - // `keepProofsSelect: ${sumProofs(keepProofsSelect)} | sendProofs: ${sumProofs( - // sendProofs - // )} | sendProofs amounts: ${sendProofs.map((p: Proof) => p.amount)}` - // ); - // if (options && !options?.outputAmounts?.keepAmounts && options?.proofsWeHave) { - // const keyset = await this.getKeys(options?.keysetId); - // options.outputAmounts = { - // keepAmounts: getKeepAmounts(keepProofsSelect, returnAmount, keyset.keys, 3), - // sendAmounts: options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys) - // }; - // } - // console.log( - // `keepAmounts: ${options?.outputAmounts?.keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` - // ); const { returnChange, send } = await this.swap(amount, sendProofs, options); console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); @@ -820,7 +801,7 @@ class CashuWallet { blindedMessages: BlindedTransaction; } { const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); - if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts?.length) { + if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { outputAmounts.keepAmounts = splitAmount(totalAmount - amount, keyset.keys); } const keepBlindedMessages = this.createRandomBlindedMessages( From fd8b0af8b126d40cf47d6e39466f5989d34932cf Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 2 Oct 2024 10:49:53 +0200 Subject: [PATCH 061/246] linter: removed typedef --- .eslintrc.json | 1 - src/utils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index b74d1793e..ffa38d654 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,7 +10,6 @@ "rules": { "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/typedef": ["error", { "parameter": true, "arrowParameter": true }], "@typescript-eslint/consistent-type-definitions": ["warn", "type"], "@typescript-eslint/array-type": ["error", { "default": "generic" }], "require-await": "off", diff --git a/src/utils.ts b/src/utils.ts index bf0d0d313..a1e72b7fe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -42,7 +42,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => ( isDesc ? b - a : a - b)); + return chunks.sort((a, b) => (isDesc ? b - a : a - b)); } /* From bded72022391d48ad2581c9c96f08540757b4e59 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 28 Aug 2024 13:41:17 +0200 Subject: [PATCH 062/246] added payment request en/decoding --- src/index.ts | 2 + src/model/PaymentRequest.ts | 63 ++++++++++++++++++++++++++++++++ src/model/types/wallet/tokens.ts | 20 ++++++++++ src/utils.ts | 24 ++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/model/PaymentRequest.ts diff --git a/src/index.ts b/src/index.ts index 530629f13..5d87a0015 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; +import { PaymentRequest } from './model/PaymentRequest.js'; import { setGlobalRequestOptions } from './request.js'; import { generateNewMnemonic, deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; import { getEncodedToken, getEncodedTokenV4, getDecodedToken, deriveKeysetId } from './utils.js'; @@ -9,6 +10,7 @@ export * from './model/types/index.js'; export { CashuMint, CashuWallet, + PaymentRequest, getDecodedToken, getEncodedToken, getEncodedTokenV4, diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts new file mode 100644 index 000000000..990006856 --- /dev/null +++ b/src/model/PaymentRequest.ts @@ -0,0 +1,63 @@ +import { encodeBase64toUint8 } from '../base64'; +import { decodeCBOR, encodeCBOR } from '../cbor'; +import { RawPaymentRequest, Transport } from './types'; + +export class PaymentRequest { + constructor( + public unit: string, + public transport: Array, + public memo: string, + public amount?: number, + public mint?: string, + public description?: string, + public lock?: string + ) {} + + toEncodedRequest() { + const rawRequest: RawPaymentRequest = { + u: this.unit, + t: this.transport.map((t) => ({ t: t.type, a: t.target })) + }; + if (this.lock) { + rawRequest.l = this.lock; + } + if (this.memo) { + rawRequest.m = this.memo; + } + if (this.mint) { + rawRequest.r = this.mint; + } + if (this.amount) { + rawRequest.a = this.amount; + } + if (this.description) { + rawRequest.d = this.description; + } + const data = encodeCBOR(rawRequest); + const encodedData = Buffer.from(data).toString('base64'); + return 'creq' + 'A' + encodedData; + } + + static fromEncodedRequest(encodedRequest: string): PaymentRequest { + const version = encodedRequest[4]; + if (version !== 'A') { + throw new Error('unsupported version...'); + } + const encodedData = encodedRequest.slice(5); + const data = encodeBase64toUint8(encodedData); + const decoded = decodeCBOR(data) as RawPaymentRequest; + if (!decoded.m) { + throw new Error('unsupported pr: memo undefined'); + } + const transports = decoded.t.map((t) => ({ type: t.t, target: t.a })); + return new PaymentRequest( + decoded.u, + transports, + decoded.m, + decoded.a, + decoded.r, + decoded.d, + decoded.l + ); + } +} diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index 636fe039d..e6f13d120 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -84,3 +84,23 @@ export type TokenV4Template = { */ u: string; }; + +export type RawTransport = { + t: string; + a: string; +}; + +export type RawPaymentRequest = { + a?: number; + u: string; + r?: string; + d?: string; + m?: string; + l?: string; + t: Array; +}; + +export type Transport = { + type: string; + target: string; +}; diff --git a/src/utils.ts b/src/utils.ts index bf0d0d313..90d56ca52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,6 +18,7 @@ import { TOKEN_PREFIX, TOKEN_VERSION } from './utils/Constants.js'; import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; +import { PaymentRequest } from './model/PaymentRequest.js'; function splitAmount( value: number, @@ -248,6 +249,29 @@ export function sanitizeUrl(url: string): string { return url.replace(/\/$/, ''); } +export function decodePaymentRequest(paymentRequest: string) { + const version = paymentRequest[4]; + if (version !== 'A') { + throw new Error('unsupported version...'); + } + const encodedData = paymentRequest.slice(5); + const data = encodeBase64toUint8(encodedData); + const decoded = decodeCBOR(data); + if (!decoded.m) { + throw new Error('unsupported pr: memo undefined'); + } + const transports = decoded.t.map((t: { t: string; a: string }) => ({ type: t.t, target: t.a })); + return new PaymentRequest( + decoded.u, + transports, + decoded.m, + decoded.a, + decoded.r, + decoded.d, + decoded.l + ); +} + export { bigIntStringify, bytesToNumber, From 1a11182333bcbefc06b1be2dfd6f1de7490d3bb9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:00:04 +0200 Subject: [PATCH 063/246] mintTokens and meltTokens are renamed to mintProofs and meltProofs --- README.md | 4 +- migration-1.0.0.md | 6 +- src/CashuWallet.ts | 88 ++++++++++++++--------------- src/model/types/wallet/responses.ts | 9 +-- src/utils.ts | 6 +- test/integration.test.ts | 20 +++---- test/wallet.test.ts | 12 ++-- 7 files changed, 71 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 1e1e6ca99..df416db34 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ const mintQuote = await wallet.createMintQuote(64); // pay the invoice here before you continue... const mintQuoteChecked = await wallet.checkMintQuote(mintQuote.quote); if (mintQuoteChecked.state == MintQuoteState.PAID) { - const { proofs } = await wallet.mintTokens(64, mintQuote.quote); + const { proofs } = await wallet.mintProofs(64, mintQuote.quote); } ``` @@ -88,7 +88,7 @@ const amountToSend = meltQuote.amount + meltQuote.fee_reserve; const { returnChange: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs); // store proofsToKeep in wallet .. -const meltResponse = await wallet.meltTokens(meltQuote, proofsToSend); +const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); // store meltResponse.change in wallet .. ``` diff --git a/migration-1.0.0.md b/migration-1.0.0.md index ecc6ac8ca..c91aa9b5a 100644 --- a/migration-1.0.0.md +++ b/migration-1.0.0.md @@ -46,9 +46,9 @@ type MintQuoteResponse = { }; ``` -where `request` is the invoice to be paid, and `quote` is the identifier used to pass to `mintTokens()`. +where `request` is the invoice to be paid, and `quote` is the identifier used to pass to `mintProofs()`. -**`requestTokens()` --> `mintTokens()`** +**`requestTokens()` --> `mintProofs()`** --- @@ -67,7 +67,7 @@ type MeltQuoteResponse = { }; ``` -where `quote` is the identifier to pass to `meltTokens()` +where `quote` is the identifier to pass to `meltProofs()` --- diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bf38811e4..8b6b752a8 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -8,7 +8,7 @@ import { type MeltQuoteResponse, type MintKeys, type MintKeyset, - type MeltTokensResponse, + type meltProofsResponse, type MintPayload, type Proof, type MintQuotePayload, @@ -337,9 +337,9 @@ class CashuWallet { options?.privkey || options?.keysetId // these options require a swap ) { - console.log( - `>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}` - ); + // console.log( + // `>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}` + // ); // input selection const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, @@ -348,12 +348,12 @@ class CashuWallet { ); options?.proofsWeHave?.push(...keepProofsSelect); const { returnChange, send } = await this.swap(amount, sendProofs, options); - console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); + // console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); const returnChangeProofs = keepProofsSelect.concat(returnChange); - console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); + // console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); return { returnChange: returnChangeProofs, send }; } - console.log('>> no swap'); + // console.log('>> no swap'); // console.log(`keepProofsOffline: ${sumProofs(keepProofsOffline)} | sendProofOffline: ${sumProofs(sendProofOffline)}`); return { returnChange: keepProofsOffline, send: sendProofOffline }; } @@ -461,21 +461,21 @@ class CashuWallet { const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { - console.log( - `amount: ${amount} | fees: ${this.getFeesForProofs( - proofsToSend - )} | amountAvailable: ${amountAvailable}` - ); + // console.log( + // `amount: ${amount} | fees: ${this.getFeesForProofs( + // proofsToSend + // )} | amountAvailable: ${amountAvailable}` + // ); throw new Error('Not enough funds available'); } // output selection - if (options.proofsWeHave) { - console.log( - `proofsWeHave: ${sumProofs(options.proofsWeHave)} | sendProofs: ${sumProofs( - proofsToSend - )} | sendProofs amounts: ${proofsToSend.map((p: Proof) => p.amount)}` - ); - } + // if (options.proofsWeHave) { + // console.log( + // `proofsWeHave: ${sumProofs(options.proofsWeHave)} | sendProofs: ${sumProofs( + // proofsToSend + // )} | sendProofs amounts: ${proofsToSend.map((p: Proof) => p.amount)}` + // ); + // } let keepAmounts; if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3) @@ -487,10 +487,10 @@ class CashuWallet { keepAmounts: keepAmounts, sendAmounts: sendAmounts }; - console.log( - `keepAmounts: ${keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` - ); - console.log(`>> amountToSend: ${amountToSend}`); + // console.log( + // `keepAmounts: ${keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` + // ); + // console.log(`>> amountToSend: ${amountToSend}`); const { payload, blindedMessages } = this.createSwapPayload( amountToSend, proofsToSend, @@ -585,7 +585,7 @@ class CashuWallet { } /** - * Mint tokens for a given mint quote + * Mint proofs for a given mint quote * @param amount amount to request * @param quote ID of mint quote * @param options.keysetId? optionally set keysetId for blank outputs for returned change. @@ -595,7 +595,7 @@ class CashuWallet { * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @returns proofs */ - async mintTokens( + async mintProofs( amount: number, quote: string, options?: { @@ -618,12 +618,12 @@ class CashuWallet { sendAmounts: [] }; } - console.log( - `outputAmounts: ${options?.outputAmounts?.keepAmounts - } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a: number, b: number) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts - } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a: number, b: number) => a + b, 0)})` - ); - console.log(JSON.stringify(options?.outputAmounts)); + // console.log( + // `outputAmounts: ${options?.outputAmounts?.keepAmounts + // } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a: number, b: number) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts + // } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a: number, b: number) => a + b, 0)})` + // ); + // console.log(JSON.stringify(options?.outputAmounts)); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, @@ -667,8 +667,8 @@ class CashuWallet { } /** - * Melt tokens for a melt quote. proofsToSend must be at least amount+fee_reserve form the melt quote. - * Returns payment proof and change proofs + * Melt proofs for a melt quote. proofsToSend must be at least amount+fee_reserve form the melt quote. + * Returns melt quote and change proofs * @param meltQuote ID of the melt quote * @param proofsToSend proofs to melt * @param options.keysetId? optionally set keysetId for blank outputs for returned change. @@ -676,7 +676,7 @@ class CashuWallet { * @param options.privkey? optionally set a private key to unlock P2PK locked secrets * @returns */ - async meltTokens( + async meltProofs( meltQuote: MeltQuoteResponse, proofsToSend: Array, options?: { @@ -684,7 +684,7 @@ class CashuWallet { counter?: number; privkey?: string; } - ): Promise { + ): Promise { const keys = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createBlankOutputs( meltQuote.fee_reserve, @@ -710,13 +710,13 @@ class CashuWallet { outputs: [...blindedMessages] }; const meltResponse = await this.mint.melt(meltPayload); - + let change: Array = []; + if (meltResponse.change) { + change = this.constructProofs(meltResponse.change, rs, secrets, keys) + } return { - isPaid: meltResponse.state === MeltQuoteState.PAID, - preimage: meltResponse.payment_preimage, - change: meltResponse?.change - ? this.constructProofs(meltResponse.change, rs, secrets, keys) - : [] + quote: meltResponse, + change: change }; } @@ -740,11 +740,11 @@ class CashuWallet { counter?: number; privkey?: string; } - ): Promise { + ): Promise { if (!meltQuote) { meltQuote = await this.mint.createMeltQuote({ unit: this._unit, request: invoice }); } - return await this.meltTokens(meltQuote, proofsToSend, { + return await this.meltProofs(meltQuote, proofsToSend, { keysetId: options?.keysetId, counter: options?.counter, privkey: options?.privkey @@ -767,7 +767,7 @@ class CashuWallet { keysetId?: string; counter?: number; } - ): Promise { + ): Promise { const decodedToken = getDecodedToken(token); const proofs = decodedToken.token .filter((x: TokenEntry) => x.mint === this.mint.mintUrl) diff --git a/src/model/types/wallet/responses.ts b/src/model/types/wallet/responses.ts index 957b90f06..f14918d4a 100644 --- a/src/model/types/wallet/responses.ts +++ b/src/model/types/wallet/responses.ts @@ -1,17 +1,14 @@ +import { MeltQuoteResponse } from '../mint'; import { Proof, Token } from './index'; /** * Response after paying a Lightning invoice */ -export type MeltTokensResponse = { +export type meltProofsResponse = { /** * if false, the proofs have not been invalidated and the payment can be tried later again with the same proofs */ - isPaid: boolean; - /** - * preimage of the paid invoice. can be null, depending on which LN-backend the mint uses - */ - preimage: string | null; + quote: MeltQuoteResponse; /** * Return/Change from overpaid fees. This happens due to Lighting fee estimation being inaccurate */ diff --git a/src/utils.ts b/src/utils.ts index 9710a8a4c..b650363ff 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -79,9 +79,9 @@ function getKeepAmounts( }); } const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b); - console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`); - console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`); - console.log(`# getKeepAmounts: amountsWeWant: ${sortedAmountsWeWant}`); + // console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`); + // console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`); + // console.log(`# getKeepAmounts: amountsWeWant: ${sortedAmountsWeWant}`); return sortedAmountsWeWant; } diff --git a/test/integration.test.ts b/test/integration.test.ts index 0c060f176..11bab234c 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -47,7 +47,7 @@ describe('mint api', () => { const request = await wallet.createMintQuote(1337); expect(request).toBeDefined(); expect(request.request).toContain('lnbc1337'); - const tokens = await wallet.mintTokens(1337, request.quote); + const tokens = await wallet.mintProofs(1337, request.quote); expect(tokens).toBeDefined(); // expect that the sum of all tokens.proofs.amount is equal to the requested amount expect(tokens.proofs.reduce((a, b) => a + b.amount, 0)).toBe(1337); @@ -80,7 +80,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintTokens(100, request.quote); + const tokens = await wallet.mintProofs(100, request.quote); // expect no fee because local invoice const mintQuote = await wallet.createMintQuote(10); @@ -112,7 +112,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(3000); - const tokens = await wallet.mintTokens(3000, request.quote); + const tokens = await wallet.mintProofs(3000, request.quote); const meltQuote = await wallet.createMeltQuote(externalInvoice); const fee = meltQuote.fee_reserve; @@ -143,7 +143,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintTokens(64, request.quote); + const tokens = await wallet.mintProofs(64, request.quote); const sendResponse = await wallet.send(64, tokens.proofs); expect(sendResponse).toBeDefined(); @@ -157,7 +157,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintTokens(100, request.quote); + const tokens = await wallet.mintProofs(100, request.quote); const sendResponse = await wallet.send(10, tokens.proofs); expect(sendResponse).toBeDefined(); @@ -172,7 +172,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintTokens(100, request.quote); + const tokens = await wallet.mintProofs(100, request.quote); const sendResponse = await wallet.send(10, tokens.proofs); const encoded = getEncodedToken({ @@ -185,7 +185,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintTokens(64, request.quote); + const tokens = await wallet.mintProofs(64, request.quote); const encoded = getEncodedToken({ token: [{ mint: mintUrl, proofs: tokens.proofs }] }); @@ -203,7 +203,7 @@ describe('mint api', () => { const pubKeyBob = secp256k1.getPublicKey(privKeyBob); const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintTokens(64, request.quote); + const tokens = await wallet.mintProofs(64, request.quote); const { send } = await wallet.send(64, tokens.proofs, { pubkey: bytesToHex(pubKeyBob) }); const encoded = getEncodedToken({ @@ -233,14 +233,14 @@ describe('mint api', () => { const mintRequest = await wallet.createMintQuote(3000); - const proofs = await wallet.mintTokens(3000, mintRequest.quote, { + const proofs = await wallet.mintProofs(3000, mintRequest.quote, { pubkey: bytesToHex(pubKeyBob) }); const meltRequest = await wallet.createMeltQuote(externalInvoice); const fee = meltRequest.fee_reserve; expect(fee).toBeGreaterThan(0); - const response = await wallet.meltTokens(meltRequest, proofs.proofs, { + const response = await wallet.meltProofs(meltRequest, proofs.proofs, { privkey: bytesToHex(privKeyBob) }); expect(response).toBeDefined(); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 09c0742e9..0041984d0 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,7 +1,7 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { MeltQuoteResponse, ReceiveResponse } from '../src/model/types/index.js'; +import { MeltQuoteResponse, MeltQuoteState, ReceiveResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; import { AmountPreference } from '../src/model/types/index'; import { Proof } from '@cashu/crypto/modules/common'; @@ -260,7 +260,7 @@ describe('payLnInvoice', () => { const result = await wallet.payLnInvoice(invoice, proofs, meltQuote); - expect(result).toEqual({ isPaid: true, preimage: null, change: [] }); + expect(result).toEqual({ quote: meltQuote, change: [] }); }); test('test payLnInvoice change', async () => { nock(mintUrl) @@ -293,8 +293,8 @@ describe('payLnInvoice', () => { const meltQuote = await wallet.checkMeltQuote('test'); const result = await wallet.payLnInvoice(invoice, [{ ...proofs[0], amount: 3 }], meltQuote); - expect(result.isPaid).toBe(true); - expect(result.preimage).toBe('asd'); + expect(result.quote.state == MeltQuoteState.PAID).toBe(true); + expect(result.quote.payment_preimage).toBe('asd'); expect(result.change).toHaveLength(1); }); test('test payLnInvoice bad resonse', async () => { @@ -323,7 +323,7 @@ describe('requestTokens', () => { }); const wallet = new CashuWallet(mint, { unit }); - const { proofs } = await wallet.mintTokens(1, ''); + const { proofs } = await wallet.mintProofs(1, ''); expect(proofs).toHaveLength(1); expect(proofs[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); @@ -334,7 +334,7 @@ describe('requestTokens', () => { nock(mintUrl).post('/v1/mint/bolt11').reply(200, {}); const wallet = new CashuWallet(mint, { unit }); - const result = await wallet.mintTokens(1, '').catch((e) => e); + const result = await wallet.mintProofs(1, '').catch((e) => e); expect(result).toEqual(new Error('bad response')); }); From 568f07d8485e7573aab6c67a046d650a54d58d91 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:06:29 +0200 Subject: [PATCH 064/246] returnChange -> keep --- README.md | 2 +- src/CashuWallet.ts | 55 +++++++---------------------- src/model/types/wallet/responses.ts | 2 +- test/integration.test.ts | 26 +++++++------- test/wallet.test.ts | 24 ++++++------- 5 files changed, 39 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index df416db34..d679e97d4 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ const amountToSend = meltQuote.amount + meltQuote.fee_reserve; // in a real wallet, we would coin select the correct amount of proofs from the wallet's storage // instead of that, here we swap `proofs` with the mint to get the correct amount of proofs -const { returnChange: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs); +const { keep: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs); // store proofsToKeep in wallet .. const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8b6b752a8..0148c4a88 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,7 +20,6 @@ import { type TokenEntry, CheckStateEnum, SerializedBlindedSignature, - MeltQuoteState, GetInfoResponse, OutputAmounts, CheckStateEntry, @@ -326,7 +325,7 @@ class CashuWallet { if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } - const { returnChange: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( + const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, amount ); @@ -337,25 +336,18 @@ class CashuWallet { options?.privkey || options?.keysetId // these options require a swap ) { - // console.log( - // `>> yes swap | sendProofOffline: ${sumProofs(sendProofOffline)} | amount: ${amount}` - // ); // input selection - const { returnChange: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( + const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, true ); options?.proofsWeHave?.push(...keepProofsSelect); - const { returnChange, send } = await this.swap(amount, sendProofs, options); - // console.log(`returnChange: ${sumProofs(returnChange)} | send: ${sumProofs(send)}`); - const returnChangeProofs = keepProofsSelect.concat(returnChange); - // console.log(`returnChangeProofs: ${sumProofs(returnChangeProofs)}`); - return { returnChange: returnChangeProofs, send }; + const { keep, send } = await this.swap(amount, sendProofs, options); + const keepProofs = keepProofsSelect.concat(keep); + return { keep: keepProofs, send }; } - // console.log('>> no swap'); - // console.log(`keepProofsOffline: ${sumProofs(keepProofsOffline)} | sendProofOffline: ${sumProofs(sendProofOffline)}`); - return { returnChange: keepProofsOffline, send: sendProofOffline }; + return { keep: keepProofsOffline, send: sendProofOffline }; } private selectProofsToSend( @@ -363,7 +355,6 @@ class CashuWallet { amountToSend: number, includeFees = false ): SendResponse { - // heavy logging in this function const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -374,13 +365,13 @@ class CashuWallet { const nextBigger = biggerProofs[0]; if (!smallerProofs.length && nextBigger) { return { - returnChange: proofs.filter((p: Proof) => p.secret !== nextBigger.secret), + keep: proofs.filter((p: Proof) => p.secret !== nextBigger.secret), send: [nextBigger] }; } if (!smallerProofs.length && !nextBigger) { - return { returnChange: proofs, send: [] }; + return { keep: proofs, send: [] }; } let remainder = amountToSend; @@ -389,20 +380,20 @@ class CashuWallet { const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= smallerProofs[0].amount - feePPK / 1000; if (remainder > 0) { - const { returnChange, send } = this.selectProofsToSend( + const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), remainder, includeFees ); selectedProofs.push(...send); - returnedProofs.push(...returnChange); + returnedProofs.push(...keep); } if (sumProofs(selectedProofs) < amountToSend && nextBigger) { selectedProofs = [nextBigger]; } return { - returnChange: proofs.filter((p: Proof) => !selectedProofs.includes(p)), + keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs }; } @@ -461,21 +452,9 @@ class CashuWallet { const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { - // console.log( - // `amount: ${amount} | fees: ${this.getFeesForProofs( - // proofsToSend - // )} | amountAvailable: ${amountAvailable}` - // ); throw new Error('Not enough funds available'); } // output selection - // if (options.proofsWeHave) { - // console.log( - // `proofsWeHave: ${sumProofs(options.proofsWeHave)} | sendProofs: ${sumProofs( - // proofsToSend - // )} | sendProofs amounts: ${proofsToSend.map((p: Proof) => p.amount)}` - // ); - // } let keepAmounts; if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3) @@ -487,10 +466,6 @@ class CashuWallet { keepAmounts: keepAmounts, sendAmounts: sendAmounts }; - // console.log( - // `keepAmounts: ${keepAmounts} | sendAmounts: ${options?.outputAmounts?.sendAmounts}` - // ); - // console.log(`>> amountToSend: ${amountToSend}`); const { payload, blindedMessages } = this.createSwapPayload( amountToSend, proofsToSend, @@ -519,7 +494,7 @@ class CashuWallet { splitProofsToSend.push(proof); }); return { - returnChange: splitProofsToKeep, + keep: splitProofsToKeep, send: splitProofsToSend }; } @@ -618,12 +593,6 @@ class CashuWallet { sendAmounts: [] }; } - // console.log( - // `outputAmounts: ${options?.outputAmounts?.keepAmounts - // } (sum: ${options?.outputAmounts?.keepAmounts?.reduce((a: number, b: number) => a + b, 0)}) | ${options?.outputAmounts?.sendAmounts - // } (sum: ${options?.outputAmounts?.sendAmounts.reduce((a: number, b: number) => a + b, 0)})` - // ); - // console.log(JSON.stringify(options?.outputAmounts)); const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( amount, diff --git a/src/model/types/wallet/responses.ts b/src/model/types/wallet/responses.ts index f14918d4a..6d830784d 100644 --- a/src/model/types/wallet/responses.ts +++ b/src/model/types/wallet/responses.ts @@ -36,7 +36,7 @@ export type SendResponse = { /** * Proofs that exceeded the needed amount */ - returnChange: Array; + keep: Array; /** * Proofs to be sent, matching the chosen amount */ diff --git a/test/integration.test.ts b/test/integration.test.ts index 11bab234c..0b4c42138 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -103,10 +103,10 @@ describe('mint api', () => { expect(sentProofsSpent).toBeDefined(); // expect that all proofs are spent, i.e. sendProofsSpent == sendResponse.send expect(sentProofsSpent).toEqual(sendResponse.send); - // expect none of the sendResponse.returnChange to be spent - const returnChangeSpent = await wallet.checkProofsSpent(sendResponse.returnChange); - expect(returnChangeSpent).toBeDefined(); - expect(returnChangeSpent).toEqual([]); + // expect none of the sendResponse.keep to be spent + const keepSpent = await wallet.checkProofsSpent(sendResponse.keep); + expect(keepSpent).toBeDefined(); + expect(keepSpent).toEqual([]); }); test('pay external invoice', async () => { const mint = new CashuMint(mintUrl); @@ -134,10 +134,10 @@ describe('mint api', () => { expect(sentProofsSpent).toBeDefined(); // expect that all proofs are spent, i.e. sendProofsSpent == sendResponse.send expect(sentProofsSpent).toEqual(sendResponse.send); - // expect none of the sendResponse.returnChange to be spent - const returnChangeSpent = await wallet.checkProofsSpent(sendResponse.returnChange); - expect(returnChangeSpent).toBeDefined(); - expect(returnChangeSpent).toEqual([]); + // expect none of the sendResponse.keep to be spent + const keepSpent = await wallet.checkProofsSpent(sendResponse.keep); + expect(keepSpent).toBeDefined(); + expect(keepSpent).toEqual([]); }); test('test send tokens exact without previous split', async () => { const mint = new CashuMint(mintUrl); @@ -148,9 +148,9 @@ describe('mint api', () => { const sendResponse = await wallet.send(64, tokens.proofs); expect(sendResponse).toBeDefined(); expect(sendResponse.send).toBeDefined(); - expect(sendResponse.returnChange).toBeDefined(); + expect(sendResponse.keep).toBeDefined(); expect(sendResponse.send.length).toBe(1); - expect(sendResponse.returnChange.length).toBe(0); + expect(sendResponse.keep.length).toBe(0); expect(sumProofs(sendResponse.send)).toBe(64); }); test('test send tokens with change', async () => { @@ -162,11 +162,11 @@ describe('mint api', () => { const sendResponse = await wallet.send(10, tokens.proofs); expect(sendResponse).toBeDefined(); expect(sendResponse.send).toBeDefined(); - expect(sendResponse.returnChange).toBeDefined(); + expect(sendResponse.keep).toBeDefined(); expect(sendResponse.send.length).toBe(2); - expect(sendResponse.returnChange.length).toBe(5); + expect(sendResponse.keep.length).toBe(5); expect(sumProofs(sendResponse.send)).toBe(10); - expect(sumProofs(sendResponse.returnChange)).toBe(90); + expect(sumProofs(sendResponse.keep)).toBe(90); }, 10000000); test('receive tokens with previous split', async () => { const mint = new CashuMint(mintUrl); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 0041984d0..32f0f1c73 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -365,7 +365,7 @@ describe('send', () => { const result = await wallet.send(1, proofs); - expect(result.returnChange).toHaveLength(0); + expect(result.keep).toHaveLength(0); expect(result.send).toHaveLength(1); expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); expect(/[0-9a-f]{64}/.test(result.send[0].C)).toBe(true); @@ -403,10 +403,10 @@ describe('send', () => { expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); expect(/[0-9a-f]{64}/.test(result.send[0].C)).toBe(true); expect(/[0-9a-f]{64}/.test(result.send[0].secret)).toBe(true); - expect(result.returnChange).toHaveLength(1); - expect(result.returnChange[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); - expect(/[0-9a-f]{64}/.test(result.returnChange[0].C)).toBe(true); - expect(/[0-9a-f]{64}/.test(result.returnChange[0].secret)).toBe(true); + expect(result.keep).toHaveLength(1); + expect(result.keep[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); + expect(/[0-9a-f]{64}/.test(result.keep[0].C)).toBe(true); + expect(/[0-9a-f]{64}/.test(result.keep[0].secret)).toBe(true); }); test('test send over paying2', async () => { @@ -442,10 +442,10 @@ describe('send', () => { expect(result.send[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); expect(/[0-9a-f]{64}/.test(result.send[0].C)).toBe(true); expect(/[0-9a-f]{64}/.test(result.send[0].secret)).toBe(true); - expect(result.returnChange).toHaveLength(1); - expect(result.returnChange[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); - expect(/[0-9a-f]{64}/.test(result.returnChange[0].C)).toBe(true); - expect(/[0-9a-f]{64}/.test(result.returnChange[0].secret)).toBe(true); + expect(result.keep).toHaveLength(1); + expect(result.keep[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); + expect(/[0-9a-f]{64}/.test(result.keep[0].C)).toBe(true); + expect(/[0-9a-f]{64}/.test(result.keep[0].secret)).toBe(true); }); test('test send preference', async () => { nock(mintUrl) @@ -502,7 +502,7 @@ describe('send', () => { expect(result.send[3]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); expect(/[0-9a-f]{64}/.test(result.send[0].C)).toBe(true); expect(/[0-9a-f]{64}/.test(result.send[0].secret)).toBe(true); - expect(result.returnChange).toHaveLength(0); + expect(result.keep).toHaveLength(0); }); test('test send preference overpay', async () => { @@ -559,8 +559,8 @@ describe('send', () => { expect(result.send[2]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); expect(/[0-9a-f]{64}/.test(result.send[0].C)).toBe(true); expect(/[0-9a-f]{64}/.test(result.send[0].secret)).toBe(true); - expect(result.returnChange).toHaveLength(1); - expect(result.returnChange[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); + expect(result.keep).toHaveLength(1); + expect(result.keep[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); }); test('test send not enough funds', async () => { From 55e6acc6016dbe99230767e54591bdfa3104a0ea Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:14:28 +0200 Subject: [PATCH 065/246] create blank outputs for entire overpaid amount, not only for fee_reserve --- migration-1.0.0.md | 10 ++++++++++ src/CashuWallet.ts | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/migration-1.0.0.md b/migration-1.0.0.md index c91aa9b5a..69d521f7c 100644 --- a/migration-1.0.0.md +++ b/migration-1.0.0.md @@ -119,6 +119,16 @@ type BlindedMessage { --- +**`amountPreference`** is not used anymore. + +`preference?: Array;` -> `outputAmounts?: OutputAmounts;` + +- in `SendResponse`, `returnChange` is now called `keep` +- `mintTokens` is now called `mintProofs` +- `meltTokens` is now called `meltProofs` + +--- + ### Pattern changes **removed `newKeys` from returns**: Functions no longer return `newKeys`. Wallets now specify the keyset they use in the BlindedMessage via the `id` field. diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 0148c4a88..02a0f3718 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -636,7 +636,7 @@ class CashuWallet { } /** - * Melt proofs for a melt quote. proofsToSend must be at least amount+fee_reserve form the melt quote. + * Melt proofs for a melt quote. proofsToSend must be at least amount+fee_reserve form the melt quote. This function does not perform coin selection!. * Returns melt quote and change proofs * @param meltQuote ID of the melt quote * @param proofsToSend proofs to melt @@ -656,7 +656,7 @@ class CashuWallet { ): Promise { const keys = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createBlankOutputs( - meltQuote.fee_reserve, + sumProofs(proofsToSend) - meltQuote.amount, keys.id, options?.counter ); @@ -910,17 +910,17 @@ class CashuWallet { /** * Creates NUT-08 blank outputs (fee returns) for a given fee reserve * See: https://github.com/cashubtc/nuts/blob/main/08.md - * @param feeReserve amount to cover with blank outputs + * @param amount amount to cover with blank outputs * @param keysetId mint keysetId * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @returns blinded messages, secrets, and rs */ private createBlankOutputs( - feeReserve: number, + amount: number, keysetId: string, counter?: number ): BlindedMessageData { - let count = Math.ceil(Math.log2(feeReserve)) || 1; + let count = Math.ceil(Math.log2(amount)) || 1; //Prevent count from being -Infinity if (count < 0) { count = 0; From cb54e70a5de8c51f5c506205540ca7ee242df2b3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:16:50 +0200 Subject: [PATCH 066/246] rename CashuMint.split to CashuMint.swap --- migration-1.0.0.md | 1 + src/CashuMint.ts | 6 +++--- src/CashuWallet.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/migration-1.0.0.md b/migration-1.0.0.md index 69d521f7c..aad4c4933 100644 --- a/migration-1.0.0.md +++ b/migration-1.0.0.md @@ -126,6 +126,7 @@ type BlindedMessage { - in `SendResponse`, `returnChange` is now called `keep` - `mintTokens` is now called `mintProofs` - `meltTokens` is now called `meltProofs` +- `CashuMint.split` is now called `CashuMint.swap` --- diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 8df76b83d..b86a07eba 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -76,7 +76,7 @@ class CashuMint { * @param customRequest * @returns signed outputs */ - public static async split( + public static async swap( mintUrl: string, swapPayload: SwapPayload, customRequest?: typeof request @@ -99,8 +99,8 @@ class CashuMint { * @param swapPayload payload containing inputs and outputs * @returns signed outputs */ - async split(swapPayload: SwapPayload): Promise { - return CashuMint.split(this._mintUrl, swapPayload, this._customRequest); + async swap(swapPayload: SwapPayload): Promise { + return CashuMint.swap(this._mintUrl, swapPayload, this._customRequest); } /** diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 02a0f3718..4fee29b3d 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -297,7 +297,7 @@ class CashuWallet { options?.pubkey, options?.privkey ); - const { signatures } = await this.mint.split(payload); + const { signatures } = await this.mint.swap(payload); const newProofs = this.constructProofs( signatures, blindedMessages.rs, @@ -475,7 +475,7 @@ class CashuWallet { options?.pubkey, options?.privkey ); - const { signatures } = await this.mint.split(payload); + const { signatures } = await this.mint.swap(payload); const swapProofs = this.constructProofs( signatures, blindedMessages.rs, From fd4ef99a2c4f02855dd26aa76f54ab71d2554a95 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:24:32 +0200 Subject: [PATCH 067/246] remove amountPreference and replace with outputAmounts --- src/CashuWallet.ts | 23 ++--------------------- src/model/types/index.ts | 4 ---- src/utils.ts | 12 ------------ test/wallet.test.ts | 5 ++--- 4 files changed, 4 insertions(+), 40 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 4fee29b3d..40998d833 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -22,15 +22,13 @@ import { SerializedBlindedSignature, GetInfoResponse, OutputAmounts, - CheckStateEntry, - AmountPreference + CheckStateEntry } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, - deprecatedPreferenceToOutputAmounts, getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; @@ -236,7 +234,6 @@ class CashuWallet { token: string | Token, options?: { keysetId?: string; - preference?: Array; outputAmounts?: OutputAmounts; proofsWeHave?: Array; counter?: number; @@ -244,9 +241,6 @@ class CashuWallet { privkey?: string; } ): Promise> { - if (options?.preference) - // preference is only kept for backwards compatibility - options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); if (typeof token === 'string') { token = getDecodedToken(token); } @@ -275,16 +269,12 @@ class CashuWallet { tokenEntry: TokenEntry, options?: { keysetId?: string; - preference?: Array; outputAmounts?: OutputAmounts; counter?: number; pubkey?: string; privkey?: string; } ): Promise> { - if (options?.preference) - // preference is only kept for backwards compatibility - options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const proofs: Array = []; const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); const keys = await this.getKeys(options?.keysetId); @@ -312,7 +302,6 @@ class CashuWallet { amount: number, proofs: Array, options?: { - preference?: Array; outputAmounts?: OutputAmounts; proofsWeHave?: Array; counter?: number; @@ -431,7 +420,6 @@ class CashuWallet { amount: number, proofs: Array, options?: { - preference?: Array; outputAmounts?: OutputAmounts; proofsWeHave?: Array; counter?: number; @@ -443,8 +431,6 @@ class CashuWallet { if (!options) { options = {}; } - if (options.preference) - options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); const keyset = await this.getKeys(options.keysetId); const proofsToSend = proofs; const amountToSend = amount @@ -575,7 +561,6 @@ class CashuWallet { quote: string, options?: { keysetId?: string; - preference?: Array; outputAmounts?: OutputAmounts; proofsWeHave?: Array; counter?: number; @@ -583,10 +568,6 @@ class CashuWallet { } ): Promise<{ proofs: Array }> { const keyset = await this.getKeys(options?.keysetId); - - if (options?.preference) - options.outputAmounts = deprecatedPreferenceToOutputAmounts(options.preference); - if (!options?.outputAmounts && options?.proofsWeHave) { options.outputAmounts = { keepAmounts: getKeepAmounts(options.proofsWeHave, amount, keyset.keys, 3), @@ -843,7 +824,7 @@ class CashuWallet { /** * Creates blinded messages for a given amount * @param amount amount to create blinded messages for - * @param amountPreference optional preference for splitting proofs into specific amounts. overrides amount param + * @param split optional preference for splitting proofs into specific amounts. overrides amount param * @param keyksetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 2e08cf9ee..8203aee60 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -7,10 +7,6 @@ export type OutputAmounts = { }; // deprecated -export type AmountPreference = { - amount: number; - count: number; -}; export type InvoiceData = { paymentRequest: string; diff --git a/src/utils.ts b/src/utils.ts index b650363ff..3c264f8e3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,8 +5,6 @@ import { encodeUint8toBase64Url } from './base64.js'; import { - AmountPreference, - OutputAmounts, Keys, Proof, Token, @@ -114,15 +112,6 @@ function getPreference(amount: number, keyset: Keys, split: Array): Arra return chunks; } -function deprecatedPreferenceToOutputAmounts(preference?: Array): OutputAmounts { - const sendAmounts: Array = []; - preference?.forEach(({ count, amount }) => { - for (let i = 0; i < count; i++) { - sendAmounts.push(amount); - } - }); - return { sendAmounts }; -} function bytesToNumber(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(bytes)); } @@ -299,6 +288,5 @@ export { getEncodedTokenV4, hexToNumber, splitAmount, - deprecatedPreferenceToOutputAmounts, getKeepAmounts }; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 32f0f1c73..732c2494a 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,9 +1,8 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { MeltQuoteResponse, MeltQuoteState, ReceiveResponse } from '../src/model/types/index.js'; +import { MeltQuoteResponse, MeltQuoteState, OutputAmounts } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; -import { AmountPreference } from '../src/model/types/index'; import { Proof } from '@cashu/crypto/modules/common'; const dummyKeysResp = { @@ -176,7 +175,7 @@ describe('receive', () => { 'cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMSwgInNlY3JldCI6ICJlN2MxYjc2ZDFiMzFlMmJjYTJiMjI5ZDE2MGJkZjYwNDZmMzNiYzQ1NzAyMjIzMDRiNjUxMTBkOTI2ZjdhZjg5IiwgIkMiOiAiMDM4OWNkOWY0Zjk4OGUzODBhNzk4OWQ0ZDQ4OGE3YzkxYzUyNzdmYjkzMDQ3ZTdhMmNjMWVkOGUzMzk2Yjg1NGZmIn0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogImRlNTVjMTVmYWVmZGVkN2Y5Yzk5OWMzZDRjNjJmODFiMGM2ZmUyMWE3NTJmZGVmZjZiMDg0Y2YyZGYyZjVjZjMiLCAiQyI6ICIwMmRlNDBjNTlkOTAzODNiODg1M2NjZjNhNGIyMDg2NGFjODNiYTc1OGZjZTNkOTU5ZGJiODkzNjEwMDJlOGNlNDcifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCJ9XX0='; const proofs = await wallet.receive(token3sat, { - preference: [{ amount: 1, count: 3 }] + outputAmounts: { keepAmounts: [1, 1, 1], sendAmounts: [] } }); expect(proofs).toHaveLength(3); From 2b6e0bd017688a45991804f4d060bcd1e47b2ef9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:29:48 +0200 Subject: [PATCH 068/246] fix tests --- test/wallet.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 732c2494a..c8db26e88 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -175,7 +175,7 @@ describe('receive', () => { 'cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMSwgInNlY3JldCI6ICJlN2MxYjc2ZDFiMzFlMmJjYTJiMjI5ZDE2MGJkZjYwNDZmMzNiYzQ1NzAyMjIzMDRiNjUxMTBkOTI2ZjdhZjg5IiwgIkMiOiAiMDM4OWNkOWY0Zjk4OGUzODBhNzk4OWQ0ZDQ4OGE3YzkxYzUyNzdmYjkzMDQ3ZTdhMmNjMWVkOGUzMzk2Yjg1NGZmIn0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogImRlNTVjMTVmYWVmZGVkN2Y5Yzk5OWMzZDRjNjJmODFiMGM2ZmUyMWE3NTJmZGVmZjZiMDg0Y2YyZGYyZjVjZjMiLCAiQyI6ICIwMmRlNDBjNTlkOTAzODNiODg1M2NjZjNhNGIyMDg2NGFjODNiYTc1OGZjZTNkOTU5ZGJiODkzNjEwMDJlOGNlNDcifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzOCJ9XX0='; const proofs = await wallet.receive(token3sat, { - outputAmounts: { keepAmounts: [1, 1, 1], sendAmounts: [] } + outputAmounts: { keepAmounts: [], sendAmounts: [1, 1, 1] } }); expect(proofs).toHaveLength(3); From 29a4a93722d21480e1005ae1413001ad9330eace Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:06:33 +0200 Subject: [PATCH 069/246] fix --- test/integration.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 0b4c42138..c5cf199ed 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -5,6 +5,7 @@ import dns from 'node:dns'; import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; +import { MeltQuoteState } from '../src/model/types/index.js'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = @@ -244,6 +245,6 @@ describe('mint api', () => { privkey: bytesToHex(privKeyBob) }); expect(response).toBeDefined(); - expect(response.isPaid).toBe(true); + expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); }); From 824f0a096728c2d0f02febbca8bdcf48741d3462 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:58:50 +0200 Subject: [PATCH 070/246] changes according to https://github.com/cashubtc/nuts/pull/124/commits/1632d882f8e3a8de05a3303734ff97124a78a18f --- src/model/PaymentRequest.ts | 41 +++++++++++------------ src/model/types/wallet/index.ts | 1 + src/model/types/wallet/paymentRequests.ts | 26 ++++++++++++++ src/model/types/wallet/tokens.ts | 22 +----------- 4 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 src/model/types/wallet/paymentRequests.ts diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 990006856..fbdb2e05d 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -1,40 +1,38 @@ import { encodeBase64toUint8 } from '../base64'; import { decodeCBOR, encodeCBOR } from '../cbor'; -import { RawPaymentRequest, Transport } from './types'; +import { RawPaymentRequest, RawTransport, Transport } from './types'; export class PaymentRequest { constructor( - public unit: string, public transport: Array, - public memo: string, + public id?: string, public amount?: number, - public mint?: string, + public unit?: string, + public mints?: Array, public description?: string, - public lock?: string - ) {} + ) { } toEncodedRequest() { const rawRequest: RawPaymentRequest = { - u: this.unit, - t: this.transport.map((t) => ({ t: t.type, a: t.target })) + t: this.transport.map((t: Transport) => ({ t: t.type, a: t.target })) }; - if (this.lock) { - rawRequest.l = this.lock; - } - if (this.memo) { - rawRequest.m = this.memo; - } - if (this.mint) { - rawRequest.r = this.mint; + if (this.id) { + rawRequest.i = this.id; } if (this.amount) { rawRequest.a = this.amount; } + if (this.unit) { + rawRequest.u = this.unit; + } + if (this.mints) { + rawRequest.m = this.mints; + } if (this.description) { rawRequest.d = this.description; } const data = encodeCBOR(rawRequest); - const encodedData = Buffer.from(data).toString('base64'); + const encodedData = Buffer.from(data).toString('base64url'); return 'creq' + 'A' + encodedData; } @@ -49,15 +47,14 @@ export class PaymentRequest { if (!decoded.m) { throw new Error('unsupported pr: memo undefined'); } - const transports = decoded.t.map((t) => ({ type: t.t, target: t.a })); + const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a })); return new PaymentRequest( - decoded.u, transports, - decoded.m, + decoded.i, decoded.a, - decoded.r, + decoded.u, + decoded.m, decoded.d, - decoded.l ); } } diff --git a/src/model/types/wallet/index.ts b/src/model/types/wallet/index.ts index 93c085fca..3efc7d399 100644 --- a/src/model/types/wallet/index.ts +++ b/src/model/types/wallet/index.ts @@ -1,6 +1,7 @@ export * from './payloads'; export * from './responses'; export * from './tokens'; +export * from './paymentRequests'; export type AmountPreference = { amount: number; diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts new file mode 100644 index 000000000..ad66cddf7 --- /dev/null +++ b/src/model/types/wallet/paymentRequests.ts @@ -0,0 +1,26 @@ +export type RawTransport = { + t: PaymentRequestTransportType; + a: string; + g?: Array>; +}; + +export type RawPaymentRequest = { + i?: string; + a?: number; + u?: string; + r?: boolean; + m?: Array; + d?: string; + t: Array; +}; + +export type Transport = { + type: PaymentRequestTransportType; + target: string; + tags?: Array>; +}; + +export enum PaymentRequestTransportType { + POST = 'post', + NOSTR = 'nostr', +} \ No newline at end of file diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index e6f13d120..b6840e27f 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -83,24 +83,4 @@ export type TokenV4Template = { * Unit */ u: string; -}; - -export type RawTransport = { - t: string; - a: string; -}; - -export type RawPaymentRequest = { - a?: number; - u: string; - r?: string; - d?: string; - m?: string; - l?: string; - t: Array; -}; - -export type Transport = { - type: string; - target: string; -}; +}; \ No newline at end of file From 6a1df2d89e9a6dc7fd00020bdbfba727650eb62c Mon Sep 17 00:00:00 2001 From: gudnuf Date: Tue, 15 Oct 2024 11:18:21 -0700 Subject: [PATCH 071/246] fix payment request utils --- src/utils.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 90d56ca52..85ffb4bc3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,8 @@ import { AmountPreference, Keys, Proof, + RawPaymentRequest, + RawTransport, Token, TokenEntry, TokenV4Template, @@ -43,7 +45,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => ( isDesc ? b - a : a - b)); + return chunks.sort((a, b) => (isDesc ? b - a : a - b)); } /* @@ -256,20 +258,12 @@ export function decodePaymentRequest(paymentRequest: string) { } const encodedData = paymentRequest.slice(5); const data = encodeBase64toUint8(encodedData); - const decoded = decodeCBOR(data); + const decoded = decodeCBOR(data) as RawPaymentRequest; if (!decoded.m) { throw new Error('unsupported pr: memo undefined'); } - const transports = decoded.t.map((t: { t: string; a: string }) => ({ type: t.t, target: t.a })); - return new PaymentRequest( - decoded.u, - transports, - decoded.m, - decoded.a, - decoded.r, - decoded.d, - decoded.l - ); + const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); + return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); } export { From e6e42a1ef04865ca0c72a4fc1ac588e0e8ba81d2 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Tue, 15 Oct 2024 11:55:01 -0700 Subject: [PATCH 072/246] add `PaymentRequestPayload` and change `Transport` type name --- src/model/PaymentRequest.ts | 19 +++------- src/model/types/wallet/paymentRequests.ts | 46 ++++++++++++++--------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index fbdb2e05d..b3126ec31 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -1,20 +1,20 @@ import { encodeBase64toUint8 } from '../base64'; import { decodeCBOR, encodeCBOR } from '../cbor'; -import { RawPaymentRequest, RawTransport, Transport } from './types'; +import { RawPaymentRequest, RawTransport, PaymentRequestTransport } from './types'; export class PaymentRequest { constructor( - public transport: Array, + public transport: Array, public id?: string, public amount?: number, public unit?: string, public mints?: Array, - public description?: string, - ) { } + public description?: string + ) {} toEncodedRequest() { const rawRequest: RawPaymentRequest = { - t: this.transport.map((t: Transport) => ({ t: t.type, a: t.target })) + t: this.transport.map((t: PaymentRequestTransport) => ({ t: t.type, a: t.target })) }; if (this.id) { rawRequest.i = this.id; @@ -48,13 +48,6 @@ export class PaymentRequest { throw new Error('unsupported pr: memo undefined'); } const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a })); - return new PaymentRequest( - transports, - decoded.i, - decoded.a, - decoded.u, - decoded.m, - decoded.d, - ); + return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); } } diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts index ad66cddf7..766f31829 100644 --- a/src/model/types/wallet/paymentRequests.ts +++ b/src/model/types/wallet/paymentRequests.ts @@ -1,26 +1,38 @@ +import { Proof } from './index'; + export type RawTransport = { - t: PaymentRequestTransportType; - a: string; - g?: Array>; + t: PaymentRequestTransportType; + a: string; + g?: Array>; }; export type RawPaymentRequest = { - i?: string; - a?: number; - u?: string; - r?: boolean; - m?: Array; - d?: string; - t: Array; + i?: string; + a?: number; + u?: string; + r?: boolean; + m?: Array; + d?: string; + t: Array; +}; + +export type PaymentRequestTag = Array; + +export type PaymentRequestTransport = { + type: PaymentRequestTransportType; + target: string; + tags?: Array; }; -export type Transport = { - type: PaymentRequestTransportType; - target: string; - tags?: Array>; +export type PaymentRequestPayload = { + id?: string; + memo?: string; + unit: string; + mint: string; + proofs: Array; }; export enum PaymentRequestTransportType { - POST = 'post', - NOSTR = 'nostr', -} \ No newline at end of file + POST = 'post', + NOSTR = 'nostr' +} From 736088f3419d173a9aa9ac9f8727fc5b992a3841 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:07:56 +0200 Subject: [PATCH 073/246] import Buffer and use base64 instead of base64url for now (buffer error) --- src/model/PaymentRequest.ts | 3 ++- src/model/types/wallet/tokens.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index b3126ec31..2ca430ea1 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -1,6 +1,7 @@ import { encodeBase64toUint8 } from '../base64'; import { decodeCBOR, encodeCBOR } from '../cbor'; import { RawPaymentRequest, RawTransport, PaymentRequestTransport } from './types'; +import { Buffer } from 'buffer'; export class PaymentRequest { constructor( @@ -32,7 +33,7 @@ export class PaymentRequest { rawRequest.d = this.description; } const data = encodeCBOR(rawRequest); - const encodedData = Buffer.from(data).toString('base64url'); + const encodedData = Buffer.from(data).toString('base64'); return 'creq' + 'A' + encodedData; } diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index b6840e27f..636fe039d 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -83,4 +83,4 @@ export type TokenV4Template = { * Unit */ u: string; -}; \ No newline at end of file +}; From b7e9823471cd4a16160c80f7d4c1d7ef1a032e64 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:56:08 +0200 Subject: [PATCH 074/246] decodePaymentRequest parsing --- src/utils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 85ffb4bc3..c6edb4163 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,7 +41,7 @@ function splitAmount( .map((k) => parseInt(k)) .sort((a, b) => b - a); sortedKeyAmounts.forEach((amt) => { - let q = Math.floor(value / amt); + const q = Math.floor(value / amt); for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); @@ -252,16 +252,16 @@ export function sanitizeUrl(url: string): string { } export function decodePaymentRequest(paymentRequest: string) { + if (!paymentRequest.startsWith('creq')) { + throw new Error('unsupported pr: invalid prefix'); + } const version = paymentRequest[4]; if (version !== 'A') { - throw new Error('unsupported version...'); + throw new Error('unsupported pr version'); } const encodedData = paymentRequest.slice(5); const data = encodeBase64toUint8(encodedData); const decoded = decodeCBOR(data) as RawPaymentRequest; - if (!decoded.m) { - throw new Error('unsupported pr: memo undefined'); - } const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); } @@ -274,5 +274,5 @@ export { getEncodedTokenV4, hexToNumber, splitAmount, - getDefaultAmountPreference + getDefaultAmountPreference, }; From c64ab6e752ff87edb4cfb8c095c2658cc6ce4293 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Tue, 15 Oct 2024 12:58:36 -0700 Subject: [PATCH 075/246] export decodePaymentRequest --- src/index.ts | 9 ++++++++- src/utils.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5d87a0015..c3cc1e06d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,13 @@ import { CashuWallet } from './CashuWallet.js'; import { PaymentRequest } from './model/PaymentRequest.js'; import { setGlobalRequestOptions } from './request.js'; import { generateNewMnemonic, deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; -import { getEncodedToken, getEncodedTokenV4, getDecodedToken, deriveKeysetId } from './utils.js'; +import { + getEncodedToken, + getEncodedTokenV4, + getDecodedToken, + deriveKeysetId, + decodePaymentRequest +} from './utils.js'; export * from './model/types/index.js'; @@ -14,6 +20,7 @@ export { getDecodedToken, getEncodedToken, getEncodedTokenV4, + decodePaymentRequest, deriveKeysetId, generateNewMnemonic, deriveSeedFromMnemonic, diff --git a/src/utils.ts b/src/utils.ts index c6edb4163..9d7d45f5d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -251,7 +251,7 @@ export function sanitizeUrl(url: string): string { return url.replace(/\/$/, ''); } -export function decodePaymentRequest(paymentRequest: string) { +function decodePaymentRequest(paymentRequest: string) { if (!paymentRequest.startsWith('creq')) { throw new Error('unsupported pr: invalid prefix'); } @@ -275,4 +275,5 @@ export { hexToNumber, splitAmount, getDefaultAmountPreference, + decodePaymentRequest }; From 70600d5d6fcc1dc853e293397dab66b18be592f1 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 15 Oct 2024 22:10:58 +0200 Subject: [PATCH 076/246] updated static pr decoder --- src/model/PaymentRequest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 2ca430ea1..84ff6f190 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -38,17 +38,17 @@ export class PaymentRequest { } static fromEncodedRequest(encodedRequest: string): PaymentRequest { + if (!encodedRequest.startsWith('creq')) { + throw new Error('unsupported pr: invalid prefix'); + } const version = encodedRequest[4]; if (version !== 'A') { - throw new Error('unsupported version...'); + throw new Error('unsupported pr version'); } const encodedData = encodedRequest.slice(5); const data = encodeBase64toUint8(encodedData); const decoded = decodeCBOR(data) as RawPaymentRequest; - if (!decoded.m) { - throw new Error('unsupported pr: memo undefined'); - } - const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a })); + const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); } } From 7e253d943a49853c80b0dbf67f1fc33acb0a357b Mon Sep 17 00:00:00 2001 From: gudnuf Date: Tue, 15 Oct 2024 15:11:52 -0700 Subject: [PATCH 077/246] add getters to `PaymentRequest` --- src/model/PaymentRequest.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 84ff6f190..8f896984a 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -1,9 +1,17 @@ import { encodeBase64toUint8 } from '../base64'; import { decodeCBOR, encodeCBOR } from '../cbor'; -import { RawPaymentRequest, RawTransport, PaymentRequestTransport } from './types'; +import { + RawPaymentRequest, + RawTransport, + PaymentRequestTransport, + PaymentRequestTransportType, + PaymentRequestTag +} from './types'; import { Buffer } from 'buffer'; export class PaymentRequest { + private tags: Map; + constructor( public transport: Array, public id?: string, @@ -11,7 +19,20 @@ export class PaymentRequest { public unit?: string, public mints?: Array, public description?: string - ) {} + ) { + this.tags = new Map(); + this.transport.forEach((t: PaymentRequestTransport) => { + if (t.tags) { + t.tags.forEach((tag: PaymentRequestTag) => { + if (Array.isArray(tag) && tag.length === 2) { + this.tags.set(tag[0], tag[1]); + } else { + throw new Error('invalid tag'); + } + }); + } + }); + } toEncodedRequest() { const rawRequest: RawPaymentRequest = { @@ -37,6 +58,14 @@ export class PaymentRequest { return 'creq' + 'A' + encodedData; } + getTransport(type: PaymentRequestTransportType) { + return this.transport.find((t: PaymentRequestTransport) => t.type === type); + } + + getTag(tag: string) { + return this.tags.get(tag); + } + static fromEncodedRequest(encodedRequest: string): PaymentRequest { if (!encodedRequest.startsWith('creq')) { throw new Error('unsupported pr: invalid prefix'); From 6df53ba1863c275a1f41e243b565d3b206ec2e39 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 16 Oct 2024 02:12:32 +0200 Subject: [PATCH 078/246] remove fancy tags --- src/model/PaymentRequest.ts | 26 +++-------------------- src/model/types/wallet/paymentRequests.ts | 4 +--- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 8f896984a..1a782d4b7 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -4,14 +4,11 @@ import { RawPaymentRequest, RawTransport, PaymentRequestTransport, - PaymentRequestTransportType, - PaymentRequestTag + PaymentRequestTransportType } from './types'; import { Buffer } from 'buffer'; export class PaymentRequest { - private tags: Map; - constructor( public transport: Array, public id?: string, @@ -19,24 +16,11 @@ export class PaymentRequest { public unit?: string, public mints?: Array, public description?: string - ) { - this.tags = new Map(); - this.transport.forEach((t: PaymentRequestTransport) => { - if (t.tags) { - t.tags.forEach((tag: PaymentRequestTag) => { - if (Array.isArray(tag) && tag.length === 2) { - this.tags.set(tag[0], tag[1]); - } else { - throw new Error('invalid tag'); - } - }); - } - }); - } + ) {} toEncodedRequest() { const rawRequest: RawPaymentRequest = { - t: this.transport.map((t: PaymentRequestTransport) => ({ t: t.type, a: t.target })) + t: this.transport.map((t: PaymentRequestTransport) => ({ t: t.type, a: t.target, g: t.tags })) }; if (this.id) { rawRequest.i = this.id; @@ -62,10 +46,6 @@ export class PaymentRequest { return this.transport.find((t: PaymentRequestTransport) => t.type === type); } - getTag(tag: string) { - return this.tags.get(tag); - } - static fromEncodedRequest(encodedRequest: string): PaymentRequest { if (!encodedRequest.startsWith('creq')) { throw new Error('unsupported pr: invalid prefix'); diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts index 766f31829..bcd14c319 100644 --- a/src/model/types/wallet/paymentRequests.ts +++ b/src/model/types/wallet/paymentRequests.ts @@ -16,12 +16,10 @@ export type RawPaymentRequest = { t: Array; }; -export type PaymentRequestTag = Array; - export type PaymentRequestTransport = { type: PaymentRequestTransportType; target: string; - tags?: Array; + tags?: Array>; }; export type PaymentRequestPayload = { From 8044364c9a222a2fd29cf296cc5724c6ddcad748 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 16 Oct 2024 12:50:23 +0200 Subject: [PATCH 079/246] remove bip39 logic --- src/CashuWallet.ts | 26 ++++++++------------------ test/wallet.test.ts | 6 +----- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6f8280ce7..8d17ec99f 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -31,19 +31,13 @@ import { splitAmount } from './utils.js'; import { isAmountPreferenceArray, deprecatedAmountPreferences } from './legacy/cashu-ts'; -import { validateMnemonic } from '@scure/bip39'; -import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { blindMessage, constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; -import { - deriveBlindingFactor, - deriveSecret, - deriveSeedFromMnemonic -} from '@cashu/crypto/modules/client/NUT09'; +import { deriveBlindingFactor, deriveSecret } from '@cashu/crypto/modules/client/NUT09'; import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; @@ -69,7 +63,7 @@ class CashuWallet { options?: { unit?: string; keys?: MintKeys; - mnemonicOrSeed?: string | Uint8Array; + bip39seed?: Uint8Array; } ) { this.mint = mint; @@ -78,17 +72,13 @@ class CashuWallet { this._keys = options.keys; this._unit = options.keys.unit; } - if (!options?.mnemonicOrSeed) { - return; - } - if (options?.mnemonicOrSeed instanceof Uint8Array) { - this._seed = options.mnemonicOrSeed; - return; - } - if (!validateMnemonic(options.mnemonicOrSeed, wordlist)) { - throw new Error('Tried to instantiate with mnemonic, but mnemonic was invalid'); + if (options?.bip39seed) { + if (options.bip39seed instanceof Uint8Array) { + this._seed = options.bip39seed; + return; + } + throw new Error('bip39seed must be a valid UInt8Array'); } - this._seed = deriveSeedFromMnemonic(options.mnemonicOrSeed); } get unit(): string { diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 85608d170..180b1a13e 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,10 +1,8 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { MeltQuoteResponse, ReceiveResponse } from '../src/model/types/index.js'; +import { MeltQuoteResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; -import { AmountPreference } from '../src/model/types/index'; -import { Proof } from '@cashu/crypto/modules/common'; const dummyKeysResp = { keysets: [ @@ -21,8 +19,6 @@ const unit = 'sat'; const invoice = 'lnbc20u1p3u27nppp5pm074ffk6m42lvae8c6847z7xuvhyknwgkk7pzdce47grf2ksqwsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzpgxqyz5vqsp5sw6n7cztudpl5m5jv3z6dtqpt2zhd3q6dwgftey9qxv09w82rgjq9qyyssqhtfl8wv7scwp5flqvmgjjh20nf6utvv5daw5h43h69yqfwjch7wnra3cn94qkscgewa33wvfh7guz76rzsfg9pwlk8mqd27wavf2udsq3yeuju'; -const mnemonic = 'half depart obvious quality work element tank gorilla view sugar picture humble'; - beforeAll(() => { nock.disableNetConnect(); }); From cd4230504f6272726e688683c6062103df03aacd Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 16 Oct 2024 12:50:29 +0200 Subject: [PATCH 080/246] remove bip39 package --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69d22b893..eb0593c26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", "buffer": "^6.0.3" }, "devDependencies": { diff --git a/package.json b/package.json index 5e33c1f35..8cce56fef 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", "buffer": "^6.0.3" } } From 5f34712b67e9db0f1925a29951a1c5fca8ee00b8 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 16 Oct 2024 12:53:26 +0200 Subject: [PATCH 081/246] format --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index bf0d0d313..a1e72b7fe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -42,7 +42,7 @@ function splitAmount( for (let i = 0; i < q; ++i) chunks.push(amt); value %= amt; }); - return chunks.sort((a, b) => ( isDesc ? b - a : a - b)); + return chunks.sort((a, b) => (isDesc ? b - a : a - b)); } /* From 4f97e14eeceadebe48db8b3a8ba90c1016e1893e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 12:58:56 +0200 Subject: [PATCH 082/246] add tests --- test/paymentRequests.test.ts | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/paymentRequests.test.ts diff --git a/test/paymentRequests.test.ts b/test/paymentRequests.test.ts new file mode 100644 index 000000000..1517e0b99 --- /dev/null +++ b/test/paymentRequests.test.ts @@ -0,0 +1,45 @@ +import nock from 'nock'; +import { CashuMint } from '../src/CashuMint.js'; +import { CashuWallet } from '../src/CashuWallet.js'; +import { setGlobalRequestOptions } from '../src/request.js'; +import { MeltQuoteResponse } from '../src/model/types/index.js'; +import { decodePaymentRequest, PaymentRequest, PaymentRequestPayload, PaymentRequestTransport, PaymentRequestTransportType } from '../src/index.js'; + +describe('payment requests', () => { + test('encode payment requests', async () => { + const request = new PaymentRequest( + [{ type: PaymentRequestTransportType.NOSTR, target: "asd", tags: [["n", "17"]] } as PaymentRequestTransport], + '4840f51e', + 1000, + 'sat', + ["https://mint.com"], + 'test', + ) + const pr = request.toEncodedRequest(); + expect(pr).toBeDefined(); + const decodedRequest = decodePaymentRequest(pr); + expect(decodedRequest).toBeDefined(); + expect(decodedRequest.id).toBe('4840f51e'); + expect(decodedRequest.amount).toBe(1000); + expect(decodedRequest.unit).toBe('sat'); + expect(decodedRequest.mints).toStrictEqual(["https://mint.com"]) + expect(decodedRequest.description).toBe('test'); + expect(decodedRequest.transport).toHaveLength(1); + expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(decodedRequest.transport[0].target).toBe('asd') + expect(decodedRequest.transport[0].tags).toStrictEqual([["n", "17"]]) + }); + test('test decoding payment requests with no amount', async () => { + const prWithoutAmount = 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; + const request: PaymentRequest = decodePaymentRequest(prWithoutAmount) + expect(request).toBeDefined(); + expect(request.id).toBe('4840f51e'); + expect(request.amount).toBeUndefined(); + expect(request.unit).toBe('sat'); + expect(request.mints).toStrictEqual(["https://mint.com"]) + expect(request.description).toBeUndefined(); + expect(request.transport).toHaveLength(1); + expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(request.transport[0].target).toBe('nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts') + }); +}); From f71e865afcc399324923560368d10b7bc770fc33 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 12:59:11 +0200 Subject: [PATCH 083/246] format --- test/paymentRequests.test.ts | 89 +++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/test/paymentRequests.test.ts b/test/paymentRequests.test.ts index 1517e0b99..cbc4ba108 100644 --- a/test/paymentRequests.test.ts +++ b/test/paymentRequests.test.ts @@ -3,43 +3,58 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import { setGlobalRequestOptions } from '../src/request.js'; import { MeltQuoteResponse } from '../src/model/types/index.js'; -import { decodePaymentRequest, PaymentRequest, PaymentRequestPayload, PaymentRequestTransport, PaymentRequestTransportType } from '../src/index.js'; +import { + decodePaymentRequest, + PaymentRequest, + PaymentRequestPayload, + PaymentRequestTransport, + PaymentRequestTransportType +} from '../src/index.js'; describe('payment requests', () => { - test('encode payment requests', async () => { - const request = new PaymentRequest( - [{ type: PaymentRequestTransportType.NOSTR, target: "asd", tags: [["n", "17"]] } as PaymentRequestTransport], - '4840f51e', - 1000, - 'sat', - ["https://mint.com"], - 'test', - ) - const pr = request.toEncodedRequest(); - expect(pr).toBeDefined(); - const decodedRequest = decodePaymentRequest(pr); - expect(decodedRequest).toBeDefined(); - expect(decodedRequest.id).toBe('4840f51e'); - expect(decodedRequest.amount).toBe(1000); - expect(decodedRequest.unit).toBe('sat'); - expect(decodedRequest.mints).toStrictEqual(["https://mint.com"]) - expect(decodedRequest.description).toBe('test'); - expect(decodedRequest.transport).toHaveLength(1); - expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(decodedRequest.transport[0].target).toBe('asd') - expect(decodedRequest.transport[0].tags).toStrictEqual([["n", "17"]]) - }); - test('test decoding payment requests with no amount', async () => { - const prWithoutAmount = 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; - const request: PaymentRequest = decodePaymentRequest(prWithoutAmount) - expect(request).toBeDefined(); - expect(request.id).toBe('4840f51e'); - expect(request.amount).toBeUndefined(); - expect(request.unit).toBe('sat'); - expect(request.mints).toStrictEqual(["https://mint.com"]) - expect(request.description).toBeUndefined(); - expect(request.transport).toHaveLength(1); - expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(request.transport[0].target).toBe('nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts') - }); + test('encode payment requests', async () => { + const request = new PaymentRequest( + [ + { + type: PaymentRequestTransportType.NOSTR, + target: 'asd', + tags: [['n', '17']] + } as PaymentRequestTransport + ], + '4840f51e', + 1000, + 'sat', + ['https://mint.com'], + 'test' + ); + const pr = request.toEncodedRequest(); + expect(pr).toBeDefined(); + const decodedRequest = decodePaymentRequest(pr); + expect(decodedRequest).toBeDefined(); + expect(decodedRequest.id).toBe('4840f51e'); + expect(decodedRequest.amount).toBe(1000); + expect(decodedRequest.unit).toBe('sat'); + expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); + expect(decodedRequest.description).toBe('test'); + expect(decodedRequest.transport).toHaveLength(1); + expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(decodedRequest.transport[0].target).toBe('asd'); + expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); + }); + test('test decoding payment requests with no amount', async () => { + const prWithoutAmount = + 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; + const request: PaymentRequest = decodePaymentRequest(prWithoutAmount); + expect(request).toBeDefined(); + expect(request.id).toBe('4840f51e'); + expect(request.amount).toBeUndefined(); + expect(request.unit).toBe('sat'); + expect(request.mints).toStrictEqual(['https://mint.com']); + expect(request.description).toBeUndefined(); + expect(request.transport).toHaveLength(1); + expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(request.transport[0].target).toBe( + 'nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts' + ); + }); }); From bdf4de98f942909cdfed36cac011f8e34f5622b0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:00:51 +0200 Subject: [PATCH 084/246] fix single-use and add comments --- src/model/types/wallet/paymentRequests.ts | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts index bcd14c319..46266cfcb 100644 --- a/src/model/types/wallet/paymentRequests.ts +++ b/src/model/types/wallet/paymentRequests.ts @@ -1,36 +1,36 @@ import { Proof } from './index'; export type RawTransport = { - t: PaymentRequestTransportType; - a: string; - g?: Array>; + t: PaymentRequestTransportType; // type + a: string; // target + g?: Array>; // tags }; export type RawPaymentRequest = { - i?: string; - a?: number; - u?: string; - r?: boolean; - m?: Array; - d?: string; - t: Array; + i?: string; // id + a?: number; // amount + u?: string; // unit + s?: boolean; // single use + m?: Array; // mints + d?: string; // description + t: Array; // transports }; export type PaymentRequestTransport = { - type: PaymentRequestTransportType; - target: string; - tags?: Array>; + type: PaymentRequestTransportType; + target: string; + tags?: Array>; }; export type PaymentRequestPayload = { - id?: string; - memo?: string; - unit: string; - mint: string; - proofs: Array; + id?: string; + memo?: string; + unit: string; + mint: string; + proofs: Array; }; export enum PaymentRequestTransportType { - POST = 'post', - NOSTR = 'nostr' + POST = 'post', + NOSTR = 'nostr' } From cfab4d1db250c451ea4f47813d28d4c9c2b9ccd6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:03:50 +0200 Subject: [PATCH 085/246] format --- src/model/types/wallet/paymentRequests.ts | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts index 46266cfcb..1ec58c929 100644 --- a/src/model/types/wallet/paymentRequests.ts +++ b/src/model/types/wallet/paymentRequests.ts @@ -1,36 +1,36 @@ import { Proof } from './index'; export type RawTransport = { - t: PaymentRequestTransportType; // type - a: string; // target - g?: Array>; // tags + t: PaymentRequestTransportType; // type + a: string; // target + g?: Array>; // tags }; export type RawPaymentRequest = { - i?: string; // id - a?: number; // amount - u?: string; // unit - s?: boolean; // single use - m?: Array; // mints - d?: string; // description - t: Array; // transports + i?: string; // id + a?: number; // amount + u?: string; // unit + s?: boolean; // single use + m?: Array; // mints + d?: string; // description + t: Array; // transports }; export type PaymentRequestTransport = { - type: PaymentRequestTransportType; - target: string; - tags?: Array>; + type: PaymentRequestTransportType; + target: string; + tags?: Array>; }; export type PaymentRequestPayload = { - id?: string; - memo?: string; - unit: string; - mint: string; - proofs: Array; + id?: string; + memo?: string; + unit: string; + mint: string; + proofs: Array; }; export enum PaymentRequestTransportType { - POST = 'post', - NOSTR = 'nostr' + POST = 'post', + NOSTR = 'nostr' } From 7808c59c78f2474517c5e618729069c11a9cf1d9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:13:31 +0200 Subject: [PATCH 086/246] single-use --- src/model/PaymentRequest.ts | 7 ++++++- src/utils.ts | 10 +++++++++- test/paymentRequests.test.ts | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 1a782d4b7..24d277234 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -15,7 +15,8 @@ export class PaymentRequest { public amount?: number, public unit?: string, public mints?: Array, - public description?: string + public description?: string, + public singleUse: boolean = false ) {} toEncodedRequest() { @@ -37,6 +38,10 @@ export class PaymentRequest { if (this.description) { rawRequest.d = this.description; } + if (this.singleUse) { + rawRequest.s = this.singleUse; + } + const data = encodeCBOR(rawRequest); const encodedData = Buffer.from(data).toString('base64'); return 'creq' + 'A' + encodedData; diff --git a/src/utils.ts b/src/utils.ts index 9d7d45f5d..e43438888 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -263,7 +263,15 @@ function decodePaymentRequest(paymentRequest: string) { const data = encodeBase64toUint8(encodedData); const decoded = decodeCBOR(data) as RawPaymentRequest; const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); - return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); + return new PaymentRequest( + transports, + decoded.i, + decoded.a, + decoded.u, + decoded.m, + decoded.d, + decoded.s + ); } export { diff --git a/test/paymentRequests.test.ts b/test/paymentRequests.test.ts index cbc4ba108..797623383 100644 --- a/test/paymentRequests.test.ts +++ b/test/paymentRequests.test.ts @@ -25,7 +25,8 @@ describe('payment requests', () => { 1000, 'sat', ['https://mint.com'], - 'test' + 'test', + true // single use ); const pr = request.toEncodedRequest(); expect(pr).toBeDefined(); @@ -37,6 +38,7 @@ describe('payment requests', () => { expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); expect(decodedRequest.description).toBe('test'); expect(decodedRequest.transport).toHaveLength(1); + expect(decodedRequest.singleUse).toBe(true); expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); expect(decodedRequest.transport[0].target).toBe('asd'); expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); From 5f9fed3b11a89a88dbe49287e18867206f3a3967 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:22:04 +0200 Subject: [PATCH 087/246] remove duplicate code --- src/model/PaymentRequest.ts | 10 +++- src/utils.ts | 21 +------ test/paymentRequests.test.ts | 107 ++++++++++++++++++----------------- 3 files changed, 65 insertions(+), 73 deletions(-) diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index 24d277234..edfa6cf03 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -63,6 +63,14 @@ export class PaymentRequest { const data = encodeBase64toUint8(encodedData); const decoded = decodeCBOR(data) as RawPaymentRequest; const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); - return new PaymentRequest(transports, decoded.i, decoded.a, decoded.u, decoded.m, decoded.d); + return new PaymentRequest( + transports, + decoded.i, + decoded.a, + decoded.u, + decoded.m, + decoded.d, + decoded.s + ); } } diff --git a/src/utils.ts b/src/utils.ts index e43438888..a361eb695 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -252,26 +252,7 @@ export function sanitizeUrl(url: string): string { } function decodePaymentRequest(paymentRequest: string) { - if (!paymentRequest.startsWith('creq')) { - throw new Error('unsupported pr: invalid prefix'); - } - const version = paymentRequest[4]; - if (version !== 'A') { - throw new Error('unsupported pr version'); - } - const encodedData = paymentRequest.slice(5); - const data = encodeBase64toUint8(encodedData); - const decoded = decodeCBOR(data) as RawPaymentRequest; - const transports = decoded.t.map((t: RawTransport) => ({ type: t.t, target: t.a, tags: t.g })); - return new PaymentRequest( - transports, - decoded.i, - decoded.a, - decoded.u, - decoded.m, - decoded.d, - decoded.s - ); + return PaymentRequest.fromEncodedRequest(paymentRequest); } export { diff --git a/test/paymentRequests.test.ts b/test/paymentRequests.test.ts index 797623383..42bcd5922 100644 --- a/test/paymentRequests.test.ts +++ b/test/paymentRequests.test.ts @@ -4,59 +4,62 @@ import { CashuWallet } from '../src/CashuWallet.js'; import { setGlobalRequestOptions } from '../src/request.js'; import { MeltQuoteResponse } from '../src/model/types/index.js'; import { - decodePaymentRequest, - PaymentRequest, - PaymentRequestPayload, - PaymentRequestTransport, - PaymentRequestTransportType + decodePaymentRequest, + PaymentRequest, + PaymentRequestPayload, + PaymentRequestTransport, + PaymentRequestTransportType } from '../src/index.js'; describe('payment requests', () => { - test('encode payment requests', async () => { - const request = new PaymentRequest( - [ - { - type: PaymentRequestTransportType.NOSTR, - target: 'asd', - tags: [['n', '17']] - } as PaymentRequestTransport - ], - '4840f51e', - 1000, - 'sat', - ['https://mint.com'], - 'test', - true // single use - ); - const pr = request.toEncodedRequest(); - expect(pr).toBeDefined(); - const decodedRequest = decodePaymentRequest(pr); - expect(decodedRequest).toBeDefined(); - expect(decodedRequest.id).toBe('4840f51e'); - expect(decodedRequest.amount).toBe(1000); - expect(decodedRequest.unit).toBe('sat'); - expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); - expect(decodedRequest.description).toBe('test'); - expect(decodedRequest.transport).toHaveLength(1); - expect(decodedRequest.singleUse).toBe(true); - expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(decodedRequest.transport[0].target).toBe('asd'); - expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); - }); - test('test decoding payment requests with no amount', async () => { - const prWithoutAmount = - 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; - const request: PaymentRequest = decodePaymentRequest(prWithoutAmount); - expect(request).toBeDefined(); - expect(request.id).toBe('4840f51e'); - expect(request.amount).toBeUndefined(); - expect(request.unit).toBe('sat'); - expect(request.mints).toStrictEqual(['https://mint.com']); - expect(request.description).toBeUndefined(); - expect(request.transport).toHaveLength(1); - expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(request.transport[0].target).toBe( - 'nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts' - ); - }); + test('encode payment requests', async () => { + const request = new PaymentRequest( + [ + { + type: PaymentRequestTransportType.NOSTR, + target: 'asd', + tags: [['n', '17']] + } as PaymentRequestTransport + ], + '4840f51e', + 1000, + 'sat', + ['https://mint.com'], + 'test', + true // single use + ); + const pr = request.toEncodedRequest(); + expect(pr).toBeDefined(); + const decodedRequest = decodePaymentRequest(pr); + expect(decodedRequest).toBeDefined(); + expect(decodedRequest.id).toBe('4840f51e'); + expect(decodedRequest.amount).toBe(1000); + expect(decodedRequest.unit).toBe('sat'); + expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); + expect(decodedRequest.description).toBe('test'); + expect(decodedRequest.transport).toHaveLength(1); + expect(decodedRequest.singleUse).toBe(true); + expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(decodedRequest.transport[0].target).toBe('asd'); + expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); + + const decodedRequestClassConstructor = PaymentRequest.fromEncodedRequest(pr); + expect(decodedRequestClassConstructor).toStrictEqual(decodedRequest); + }); + test('test decoding payment requests with no amount', async () => { + const prWithoutAmount = + 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; + const request: PaymentRequest = decodePaymentRequest(prWithoutAmount); + expect(request).toBeDefined(); + expect(request.id).toBe('4840f51e'); + expect(request.amount).toBeUndefined(); + expect(request.unit).toBe('sat'); + expect(request.mints).toStrictEqual(['https://mint.com']); + expect(request.description).toBeUndefined(); + expect(request.transport).toHaveLength(1); + expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(request.transport[0].target).toBe( + 'nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts' + ); + }); }); From efd0e4f1adc04337f8e487c28d32a857c93cf400 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:23:00 +0200 Subject: [PATCH 088/246] reorder --- src/model/types/wallet/paymentRequests.ts | 10 +- test/paymentRequests.test.ts | 108 +++++++++++----------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/model/types/wallet/paymentRequests.ts b/src/model/types/wallet/paymentRequests.ts index 1ec58c929..ef3cc971f 100644 --- a/src/model/types/wallet/paymentRequests.ts +++ b/src/model/types/wallet/paymentRequests.ts @@ -22,6 +22,11 @@ export type PaymentRequestTransport = { tags?: Array>; }; +export enum PaymentRequestTransportType { + POST = 'post', + NOSTR = 'nostr' +} + export type PaymentRequestPayload = { id?: string; memo?: string; @@ -29,8 +34,3 @@ export type PaymentRequestPayload = { mint: string; proofs: Array; }; - -export enum PaymentRequestTransportType { - POST = 'post', - NOSTR = 'nostr' -} diff --git a/test/paymentRequests.test.ts b/test/paymentRequests.test.ts index 42bcd5922..b06689dcf 100644 --- a/test/paymentRequests.test.ts +++ b/test/paymentRequests.test.ts @@ -4,62 +4,62 @@ import { CashuWallet } from '../src/CashuWallet.js'; import { setGlobalRequestOptions } from '../src/request.js'; import { MeltQuoteResponse } from '../src/model/types/index.js'; import { - decodePaymentRequest, - PaymentRequest, - PaymentRequestPayload, - PaymentRequestTransport, - PaymentRequestTransportType + decodePaymentRequest, + PaymentRequest, + PaymentRequestPayload, + PaymentRequestTransport, + PaymentRequestTransportType } from '../src/index.js'; describe('payment requests', () => { - test('encode payment requests', async () => { - const request = new PaymentRequest( - [ - { - type: PaymentRequestTransportType.NOSTR, - target: 'asd', - tags: [['n', '17']] - } as PaymentRequestTransport - ], - '4840f51e', - 1000, - 'sat', - ['https://mint.com'], - 'test', - true // single use - ); - const pr = request.toEncodedRequest(); - expect(pr).toBeDefined(); - const decodedRequest = decodePaymentRequest(pr); - expect(decodedRequest).toBeDefined(); - expect(decodedRequest.id).toBe('4840f51e'); - expect(decodedRequest.amount).toBe(1000); - expect(decodedRequest.unit).toBe('sat'); - expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); - expect(decodedRequest.description).toBe('test'); - expect(decodedRequest.transport).toHaveLength(1); - expect(decodedRequest.singleUse).toBe(true); - expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(decodedRequest.transport[0].target).toBe('asd'); - expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); + test('encode payment requests', async () => { + const request = new PaymentRequest( + [ + { + type: PaymentRequestTransportType.NOSTR, + target: 'asd', + tags: [['n', '17']] + } as PaymentRequestTransport + ], + '4840f51e', + 1000, + 'sat', + ['https://mint.com'], + 'test', + true // single use + ); + const pr = request.toEncodedRequest(); + expect(pr).toBeDefined(); + const decodedRequest = decodePaymentRequest(pr); + expect(decodedRequest).toBeDefined(); + expect(decodedRequest.id).toBe('4840f51e'); + expect(decodedRequest.amount).toBe(1000); + expect(decodedRequest.unit).toBe('sat'); + expect(decodedRequest.mints).toStrictEqual(['https://mint.com']); + expect(decodedRequest.description).toBe('test'); + expect(decodedRequest.transport).toHaveLength(1); + expect(decodedRequest.singleUse).toBe(true); + expect(decodedRequest.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(decodedRequest.transport[0].target).toBe('asd'); + expect(decodedRequest.transport[0].tags).toStrictEqual([['n', '17']]); - const decodedRequestClassConstructor = PaymentRequest.fromEncodedRequest(pr); - expect(decodedRequestClassConstructor).toStrictEqual(decodedRequest); - }); - test('test decoding payment requests with no amount', async () => { - const prWithoutAmount = - 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; - const request: PaymentRequest = decodePaymentRequest(prWithoutAmount); - expect(request).toBeDefined(); - expect(request.id).toBe('4840f51e'); - expect(request.amount).toBeUndefined(); - expect(request.unit).toBe('sat'); - expect(request.mints).toStrictEqual(['https://mint.com']); - expect(request.description).toBeUndefined(); - expect(request.transport).toHaveLength(1); - expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); - expect(request.transport[0].target).toBe( - 'nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts' - ); - }); + const decodedRequestClassConstructor = PaymentRequest.fromEncodedRequest(pr); + expect(decodedRequestClassConstructor).toStrictEqual(decodedRequest); + }); + test('test decoding payment requests with no amount', async () => { + const prWithoutAmount = + 'creqApGF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5bWRleDNndmZzZnVqcDN4eW43ZTdxcnM4eXlxOWQ4enN1MnpxdWp4dXhjYXBmcXZ6YzhncnFka3RzYWeBgmFuYjE3YWloNDg0MGY1MWVhdWNzYXRhbYFwaHR0cHM6Ly9taW50LmNvbQ=='; + const request: PaymentRequest = decodePaymentRequest(prWithoutAmount); + expect(request).toBeDefined(); + expect(request.id).toBe('4840f51e'); + expect(request.amount).toBeUndefined(); + expect(request.unit).toBe('sat'); + expect(request.mints).toStrictEqual(['https://mint.com']); + expect(request.description).toBeUndefined(); + expect(request.transport).toHaveLength(1); + expect(request.transport[0].type).toBe(PaymentRequestTransportType.NOSTR); + expect(request.transport[0].target).toBe( + 'nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgymdex3gvfsfujp3xyn7e7qrs8yyq9d8zsu2zqujxuxcapfqvzc8grqdkts' + ); + }); }); From f35277a38fce1186c320f712466e63359287c6e1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:31:26 +0200 Subject: [PATCH 089/246] cleanup mnemonic stuff --- src/CashuWallet.ts | 6 +++--- src/index.ts | 3 --- test/wallet.test.ts | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8d17ec99f..757ccc2f9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -55,7 +55,7 @@ class CashuWallet { * @param unit optionally set unit * @param keys public keys from the mint. If set, it will override the unit with the keysets unit * @param mint Cashu mint instance is used to make api calls - * @param mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. + * @param bip39seed BIP39 seed for deterministic secrets. * This can lead to poor performance, in which case the seed should be directly provided */ constructor( @@ -285,7 +285,7 @@ class CashuWallet { ): Promise<{ proofs: Array }> { const keys = await this.getKeys(options?.keysetId); if (!this._seed) { - throw new Error('CashuWallet must be initialized with mnemonic to use restore'); + throw new Error('CashuWallet must be initialized with a seed to use restore'); } // create blank amounts for unknown restore amounts const amounts = Array(count).fill(0); @@ -663,7 +663,7 @@ class CashuWallet { // if we atempt to create deterministic messages without a _seed, abort. if (counter != undefined && !this._seed) { throw new Error( - 'Cannot create deterministic messages without seed. Instantiate CashuWallet with a mnemonic, or omit counter param.' + 'Cannot create deterministic messages without seed. Instantiate CashuWallet with a bip39seed, or omit counter param.' ); } const blindedMessages: Array = []; diff --git a/src/index.ts b/src/index.ts index 530629f13..7f5e065d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; import { setGlobalRequestOptions } from './request.js'; -import { generateNewMnemonic, deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; import { getEncodedToken, getEncodedTokenV4, getDecodedToken, deriveKeysetId } from './utils.js'; export * from './model/types/index.js'; @@ -13,7 +12,5 @@ export { getEncodedToken, getEncodedTokenV4, deriveKeysetId, - generateNewMnemonic, - deriveSeedFromMnemonic, setGlobalRequestOptions }; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 180b1a13e..7bb34e06d 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -614,7 +614,7 @@ describe('deterministic', () => { .catch((e) => e); expect(result).toEqual( new Error( - 'Cannot create deterministic messages without seed. Instantiate CashuWallet with a mnemonic, or omit counter param.' + 'Cannot create deterministic messages without seed. Instantiate CashuWallet with a bip39seed, or omit counter param.' ) ); }); From d9a6b5910a53648eecf29034a09cfcdd45fa8ab5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:35:16 +0200 Subject: [PATCH 090/246] format --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 20d59a652..fad66d8e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,13 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; import { PaymentRequest } from './model/PaymentRequest.js'; import { setGlobalRequestOptions } from './request.js'; -import { getEncodedToken, getEncodedTokenV4, getDecodedToken, deriveKeysetId, decodePaymentRequest } from './utils.js'; +import { + getEncodedToken, + getEncodedTokenV4, + getDecodedToken, + deriveKeysetId, + decodePaymentRequest +} from './utils.js'; export * from './model/types/index.js'; From 2cad1adc88d78ef6f2fd6973e4ea73aa6a047ea4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:46:51 +0200 Subject: [PATCH 091/246] format --- src/CashuWallet.ts | 20 ++++++++------------ src/utils.ts | 5 ++++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 40998d833..f7eda5fb4 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -24,13 +24,7 @@ import { OutputAmounts, CheckStateEntry } from './model/types/index.js'; -import { - bytesToNumber, - getDecodedToken, - splitAmount, - sumProofs, - getKeepAmounts -} from './utils.js'; +import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -276,7 +270,9 @@ class CashuWallet { } ): Promise> { const proofs: Array = []; - const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); + const amount = + tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - + this.getFeesForProofs(tokenEntry.proofs); const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSwapPayload( amount, @@ -396,7 +392,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -433,7 +429,7 @@ class CashuWallet { } const keyset = await this.getKeys(options.keysetId); const proofsToSend = proofs; - const amountToSend = amount + const amountToSend = amount; const amountAvailable = sumProofs(proofs); const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); @@ -443,7 +439,7 @@ class CashuWallet { // output selection let keepAmounts; if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { - keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3) + keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3); } else if (options.outputAmounts) { keepAmounts = options.outputAmounts.keepAmounts; } @@ -662,7 +658,7 @@ class CashuWallet { const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; if (meltResponse.change) { - change = this.constructProofs(meltResponse.change, rs, secrets, keys) + change = this.constructProofs(meltResponse.change, rs, secrets, keys); } return { quote: meltResponse, diff --git a/src/utils.ts b/src/utils.ts index 3c264f8e3..6e7c999f9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,7 +28,10 @@ function splitAmount( if (split) { if (split.reduce((a: number, b: number) => a + b, 0) > value) { throw new Error( - `Split is greater than total amount: ${split.reduce((a: number, b: number) => a + b, 0)} > ${value}` + `Split is greater than total amount: ${split.reduce( + (a: number, b: number) => a + b, + 0 + )} > ${value}` ); } chunks.push(...getPreference(value, keyset, split)); From efe541ba6ed0ead8a515f3642bad63d01d5209d8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:49:11 +0200 Subject: [PATCH 092/246] fix import --- src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 093ff8b12..2c22f72b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -299,6 +299,5 @@ export { hexToNumber, splitAmount, getKeepAmounts, - getDefaultAmountPreference, decodePaymentRequest }; From 814a0a02f73f62f7acea09c8631f72937a7456e4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:33:20 +0200 Subject: [PATCH 093/246] uppercase --- src/CashuWallet.ts | 10 +++++----- src/model/types/wallet/responses.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index f7eda5fb4..e84aefe6b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -8,7 +8,7 @@ import { type MeltQuoteResponse, type MintKeys, type MintKeyset, - type meltProofsResponse, + type MeltProofsResponse, type MintPayload, type Proof, type MintQuotePayload, @@ -392,7 +392,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -630,7 +630,7 @@ class CashuWallet { counter?: number; privkey?: string; } - ): Promise { + ): Promise { const keys = await this.getKeys(options?.keysetId); const { blindedMessages, secrets, rs } = this.createBlankOutputs( sumProofs(proofsToSend) - meltQuote.amount, @@ -686,7 +686,7 @@ class CashuWallet { counter?: number; privkey?: string; } - ): Promise { + ): Promise { if (!meltQuote) { meltQuote = await this.mint.createMeltQuote({ unit: this._unit, request: invoice }); } @@ -713,7 +713,7 @@ class CashuWallet { keysetId?: string; counter?: number; } - ): Promise { + ): Promise { const decodedToken = getDecodedToken(token); const proofs = decodedToken.token .filter((x: TokenEntry) => x.mint === this.mint.mintUrl) diff --git a/src/model/types/wallet/responses.ts b/src/model/types/wallet/responses.ts index 6d830784d..c3f93ea66 100644 --- a/src/model/types/wallet/responses.ts +++ b/src/model/types/wallet/responses.ts @@ -4,7 +4,7 @@ import { Proof, Token } from './index'; /** * Response after paying a Lightning invoice */ -export type meltProofsResponse = { +export type MeltProofsResponse = { /** * if false, the proofs have not been invalidated and the payment can be tried later again with the same proofs */ From c3dfc43c1e4090e85d4d47373531ca7fe97b72e0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:44:31 +0200 Subject: [PATCH 094/246] test with fees --- .github/workflows/nutshell-integration.yml | 2 +- src/CashuWallet.ts | 45 ++++++++++++++++++---- test/integration.test.ts | 26 ++++++++----- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/.github/workflows/nutshell-integration.yml b/.github/workflows/nutshell-integration.yml index 94ccf849e..3e881511c 100644 --- a/.github/workflows/nutshell-integration.yml +++ b/.github/workflows/nutshell-integration.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Pull and start mint run: | - docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint + docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint - name: Check running containers run: docker ps diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e84aefe6b..bfdef0c5b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -305,6 +305,7 @@ class CashuWallet { privkey?: string; keysetId?: string; offline?: boolean; + includeFees?: boolean } ): Promise { if (sumProofs(proofs) < amount) { @@ -312,7 +313,8 @@ class CashuWallet { } const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, - amount + amount, + options?.includeFees ); if ( sumProofs(sendProofOffline) != amount || // if the exact amount cannot be selected @@ -321,13 +323,30 @@ class CashuWallet { options?.privkey || options?.keysetId // these options require a swap ) { + // we need to swap + console.log("NOW WE SWAP") // input selection const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, true ); + console.log(`Selected amount: ${sumProofs(sendProofs)}`); options?.proofsWeHave?.push(...keepProofsSelect); + if (options?.includeFees) { + const keyset = await this.getKeys(options.keysetId); + const outputAmounts = splitAmount(amount, keyset.keys) + // create dummyProofs which are `Proof` objects with amounts from the outputAmounts + const dummyProofs: Array = outputAmounts.map((amount: number) => { + return { + amount: amount, + id: keyset.id, + secret: "dummy", + C: "dummy" + } as Proof; + }); + amount += this.getFeesForProofs(dummyProofs); + } const { keep, send } = await this.swap(amount, sendProofs, options); const keepProofs = keepProofsSelect.concat(keep); return { keep: keepProofs, send }; @@ -338,7 +357,7 @@ class CashuWallet { private selectProofsToSend( proofs: Array, amountToSend: number, - includeFees = false + includeFees?: boolean, ): SendResponse { const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs @@ -422,6 +441,7 @@ class CashuWallet { pubkey?: string; privkey?: string; keysetId?: string; + includeFees?: boolean; } ): Promise { if (!options) { @@ -433,9 +453,6 @@ class CashuWallet { const amountAvailable = sumProofs(proofs); const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); - if (amount + this.getFeesForProofs(proofsToSend) > amountAvailable) { - throw new Error('Not enough funds available'); - } // output selection let keepAmounts; if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { @@ -443,7 +460,16 @@ class CashuWallet { } else if (options.outputAmounts) { keepAmounts = options.outputAmounts.keepAmounts; } - const sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amount, keyset.keys); + const sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amountToSend, keyset.keys); + + if (amountToSend + this.getFeesForProofs(proofsToSend) > amountAvailable) { + throw new Error('Not enough funds available'); + } + + console.log(`# swap: amountToKeep: ${amountToKeep}`); + console.log(`# swap: amountToSend: ${amountToSend}`); + console.log(`# swap: keepAmounts: ${keepAmounts}`); + console.log(`# swap: sendAmounts: ${sendAmounts}`); options.outputAmounts = { keepAmounts: keepAmounts, sendAmounts: sendAmounts @@ -747,8 +773,11 @@ class CashuWallet { blindedMessages: BlindedTransaction; } { const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); + console.log(`# createSwapPayload: amount: ${amount} | input amount: ${totalAmount}`); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { - outputAmounts.keepAmounts = splitAmount(totalAmount - amount, keyset.keys); + console.log(`# splitting amount: ${totalAmount} - ${amount}`); + outputAmounts.keepAmounts = splitAmount(totalAmount - amount - this.getFeesForProofs(proofsToSend), keyset.keys); + console.log(`# keepAmounts: ${outputAmounts.keepAmounts}`); } const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount - this.getFeesForProofs(proofsToSend), @@ -795,6 +824,7 @@ class CashuWallet { inputs: proofsToSend, outputs: [...blindedMessages.blindedMessages] }; + console.log(`# createSwapPayload: amount: ${amount}`); return { payload, blindedMessages }; } /** @@ -833,6 +863,7 @@ class CashuWallet { counter?: number, pubkey?: string ): BlindedMessageData & { amounts: Array } { + console.log(`# createRandomBlindedMessages: amount: ${amount} split: ${split}`); const amounts = splitAmount(amount, keyset.keys, split); return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } diff --git a/test/integration.test.ts b/test/integration.test.ts index c5cf199ed..0c6916a37 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -80,6 +80,7 @@ describe('mint api', () => { test('pay local invoice', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); + await wallet.loadMint(); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -93,8 +94,9 @@ describe('mint api', () => { const quote_ = await wallet.checkMeltQuote(quote.quote); expect(quote_).toBeDefined(); - const sendResponse = await wallet.send(10, tokens.proofs); - const response = await wallet.payLnInvoice(mintQuote.request, sendResponse.send, quote); + const sendResponse = await wallet.send(10, tokens.proofs, { includeFees: true }); + // const response = await wallet.payLnInvoice(mintQuote.request, sendResponse.send, quote); + const response = await wallet.meltProofs(quote, sendResponse.send); expect(response).toBeDefined(); // expect that we have received the fee back, since it was internal expect(response.change.reduce((a, b) => a + b.amount, 0)).toBe(fee); @@ -112,6 +114,7 @@ describe('mint api', () => { test('pay external invoice', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); + await wallet.loadMint(); const request = await wallet.createMintQuote(3000); const tokens = await wallet.mintProofs(3000, request.quote); @@ -123,8 +126,9 @@ describe('mint api', () => { const quote_ = await wallet.checkMeltQuote(meltQuote.quote); expect(quote_).toBeDefined(); - const sendResponse = await wallet.send(2000 + fee, tokens.proofs); - const response = await wallet.payLnInvoice(externalInvoice, sendResponse.send, meltQuote); + const sendResponse = await wallet.send(2000 + fee, tokens.proofs, { includeFees: true }); + // const response = await wallet.payLnInvoice(externalInvoice, sendResponse.send, meltQuote); + const response = await wallet.meltProofs(meltQuote, sendResponse.send); expect(response).toBeDefined(); // expect that we have not received the fee back, since it was external @@ -157,21 +161,23 @@ describe('mint api', () => { test('test send tokens with change', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); + await wallet.loadMint(); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); - const sendResponse = await wallet.send(10, tokens.proofs); + const sendResponse = await wallet.send(10, tokens.proofs, { includeFees: false }); expect(sendResponse).toBeDefined(); expect(sendResponse.send).toBeDefined(); expect(sendResponse.keep).toBeDefined(); expect(sendResponse.send.length).toBe(2); expect(sendResponse.keep.length).toBe(5); expect(sumProofs(sendResponse.send)).toBe(10); - expect(sumProofs(sendResponse.keep)).toBe(90); + expect(sumProofs(sendResponse.keep)).toBe(89); }, 10000000); test('receive tokens with previous split', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); + await wallet.loadMint(); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -185,6 +191,7 @@ describe('mint api', () => { test('receive tokens with previous mint', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); + await wallet.loadMint(); const request = await wallet.createMintQuote(64); const tokens = await wallet.mintProofs(64, request.quote); const encoded = getEncodedToken({ @@ -196,6 +203,7 @@ describe('mint api', () => { test('send and receive p2pk', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); + await wallet.loadMint(); const privKeyAlice = secp256k1.utils.randomPrivateKey(); const pubKeyAlice = secp256k1.getPublicKey(privKeyAlice); @@ -203,8 +211,8 @@ describe('mint api', () => { const privKeyBob = secp256k1.utils.randomPrivateKey(); const pubKeyBob = secp256k1.getPublicKey(privKeyBob); - const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintProofs(64, request.quote); + const request = await wallet.createMintQuote(128); + const tokens = await wallet.mintProofs(128, request.quote); const { send } = await wallet.send(64, tokens.proofs, { pubkey: bytesToHex(pubKeyBob) }); const encoded = getEncodedToken({ @@ -222,7 +230,7 @@ describe('mint api', () => { proofs.reduce((curr, acc) => { return curr + acc.amount; }, 0) - ).toBe(64); + ).toBe(63); }); test('mint and melt p2pk', async () => { From 12403575ac61a8f73f0b76201c9ef61f8347503a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:48:20 +0200 Subject: [PATCH 095/246] optimizations --- README.md | 17 +++- package.json | 2 +- src/CashuWallet.ts | 192 +++++++++++++++++++++++++++------------ test/integration.test.ts | 18 ++-- test/wallet.test.ts | 3 +- 5 files changed, 154 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index d679e97d4..f12c0e705 100644 --- a/README.md +++ b/README.md @@ -77,21 +77,30 @@ if (mintQuoteChecked.state == MintQuoteState.PAID) { import { CashuMint, CashuWallet } from '@cashu/cashu-ts'; const mintUrl = 'http://localhost:3338'; // the mint URL const mint = new CashuMint(mintUrl); -const wallet = new CashuWallet(mint); +const wallet = new CashuWallet(mint, { loadMint: true }); // load the keysets of the mint const invoice = 'lnbc......'; // Lightning invoice to pay const meltQuote = await wallet.createMeltQuote(invoice); const amountToSend = meltQuote.amount + meltQuote.fee_reserve; -// in a real wallet, we would coin select the correct amount of proofs from the wallet's storage -// instead of that, here we swap `proofs` with the mint to get the correct amount of proofs -const { keep: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs); +// CashuWallet.send performs coin selection and swaps the proofs with the mint +// if no appropriate amount can be selected offline. We must include potential +// ecash fees that the mint might require to melt the resulting proofsToSend later. +const { keep: proofsToKeep, send: proofsToSend } = await wallet.send(amountToSend, proofs, { + includeFees: true +}); // store proofsToKeep in wallet .. const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); // store meltResponse.change in wallet .. ``` +#### Create a token and receive it + +```typescript + +``` + ## Contribute Contributions are very welcome. diff --git a/package.json b/package.json index 5e33c1f35..3ae405f2f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "compile": "rm -rf dist/lib && tsc && tsc --build tsconfig.es5.json", - "test": "jest --coverage --testPathIgnorePatterns ./test/integration.test.ts", + "test": "jest --coverage", "test-integration": "jest --coverage --testPathPattern ./test/integration.test.ts", "dev": "tsc --watch", "lint": "eslint --ext .js,.ts . --fix", diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bfdef0c5b..80eae6b8f 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -41,6 +41,9 @@ import { import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; +const DEFAULT_DENOMINATION_TARGET = 3; +const DEFAULT_UNIT = 'sat'; + /** * Class that represents a Cashu wallet. * This class should act as the entry point for this library @@ -50,8 +53,9 @@ class CashuWallet { private _keysetId: string | undefined; private _keysets: Array = []; private _seed: Uint8Array | undefined = undefined; - private _unit = 'sat'; + private _unit = DEFAULT_UNIT; private _mintInfo: GetInfoResponse | undefined = undefined; + private _denominationTarget = DEFAULT_DENOMINATION_TARGET; mint: CashuMint; @@ -72,6 +76,8 @@ class CashuWallet { keysets?: Array; mintInfo?: GetInfoResponse; mnemonicOrSeed?: string | Uint8Array; + denominationTarget?: number; + loadMint?: boolean; } ) { this.mint = mint; @@ -84,6 +90,20 @@ class CashuWallet { if (keys) keys.forEach((key: MintKeys) => this._keys.set(key.id, key)); if (options?.unit) this._unit = options?.unit; if (options?.keysets) this._keysets = options.keysets; + if (options?.denominationTarget) { + this._denominationTarget = options.denominationTarget; + } + + if (options?.loadMint) { + this.loadMint() + .then(() => { + console.log('Mint loaded'); + }) + .catch((e: Error) => { + console.error('Failed to load mint', e); + }); + } + if (!options?.mnemonicOrSeed) { return; } else if (options?.mnemonicOrSeed instanceof Uint8Array) { @@ -136,8 +156,7 @@ class CashuWallet { async loadMint() { await this.getMintInfo(); await this.getKeySets(); - await this.getAllKeys(); - this.keysetId = this.getActiveKeyset(this._keysets).id; + await this.getKeys(); } /** @@ -178,6 +197,7 @@ class CashuWallet { async getAllKeys(): Promise> { const keysets = await this.mint.getKeys(); this._keys = new Map(keysets.keysets.map((k: MintKeys) => [k.id, k])); + this.keysetId = this.getActiveKeyset(this._keysets).id; return keysets.keysets; } @@ -192,25 +212,22 @@ class CashuWallet { * @returns keyset */ async getKeys(keysetId?: string): Promise { - if (keysetId) { - if (this._keys.get(keysetId)) { - this.keysetId = keysetId; - return this._keys.get(keysetId) as MintKeys; - } - const allKeysets = await this.mint.getKeys(keysetId); - const keyset = allKeysets.keysets[0]; - if (!keyset) { - throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); - } - this._keys.set(keysetId, keyset); + if (!keysetId) { + const allKeysets = await this.mint.getKeySets(); + const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); + keysetId = keysetToActivate.id; + } + if (this._keys.get(keysetId)) { this.keysetId = keysetId; - return keyset; + return this._keys.get(keysetId) as MintKeys; } - - // no keysetId was set, so we select an active keyset with the unit of the wallet with the lowest fees and use that - const allKeysets = await this.mint.getKeySets(); - const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); - const keyset = await this.getKeys(keysetToActivate.id); + const allKeysets = await this.mint.getKeys(keysetId); + const keyset = allKeysets.keysets[0]; + if (!keyset) { + throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); + } + this._keys.set(keysetId, keyset); + this.keysetId = keysetId; return keyset; } @@ -305,7 +322,7 @@ class CashuWallet { privkey?: string; keysetId?: string; offline?: boolean; - includeFees?: boolean + includeFees?: boolean; } ): Promise { if (sumProofs(proofs) < amount) { @@ -316,16 +333,18 @@ class CashuWallet { amount, options?.includeFees ); + const expectedFee = options?.includeFees ? this.getFeesForProofs(sendProofOffline) : 0; if ( - sumProofs(sendProofOffline) != amount || // if the exact amount cannot be selected - options?.outputAmounts || - options?.pubkey || - options?.privkey || - options?.keysetId // these options require a swap + !options?.offline && + (sumProofs(sendProofOffline) != amount + expectedFee || // if the exact amount cannot be selected + options?.outputAmounts || + options?.pubkey || + options?.privkey || + options?.keysetId) // these options require a swap ) { // we need to swap - console.log("NOW WE SWAP") - // input selection + console.log('NOW WE SWAP'); + // input selection, needs fees because of the swap const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, @@ -333,31 +352,36 @@ class CashuWallet { ); console.log(`Selected amount: ${sumProofs(sendProofs)}`); options?.proofsWeHave?.push(...keepProofsSelect); - if (options?.includeFees) { - const keyset = await this.getKeys(options.keysetId); - const outputAmounts = splitAmount(amount, keyset.keys) - // create dummyProofs which are `Proof` objects with amounts from the outputAmounts - const dummyProofs: Array = outputAmounts.map((amount: number) => { - return { - amount: amount, - id: keyset.id, - secret: "dummy", - C: "dummy" - } as Proof; - }); - amount += this.getFeesForProofs(dummyProofs); - } + + // // we need to include the fees to the outputs of the swap + // if (options?.includeFees) { + // const keyset = await this.getKeys(options.keysetId); + // const outputAmounts = splitAmount(amount, keyset.keys) + // const dummyProofs: Array = outputAmounts.map((amount: number) => { + // return { + // amount: amount, + // id: keyset.id, + // secret: "dummy", + // C: "dummy" + // } as Proof; + // }); + // amount += this.getFeesForProofs(dummyProofs); + // } const { keep, send } = await this.swap(amount, sendProofs, options); const keepProofs = keepProofsSelect.concat(keep); return { keep: keepProofs, send }; } + + if (sumProofs(sendProofOffline) < amount + expectedFee) { + throw new Error('Not enough funds available to send'); + } return { keep: keepProofsOffline, send: sendProofOffline }; } private selectProofsToSend( proofs: Array, amountToSend: number, - includeFees?: boolean, + includeFees?: boolean ): SendResponse { const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs @@ -411,7 +435,19 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, + 0 + ) + ); + return fees; + } + + getFeesForKeyset(nInputs: number, keysetId: string): number { + const fees = Math.floor( + Math.max( + (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + + 999) / + 1000, 0 ) ); @@ -444,32 +480,62 @@ class CashuWallet { includeFees?: boolean; } ): Promise { - if (!options) { - options = {}; - } + if (!options) options = {}; const keyset = await this.getKeys(options.keysetId); const proofsToSend = proofs; - const amountToSend = amount; + let amountToSend = amount; const amountAvailable = sumProofs(proofs); - const amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); + let amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); + + // send output selection + let sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amountToSend, keyset.keys); + + // include the fees to spend the the outputs of the swap + if (options?.includeFees) { + let outputFee = this.getFeesForKeyset(sendAmounts.length, keyset.id); + let sendAmountsFee = splitAmount(outputFee, keyset.keys); + while ( + this.getFeesForKeyset(sendAmounts.concat(sendAmountsFee).length, keyset.id) > outputFee + ) { + outputFee++; + sendAmountsFee = splitAmount(outputFee, keyset.keys); + } + sendAmounts = sendAmounts.concat(sendAmountsFee); + amountToSend += outputFee; + amountToKeep -= outputFee; + } - // output selection + // keep output selection let keepAmounts; if (options && !options.outputAmounts?.keepAmounts && options.proofsWeHave) { - keepAmounts = getKeepAmounts(options.proofsWeHave, amountToKeep, keyset.keys, 3); + keepAmounts = getKeepAmounts( + options.proofsWeHave, + amountToKeep, + keyset.keys, + this._denominationTarget + ); } else if (options.outputAmounts) { + if ( + options.outputAmounts.keepAmounts?.reduce((a: number, b: number) => a + b, 0) != + amountToKeep + ) { + throw new Error('Keep amounts do not match amount to keep'); + } keepAmounts = options.outputAmounts.keepAmounts; } - const sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amountToSend, keyset.keys); if (amountToSend + this.getFeesForProofs(proofsToSend) > amountAvailable) { - throw new Error('Not enough funds available'); + throw new Error('Not enough funds available for swap'); } - console.log(`# swap: amountToKeep: ${amountToKeep}`); - console.log(`# swap: amountToSend: ${amountToSend}`); - console.log(`# swap: keepAmounts: ${keepAmounts}`); - console.log(`# swap: sendAmounts: ${sendAmounts}`); + if (amountToSend + this.getFeesForProofs(proofsToSend) + amountToKeep != amountAvailable) { + throw new Error('Amounts do not match for swap'); + } + + // console.log(`# swap: amountToKeep: ${amountToKeep}`); + // console.log(`# swap: amountToSend: ${amountToSend}`); + // console.log(`# swap: keepAmounts: ${keepAmounts}`); + // console.log(`# swap: sendAmounts: ${sendAmounts}`); options.outputAmounts = { keepAmounts: keepAmounts, sendAmounts: sendAmounts @@ -592,7 +658,12 @@ class CashuWallet { const keyset = await this.getKeys(options?.keysetId); if (!options?.outputAmounts && options?.proofsWeHave) { options.outputAmounts = { - keepAmounts: getKeepAmounts(options.proofsWeHave, amount, keyset.keys, 3), + keepAmounts: getKeepAmounts( + options.proofsWeHave, + amount, + keyset.keys, + this._denominationTarget + ), sendAmounts: [] }; } @@ -776,7 +847,10 @@ class CashuWallet { console.log(`# createSwapPayload: amount: ${amount} | input amount: ${totalAmount}`); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { console.log(`# splitting amount: ${totalAmount} - ${amount}`); - outputAmounts.keepAmounts = splitAmount(totalAmount - amount - this.getFeesForProofs(proofsToSend), keyset.keys); + outputAmounts.keepAmounts = splitAmount( + totalAmount - amount - this.getFeesForProofs(proofsToSend), + keyset.keys + ); console.log(`# keepAmounts: ${outputAmounts.keepAmounts}`); } const keepBlindedMessages = this.createRandomBlindedMessages( diff --git a/test/integration.test.ts b/test/integration.test.ts index 0c6916a37..be751ac34 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -79,8 +79,7 @@ describe('mint api', () => { }); test('pay local invoice', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit }); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -113,8 +112,7 @@ describe('mint api', () => { }); test('pay external invoice', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit }); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const request = await wallet.createMintQuote(3000); const tokens = await wallet.mintProofs(3000, request.quote); @@ -160,8 +158,7 @@ describe('mint api', () => { }); test('test send tokens with change', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit }); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -176,8 +173,7 @@ describe('mint api', () => { }, 10000000); test('receive tokens with previous split', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit }); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -190,8 +186,7 @@ describe('mint api', () => { }); test('receive tokens with previous mint', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit }); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const request = await wallet.createMintQuote(64); const tokens = await wallet.mintProofs(64, request.quote); const encoded = getEncodedToken({ @@ -202,8 +197,7 @@ describe('mint api', () => { }); test('send and receive p2pk', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint); - await wallet.loadMint(); + const wallet = new CashuWallet(mint, { unit, loadMint: true }); const privKeyAlice = secp256k1.utils.randomPrivateKey(); const pubKeyAlice = secp256k1.getPublicKey(privKeyAlice); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index c8db26e88..7a572fa3e 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -548,8 +548,7 @@ describe('send', () => { } ]; const result = await wallet.send(3, overpayProofs, { - // preference: { sendPreference: [{ amount: 1, count: 3 }] } - outputAmounts: { sendAmounts: [1, 1, 1], keepAmounts: [] } + outputAmounts: { sendAmounts: [1, 1, 1], keepAmounts: [1] } }); expect(result.send).toHaveLength(3); From 57d6d02bfe8854424edb4e6c5b32dd8ea594d718 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:16:25 +0200 Subject: [PATCH 096/246] works, still with comments --- src/CashuWallet.ts | 63 +++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 80eae6b8f..0dbf46502 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -211,11 +211,16 @@ class CashuWallet { * @param unit optional unit to get keys for * @returns keyset */ - async getKeys(keysetId?: string): Promise { + async getKeys(keysetId?: string, forceRefresh?: boolean): Promise { if (!keysetId) { - const allKeysets = await this.mint.getKeySets(); - const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); - keysetId = keysetToActivate.id; + if (!(this._keysets.length > 0) || forceRefresh) { + const allKeysets = await this.mint.getKeySets(); + const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); + keysetId = keysetToActivate.id; + } else { + const localKeyset = this.getActiveKeyset(this._keysets); + keysetId = localKeyset.id; + } } if (this._keys.get(keysetId)) { this.keysetId = keysetId; @@ -252,6 +257,7 @@ class CashuWallet { privkey?: string; } ): Promise> { + console.log(`enter receive: token: ${token} | options: ${JSON.stringify(options)}`); if (typeof token === 'string') { token = getDecodedToken(token); } @@ -325,6 +331,7 @@ class CashuWallet { includeFees?: boolean; } ): Promise { + console.log(`enter send: amount: ${amount} | proofs: ${proofs.length} | options: ${JSON.stringify(options)}`); if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } @@ -343,30 +350,15 @@ class CashuWallet { options?.keysetId) // these options require a swap ) { // we need to swap - console.log('NOW WE SWAP'); // input selection, needs fees because of the swap const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, true ); - console.log(`Selected amount: ${sumProofs(sendProofs)}`); + console.log(`Keep proofs: ${keepProofsSelect.length} | Amount: ${sumProofs(keepProofsSelect)} <> Send proofs: ${sendProofs.length} | Amount: ${sumProofs(sendProofs)}`); options?.proofsWeHave?.push(...keepProofsSelect); - // // we need to include the fees to the outputs of the swap - // if (options?.includeFees) { - // const keyset = await this.getKeys(options.keysetId); - // const outputAmounts = splitAmount(amount, keyset.keys) - // const dummyProofs: Array = outputAmounts.map((amount: number) => { - // return { - // amount: amount, - // id: keyset.id, - // secret: "dummy", - // C: "dummy" - // } as Proof; - // }); - // amount += this.getFeesForProofs(dummyProofs); - // } const { keep, send } = await this.swap(amount, sendProofs, options); const keepProofs = keepProofsSelect.concat(keep); return { keep: keepProofs, send }; @@ -383,6 +375,7 @@ class CashuWallet { amountToSend: number, includeFees?: boolean ): SendResponse { + console.log(`selectProofsToSend: length: ${proofs.length} | sum: ${sumProofs(proofs)} | amountToSend: ${amountToSend} | includeFees: ${includeFees}`); const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -392,6 +385,7 @@ class CashuWallet { .sort((a: Proof, b: Proof) => a.amount - b.amount); const nextBigger = biggerProofs[0]; if (!smallerProofs.length && nextBigger) { + console.log(`No smaller proofs found. Next bigger proof: ${nextBigger.amount}`); return { keep: proofs.filter((p: Proof) => p.secret !== nextBigger.secret), send: [nextBigger] @@ -399,6 +393,7 @@ class CashuWallet { } if (!smallerProofs.length && !nextBigger) { + console.log('No proofs found, returning empty'); return { keep: proofs, send: [] }; } @@ -406,7 +401,8 @@ class CashuWallet { let selectedProofs = [smallerProofs[0]]; const returnedProofs = []; const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; - remainder -= smallerProofs[0].amount - feePPK / 1000; + remainder -= selectedProofs[0].amount - feePPK / 1000; + console.log(`Selected proof: ${smallerProofs[0].amount} | Remainder: ${remainder} | Fee: ${feePPK}`); if (remainder > 0) { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), @@ -417,9 +413,12 @@ class CashuWallet { returnedProofs.push(...keep); } - if (sumProofs(selectedProofs) < amountToSend && nextBigger) { + const selectedFeePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; + if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { + console.log("Selecting next bigger proof"); selectedProofs = [nextBigger]; } + console.log(`Selected proofs: ${selectedProofs.length} | Amount: ${sumProofs(selectedProofs)}`); return { keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs @@ -435,7 +434,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -447,7 +446,7 @@ class CashuWallet { Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); @@ -486,7 +485,7 @@ class CashuWallet { let amountToSend = amount; const amountAvailable = sumProofs(proofs); let amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); - + console.log(`Amount to send: ${amountToSend} | Amount to keep: ${amountToKeep} | Amount available: ${amountAvailable}`); // send output selection let sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amountToSend, keyset.keys); @@ -503,6 +502,7 @@ class CashuWallet { sendAmounts = sendAmounts.concat(sendAmountsFee); amountToSend += outputFee; amountToKeep -= outputFee; + console.log(`Amount to send: ${amountToSend} | Amount to keep: ${amountToKeep} | Output fee: ${outputFee}`); } // keep output selection @@ -525,17 +525,14 @@ class CashuWallet { } if (amountToSend + this.getFeesForProofs(proofsToSend) > amountAvailable) { - throw new Error('Not enough funds available for swap'); + console.error(`Not enough funds available (${amountAvailable}) for swap amountToSend: ${amountToSend} + fee: ${this.getFeesForProofs(proofsToSend)} | length: ${proofsToSend.length}`); + throw new Error(`Not enough funds available for swap`); } if (amountToSend + this.getFeesForProofs(proofsToSend) + amountToKeep != amountAvailable) { throw new Error('Amounts do not match for swap'); } - // console.log(`# swap: amountToKeep: ${amountToKeep}`); - // console.log(`# swap: amountToSend: ${amountToSend}`); - // console.log(`# swap: keepAmounts: ${keepAmounts}`); - // console.log(`# swap: sendAmounts: ${sendAmounts}`); options.outputAmounts = { keepAmounts: keepAmounts, sendAmounts: sendAmounts @@ -843,15 +840,13 @@ class CashuWallet { payload: SwapPayload; blindedMessages: BlindedTransaction; } { + console.log(`### createSwapPayload: amount: ${amount}, proofsToSend: ${proofsToSend.length}, keyset: ${keyset.id}, outputAmounts: ${outputAmounts}, counter: ${counter}, pubkey: ${pubkey}, privkey: ${privkey}`); const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); - console.log(`# createSwapPayload: amount: ${amount} | input amount: ${totalAmount}`); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { - console.log(`# splitting amount: ${totalAmount} - ${amount}`); outputAmounts.keepAmounts = splitAmount( totalAmount - amount - this.getFeesForProofs(proofsToSend), keyset.keys ); - console.log(`# keepAmounts: ${outputAmounts.keepAmounts}`); } const keepBlindedMessages = this.createRandomBlindedMessages( totalAmount - amount - this.getFeesForProofs(proofsToSend), @@ -898,7 +893,6 @@ class CashuWallet { inputs: proofsToSend, outputs: [...blindedMessages.blindedMessages] }; - console.log(`# createSwapPayload: amount: ${amount}`); return { payload, blindedMessages }; } /** @@ -937,7 +931,6 @@ class CashuWallet { counter?: number, pubkey?: string ): BlindedMessageData & { amounts: Array } { - console.log(`# createRandomBlindedMessages: amount: ${amount} split: ${split}`); const amounts = splitAmount(amount, keyset.keys, split); return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } From 2f4df3c765de1d77f8a36f5c999ef37091cf11bd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:31:09 +0200 Subject: [PATCH 097/246] remove logging --- src/CashuWallet.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 0dbf46502..2dec36b97 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -257,7 +257,6 @@ class CashuWallet { privkey?: string; } ): Promise> { - console.log(`enter receive: token: ${token} | options: ${JSON.stringify(options)}`); if (typeof token === 'string') { token = getDecodedToken(token); } @@ -331,7 +330,7 @@ class CashuWallet { includeFees?: boolean; } ): Promise { - console.log(`enter send: amount: ${amount} | proofs: ${proofs.length} | options: ${JSON.stringify(options)}`); + console.log(`### SEND: ${amount} | ${sumProofs(proofs)}`); if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } @@ -356,7 +355,6 @@ class CashuWallet { amount, true ); - console.log(`Keep proofs: ${keepProofsSelect.length} | Amount: ${sumProofs(keepProofsSelect)} <> Send proofs: ${sendProofs.length} | Amount: ${sumProofs(sendProofs)}`); options?.proofsWeHave?.push(...keepProofsSelect); const { keep, send } = await this.swap(amount, sendProofs, options); @@ -367,6 +365,7 @@ class CashuWallet { if (sumProofs(sendProofOffline) < amount + expectedFee) { throw new Error('Not enough funds available to send'); } + return { keep: keepProofsOffline, send: sendProofOffline }; } @@ -375,7 +374,6 @@ class CashuWallet { amountToSend: number, includeFees?: boolean ): SendResponse { - console.log(`selectProofsToSend: length: ${proofs.length} | sum: ${sumProofs(proofs)} | amountToSend: ${amountToSend} | includeFees: ${includeFees}`); const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -385,7 +383,6 @@ class CashuWallet { .sort((a: Proof, b: Proof) => a.amount - b.amount); const nextBigger = biggerProofs[0]; if (!smallerProofs.length && nextBigger) { - console.log(`No smaller proofs found. Next bigger proof: ${nextBigger.amount}`); return { keep: proofs.filter((p: Proof) => p.secret !== nextBigger.secret), send: [nextBigger] @@ -393,7 +390,6 @@ class CashuWallet { } if (!smallerProofs.length && !nextBigger) { - console.log('No proofs found, returning empty'); return { keep: proofs, send: [] }; } @@ -402,7 +398,6 @@ class CashuWallet { const returnedProofs = []; const feePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; remainder -= selectedProofs[0].amount - feePPK / 1000; - console.log(`Selected proof: ${smallerProofs[0].amount} | Remainder: ${remainder} | Fee: ${feePPK}`); if (remainder > 0) { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), @@ -415,10 +410,8 @@ class CashuWallet { const selectedFeePPK = includeFees ? this.getFeesForProofs(selectedProofs) : 0; if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { - console.log("Selecting next bigger proof"); selectedProofs = [nextBigger]; } - console.log(`Selected proofs: ${selectedProofs.length} | Amount: ${sumProofs(selectedProofs)}`); return { keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs @@ -485,7 +478,6 @@ class CashuWallet { let amountToSend = amount; const amountAvailable = sumProofs(proofs); let amountToKeep = amountAvailable - amountToSend - this.getFeesForProofs(proofsToSend); - console.log(`Amount to send: ${amountToSend} | Amount to keep: ${amountToKeep} | Amount available: ${amountAvailable}`); // send output selection let sendAmounts = options?.outputAmounts?.sendAmounts || splitAmount(amountToSend, keyset.keys); @@ -502,7 +494,6 @@ class CashuWallet { sendAmounts = sendAmounts.concat(sendAmountsFee); amountToSend += outputFee; amountToKeep -= outputFee; - console.log(`Amount to send: ${amountToSend} | Amount to keep: ${amountToKeep} | Output fee: ${outputFee}`); } // keep output selection @@ -840,7 +831,6 @@ class CashuWallet { payload: SwapPayload; blindedMessages: BlindedTransaction; } { - console.log(`### createSwapPayload: amount: ${amount}, proofsToSend: ${proofsToSend.length}, keyset: ${keyset.id}, outputAmounts: ${outputAmounts}, counter: ${counter}, pubkey: ${pubkey}, privkey: ${privkey}`); const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { outputAmounts.keepAmounts = splitAmount( From 13519665ecad3c3af67b33e3171fff6676e416fd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:31:49 +0200 Subject: [PATCH 098/246] npm run format --- src/CashuWallet.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2dec36b97..e2b5eaea6 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -427,7 +427,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -439,7 +439,7 @@ class CashuWallet { Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); @@ -516,7 +516,11 @@ class CashuWallet { } if (amountToSend + this.getFeesForProofs(proofsToSend) > amountAvailable) { - console.error(`Not enough funds available (${amountAvailable}) for swap amountToSend: ${amountToSend} + fee: ${this.getFeesForProofs(proofsToSend)} | length: ${proofsToSend.length}`); + console.error( + `Not enough funds available (${amountAvailable}) for swap amountToSend: ${amountToSend} + fee: ${this.getFeesForProofs( + proofsToSend + )} | length: ${proofsToSend.length}` + ); throw new Error(`Not enough funds available for swap`); } From 3f55dba6c1ecf39e3da5c6d63e31d7123e9e27a3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:35:45 +0200 Subject: [PATCH 099/246] remove integration tests from unit test pipeline --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ae405f2f..5e33c1f35 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "compile": "rm -rf dist/lib && tsc && tsc --build tsconfig.es5.json", - "test": "jest --coverage", + "test": "jest --coverage --testPathIgnorePatterns ./test/integration.test.ts", "test-integration": "jest --coverage --testPathPattern ./test/integration.test.ts", "dev": "tsc --watch", "lint": "eslint --ext .js,.ts . --fix", From e2247e01cb5d425feaba31da35777f5c18988f89 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 23 Oct 2024 14:37:28 +0100 Subject: [PATCH 100/246] removed payLnInvoice handlers --- src/CashuWallet.ts | 58 ---------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e2b5eaea6..c01b907b5 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -755,64 +755,6 @@ class CashuWallet { }; } - /** - * Helper function that pays a Lightning invoice directly without having to create a melt quote before - * The combined amount of Proofs must match the payment amount including fees. - * @param invoice - * @param proofsToSend the exact amount to send including fees - * @param meltQuote melt quote for the invoice - * @param options.keysetId? optionally set keysetId for blank outputs for returned change. - * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param options.privkey? optionally set a private key to unlock P2PK locked secrets - * @returns - */ - async payLnInvoice( - invoice: string, - proofsToSend: Array, - meltQuote?: MeltQuoteResponse, - options?: { - keysetId?: string; - counter?: number; - privkey?: string; - } - ): Promise { - if (!meltQuote) { - meltQuote = await this.mint.createMeltQuote({ unit: this._unit, request: invoice }); - } - return await this.meltProofs(meltQuote, proofsToSend, { - keysetId: options?.keysetId, - counter: options?.counter, - privkey: options?.privkey - }); - } - - /** - * Helper function to ingest a Cashu token and pay a Lightning invoice with it. - * @param invoice Lightning invoice - * @param token cashu token - * @param meltQuote melt quote for the invoice - * @param options.keysetId? optionally set keysetId for blank outputs for returned change. - * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - */ - async payLnInvoiceWithToken( - invoice: string, - token: string, - meltQuote: MeltQuoteResponse, - options?: { - keysetId?: string; - counter?: number; - } - ): Promise { - const decodedToken = getDecodedToken(token); - const proofs = decodedToken.token - .filter((x: TokenEntry) => x.mint === this.mint.mintUrl) - .flatMap((t: TokenEntry) => t.proofs); - return this.payLnInvoice(invoice, proofs, meltQuote, { - keysetId: options?.keysetId, - counter: options?.counter - }); - } - /** * Creates a split payload * @param amount amount to send From bfeade5dd4bbffe3f8e8e796f904b42a694d6299 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 23 Oct 2024 14:37:39 +0100 Subject: [PATCH 101/246] remove payLn tests --- test/integration.test.ts | 2 - test/wallet.test.ts | 82 ---------------------------------------- 2 files changed, 84 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index be751ac34..b5332da92 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -94,7 +94,6 @@ describe('mint api', () => { expect(quote_).toBeDefined(); const sendResponse = await wallet.send(10, tokens.proofs, { includeFees: true }); - // const response = await wallet.payLnInvoice(mintQuote.request, sendResponse.send, quote); const response = await wallet.meltProofs(quote, sendResponse.send); expect(response).toBeDefined(); // expect that we have received the fee back, since it was internal @@ -125,7 +124,6 @@ describe('mint api', () => { expect(quote_).toBeDefined(); const sendResponse = await wallet.send(2000 + fee, tokens.proofs, { includeFees: true }); - // const response = await wallet.payLnInvoice(externalInvoice, sendResponse.send, meltQuote); const response = await wallet.meltProofs(meltQuote, sendResponse.send); expect(response).toBeDefined(); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7a572fa3e..f8b5c51a0 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -225,88 +225,6 @@ describe('checkProofsSpent', () => { }); }); -describe('payLnInvoice', () => { - const proofs = [ - { - id: '009a1f293253e41e', - amount: 1, - secret: '1f98e6837a434644c9411825d7c6d6e13974b931f8f0652217cea29010674a13', - C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' - } - ]; - test('test payLnInvoice base case', async () => { - nock(mintUrl) - .get('/v1/melt/quote/bolt11/test') - .reply(200, { - quote: 'test_melt_quote_id', - amount: 2000, - fee_reserve: 20, - payment_preimage: null, - state: 'PAID' - } as MeltQuoteResponse); - nock(mintUrl) - .post('/v1/melt/bolt11') - .reply(200, { - quote: 'test_melt_quote_id', - amount: 2000, - fee_reserve: 20, - payment_preimage: null, - state: 'PAID' - } as MeltQuoteResponse); - - const wallet = new CashuWallet(mint, { unit }); - const meltQuote = await wallet.checkMeltQuote('test'); - - const result = await wallet.payLnInvoice(invoice, proofs, meltQuote); - - expect(result).toEqual({ quote: meltQuote, change: [] }); - }); - test('test payLnInvoice change', async () => { - nock(mintUrl) - .get('/v1/melt/quote/bolt11/test') - .reply(200, { - quote: 'test_melt_quote_id', - amount: 2000, - fee_reserve: 20, - payment_preimage: 'asd', - state: 'PAID' - } as MeltQuoteResponse); - nock(mintUrl) - .post('/v1/melt/bolt11') - .reply(200, { - quote: 'test_melt_quote_id', - amount: 2000, - fee_reserve: 20, - payment_preimage: 'asd', - state: 'PAID', - change: [ - { - id: '009a1f293253e41e', - amount: 2, - C_: '0361a2725cfd88f60ded718378e8049a4a6cee32e214a9870b44c3ffea2dc9e625' - } - ] - }); - - const wallet = new CashuWallet(mint, { unit }); - const meltQuote = await wallet.checkMeltQuote('test'); - const result = await wallet.payLnInvoice(invoice, [{ ...proofs[0], amount: 3 }], meltQuote); - - expect(result.quote.state == MeltQuoteState.PAID).toBe(true); - expect(result.quote.payment_preimage).toBe('asd'); - expect(result.change).toHaveLength(1); - }); - test('test payLnInvoice bad resonse', async () => { - nock(mintUrl).post('/v1/melt/bolt11').reply(200, {}); - const wallet = new CashuWallet(mint, { unit }); - const result = await wallet - .payLnInvoice(invoice, proofs, {} as MeltQuoteResponse) - .catch((e) => e); - - expect(result).toEqual(new Error('bad response')); - }); -}); - describe('requestTokens', () => { test('test requestTokens', async () => { nock(mintUrl) From c6efe40930ae0408aae656ab2e62a6d9598e5a0d Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 23 Oct 2024 14:50:43 +0100 Subject: [PATCH 102/246] added removal to migration doc --- migration-2.0.0.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 migration-2.0.0.md diff --git a/migration-2.0.0.md b/migration-2.0.0.md new file mode 100644 index 000000000..8881ca82d --- /dev/null +++ b/migration-2.0.0.md @@ -0,0 +1,18 @@ +# Version 2.0.0 Migration guide + +⚠️ Upgrading to version 2.0.0 will come with breaking changes! Please follow the migration guide for a smooth transition to the new version. + +## Breaking changes + +### `CashuWallet` interface changes + +#### removed `payLnInvoice` helper + +The helper function was removed. Instead users will have to manage a melt quote manually: + +```ts +const quote = await wallet.createMeltQuote(invoice); +const totalAmount = quote.fee_reserve + invoiceAmount; +const { keep, send } = await wallet.send(totalAmount, proofs); +const payRes = await wallet.meltProofs(quote, send); +``` From c6a7ab7689a9f7e645fd7fcbbca4c2127248b6ee Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 19:11:05 +0900 Subject: [PATCH 103/246] clean up version migration files --- migration-1.0.0.md | 17 +++-------------- migration-2.0.0.md | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/migration-1.0.0.md b/migration-1.0.0.md index aad4c4933..ecc6ac8ca 100644 --- a/migration-1.0.0.md +++ b/migration-1.0.0.md @@ -46,9 +46,9 @@ type MintQuoteResponse = { }; ``` -where `request` is the invoice to be paid, and `quote` is the identifier used to pass to `mintProofs()`. +where `request` is the invoice to be paid, and `quote` is the identifier used to pass to `mintTokens()`. -**`requestTokens()` --> `mintProofs()`** +**`requestTokens()` --> `mintTokens()`** --- @@ -67,7 +67,7 @@ type MeltQuoteResponse = { }; ``` -where `quote` is the identifier to pass to `meltProofs()` +where `quote` is the identifier to pass to `meltTokens()` --- @@ -119,17 +119,6 @@ type BlindedMessage { --- -**`amountPreference`** is not used anymore. - -`preference?: Array;` -> `outputAmounts?: OutputAmounts;` - -- in `SendResponse`, `returnChange` is now called `keep` -- `mintTokens` is now called `mintProofs` -- `meltTokens` is now called `meltProofs` -- `CashuMint.split` is now called `CashuMint.swap` - ---- - ### Pattern changes **removed `newKeys` from returns**: Functions no longer return `newKeys`. Wallets now specify the keyset they use in the BlindedMessage via the `id` field. diff --git a/migration-2.0.0.md b/migration-2.0.0.md index 8881ca82d..cde883d36 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -16,3 +16,30 @@ const totalAmount = quote.fee_reserve + invoiceAmount; const { keep, send } = await wallet.send(totalAmount, proofs); const payRes = await wallet.meltProofs(quote, send); ``` + +--- + +#### Preference for outputs are now passed as a object of simple arrays + +**`AmountPreference`** is not used anymore. + +`preference?: Array;` -> `outputAmounts?: OutputAmounts;` + +where + +```typescript + +export type OutputAmounts = { + sendAmounts: Array; + keepAmounts?: Array; +}; + + +``` + +#### renamed functions + +- in `SendResponse`, `returnChange` is now called `keep` +- `CashuWallet.mintTokens()` is now called `CashuWallet.mintProofs()` +- `CashuWallet.meltTokens()` is now called `CashuWallet.meltProofs()` +- `CashuMint.split()` is now called `CashuMint.swap()` \ No newline at end of file From cfad6576b736fa5532825ea659efe6b9ab3c0945 Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 19:21:35 +0900 Subject: [PATCH 104/246] union type for order --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 2c22f72b7..964029bbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,7 @@ function splitAmount( value: number, keyset: Keys, split?: Array, - order?: string + order?: "desc" | "asc" ): Array { const chunks: Array = []; if (split) { From a06ada13afc4b0c8530b8aefbbcc370f2e1499fb Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 21:26:14 +0900 Subject: [PATCH 105/246] some review fixes & docs completion --- src/CashuWallet.ts | 96 +++++++++++++++++++++++---------- src/utils.ts | 132 +++++++++++++++++++++++++++------------------ 2 files changed, 148 insertions(+), 80 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index c01b907b5..7fb6ec12a 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -41,7 +41,15 @@ import { import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; + +/** + * The default number of proofs per denomination to keep in a wallet. +*/ const DEFAULT_DENOMINATION_TARGET = 3; + +/** + * The default unit for the wallet, if not specified in constructor. + */ const DEFAULT_UNIT = 'sat'; /** @@ -67,6 +75,7 @@ class CashuWallet { * @param mintInfo mint info from the mint (will be fetched from mint if not provided) * @param mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided + * @param loadMint if set to true info will be loaded from mint */ constructor( mint: CashuMint, @@ -183,9 +192,10 @@ class CashuWallet { } return activeKeyset; } + /** * Get keysets from the mint with the unit of the wallet - * @returns keysets + * @returns keysets with wallets unit */ async getKeySets(): Promise> { const allKeysets = await this.mint.getKeySets(); @@ -194,6 +204,10 @@ class CashuWallet { return this._keysets; } + /** + * Get all active keys from the mint and set the keyset with the lowest fees as the active wallet keyset. + * @returns keyset + */ async getAllKeys(): Promise> { const keysets = await this.mint.getKeys(); this._keys = new Map(keysets.keysets.map((k: MintKeys) => [k.id, k])); @@ -208,7 +222,7 @@ class CashuWallet { * Otherwise, we select an active keyset with the unit of the wallet. * * @param keysetId optional keysetId to get keys for - * @param unit optional unit to get keys for + * @param forceRefresh? if set to true, it will force refresh the keyset from the mint * @returns keyset */ async getKeys(keysetId?: string, forceRefresh?: boolean): Promise { @@ -238,12 +252,13 @@ class CashuWallet { /** * Receive an encoded or raw Cashu token (only supports single tokens. It will only process the first token in the token array) - * @param {(string|Token)} token - Cashu token - * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. - * @param outputAmounts? optionally specify the output's amounts to keep and to send. - * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param privkey? will create a signature on the @param token secrets if set + * @param {(string|Token)} token - Cashu token, either as string or decoded + * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint + * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.privkey? will create a signature on the @param token secrets if set * @returns New token with newly created proofs, token entries that had errors */ async receive( @@ -274,12 +289,12 @@ class CashuWallet { /** * Receive a single cashu token entry * @param tokenEntry a single entry of a cashu token - * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. - * @param outputAmounts? optionally specify the output's amounts to keep. - * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param privkey? will create a signature on the @param tokenEntry secrets if set - * @returns New token entry with newly created proofs, proofs that had errors + * @param options.keyksetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint + * @param options.outputAmounts? optionally specify the output's amounts to keep. + * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.privkey? will create a signature on the @param tokenEntry secrets if set + * @returns {Promise>} New token entry with newly created proofs, proofs that had errors */ async receiveTokenEntry( tokenEntry: TokenEntry, @@ -316,6 +331,20 @@ class CashuWallet { return proofs; } + /** + * Send proofs of a given amount, by providing at least the required amount of proofs + * @param amount amount to send + * @param proofs array of proofs (accumulated amount of proofs must be >= than amount) + * @param options.outputAmounts? optionally specify the output's amounts to keep and send. + * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.privkey? will create a signature on the output secrets if set + * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint + * @param options.offline? optionally send proofs offline. + * @param options.includeFees? optionally include fees in the response. + * @returns {SendResponse} + */ async send( amount: number, proofs: Array, @@ -330,7 +359,6 @@ class CashuWallet { includeFees?: boolean; } ): Promise { - console.log(`### SEND: ${amount} | ${sumProofs(proofs)}`); if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } @@ -418,6 +446,11 @@ class CashuWallet { }; } + /** + * calculates the fees based on inputs (proofs) + * @param proofs input proofs to calculate fees for + * @returns fee amount + */ getFeesForProofs(proofs: Array): number { const fees = Math.floor( Math.max( @@ -427,19 +460,25 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); return fees; } + /** + * calculates the fees based on inputs for a given keyset + * @param nInputs number of inputs + * @param keysetId keysetId used to lookup `input_fee_ppk` + * @returns fee amount + */ getFeesForKeyset(nInputs: number, keysetId: string): number { const fees = Math.floor( Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); @@ -452,11 +491,13 @@ class CashuWallet { * if both amount and preference are set, but the preference cannot fulfill the amount, then we use the default split * @param amount amount to send while performing the optimal split (least proofs possible). can be set to undefined if preference is set * @param proofs proofs matching that amount - * @param preference Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. - * @param outputAmounts? optionally specify the output's amounts to keep and to send. - * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param privkey? will create a signature on the @param proofs secrets if set + * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. + * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint + * @param options.includeFees? include estimated fees for the receiver to receive the proofs + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.privkey? will create a signature on the @param proofs secrets if set * @returns promise of the change- and send-proofs */ async swap( @@ -569,6 +610,7 @@ class CashuWallet { * Regenerates * @param start set starting point for count (first cycle for each keyset should usually be 0) * @param count set number of blinded messages that should be generated + * @param options.keysetId set a custom keysetId to restore from. keysetIds can be loaded with `CashuMint.getKeySets()` * @returns proofs */ async restore( @@ -630,10 +672,10 @@ class CashuWallet { * @param amount amount to request * @param quote ID of mint quote * @param options.keysetId? optionally set keysetId for blank outputs for returned change. - * @param preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. - * @param outputAmounts? optionally specify the output's amounts to keep and to send. - * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.preference? Deprecated. Use `outputAmounts` instead. Optional preference for splitting proofs into specific amounts. + * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. + * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect + * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @returns proofs */ async mintProofs( @@ -833,7 +875,7 @@ class CashuWallet { } /** * returns proofs that are already spent (use for keeping wallet state clean) - * @param proofs (only the 'Y' field is required) + * @param proofs (only the `secret` field is required) * @returns */ async checkProofsSpent(proofs: Array): Promise> { diff --git a/src/utils.ts b/src/utils.ts index 964029bbf..a580f2007 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,13 +21,21 @@ import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; import { PaymentRequest } from './model/PaymentRequest.js'; -function splitAmount( +/** + * Splits the amount into denominations of the provided @param keyset + * @param value amount to split + * @param keyset keys to look up split amounts + * @param split? optional custom split amounts + * @param order? optional order for split amounts (default: "asc") + * @returns Array of split amounts + * @throws Error if @param split amount is greater than @param value amount + */ +export function splitAmount( value: number, keyset: Keys, split?: Array, order?: "desc" | "asc" ): Array { - const chunks: Array = []; if (split) { if (split.reduce((a: number, b: number) => a + b, 0) > value) { throw new Error( @@ -37,23 +45,38 @@ function splitAmount( )} > ${value}` ); } - chunks.push(...getPreference(value, keyset, split)); + split.forEach((amt: number) => { + if (!hasCorrespondingKey(amt, keyset)) { + throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); + } + }) value = value - - chunks.reduce((curr: number, acc: number) => { + split.reduce((curr: number, acc: number) => { return curr + acc; }, 0); } + else { + split = []; + } const sortedKeyAmounts = getKeysetAmounts(keyset); sortedKeyAmounts.forEach((amt: number) => { const q = Math.floor(value / amt); - for (let i = 0; i < q; ++i) chunks.push(amt); + for (let i = 0; i < q; ++i) split?.push(amt); value %= amt; }); - return chunks.sort((a, b) => (order === 'desc' ? b - a : a - b)); + return split.sort((a, b) => (order === 'desc' ? b - a : a - b)); } -function getKeepAmounts( +/** + * Creates a list of amounts to keep based on the proofs we have and the proofs we want to reach. + * @param proofsWeHave complete set of proofs stored (from current mint) + * @param amountToKeep amount to keep + * @param keys keys of current keyset + * @param targetCount the target number of proofs to reach + * @returns an array of amounts to keep + */ +export function getKeepAmounts( proofsWeHave: Array, amountToKeep: number, keys: Keys, @@ -83,16 +106,15 @@ function getKeepAmounts( }); } const sortedAmountsWeWant = amountsWeWant.sort((a, b) => a - b); - // console.log(`# getKeepAmounts: amountToKeep: ${amountToKeep}`); - // console.log(`# getKeepAmounts: amountsWeHave: ${amountsWeHave}`); - // console.log(`# getKeepAmounts: amountsWeWant: ${sortedAmountsWeWant}`); return sortedAmountsWeWant; } - -// function isPowerOfTwo(number: number) { -// return number && !(number & (number - 1)); -// } -function getKeysetAmounts(keyset: Keys, order = 'desc'): Array { +/** + * returns the amounts in the keyset sorted by the order specified + * @param keyset to search in + * @param order order to sort the amounts in + * @returns the amounts in the keyset sorted by the order specified + */ +export function getKeysetAmounts(keyset: Keys, order: "asc" | "desc" = 'desc'): Array { if (order == 'desc') { return Object.keys(keyset) .map((k: string) => parseInt(k)) @@ -103,44 +125,59 @@ function getKeysetAmounts(keyset: Keys, order = 'desc'): Array { .sort((a: number, b: number) => a - b); } -function hasCorrespondingKey(amount: number, keyset: Keys) { +/** + * Checks if the provided amount is in the keyset. + * @param amount amount to check + * @param keyset to search in + * @returns true if the amount is in the keyset, false otherwise + */ +export function hasCorrespondingKey(amount: number, keyset: Keys): boolean { return amount in keyset; } -function getPreference(amount: number, keyset: Keys, split: Array): Array { - const chunks: Array = []; - split.forEach((splitAmount: number) => { - if (!hasCorrespondingKey(splitAmount, keyset)) { - throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); - } - chunks.push(splitAmount); - }); - return chunks; -} - -function bytesToNumber(bytes: Uint8Array): bigint { +/** + * Converts a bytes array to a number. + * @param bytes to convert to number + * @returns number + */ +export function bytesToNumber(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(bytes)); } -function hexToNumber(hex: string): bigint { +/** + * Converts a hex string to a number. + * @param hex to convert to number + * @returns number + */ +export function hexToNumber(hex: string): bigint { return BigInt(`0x${hex}`); } -//used for json serialization -function bigIntStringify(_key: unknown, value: T) { +/** + * Helper function to stringify a bigint + * @param _key + * @param value to stringify + * @returns stringified bigint + */ +export function bigIntStringify(_key: unknown, value: T): string | T { return typeof value === 'bigint' ? value.toString() : value; } /** * Helper function to encode a v3 cashu token - * @param token - * @returns + * @param token to encode + * @returns encoded token */ -function getEncodedToken(token: Token): string { +export function getEncodedToken(token: Token): string { return TOKEN_PREFIX + TOKEN_VERSION + encodeJsonToBase64(token); } -function getEncodedTokenV4(token: Token): string { +/** + * Helper function to encode a v4 cashu token + * @param token to encode + * @returns encoded token + */ +export function getEncodedTokenV4(token: Token): string { const idMap: { [id: string]: Array } = {}; let mint: string | undefined = undefined; for (let i = 0; i < token.token.length; i++) { @@ -189,7 +226,7 @@ function getEncodedTokenV4(token: Token): string { * @param token an encoded cashu token (cashuAey...) * @returns cashu token object */ -function getDecodedToken(token: string) { +export function getDecodedToken(token: string) { // remove prefixes const uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:', 'cashu']; uriPrefixes.forEach((prefix: string) => { @@ -202,10 +239,11 @@ function getDecodedToken(token: string) { } /** - * @param token - * @returns + * Helper function to decode different versions of cashu tokens into an object + * @param token an encoded cashu token (cashuAey...) + * @returns cashu Token object */ -function handleTokens(token: string): Token { +export function handleTokens(token: string): Token { const version = token.slice(0, 1); const encodedToken = token.slice(1); if (version === 'A') { @@ -248,7 +286,7 @@ export function deriveKeysetId(keys: Keys) { return '00' + hashHex; } -function mergeUInt8Arrays(a1: Uint8Array, a2: Uint8Array): Uint8Array { +export function mergeUInt8Arrays(a1: Uint8Array, a2: Uint8Array): Uint8Array { // sum of individual array lengths const mergedArray = new Uint8Array(a1.length + a2.length); mergedArray.set(a1); @@ -286,18 +324,6 @@ export function sumProofs(proofs: Array) { return proofs.reduce((acc: number, proof: Proof) => acc + proof.amount, 0); } -function decodePaymentRequest(paymentRequest: string) { +export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } - -export { - bigIntStringify, - bytesToNumber, - getDecodedToken, - getEncodedToken, - getEncodedTokenV4, - hexToNumber, - splitAmount, - getKeepAmounts, - decodePaymentRequest -}; From 3c92c218221bc4d5e1edf72af870196cb59173cf Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 21:30:01 +0900 Subject: [PATCH 106/246] add missing param description --- src/CashuWallet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 7fb6ec12a..5760d3b22 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -73,6 +73,7 @@ class CashuWallet { * @param keys public keys from the mint (will be fetched from mint if not provided) * @param keysets keysets from the mint (will be fetched from mint if not provided) * @param mintInfo mint info from the mint (will be fetched from mint if not provided) + * @param denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) * @param mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided * @param loadMint if set to true info will be loaded from mint From 3f9d4a8006992201943151e5da6da3fb9147798f Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 21:30:46 +0900 Subject: [PATCH 107/246] mark optional params --- src/CashuWallet.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 5760d3b22..f4631d702 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -69,14 +69,14 @@ class CashuWallet { /** * @param mint Cashu mint instance is used to make api calls - * @param unit optionally set unit (default is 'sat') - * @param keys public keys from the mint (will be fetched from mint if not provided) - * @param keysets keysets from the mint (will be fetched from mint if not provided) - * @param mintInfo mint info from the mint (will be fetched from mint if not provided) - * @param denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) - * @param mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. + * @param options.unit optionally set unit (default is 'sat') + * @param options.keys public keys from the mint (will be fetched from mint if not provided) + * @param options.keysets keysets from the mint (will be fetched from mint if not provided) + * @param options.mintInfo mint info from the mint (will be fetched from mint if not provided) + * @param options.denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) + * @param options.mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided - * @param loadMint if set to true info will be loaded from mint + * @param options.loadMint if set to true info will be loaded from mint */ constructor( mint: CashuMint, From e3e26b017adec94f0dc2e8f6fd30e2b28dff86fb Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Thu, 24 Oct 2024 21:49:02 +0900 Subject: [PATCH 108/246] format --- migration-2.0.0.md | 9 +++------ src/CashuWallet.ts | 17 ++++++++--------- src/utils.ts | 31 +++++++++++++++---------------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index cde883d36..a4b30c1f5 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -19,7 +19,7 @@ const payRes = await wallet.meltProofs(quote, send); --- -#### Preference for outputs are now passed as a object of simple arrays +#### Preference for outputs are now passed as a object of simple arrays **`AmountPreference`** is not used anymore. @@ -28,18 +28,15 @@ const payRes = await wallet.meltProofs(quote, send); where ```typescript - export type OutputAmounts = { sendAmounts: Array; keepAmounts?: Array; }; - - ``` -#### renamed functions +#### renamed functions - in `SendResponse`, `returnChange` is now called `keep` - `CashuWallet.mintTokens()` is now called `CashuWallet.mintProofs()` - `CashuWallet.meltTokens()` is now called `CashuWallet.meltProofs()` -- `CashuMint.split()` is now called `CashuMint.swap()` \ No newline at end of file +- `CashuMint.split()` is now called `CashuMint.swap()` diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index f4631d702..8d2209608 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -41,10 +41,9 @@ import { import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; - /** - * The default number of proofs per denomination to keep in a wallet. -*/ + * The default number of proofs per denomination to keep in a wallet. + */ const DEFAULT_DENOMINATION_TARGET = 3; /** @@ -76,7 +75,7 @@ class CashuWallet { * @param options.denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) * @param options.mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided - * @param options.loadMint if set to true info will be loaded from mint + * @param options.loadMint if set to true info will be loaded from mint */ constructor( mint: CashuMint, @@ -256,7 +255,7 @@ class CashuWallet { * @param {(string|Token)} token - Cashu token, either as string or decoded * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. - * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param token secrets if set @@ -338,7 +337,7 @@ class CashuWallet { * @param proofs array of proofs (accumulated amount of proofs must be >= than amount) * @param options.outputAmounts? optionally specify the output's amounts to keep and send. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the output secrets if set * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint @@ -461,7 +460,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -479,7 +478,7 @@ class CashuWallet { Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); @@ -496,7 +495,7 @@ class CashuWallet { * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint * @param options.includeFees? include estimated fees for the receiver to receive the proofs - * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts + * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param proofs secrets if set * @returns promise of the change- and send-proofs diff --git a/src/utils.ts b/src/utils.ts index a580f2007..67e541519 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,7 @@ import { PaymentRequest } from './model/PaymentRequest.js'; * Splits the amount into denominations of the provided @param keyset * @param value amount to split * @param keyset keys to look up split amounts - * @param split? optional custom split amounts + * @param split? optional custom split amounts * @param order? optional order for split amounts (default: "asc") * @returns Array of split amounts * @throws Error if @param split amount is greater than @param value amount @@ -34,7 +34,7 @@ export function splitAmount( value: number, keyset: Keys, split?: Array, - order?: "desc" | "asc" + order?: 'desc' | 'asc' ): Array { if (split) { if (split.reduce((a: number, b: number) => a + b, 0) > value) { @@ -45,18 +45,17 @@ export function splitAmount( )} > ${value}` ); } - split.forEach((amt: number) => { + split.forEach((amt: number) => { if (!hasCorrespondingKey(amt, keyset)) { throw new Error('Provided amount preferences do not match the amounts of the mint keyset.'); } - }) + }); value = value - split.reduce((curr: number, acc: number) => { return curr + acc; }, 0); - } - else { + } else { split = []; } const sortedKeyAmounts = getKeysetAmounts(keyset); @@ -114,7 +113,7 @@ export function getKeepAmounts( * @param order order to sort the amounts in * @returns the amounts in the keyset sorted by the order specified */ -export function getKeysetAmounts(keyset: Keys, order: "asc" | "desc" = 'desc'): Array { +export function getKeysetAmounts(keyset: Keys, order: 'asc' | 'desc' = 'desc'): Array { if (order == 'desc') { return Object.keys(keyset) .map((k: string) => parseInt(k)) @@ -154,11 +153,11 @@ export function hexToNumber(hex: string): bigint { } /** - * Helper function to stringify a bigint - * @param _key - * @param value to stringify - * @returns stringified bigint - */ + * Helper function to stringify a bigint + * @param _key + * @param value to stringify + * @returns stringified bigint + */ export function bigIntStringify(_key: unknown, value: T): string | T { return typeof value === 'bigint' ? value.toString() : value; } @@ -173,10 +172,10 @@ export function getEncodedToken(token: Token): string { } /** - * Helper function to encode a v4 cashu token - * @param token to encode - * @returns encoded token - */ + * Helper function to encode a v4 cashu token + * @param token to encode + * @returns encoded token + */ export function getEncodedTokenV4(token: Token): string { const idMap: { [id: string]: Array } = {}; let mint: string | undefined = undefined; From dd0af825df967eafdd70a630054dac9354e5af4a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:16:23 +0200 Subject: [PATCH 109/246] add getKeepAmount tests --- src/CashuWallet.ts | 4 ++-- src/utils.ts | 2 +- test/utils.test.ts | 32 +++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8d2209608..71cdb3122 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -73,7 +73,7 @@ class CashuWallet { * @param options.keysets keysets from the mint (will be fetched from mint if not provided) * @param options.mintInfo mint info from the mint (will be fetched from mint if not provided) * @param options.denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) - * @param options.mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallets deterministic secrets. When the mnemonic is provided, the seed will be derived from it. + * @param options.mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallet's deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided * @param options.loadMint if set to true info will be loaded from mint */ @@ -195,7 +195,7 @@ class CashuWallet { /** * Get keysets from the mint with the unit of the wallet - * @returns keysets with wallets unit + * @returns keysets with wallet's unit */ async getKeySets(): Promise> { const allKeysets = await this.mint.getKeySets(); diff --git a/src/utils.ts b/src/utils.ts index 67e541519..d7320b69f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -88,7 +88,7 @@ export function getKeepAmounts( const sortedKeyAmounts = getKeysetAmounts(keys, 'asc'); sortedKeyAmounts.forEach((amt) => { const countWeHave = amountsWeHave.filter((a) => a === amt).length; - const countWeWant = Math.floor(targetCount - countWeHave); + const countWeWant = Math.max(targetCount - countWeHave, 0); for (let i = 0; i < countWeWant; ++i) { if (amountsWeWant.reduce((a, b) => a + b, 0) + amt > amountToKeep) { break; diff --git a/test/utils.test.ts b/test/utils.test.ts index 675902f7b..8091c0640 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,4 +1,4 @@ -import { Token, Keys } from '../src/model/types/index.js'; +import { Token, Keys, Proof } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; @@ -294,3 +294,33 @@ describe('test v4 encoding', () => { expect(decodedExpectedToken).toEqual(decodedEncodedToken); }); }); + +describe('test output selection', () => { + test('keep amounts', () => { + const amountsWeHave = [1, 2, 4, 4, 4, 8]; + const proofsWeHave = amountsWeHave.map((amount) => { + return { + amount: amount, + id: 'id', + C: 'C' + } as Proof; + }); + const keys = PUBKEYS as Keys; + + // info: getKeepAmounts returns the amounts we need to fill up + // the wallet to a target number of denominations plus an optimal + // split of the remaining amount (to reach the total amount) + + let amountsToKeep = utils.getKeepAmounts(proofsWeHave, 22, keys, 3); + // keeping 22 with a target count of 3, we expect two 1s, two 2s, no 4s, and two 8s, and no extra to reach 22 + expect(amountsToKeep).toEqual([1, 1, 2, 2, 8, 8]); + + // keeping 22 with a target count of 4, we expect three 1s, three 2s, one 4, and one 8 and another 1 to reach 22 + amountsToKeep = utils.getKeepAmounts(proofsWeHave, 22, keys, 4); + expect(amountsToKeep).toEqual([1, 1, 1, 1, 2, 2, 2, 4, 8]); + + // keeping 22 with a target of 2, we expect one 1, one 2, no 4s, one 8, and another 1, 2, 8 to reach 22 + amountsToKeep = utils.getKeepAmounts(proofsWeHave, 22, keys, 2); + expect(amountsToKeep).toEqual([1, 1, 2, 2, 8, 8]); + }); +}); From 21bd131403e1f3c45a592600aa0155c3f705fae1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:25:43 +0200 Subject: [PATCH 110/246] add example of send and receive --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f12c0e705..c47fd2438 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ npm i @cashu/cashu-ts import { CashuMint, CashuWallet, MintQuoteState } from '@cashu/cashu-ts'; const mintUrl = 'http://localhost:3338'; // the mint URL const mint = new CashuMint(mintUrl); -const wallet = new CashuWallet(mint); +const wallet = new CashuWallet(mint, { loadMint: true }); const mintQuote = await wallet.createMintQuote(64); // pay the invoice here before you continue... const mintQuoteChecked = await wallet.checkMintQuote(mintQuote.quote); @@ -98,7 +98,13 @@ const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); #### Create a token and receive it ```typescript +// we assume that `wallet` already minted `proofs`, as above +const wallet2 = new CashuWallet(mint, { loadMint: true }) // receiving wallet +const { keep, send } = await wallet.send(32, proofs); +const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] }); +console.log(token); +const receiveProofs = await wallet2.receive(token); ``` ## Contribute From d4ef4951ef69b1bd881936f089432a9744b6fa4e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:26:00 +0200 Subject: [PATCH 111/246] format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c47fd2438..c4f1f428d 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); ```typescript // we assume that `wallet` already minted `proofs`, as above -const wallet2 = new CashuWallet(mint, { loadMint: true }) // receiving wallet +const wallet2 = new CashuWallet(mint, { loadMint: true }); // receiving wallet const { keep, send } = await wallet.send(32, proofs); const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] }); console.log(token); From 08e62ff00a1110264699a62a1428748e290d155e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 26 Oct 2024 15:36:06 +0200 Subject: [PATCH 112/246] remove loadMint from constructor --- README.md | 6 ++-- src/CashuWallet.ts | 73 ++++++++++++++++++++-------------------- test/integration.test.ts | 12 +++---- test/wallet.test.ts | 5 ++- 4 files changed, 49 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index c4f1f428d..f4636028d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ npm i @cashu/cashu-ts import { CashuMint, CashuWallet, MintQuoteState } from '@cashu/cashu-ts'; const mintUrl = 'http://localhost:3338'; // the mint URL const mint = new CashuMint(mintUrl); -const wallet = new CashuWallet(mint, { loadMint: true }); +const wallet = new CashuWallet(mint); const mintQuote = await wallet.createMintQuote(64); // pay the invoice here before you continue... const mintQuoteChecked = await wallet.checkMintQuote(mintQuote.quote); @@ -77,7 +77,7 @@ if (mintQuoteChecked.state == MintQuoteState.PAID) { import { CashuMint, CashuWallet } from '@cashu/cashu-ts'; const mintUrl = 'http://localhost:3338'; // the mint URL const mint = new CashuMint(mintUrl); -const wallet = new CashuWallet(mint, { loadMint: true }); // load the keysets of the mint +const wallet = new CashuWallet(mint); // load the keysets of the mint const invoice = 'lnbc......'; // Lightning invoice to pay const meltQuote = await wallet.createMeltQuote(invoice); @@ -99,7 +99,7 @@ const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); ```typescript // we assume that `wallet` already minted `proofs`, as above -const wallet2 = new CashuWallet(mint, { loadMint: true }); // receiving wallet +const wallet2 = new CashuWallet(mint); // receiving wallet const { keep, send } = await wallet.send(32, proofs); const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] }); console.log(token); diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 71cdb3122..1bb984262 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -75,7 +75,6 @@ class CashuWallet { * @param options.denominationTarget target number proofs per denomination (default: see @constant DEFAULT_DENOMINATION_TARGET) * @param options.mnemonicOrSeed mnemonic phrase or Seed to initial derivation key for this wallet's deterministic secrets. When the mnemonic is provided, the seed will be derived from it. * This can lead to poor performance, in which case the seed should be directly provided - * @param options.loadMint if set to true info will be loaded from mint */ constructor( mint: CashuMint, @@ -86,7 +85,6 @@ class CashuWallet { mintInfo?: GetInfoResponse; mnemonicOrSeed?: string | Uint8Array; denominationTarget?: number; - loadMint?: boolean; } ) { this.mint = mint; @@ -103,16 +101,6 @@ class CashuWallet { this._denominationTarget = options.denominationTarget; } - if (options?.loadMint) { - this.loadMint() - .then(() => { - console.log('Mint loaded'); - }) - .catch((e: Error) => { - console.error('Failed to load mint', e); - }); - } - if (!options?.mnemonicOrSeed) { return; } else if (options?.mnemonicOrSeed instanceof Uint8Array) { @@ -184,6 +172,10 @@ class CashuWallet { activeKeysets = hexKeysets; } // end deprecated + + // we only consider keyset IDs that start with "00" + activeKeysets = activeKeysets.filter((k: MintKeyset) => k.id.startsWith('00')); + const activeKeyset = activeKeysets.sort( (a: MintKeyset, b: MintKeyset) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0) )[0]; @@ -226,28 +218,31 @@ class CashuWallet { * @returns keyset */ async getKeys(keysetId?: string, forceRefresh?: boolean): Promise { + if (!(this._keysets.length > 0) || forceRefresh) { + await this.getKeySets(); + } + // no keyset id is chosen, let's choose one if (!keysetId) { - if (!(this._keysets.length > 0) || forceRefresh) { - const allKeysets = await this.mint.getKeySets(); - const keysetToActivate = this.getActiveKeyset(allKeysets.keysets); - keysetId = keysetToActivate.id; - } else { - const localKeyset = this.getActiveKeyset(this._keysets); - keysetId = localKeyset.id; + const localKeyset = this.getActiveKeyset(this._keysets); + keysetId = localKeyset.id; + } + // make sure we have keyset for this id + if (!this._keysets.find((k: MintKeyset) => k.id === keysetId)) { + await this.getKeySets(); + if (!this._keysets.find((k: MintKeyset) => k.id === keysetId)) { + throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); } } - if (this._keys.get(keysetId)) { - this.keysetId = keysetId; - return this._keys.get(keysetId) as MintKeys; - } - const allKeysets = await this.mint.getKeys(keysetId); - const keyset = allKeysets.keysets[0]; - if (!keyset) { - throw new Error(`could not initialize keys. No keyset with id '${keysetId}' found`); + + // make sure we have keys for this id + if (!this._keys.get(keysetId)) { + const keys = await this.mint.getKeys(keysetId); + this._keys.set(keysetId, keys.keysets[0]); } - this._keys.set(keysetId, keyset); + + // set and return this.keysetId = keysetId; - return keyset; + return this._keys.get(keysetId) as MintKeys; } /** @@ -276,13 +271,7 @@ class CashuWallet { token = getDecodedToken(token); } const tokenEntries: Array = token.token; - const proofs = await this.receiveTokenEntry(tokenEntries[0], { - keysetId: options?.keysetId, - outputAmounts: options?.outputAmounts, - counter: options?.counter, - pubkey: options?.pubkey, - privkey: options?.privkey - }); + const proofs = await this.receiveTokenEntry(tokenEntries[0], options); return proofs; } @@ -307,10 +296,10 @@ class CashuWallet { } ): Promise> { const proofs: Array = []; + const keys = await this.getKeys(options?.keysetId); const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); - const keys = await this.getKeys(options?.keysetId); const { payload, blindedMessages } = this.createSwapPayload( amount, tokenEntry.proofs, @@ -452,6 +441,16 @@ class CashuWallet { * @returns fee amount */ getFeesForProofs(proofs: Array): number { + if (!this._keysets.length) { + throw new Error('Could not calculate fees. No keysets found'); + } + const keysetIds = new Set(proofs.map((p: Proof) => p.id)); + keysetIds.forEach((id: string) => { + if (!this._keysets.find((k: MintKeyset) => k.id === id)) { + throw new Error(`Could not calculate fees. No keyset found with id: ${id}`); + } + }); + const fees = Math.floor( Math.max( (proofs.reduce( diff --git a/test/integration.test.ts b/test/integration.test.ts index b5332da92..8ca42730f 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -79,7 +79,7 @@ describe('mint api', () => { }); test('pay local invoice', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -111,7 +111,7 @@ describe('mint api', () => { }); test('pay external invoice', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(3000); const tokens = await wallet.mintProofs(3000, request.quote); @@ -156,7 +156,7 @@ describe('mint api', () => { }); test('test send tokens with change', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -171,7 +171,7 @@ describe('mint api', () => { }, 10000000); test('receive tokens with previous split', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); const tokens = await wallet.mintProofs(100, request.quote); @@ -184,7 +184,7 @@ describe('mint api', () => { }); test('receive tokens with previous mint', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); const tokens = await wallet.mintProofs(64, request.quote); const encoded = getEncodedToken({ @@ -195,7 +195,7 @@ describe('mint api', () => { }); test('send and receive p2pk', async () => { const mint = new CashuMint(mintUrl); - const wallet = new CashuWallet(mint, { unit, loadMint: true }); + const wallet = new CashuWallet(mint, { unit }); const privKeyAlice = secp256k1.utils.randomPrivateKey(); const pubKeyAlice = secp256k1.getPublicKey(privKeyAlice); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index f8b5c51a0..3d702a494 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -407,6 +407,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; + await wallet.getKeys(); const result = await wallet.send(4, overpayProofs, { // preference: { sendPreference: [{ amount: 1, count: 4 }] } outputAmounts: { sendAmounts: [1, 1, 1, 1], keepAmounts: [] } @@ -465,6 +466,7 @@ describe('send', () => { C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' } ]; + await wallet.getKeys(); const result = await wallet.send(3, overpayProofs, { outputAmounts: { sendAmounts: [1, 1, 1], keepAmounts: [1] } }); @@ -519,12 +521,13 @@ describe('send', () => { describe('deterministic', () => { test('no seed', async () => { const wallet = new CashuWallet(mint); + await wallet.getKeys(); const result = await wallet .send( 1, [ { - id: 'z32vUtKgNCm1', + id: '009a1f293253e41e', amount: 2, secret: '1f98e6837a434644c9411825d7c6d6e13974b931f8f0652217cea29010674a13', C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' From 691accb38ab756b83165052ffdf6a83b123a4ac7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 26 Oct 2024 15:45:48 +0200 Subject: [PATCH 113/246] clean up --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f4636028d..1fda2f758 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ import { CashuMint, CashuWallet, MintQuoteState } from '@cashu/cashu-ts'; const mintUrl = 'http://localhost:3338'; // the mint URL const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); +await wallet.loadMint(); // persist wallet.keys and wallet.keysets to avoid calling loadMint() in the future const mintQuote = await wallet.createMintQuote(64); // pay the invoice here before you continue... const mintQuoteChecked = await wallet.checkMintQuote(mintQuote.quote); @@ -99,11 +100,11 @@ const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); ```typescript // we assume that `wallet` already minted `proofs`, as above -const wallet2 = new CashuWallet(mint); // receiving wallet const { keep, send } = await wallet.send(32, proofs); const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] }); console.log(token); +const wallet2 = new CashuWallet(mint); // receiving wallet const receiveProofs = await wallet2.receive(token); ``` From c0bfb76a55899e59b335c82f564c15b4b96bf56a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 26 Oct 2024 15:48:01 +0200 Subject: [PATCH 114/246] filter base64 implicitly using keyset ID version prefix --- src/CashuWallet.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1bb984262..48d063263 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -166,12 +166,6 @@ class CashuWallet { */ getActiveKeyset(keysets: Array): MintKeyset { let activeKeysets = keysets.filter((k: MintKeyset) => k.active); - // begin deprecated: if there are keyset IDs that are not hex strings, we need to filter them out - const hexKeysets = activeKeysets.filter((k: MintKeyset) => /^[0-9a-fA-F]+$/.test(k.id)); - if (hexKeysets.length > 0) { - activeKeysets = hexKeysets; - } - // end deprecated // we only consider keyset IDs that start with "00" activeKeysets = activeKeysets.filter((k: MintKeyset) => k.id.startsWith('00')); From 110756285c9ec45020c571ab040bbac69e3208cb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:26:11 +0200 Subject: [PATCH 115/246] expose selectProofsToSend --- src/CashuWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 48d063263..b5e5d97e5 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -380,7 +380,7 @@ class CashuWallet { return { keep: keepProofsOffline, send: sendProofOffline }; } - private selectProofsToSend( + selectProofsToSend( proofs: Array, amountToSend: number, includeFees?: boolean From ae66ef2395778ad3ad38c226a9fe008a52ee1a6d Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 27 Oct 2024 11:48:44 +0000 Subject: [PATCH 116/246] replaced BlindTransacion with BlindedMessageData --- src/CashuWallet.ts | 24 +++++++++++------------- src/model/types/wallet/payloads.ts | 22 ---------------------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index b5e5d97e5..f720480a6 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -3,7 +3,6 @@ import { CashuMint } from './CashuMint.js'; import { BlindedMessage } from './model/BlindedMessage.js'; import { type BlindedMessageData, - type BlindedTransaction, type MeltPayload, type MeltQuoteResponse, type MintKeys, @@ -294,7 +293,7 @@ class CashuWallet { const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); - const { payload, blindedMessages } = this.createSwapPayload( + const { payload, blindedMessageData } = this.createSwapPayload( amount, tokenEntry.proofs, keys, @@ -306,8 +305,8 @@ class CashuWallet { const { signatures } = await this.mint.swap(payload); const newProofs = this.constructProofs( signatures, - blindedMessages.rs, - blindedMessages.secrets, + blindedMessageData.rs, + blindedMessageData.secrets, keys ); proofs.push(...newProofs); @@ -566,7 +565,7 @@ class CashuWallet { keepAmounts: keepAmounts, sendAmounts: sendAmounts }; - const { payload, blindedMessages } = this.createSwapPayload( + const { payload, blindedMessageData } = this.createSwapPayload( amountToSend, proofsToSend, keyset, @@ -578,8 +577,8 @@ class CashuWallet { const { signatures } = await this.mint.swap(payload); const swapProofs = this.constructProofs( signatures, - blindedMessages.rs, - blindedMessages.secrets, + blindedMessageData.rs, + blindedMessageData.secrets, keyset ); const splitProofsToKeep: Array = []; @@ -810,7 +809,7 @@ class CashuWallet { privkey?: string ): { payload: SwapPayload; - blindedMessages: BlindedTransaction; + blindedMessageData: BlindedMessageData; } { const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { @@ -850,21 +849,20 @@ class CashuWallet { } // join keepBlindedMessages and sendBlindedMessages - const blindedMessages: BlindedTransaction = { + const blindedMessageData: BlindedMessageData = { blindedMessages: [ ...keepBlindedMessages.blindedMessages, ...sendBlindedMessages.blindedMessages ], secrets: [...keepBlindedMessages.secrets, ...sendBlindedMessages.secrets], - rs: [...keepBlindedMessages.rs, ...sendBlindedMessages.rs], - amounts: [...keepBlindedMessages.amounts, ...sendBlindedMessages.amounts] + rs: [...keepBlindedMessages.rs, ...sendBlindedMessages.rs] }; const payload = { inputs: proofsToSend, - outputs: [...blindedMessages.blindedMessages] + outputs: [...blindedMessageData.blindedMessages] }; - return { payload, blindedMessages }; + return { payload, blindedMessageData }; } /** * returns proofs that are already spent (use for keeping wallet state clean) diff --git a/src/model/types/wallet/payloads.ts b/src/model/types/wallet/payloads.ts index 7e8b67662..386b5644a 100644 --- a/src/model/types/wallet/payloads.ts +++ b/src/model/types/wallet/payloads.ts @@ -18,28 +18,6 @@ export type BlindedMessageData = { rs: Array; }; -/** - * Data that the library needs to hold in memory while it awaits the blinded signatures for the mint. It is later used for unblinding the signatures. - */ -export type BlindedTransaction = { - /** - * Blinded messages sent to the mint for signing. - */ - blindedMessages: Array; - /** - * secrets, kept client side for constructing proofs later. - */ - secrets: Array; - /** - * Blinding factor used for blinding messages and unblinding signatures after they are received from the mint. - */ - rs: Array; - /** - * amounts denominated in Satoshi - */ - amounts: Array; -}; - /** * Payload that needs to be sent to the mint when melting. Includes Return for overpaid fees */ From ba10bb9e442a69c458c3201428a763067433a9da Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 27 Oct 2024 11:54:35 +0000 Subject: [PATCH 117/246] renames rs and BlindMessageData --- src/CashuWallet.ts | 71 ++++++++++++++++-------------- src/model/types/wallet/payloads.ts | 4 +- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index f720480a6..2003b4e54 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -2,7 +2,6 @@ import { bytesToHex, randomBytes } from '@noble/hashes/utils'; import { CashuMint } from './CashuMint.js'; import { BlindedMessage } from './model/BlindedMessage.js'; import { - type BlindedMessageData, type MeltPayload, type MeltQuoteResponse, type MintKeys, @@ -21,7 +20,8 @@ import { SerializedBlindedSignature, GetInfoResponse, OutputAmounts, - CheckStateEntry + CheckStateEntry, + BlindingData } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; @@ -293,7 +293,7 @@ class CashuWallet { const amount = tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - this.getFeesForProofs(tokenEntry.proofs); - const { payload, blindedMessageData } = this.createSwapPayload( + const { payload, blindingData } = this.createSwapPayload( amount, tokenEntry.proofs, keys, @@ -305,8 +305,8 @@ class CashuWallet { const { signatures } = await this.mint.swap(payload); const newProofs = this.constructProofs( signatures, - blindedMessageData.rs, - blindedMessageData.secrets, + blindingData.blindingFactors, + blindingData.secrets, keys ); proofs.push(...newProofs); @@ -565,7 +565,7 @@ class CashuWallet { keepAmounts: keepAmounts, sendAmounts: sendAmounts }; - const { payload, blindedMessageData } = this.createSwapPayload( + const { payload, blindingData } = this.createSwapPayload( amountToSend, proofsToSend, keyset, @@ -577,8 +577,8 @@ class CashuWallet { const { signatures } = await this.mint.swap(payload); const swapProofs = this.constructProofs( signatures, - blindedMessageData.rs, - blindedMessageData.secrets, + blindingData.blindingFactors, + blindingData.secrets, keyset ); const splitProofsToKeep: Array = []; @@ -618,12 +618,16 @@ class CashuWallet { } // create blank amounts for unknown restore amounts const amounts = Array(count).fill(0); - const { blindedMessages, rs, secrets } = this.createBlindedMessages(amounts, keys.id, start); + const { blindedMessages, blindingFactors, secrets } = this.createBlindedMessages( + amounts, + keys.id, + start + ); const { outputs, promises } = await this.mint.restore({ outputs: blindedMessages }); // Collect and map the secrets and blinding factors with the blinded messages that were returned from the mint - const validRs = rs.filter((_: bigint, i: number) => + const validBlindingFactors = blindingFactors.filter((_: bigint, i: number) => outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); const validSecrets = secrets.filter((_: Uint8Array, i: number) => @@ -631,7 +635,7 @@ class CashuWallet { ); return { - proofs: this.constructProofs(promises, validRs, validSecrets, keys) + proofs: this.constructProofs(promises, validBlindingFactors, validSecrets, keys) }; } @@ -694,7 +698,7 @@ class CashuWallet { }; } - const { blindedMessages, secrets, rs } = this.createRandomBlindedMessages( + const { blindedMessages, secrets, blindingFactors } = this.createRandomBlindedMessages( amount, keyset, options?.outputAmounts?.keepAmounts, @@ -707,7 +711,7 @@ class CashuWallet { }; const { signatures } = await this.mint.mint(mintPayload); return { - proofs: this.constructProofs(signatures, rs, secrets, keyset) + proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) }; } @@ -755,7 +759,7 @@ class CashuWallet { } ): Promise { const keys = await this.getKeys(options?.keysetId); - const { blindedMessages, secrets, rs } = this.createBlankOutputs( + const { blindedMessages, secrets, blindingFactors } = this.createBlankOutputs( sumProofs(proofsToSend) - meltQuote.amount, keys.id, options?.counter @@ -781,7 +785,7 @@ class CashuWallet { const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; if (meltResponse.change) { - change = this.constructProofs(meltResponse.change, rs, secrets, keys); + change = this.constructProofs(meltResponse.change, blindingFactors, secrets, keys); } return { quote: meltResponse, @@ -809,7 +813,7 @@ class CashuWallet { privkey?: string ): { payload: SwapPayload; - blindedMessageData: BlindedMessageData; + blindingData: BlindingData; } { const totalAmount = proofsToSend.reduce((total: number, curr: Proof) => total + curr.amount, 0); if (outputAmounts && outputAmounts.sendAmounts && !outputAmounts.keepAmounts) { @@ -849,20 +853,23 @@ class CashuWallet { } // join keepBlindedMessages and sendBlindedMessages - const blindedMessageData: BlindedMessageData = { + const blindingData: BlindingData = { blindedMessages: [ ...keepBlindedMessages.blindedMessages, ...sendBlindedMessages.blindedMessages ], secrets: [...keepBlindedMessages.secrets, ...sendBlindedMessages.secrets], - rs: [...keepBlindedMessages.rs, ...sendBlindedMessages.rs] + blindingFactors: [ + ...keepBlindedMessages.blindingFactors, + ...sendBlindedMessages.blindingFactors + ] }; const payload = { inputs: proofsToSend, - outputs: [...blindedMessageData.blindedMessages] + outputs: [...blindingData.blindedMessages] }; - return { payload, blindedMessageData }; + return { payload, blindingData }; } /** * returns proofs that are already spent (use for keeping wallet state clean) @@ -899,7 +906,7 @@ class CashuWallet { split?: Array, counter?: number, pubkey?: string - ): BlindedMessageData & { amounts: Array } { + ): BlindingData & { amounts: Array } { const amounts = splitAmount(amount, keyset.keys, split); return this.createBlindedMessages(amounts, keyset.id, counter, pubkey); } @@ -917,7 +924,7 @@ class CashuWallet { keysetId: string, counter?: number, pubkey?: string - ): BlindedMessageData & { amounts: Array } { + ): BlindingData & { amounts: Array } { // if we atempt to create deterministic messages without a _seed, abort. if (counter != undefined && !this._seed) { throw new Error( @@ -926,7 +933,7 @@ class CashuWallet { } const blindedMessages: Array = []; const secrets: Array = []; - const rs: Array = []; + const blindingFactors: Array = []; for (let i = 0; i < amounts.length; i++) { let deterministicR = undefined; let secretBytes = undefined; @@ -944,11 +951,11 @@ class CashuWallet { } secrets.push(secretBytes); const { B_, r } = blindMessage(secretBytes, deterministicR); - rs.push(r); + blindingFactors.push(r); const blindedMessage = new BlindedMessage(amounts[i], B_, keysetId); blindedMessages.push(blindedMessage.getSerializedBlindedMessage()); } - return { blindedMessages, secrets, rs, amounts }; + return { blindedMessages, secrets, blindingFactors, amounts }; } /** @@ -959,19 +966,19 @@ class CashuWallet { * @param counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @returns blinded messages, secrets, and rs */ - private createBlankOutputs( - amount: number, - keysetId: string, - counter?: number - ): BlindedMessageData { + private createBlankOutputs(amount: number, keysetId: string, counter?: number): BlindingData { let count = Math.ceil(Math.log2(amount)) || 1; //Prevent count from being -Infinity if (count < 0) { count = 0; } const amounts = count ? Array(count).fill(1) : []; - const { blindedMessages, rs, secrets } = this.createBlindedMessages(amounts, keysetId, counter); - return { blindedMessages, secrets, rs }; + const { blindedMessages, blindingFactors, secrets } = this.createBlindedMessages( + amounts, + keysetId, + counter + ); + return { blindedMessages, secrets, blindingFactors }; } /** diff --git a/src/model/types/wallet/payloads.ts b/src/model/types/wallet/payloads.ts index 386b5644a..2ab98720f 100644 --- a/src/model/types/wallet/payloads.ts +++ b/src/model/types/wallet/payloads.ts @@ -3,7 +3,7 @@ import { Proof } from './index'; /** * Data that the library needs to hold in memory while it awaits the blinded signatures for the mint. It is later used for unblinding the signatures. */ -export type BlindedMessageData = { +export type BlindingData = { /** * Blinded messages sent to the mint for signing. */ @@ -15,7 +15,7 @@ export type BlindedMessageData = { /** * Blinding factor used for blinding messages and unblinding signatures after they are received from the mint. */ - rs: Array; + blindingFactors: Array; }; /** From 4edb4f99452713e66a1e6631c8271e6758bd2c57 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 28 Oct 2024 09:53:21 +0100 Subject: [PATCH 118/246] updated migration doc --- migration-2.0.0.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index a4b30c1f5..36f7ebc3b 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -40,3 +40,11 @@ export type OutputAmounts = { - `CashuWallet.mintTokens()` is now called `CashuWallet.mintProofs()` - `CashuWallet.meltTokens()` is now called `CashuWallet.meltProofs()` - `CashuMint.split()` is now called `CashuMint.swap()` + +### Type changes + +#### Wallet payload types + +- `BlindedTransaction` has been removed +- `BlindedMessageData` has been replaced by `BlindingData` + - In `BlindingData` `rs` has been renamed to `blindingFactors` From 9714380e657282e6f2dc14eec7e9e388df31e69b Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 23 Oct 2024 14:25:00 +0100 Subject: [PATCH 119/246] restructured token type --- src/CashuWallet.ts | 40 ++++----------------------- src/model/types/wallet/tokens.ts | 21 ++++---------- src/utils.ts | 47 ++++++++++++-------------------- 3 files changed, 29 insertions(+), 79 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2003b4e54..e51b4be77 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -15,7 +15,6 @@ import { type SerializedBlindedMessage, type SwapPayload, type Token, - type TokenEntry, CheckStateEnum, SerializedBlindedSignature, GetInfoResponse, @@ -263,39 +262,13 @@ class CashuWallet { if (typeof token === 'string') { token = getDecodedToken(token); } - const tokenEntries: Array = token.token; - const proofs = await this.receiveTokenEntry(tokenEntries[0], options); - return proofs; - } - - /** - * Receive a single cashu token entry - * @param tokenEntry a single entry of a cashu token - * @param options.keyksetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint - * @param options.outputAmounts? optionally specify the output's amounts to keep. - * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect - * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param options.privkey? will create a signature on the @param tokenEntry secrets if set - * @returns {Promise>} New token entry with newly created proofs, proofs that had errors - */ - async receiveTokenEntry( - tokenEntry: TokenEntry, - options?: { - keysetId?: string; - outputAmounts?: OutputAmounts; - counter?: number; - pubkey?: string; - privkey?: string; - } - ): Promise> { - const proofs: Array = []; const keys = await this.getKeys(options?.keysetId); const amount = - tokenEntry.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - - this.getFeesForProofs(tokenEntry.proofs); - const { payload, blindingData } = this.createSwapPayload( + token.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - + this.getFeesForProofs(token.proofs); + const { payload, blindedMessages } = this.createSwapPayload( amount, - tokenEntry.proofs, + token.proofs, keys, options?.outputAmounts, options?.counter, @@ -303,14 +276,13 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); - const newProofs = this.constructProofs( + const freshProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, keys ); - proofs.push(...newProofs); - return proofs; + return freshProofs; } /** diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index 636fe039d..32a215af9 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -5,9 +5,13 @@ import { Proof } from './index'; */ export type Token = { /** - * token entries + * the mints URL */ - token: Array; + mint: string; + /** + * a list of proofs + */ + proofs: Array; /** * a message to send along with the token */ @@ -17,19 +21,6 @@ export type Token = { */ unit?: string; }; -/** - * TokenEntry that stores proofs and mints - */ -export type TokenEntry = { - /** - * a list of proofs - */ - proofs: Array; - /** - * the mints URL - */ - mint: string; -}; /** * Template for a Proof inside a V4 Token diff --git a/src/utils.ts b/src/utils.ts index d7320b69f..21a666c3e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,10 +7,7 @@ import { import { Keys, Proof, - RawPaymentRequest, - RawTransport, Token, - TokenEntry, TokenV4Template, V4InnerToken, V4ProofTemplate @@ -178,22 +175,13 @@ export function getEncodedToken(token: Token): string { */ export function getEncodedTokenV4(token: Token): string { const idMap: { [id: string]: Array } = {}; - let mint: string | undefined = undefined; - for (let i = 0; i < token.token.length; i++) { - if (!mint) { - mint = token.token[i].mint; + const mint = token.mint; + for (let i = 0; i < token.proofs.length; i++) { + const proof = token.proofs[i]; + if (idMap[proof.id]) { + idMap[proof.id].push(proof); } else { - if (mint !== token.token[i].mint) { - throw new Error('Multimint token can not be encoded as V4 token'); - } - } - for (let j = 0; j < token.token[i].proofs.length; j++) { - const proof = token.token[i].proofs[j]; - if (idMap[proof.id]) { - idMap[proof.id].push(proof); - } else { - idMap[proof.id] = [proof]; - } + idMap[proof.id] = [proof]; } } const tokenTemplate: TokenV4Template = { @@ -249,24 +237,23 @@ export function handleTokens(token: string): Token { return encodeBase64ToJson(encodedToken); } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); - const tokenData = decodeCBOR(uInt8Token) as { - t: Array<{ p: Array<{ a: number; s: string; c: Uint8Array }>; i: Uint8Array }>; - m: string; - d: string; - u: string; - }; - const mergedTokenEntry: TokenEntry = { mint: tokenData.m, proofs: [] }; - tokenData.t.forEach((tokenEntry: V4InnerToken) => - tokenEntry.p.forEach((p: V4ProofTemplate) => { - mergedTokenEntry.proofs.push({ + const tokenData = decodeCBOR(uInt8Token) as TokenV4Template; + const proofs: Array = []; + tokenData.t.forEach((t) => + t.p.forEach((p) => { + proofs.push({ secret: p.s, C: bytesToHex(p.c), amount: p.a, - id: bytesToHex(tokenEntry.i) + id: bytesToHex(t.i) }); }) ); - return { token: [mergedTokenEntry], memo: tokenData.d || '', unit: tokenData.u || 'sat' }; + const decodedToken: Token = { mint: tokenData.m, proofs, unit: tokenData.u || 'sat' }; + if (tokenData.d) { + decodedToken.memo = tokenData.d; + } + return decodedToken; } throw new Error('Token version is not supported'); } From d1e50ca562fdcafb060a0efb326cead5059c01af Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 23 Oct 2024 15:43:51 +0100 Subject: [PATCH 120/246] fixed v3 Token parsing --- src/model/types/wallet/tokens.ts | 31 +++++++ src/utils.ts | 16 +++- test/utils.test.ts | 152 +++++++++++++------------------ 3 files changed, 110 insertions(+), 89 deletions(-) diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index 32a215af9..8d32662b2 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -75,3 +75,34 @@ export type TokenV4Template = { */ u: string; }; + +/** + * A Cashu token + */ +export type DeprecatedToken = { + /** + * token entries + */ + token: Array; + /** + * a message to send along with the token + */ + memo?: string; + /** + * the unit of the token + */ + unit?: string; +}; +/** + * TokenEntry that stores proofs and mints + */ +type TokenEntry = { + /** + * a list of proofs + */ + proofs: Array; + /** + * the mints URL + */ + mint: string; +}; diff --git a/src/utils.ts b/src/utils.ts index 21a666c3e..6af1ff0e3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { encodeUint8toBase64Url } from './base64.js'; import { + DeprecatedToken, Keys, Proof, Token, @@ -234,7 +235,20 @@ export function handleTokens(token: string): Token { const version = token.slice(0, 1); const encodedToken = token.slice(1); if (version === 'A') { - return encodeBase64ToJson(encodedToken); + const parsedV3Token = encodeBase64ToJson(encodedToken); + if (parsedV3Token.token.length > 1) { + throw new Error('Multi entry token are not supported'); + } + const entry = parsedV3Token.token[0]; + const tokenObj: Token = { + mint: entry.mint, + proofs: entry.proofs, + unit: parsedV3Token.unit || 'sat' + }; + if (parsedV3Token.memo) { + tokenObj.memo = parsedV3Token.memo; + } + return tokenObj; } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); const tokenData = decodeCBOR(uInt8Token) as TokenV4Template; diff --git a/test/utils.test.ts b/test/utils.test.ts index 8091c0640..606c4f8aa 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -109,19 +109,16 @@ describe('test decode token', () => { describe('test decode token', () => { test('testing v3 Token', async () => { const obj = { - token: [ + proofs: [ { - proofs: [ - { - C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4', - amount: 1, - id: 'I2yN+iRYfkzT', - secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=' - } - ], - mint: 'http://localhost:3338' + C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4', + amount: 1, + id: 'I2yN+iRYfkzT', + secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=' } - ] + ], + mint: 'http://localhost:3338', + unit: 'sat' }; const uriPrefixes = ['web+cashu://', 'cashu://', 'cashu:']; uriPrefixes.forEach((prefix) => { @@ -135,19 +132,16 @@ describe('test decode token', () => { }); test('testing v3 Token no prefix', async () => { const obj = { - token: [ + proofs: [ { - proofs: [ - { - C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4', - amount: 1, - id: 'I2yN+iRYfkzT', - secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=' - } - ], - mint: 'http://localhost:3338' + C: '02195081e622f98bfc19a05ebe2341d955c0d12588c5948c858d07adec007bc1e4', + amount: 1, + id: 'I2yN+iRYfkzT', + secret: '97zfmmaGf5k8Mg0gajpnbmpervTtEeE8wwKri7rWpUs=' } - ] + ], + mint: 'http://localhost:3338', + unit: 'sat' }; const token = @@ -159,17 +153,13 @@ describe('test decode token', () => { const v3Token = { memo: 'Thank you', unit: 'sat', - token: [ + mint: 'http://localhost:3338', + proofs: [ { - mint: 'http://localhost:3338', - proofs: [ - { - secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', - C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', - id: '00ad268c4d1f5826', - amount: 1 - } - ] + secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', + C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', + id: '00ad268c4d1f5826', + amount: 1 } ] }; @@ -182,31 +172,26 @@ describe('test decode token', () => { }); test('testing v4 Token with multi keyset', () => { const v3Token = { - memo: '', unit: 'sat', - token: [ + mint: 'http://localhost:3338', + proofs: [ + { + secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', + C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', + id: '00ffd48b8f5ecf80', + amount: 1 + }, + { + secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', + C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', + id: '00ad268c4d1f5826', + amount: 2 + }, { - mint: 'http://localhost:3338', - proofs: [ - { - secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', - C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', - id: '00ffd48b8f5ecf80', - amount: 1 - }, - { - secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', - C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', - id: '00ad268c4d1f5826', - amount: 2 - }, - { - secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', - C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', - id: '00ad268c4d1f5826', - amount: 1 - } - ] + secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', + C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', + id: '00ad268c4d1f5826', + amount: 1 } ] }; @@ -233,17 +218,13 @@ describe('test v4 encoding', () => { 'cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ='; const v3Token = { memo: 'Thank you', - token: [ + mint: 'http://localhost:3338', + proofs: [ { - mint: 'http://localhost:3338', - proofs: [ - { - secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', - C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', - id: '00ad268c4d1f5826', - amount: 1 - } - ] + secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', + C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', + id: '00ad268c4d1f5826', + amount: 1 } ], unit: 'sat' @@ -258,32 +239,27 @@ describe('test v4 encoding', () => { const encodedV4 = 'cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA'; const v3Token = { - token: [ + mint: 'http://localhost:3338', + proofs: [ + { + secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', + C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', + id: '00ffd48b8f5ecf80', + amount: 1 + }, + { + secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', + C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', + id: '00ad268c4d1f5826', + amount: 2 + }, { - mint: 'http://localhost:3338', - proofs: [ - { - secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', - C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', - id: '00ffd48b8f5ecf80', - amount: 1 - }, - { - secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', - C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', - id: '00ad268c4d1f5826', - amount: 2 - }, - { - secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', - C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', - id: '00ad268c4d1f5826', - amount: 1 - } - ] + secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', + C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', + id: '00ad268c4d1f5826', + amount: 1 } ], - memo: '', unit: 'sat' }; From ccb5db2e8bd3e157f205a86f9b70b542331ccfc2 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 24 Oct 2024 08:43:53 +0200 Subject: [PATCH 121/246] fixed v3 Token encoding --- src/utils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 6af1ff0e3..6259cf980 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -165,8 +165,15 @@ export function bigIntStringify(_key: unknown, value: T): string | T { * @param token to encode * @returns encoded token */ -export function getEncodedToken(token: Token): string { - return TOKEN_PREFIX + TOKEN_VERSION + encodeJsonToBase64(token); +function getEncodedToken(token: Token): string { + const v3TokenObj: DeprecatedToken = { token: [{ mint: token.mint, proofs: token.proofs }] }; + if (token.unit) { + v3TokenObj.unit = token.unit; + } + if (token.memo) { + v3TokenObj.memo = token.unit; + } + return TOKEN_PREFIX + TOKEN_VERSION + encodeJsonToBase64(v3TokenObj); } /** From c49d74737c800e6cc3dc71ad6b6f13ed573aec7b Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 24 Oct 2024 08:45:23 +0200 Subject: [PATCH 122/246] adjusted integration test --- test/integration.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 8ca42730f..966c95592 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -176,9 +176,7 @@ describe('mint api', () => { const tokens = await wallet.mintProofs(100, request.quote); const sendResponse = await wallet.send(10, tokens.proofs); - const encoded = getEncodedToken({ - token: [{ mint: mintUrl, proofs: sendResponse.send }] - }); + const encoded = getEncodedToken({ mint: mintUrl, proofs: sendResponse.send }); const response = await wallet.receive(encoded); expect(response).toBeDefined(); }); @@ -187,9 +185,7 @@ describe('mint api', () => { const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); const tokens = await wallet.mintProofs(64, request.quote); - const encoded = getEncodedToken({ - token: [{ mint: mintUrl, proofs: tokens.proofs }] - }); + const encoded = getEncodedToken({ mint: mintUrl, proofs: tokens.proofs }); const response = await wallet.receive(encoded); expect(response).toBeDefined(); }); @@ -207,9 +203,7 @@ describe('mint api', () => { const tokens = await wallet.mintProofs(128, request.quote); const { send } = await wallet.send(64, tokens.proofs, { pubkey: bytesToHex(pubKeyBob) }); - const encoded = getEncodedToken({ - token: [{ mint: mintUrl, proofs: send }] - }); + const encoded = getEncodedToken({ mint: mintUrl, proofs: send }); const result = await wallet .receive(encoded, { privkey: bytesToHex(privKeyAlice) }) From 5e6fc95c006ab403a7fb294777a748eff119ed8a Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 27 Oct 2024 18:32:19 +0100 Subject: [PATCH 123/246] exported getEncodedToken --- src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 6259cf980..4983db88d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -165,13 +165,13 @@ export function bigIntStringify(_key: unknown, value: T): string | T { * @param token to encode * @returns encoded token */ -function getEncodedToken(token: Token): string { +export function getEncodedToken(token: Token): string { const v3TokenObj: DeprecatedToken = { token: [{ mint: token.mint, proofs: token.proofs }] }; if (token.unit) { v3TokenObj.unit = token.unit; } if (token.memo) { - v3TokenObj.memo = token.unit; + v3TokenObj.memo = token.memo; } return TOKEN_PREFIX + TOKEN_VERSION + encodeJsonToBase64(v3TokenObj); } From f417c01cefccd126ddc95beef10cb428c328a90d Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 27 Oct 2024 18:37:07 +0100 Subject: [PATCH 124/246] removed v1 & v2 token tests --- test/utils.test.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index 606c4f8aa..a0181ebcc 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -85,27 +85,6 @@ describe('test split different key amount', () => { }); }); -describe('test decode token', () => { - test('testing v1 Token', () => { - const token = - 'W3siaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjIsInNlY3JldCI6Ild6ZC9vNUVHdmVKb3hTQVlGcjZ1U3lnUmFWSUFrOFc4MXNLTlRxdVd4UjQ9IiwiQyI6IjAzNWNiZmQwOTNiOWZlMWRjNjU2MGEwNDM3YzQyNDQxZjA0ZDIyYzk4MDY2NGMyNGExMGZlZGFiNTlmZWY0YmZjOSJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjQsInNlY3JldCI6InU0N2lWUkhneUNuUFhCNWxOdFpGaTBOeHpPZ1lyRk1WODV2aFpyRThIbWM9IiwiQyI6IjAyNThiYmZkZWJmZGQzYjk0OTljZDk1YzFkMWZiYTVjZTQ1MWFjOGNlZTE0NzM1Yzk2MGFiMDc1ZmI2ZTQ4ZjBkYyJ9LHsiaWQiOiIwTkkzVFVBczFTZnkiLCJhbW91bnQiOjY0LCJzZWNyZXQiOiJ1YTFaT0hjeVB3T0M0UUxPaWthQVV1MThJM2pEUDJCSVNYREFGcW91N1VNPSIsIkMiOiIwMjU2MWNhNjcyNTdlNzdhNjNjN2U3NWQ4MGVkYTI3ZDlhMmEyYzUxZTA0NGM4ZjhmODVlNzc0OTZlMGRlM2U2NWIifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJ5ZTlNRCtaQ25VUHlHOTBscmYyZ2tudnA3N2I4V05wNUxRT2ZtcERjRGNFPSIsIkMiOiIwM2UwN2M1NjExNzcwMmNmODg3MDFlYjAyOTM2YjA5MDNhZmEyMTQwZDcwNTY1N2ZkODVkM2YxZWI5MzRiYTBjYzMifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoyLCJzZWNyZXQiOiJIUHpzRmZPUDFWRU1BMW8vTnFHVXFhRXdaV2RiN3VERzM4T1grLzlZTURzPSIsIkMiOiIwMmQ3ZDE1YTBhZmIyNThjMjlhZDdmOWY4N2ZmMzIxZWRmNTgyOTM0ZWI0NWExNTE2MjhiNTJjMDExZjQ2MWZkOGEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxLCJzZWNyZXQiOiJnMVR1YXdha1RVQkJBTW9tZGpDVHkrRENNTnBaUmd3dWluNXB5V2xoTVVNPSIsIkMiOiIwMzU4Y2IxMGE5NWEzY2E1YmE5MTc5MTllMWNhODA1NjZmMTg5NTI4Njk1MTJjYWFjMDlmYmQ5MGYxN2QyZTZlYmEifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoyLCJzZWNyZXQiOiJRMTFyamNXWk55Q2dkRmxqRThaNkdwNFhDYllKcndzRGhncXVQOTU1VWU0PSIsIkMiOiIwMjAxNjBmODIwNGU4MGIxNDg4NmFlMzZjMzRiMjI3ODllMzMxZmM5MjVhNGMwOGE3ZWYxZDZjYzMyYTIwNjZjZWUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50Ijo4LCJzZWNyZXQiOiI1MVZrUXFYT2kwM0k2a0pzM0tlSEI0OVVCQTFSRktrWnMyMFljZEtOSW1JPSIsIkMiOiIwMjZiYWU2YTgzOWE3OTdjNmU5NGZlNGM5MWZlNTIwOGU4MDE3MTg2Y2NkMDk0ZmI4ZTNkZjYyNjAyZWJmMjczMjUifSx7ImlkIjoiME5JM1RVQXMxU2Z5IiwiYW1vdW50IjoxNiwic2VjcmV0IjoiVk4ySlMwUENKdGQ3MjJUTXUxdGFxNUZSMXg0dDlXM28xNndWRGVweXBxYz0iLCJDIjoiMDIxMmM4ZGE5NWE4NDEyYjgyMDE4MTgxNzQxZWY1YWQ0ZjYzMTU1NjBhMWFmODM5ZjMxOTU4NTcwZTVlYzI2ZDQyIn1d'; - let result: Token | undefined; - expect(() => { - result = utils.getDecodedToken(token); - }).toThrow(); - expect(result).toBe(undefined); - }); - test('testing v2 Token', async () => { - const token = - 'eyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IkkyeU4raVJZZmt6VCIsImFtb3VudCI6MSwic2VjcmV0IjoiOTd6Zm1tYUdmNWs4TWcwZ2FqcG5ibXBlcnZUdEVlRTh3d0tyaTdyV3BVcz0iLCJDIjoiMDIxOTUwODFlNjIyZjk4YmZjMTlhMDVlYmUyMzQxZDk1NWMwZDEyNTg4YzU5NDhjODU4ZDA3YWRlYzAwN2JjMWU0In1dLCJtaW50IjoiaHR0cDovL2xvY2FsaG9zdDozMzM4In1dfQ'; - let result: Token | undefined; - expect(() => { - result = utils.getDecodedToken(token); - }).toThrow(); - expect(result).toBe(undefined); - }); -}); - describe('test decode token', () => { test('testing v3 Token', async () => { const obj = { From ec330ce6f10e38fff587cd798c29543005c6bcf2 Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 27 Oct 2024 18:43:25 +0100 Subject: [PATCH 125/246] added v3 encoding test --- test/utils.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/utils.test.ts b/test/utils.test.ts index a0181ebcc..bff0d6bf4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -85,6 +85,43 @@ describe('test split different key amount', () => { }); }); +describe('test token v3 encoding', () => { + test('encode a v3 token', () => { + const tokenObj = { + token: [ + { + mint: 'https://8333.space:3338', + proofs: [ + { + amount: 2, + id: '009a1f293253e41e', + secret: '407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837', + C: '02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea' + }, + { + amount: 8, + id: '009a1f293253e41e', + secret: 'fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be', + C: '029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059' + } + ] + } + ], + unit: 'sat', + memo: 'Thank you.' + }; + const encoded = utils.getEncodedToken({ + mint: tokenObj.token[0].mint, + memo: tokenObj.memo, + unit: tokenObj.unit, + proofs: tokenObj.token[0].proofs + }); + expect(encoded).toBe( + 'cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9' + ); + }); +}); + describe('test decode token', () => { test('testing v3 Token', async () => { const obj = { From c3895329e2f98be400e87db8a9d46c4ce4a58a88 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 28 Oct 2024 10:00:17 +0100 Subject: [PATCH 126/246] sumProof refactor --- src/CashuWallet.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e51b4be77..3027a2f9f 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -263,9 +263,7 @@ class CashuWallet { token = getDecodedToken(token); } const keys = await this.getKeys(options?.keysetId); - const amount = - token.proofs.reduce((total: number, curr: Proof) => total + curr.amount, 0) - - this.getFeesForProofs(token.proofs); + const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); const { payload, blindedMessages } = this.createSwapPayload( amount, token.proofs, From 30445e16f32540325ac8593e6d0cd015a5e33232 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 07:39:50 +0100 Subject: [PATCH 127/246] dleq --- package-lock.json | 124 +++++++++++++++--------------- package.json | 2 +- src/CashuWallet.ts | 105 ++++++++++++++++++++----- src/model/BlindedSignature.ts | 20 ++++- src/model/types/mint/responses.ts | 14 ++++ src/model/types/wallet/index.ts | 6 ++ 6 files changed, 189 insertions(+), 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69d22b893..aee19c7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0-3", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.2.7", + "@cashu/crypto": "^0.3.1", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -621,15 +621,14 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz", - "integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==", - "license": "MIT", - "dependencies": { - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3", - "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", + "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, @@ -1168,22 +1167,25 @@ } }, "node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "dependencies": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1225,33 +1227,33 @@ } }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", - "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "dependencies": { - "@noble/curves": "~1.3.0", - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", - "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "dependencies": { - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -6833,14 +6835,14 @@ "dev": true }, "@cashu/crypto": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz", - "integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==", - "requires": { - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3", - "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", + "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "requires": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, @@ -7257,17 +7259,17 @@ } }, "@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "requires": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.5.0" } }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -7296,27 +7298,27 @@ } }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==" }, "@scure/bip32": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", - "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "requires": { - "@noble/curves": "~1.3.0", - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" } }, "@scure/bip39": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", - "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "requires": { - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" } }, "@sinclair/typebox": { diff --git a/package.json b/package.json index 5e33c1f35..80ae4399e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.2.7", + "@cashu/crypto": "^0.3.1", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2003b4e54..83970627e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -1,4 +1,4 @@ -import { bytesToHex, randomBytes } from '@noble/hashes/utils'; +import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'; import { CashuMint } from './CashuMint.js'; import { BlindedMessage } from './model/BlindedMessage.js'; import { @@ -38,7 +38,8 @@ import { deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; -import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; +import { type Proof as NUT11Proof, DLEQ } from '@cashu/crypto/modules/common/index'; +import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; /** * The default number of proofs per denomination to keep in a wallet. @@ -276,6 +277,7 @@ class CashuWallet { * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param tokenEntry secrets if set + * @param options.requireDLEQ? optionally require a DLEQ proof from the mint * @returns {Promise>} New token entry with newly created proofs, proofs that had errors */ async receiveTokenEntry( @@ -286,6 +288,7 @@ class CashuWallet { counter?: number; pubkey?: string; privkey?: string; + requireDLEQ?: boolean; } ): Promise> { const proofs: Array = []; @@ -303,11 +306,13 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); + const requireDleq = options?.requireDLEQ; const newProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keys + keys, + requireDleq ?? false ); proofs.push(...newProofs); return proofs; @@ -490,6 +495,7 @@ class CashuWallet { * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param proofs secrets if set + * @param options.requireDLEQ? optionally require a DLEQ proof from the mint. * @returns promise of the change- and send-proofs */ async swap( @@ -503,6 +509,7 @@ class CashuWallet { privkey?: string; keysetId?: string; includeFees?: boolean; + requireDLEQ?: boolean; } ): Promise { if (!options) options = {}; @@ -575,11 +582,13 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); + const requireDleq = options?.requireDLEQ; const swapProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keyset + keyset, + requireDleq ?? false ); const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; @@ -603,6 +612,7 @@ class CashuWallet { * @param start set starting point for count (first cycle for each keyset should usually be 0) * @param count set number of blinded messages that should be generated * @param options.keysetId set a custom keysetId to restore from. keysetIds can be loaded with `CashuMint.getKeySets()` + * @param options.requireDLEQ require a DLEQ proof * @returns proofs */ async restore( @@ -610,6 +620,7 @@ class CashuWallet { count: number, options?: { keysetId?: string; + requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keys = await this.getKeys(options?.keysetId); @@ -633,9 +644,15 @@ class CashuWallet { const validSecrets = secrets.filter((_: Uint8Array, i: number) => outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); - + const requireDleq = options?.requireDLEQ; return { - proofs: this.constructProofs(promises, validBlindingFactors, validSecrets, keys) + proofs: this.constructProofs( + promises, + validBlindingFactors, + validSecrets, + keys, + requireDleq ?? false + ) }; } @@ -672,6 +689,7 @@ class CashuWallet { * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.requireDLEQ? optionally require a DLEQ proof. * @returns proofs */ async mintProofs( @@ -683,6 +701,7 @@ class CashuWallet { proofsWeHave?: Array; counter?: number; pubkey?: string; + requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keyset = await this.getKeys(options?.keysetId); @@ -710,8 +729,15 @@ class CashuWallet { quote: quote }; const { signatures } = await this.mint.mint(mintPayload); + const requireDleq = options?.requireDLEQ; return { - proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) + proofs: this.constructProofs( + signatures, + blindingFactors, + secrets, + keyset, + requireDleq ?? false + ) }; } @@ -756,6 +782,7 @@ class CashuWallet { keysetId?: string; counter?: number; privkey?: string; + requireDLEQ?: boolean; } ): Promise { const keys = await this.getKeys(options?.keysetId); @@ -784,8 +811,15 @@ class CashuWallet { }; const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; + const requireDleq = options?.requireDLEQ; if (meltResponse.change) { - change = this.constructProofs(meltResponse.change, blindingFactors, secrets, keys); + change = this.constructProofs( + meltResponse.change, + blindingFactors, + secrets, + keys, + requireDleq ?? false + ); } return { quote: meltResponse, @@ -987,23 +1021,58 @@ class CashuWallet { * @param rs arrays of binding factors * @param secrets array of secrets * @param keyset mint keyset + * @param verifyDLEQ require proof of same secret (DLEQ) * @returns array of serialized proofs */ private constructProofs( promises: Array, rs: Array, secrets: Array, - keyset: MintKeys + keyset: MintKeys, + verifyDLEQ: boolean ): Array { - return promises - .map((p: SerializedBlindedSignature, i: number) => { - const blindSignature = { id: p.id, amount: p.amount, C_: pointFromHex(p.C_) }; - const r = rs[i]; - const secret = secrets[i]; - const A = pointFromHex(keyset.keys[p.amount]); - return constructProofFromPromise(blindSignature, r, secret, A); - }) - .map((p: NUT11Proof) => serializeProof(p) as Proof); + return promises.map((p: SerializedBlindedSignature, i: number) => { + const dleq = + p.dleq == undefined + ? undefined + : ({ + s: hexToBytes(p.dleq.s), + e: hexToBytes(p.dleq.e), + r: rs[i] + } as DLEQ); + const blindSignature = { + id: p.id, + amount: p.amount, + C_: pointFromHex(p.C_), + dleq: dleq + }; + const r = rs[i]; + const secret = secrets[i]; + const A = pointFromHex(keyset.keys[p.amount]); + const proof = constructProofFromPromise(blindSignature, r, secret, A); + if (verifyDLEQ) { + if (dleq == undefined) { + throw new Error('DLEQ verification required, but none found'); + } + if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) { + throw new Error('DLEQ verification failed'); + } + } + return { + id: proof.id, + amount: proof.amount, + secret: bytesToHex(proof.secret), + C: proof.C.toHex(true), + dleq: + dleq == undefined + ? undefined + : { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } + } as Proof; + }); } } diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index e998f320f..85ca7f158 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -1,19 +1,35 @@ import { ProjPointType } from '@noble/curves/abstract/weierstrass'; import { SerializedBlindedSignature } from './types/index.js'; +import { DLEQ } from '@cashu/crypto/modules/common'; +import { bytesToHex } from '@noble/hashes/utils.js'; class BlindedSignature { id: string; amount: number; C_: ProjPointType; + dleq?: DLEQ; - constructor(id: string, amount: number, C_: ProjPointType) { + constructor(id: string, amount: number, C_: ProjPointType, dleq: DLEQ) { this.id = id; this.amount = amount; this.C_ = C_; + this.dleq = dleq; } getSerializedBlindedSignature(): SerializedBlindedSignature { - return { id: this.id, amount: this.amount, C_: this.C_.toHex(true) }; + return { + id: this.id, + amount: this.amount, + C_: this.C_.toHex(true), + dleq: + this.dleq == undefined + ? undefined + : { + s: bytesToHex(this.dleq.s), + e: bytesToHex(this.dleq.e), + r: this.dleq.r?.toString(16) + } + }; } } diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 9f42b9354..3de1208c0 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -178,6 +178,16 @@ export type PostRestoreResponse = { promises: Array; }; +/* + * Zero-Knowledge that BlindedSignature + * was generated using a specific public key + */ +export type SerializedDLEQ = { + s: string; + e: string; + r?: string; +}; + /** * Blinded signature as it is received from the mint */ @@ -194,6 +204,10 @@ export type SerializedBlindedSignature = { * Blinded signature */ C_: string; + /** + * DLEQ Proof + */ + dleq?: SerializedDLEQ; }; /** diff --git a/src/model/types/wallet/index.ts b/src/model/types/wallet/index.ts index 853df5d9e..c65165739 100644 --- a/src/model/types/wallet/index.ts +++ b/src/model/types/wallet/index.ts @@ -1,3 +1,5 @@ +import { SerializedDLEQ } from '../mint'; + export * from './payloads'; export * from './responses'; export * from './tokens'; @@ -23,6 +25,10 @@ export type Proof = { * The unblinded signature for this secret, signed by the mints private key. */ C: string; + /** + * DLEQ proof + */ + dleq?: SerializedDLEQ; }; /** From d794f2c583309a9553f68712a28645d5c25d822a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 07:54:48 +0100 Subject: [PATCH 128/246] less tab --- src/model/BlindedSignature.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 85ca7f158..3dece8acf 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -28,7 +28,7 @@ class BlindedSignature { s: bytesToHex(this.dleq.s), e: bytesToHex(this.dleq.e), r: this.dleq.r?.toString(16) - } + } }; } } From 84ca38e42b1516b3d697a4035355043cd649d34b Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 29 Oct 2024 10:11:35 +0100 Subject: [PATCH 129/246] fixed tests --- src/CashuWallet.ts | 2 +- test/wallet.test.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 3027a2f9f..4492524f9 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -264,7 +264,7 @@ class CashuWallet { } const keys = await this.getKeys(options?.keysetId); const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); - const { payload, blindedMessages } = this.createSwapPayload( + const { payload, blindingData } = this.createSwapPayload( amount, token.proofs, keys, diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 3d702a494..2eb7a2d60 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,9 +1,8 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { MeltQuoteResponse, MeltQuoteState, OutputAmounts } from '../src/model/types/index.js'; +import { MeltQuoteResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; -import { Proof } from '@cashu/crypto/modules/common'; const dummyKeysResp = { keysets: [ From d5c4600cbee2a6a3628187c186e688c31d58fb51 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:26:50 +0100 Subject: [PATCH 130/246] wallet: add checkProofsStates --- src/CashuWallet.ts | 14 ++++++++++++++ test/integration.test.ts | 38 +++++++++++++++++++++++--------------- test/wallet.test.ts | 29 ++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2003b4e54..a25a26a2d 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -891,6 +891,20 @@ class CashuWallet { }); } + async checkProofsStates(proofs: Array): Promise> { + const enc = new TextEncoder(); + const Ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); + const BATCH_SIZE = 100; + const states: Array = []; + for (let i = 0; i < Ys.length; i += BATCH_SIZE) { + const { states: batchStates } = await this.mint.check({ + Ys: Ys.slice(i, i + BATCH_SIZE) + }); + states.push(...batchStates.map((state: CheckStateEntry) => state.state)); + } + return states; + } + /** * Creates blinded messages for a given amount * @param amount amount to create blinded messages for diff --git a/test/integration.test.ts b/test/integration.test.ts index 8ca42730f..26499e640 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -5,7 +5,7 @@ import dns from 'node:dns'; import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; -import { MeltQuoteState } from '../src/model/types/index.js'; +import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = @@ -100,14 +100,18 @@ describe('mint api', () => { expect(response.change.reduce((a, b) => a + b.amount, 0)).toBe(fee); // check states of spent and kept proofs after payment - const sentProofsSpent = await wallet.checkProofsSpent(sendResponse.send); - expect(sentProofsSpent).toBeDefined(); - // expect that all proofs are spent, i.e. sendProofsSpent == sendResponse.send - expect(sentProofsSpent).toEqual(sendResponse.send); + const sentProofsStates = await wallet.checkProofsStates(sendResponse.send); + expect(sentProofsStates).toBeDefined(); + // expect that all proofs are spent, i.e. all are CheckStateEnum.SPENT + sentProofsStates.forEach((state) => { + expect(state).toBe(CheckStateEnum.SPENT); + }); // expect none of the sendResponse.keep to be spent - const keepSpent = await wallet.checkProofsSpent(sendResponse.keep); - expect(keepSpent).toBeDefined(); - expect(keepSpent).toEqual([]); + const keepProofsStates = await wallet.checkProofsStates(sendResponse.keep); + expect(keepProofsStates).toBeDefined(); + keepProofsStates.forEach((state) => { + expect(state).toBe(CheckStateEnum.UNSPENT); + }); }); test('pay external invoice', async () => { const mint = new CashuMint(mintUrl); @@ -131,14 +135,18 @@ describe('mint api', () => { expect(response.change.reduce((a, b) => a + b.amount, 0)).toBeLessThan(fee); // check states of spent and kept proofs after payment - const sentProofsSpent = await wallet.checkProofsSpent(sendResponse.send); - expect(sentProofsSpent).toBeDefined(); - // expect that all proofs are spent, i.e. sendProofsSpent == sendResponse.send - expect(sentProofsSpent).toEqual(sendResponse.send); + const sentProofsStates = await wallet.checkProofsStates(sendResponse.send); + expect(sentProofsStates).toBeDefined(); + // expect that all proofs are spent, i.e. all are CheckStateEnum.SPENT + sentProofsStates.forEach((state) => { + expect(state).toBe(CheckStateEnum.SPENT); + }); // expect none of the sendResponse.keep to be spent - const keepSpent = await wallet.checkProofsSpent(sendResponse.keep); - expect(keepSpent).toBeDefined(); - expect(keepSpent).toEqual([]); + const keepProofsStates = await wallet.checkProofsStates(sendResponse.keep); + expect(keepProofsStates).toBeDefined(); + keepProofsStates.forEach((state) => { + expect(state).toBe(CheckStateEnum.UNSPENT); + }); }); test('test send tokens exact without previous split', async () => { const mint = new CashuMint(mintUrl); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 3d702a494..0f0534245 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,7 +1,12 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { MeltQuoteResponse, MeltQuoteState, OutputAmounts } from '../src/model/types/index.js'; +import { + CheckStateEnum, + MeltQuoteResponse, + MeltQuoteState, + OutputAmounts +} from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; import { Proof } from '@cashu/crypto/modules/common'; @@ -225,6 +230,28 @@ describe('checkProofsSpent', () => { }); }); +describe('checkProofsStates', () => { + const proofs = [ + { + id: '009a1f293253e41e', + amount: 1, + secret: '1f98e6837a434644c9411825d7c6d6e13974b931f8f0652217cea29010674a13', + C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' + } + ]; + test('test checkProofsStates - get proofs that are NOT spendable', async () => { + nock(mintUrl) + .post('/v1/checkstate') + .reply(200, { states: [{ Y: 'asd', state: 'UNSPENT', witness: 'witness-asd' }] }); + const wallet = new CashuWallet(mint, { unit }); + + const result = await wallet.checkProofsStates(proofs); + result.forEach((r) => { + expect(r).toEqual(CheckStateEnum.UNSPENT); + }); + }); +}); + describe('requestTokens', () => { test('test requestTokens', async () => { nock(mintUrl) From 4fbc125a6f826831ffaefcbcc38198cd20b363ed Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:29:36 +0100 Subject: [PATCH 131/246] comments --- src/CashuWallet.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index a25a26a2d..218ae2d7e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -879,11 +879,14 @@ class CashuWallet { async checkProofsSpent(proofs: Array): Promise> { const enc = new TextEncoder(); const Ys = proofs.map((p: T) => hashToCurve(enc.encode(p.secret)).toHex(true)); - const payload = { - // array of Ys of proofs to check - Ys: Ys - }; - const { states } = await this.mint.check(payload); + const BATCH_SIZE = 100; + const states: Array = []; + for (let i = 0; i < Ys.length; i += BATCH_SIZE) { + const { states: batchStates } = await this.mint.check({ + Ys: Ys.slice(i, i + BATCH_SIZE) + }); + states.push(...batchStates); + } return proofs.filter((_: T, i: number) => { const state = states.find((state: CheckStateEntry) => state.Y === Ys[i]); @@ -891,6 +894,11 @@ class CashuWallet { }); } + /** + * Get an array of the states of proofs from the mint (as an array of CheckStateEnum's) + * @param proofs (only the `secret` field is required) + * @returns + */ async checkProofsStates(proofs: Array): Promise> { const enc = new TextEncoder(); const Ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); From 40b54680463c42e39886fb655a04eaa074fd216c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 13:31:01 +0100 Subject: [PATCH 132/246] package.json --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index aee19c7ee..c7ff8a6fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0-3", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.3.1", + "@cashu/crypto": "^0.3.3", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -621,9 +621,9 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", - "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", + "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", @@ -6835,9 +6835,9 @@ "dev": true }, "@cashu/crypto": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", - "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", + "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", "requires": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", diff --git a/package.json b/package.json index 80ae4399e..876755f44 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.3.1", + "@cashu/crypto": "^0.3.3", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", From f5aa7082094cc770a8d0b0ed78f38351582b0b75 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:31:34 +0100 Subject: [PATCH 133/246] add comment about batch size --- src/CashuWallet.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 218ae2d7e..c7d073f54 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -452,7 +452,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -470,7 +470,7 @@ class CashuWallet { Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); @@ -879,6 +879,7 @@ class CashuWallet { async checkProofsSpent(proofs: Array): Promise> { const enc = new TextEncoder(); const Ys = proofs.map((p: T) => hashToCurve(enc.encode(p.secret)).toHex(true)); + // TODO: Replace this with a value from the info endpoint of the mint eventually const BATCH_SIZE = 100; const states: Array = []; for (let i = 0; i < Ys.length; i += BATCH_SIZE) { @@ -902,6 +903,7 @@ class CashuWallet { async checkProofsStates(proofs: Array): Promise> { const enc = new TextEncoder(); const Ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); + // TODO: Replace this with a value from the info endpoint of the mint eventually const BATCH_SIZE = 100; const states: Array = []; for (let i = 0; i < Ys.length; i += BATCH_SIZE) { From 98c11062754ca31c4f34c391f4804fe6ad2a85fe Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:33:25 +0100 Subject: [PATCH 134/246] format --- src/CashuWallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index c7d073f54..28127a848 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -452,7 +452,7 @@ class CashuWallet { 0 ) + 999) / - 1000, + 1000, 0 ) ); @@ -470,7 +470,7 @@ class CashuWallet { Math.max( (nInputs * (this._keysets.find((k: MintKeyset) => k.id === keysetId)?.input_fee_ppk || 0) + 999) / - 1000, + 1000, 0 ) ); From 5b5167096382d599ee7301bcb087794de889367d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:39:07 +0100 Subject: [PATCH 135/246] remove checkProofsSpent --- test/wallet.test.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 0f0534245..d94c53a86 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -209,27 +209,6 @@ describe('receive', () => { }); }); -describe('checkProofsSpent', () => { - const proofs = [ - { - id: '009a1f293253e41e', - amount: 1, - secret: '1f98e6837a434644c9411825d7c6d6e13974b931f8f0652217cea29010674a13', - C: '034268c0bd30b945adf578aca2dc0d1e26ef089869aaf9a08ba3a6da40fda1d8be' - } - ]; - test('test checkProofsSpent - get proofs that are NOT spendable', async () => { - nock(mintUrl) - .post('/v1/checkstate') - .reply(200, { states: [{ Y: 'asd', state: 'UNSPENT', witness: 'witness-asd' }] }); - const wallet = new CashuWallet(mint, { unit }); - - const result = await wallet.checkProofsSpent(proofs); - - expect(result).toStrictEqual([]); - }); -}); - describe('checkProofsStates', () => { const proofs = [ { From 668f6cf79fe51d41580be745b2d875af1169447d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:41:32 +0100 Subject: [PATCH 136/246] migration doc --- migration-2.0.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index 36f7ebc3b..f81a1d7f5 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -34,6 +34,10 @@ export type OutputAmounts = { }; ``` +#### `checkProofsStates` replaces `checkProofsSpent` + +To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkProofsStates` now returns an array of `CheckStateEnum` for each proof provided. States are now `CheckStateEnum.SPENT`, `CheckStateEnum.UNSPENT`, and `CheckStateEnum.PENDING`. + #### renamed functions - in `SendResponse`, `returnChange` is now called `keep` From c7ac6d9b245ca51326ba1c4c058ce7f50553fa1d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 15:10:23 +0100 Subject: [PATCH 137/246] fix --- src/CashuWallet.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 83970627e..ffe85fa5c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,7 +21,8 @@ import { GetInfoResponse, OutputAmounts, CheckStateEntry, - BlindingData + BlindingData, + SerializedDLEQ } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; @@ -1058,20 +1059,15 @@ class CashuWallet { throw new Error('DLEQ verification failed'); } } - return { - id: proof.id, - amount: proof.amount, - secret: bytesToHex(proof.secret), - C: proof.C.toHex(true), - dleq: - dleq == undefined - ? undefined - : { - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) - } - } as Proof; + const serializedProof = serializeProof(proof) as Proof; + serializedProof.dleq = dleq == undefined + ? undefined + : { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } as SerializedDLEQ; + return serializedProof; }); } } From 11924ab07c8f0098aad5c1b644832f37d8b7ee05 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:24:38 +0100 Subject: [PATCH 138/246] akchuallay --- src/CashuWallet.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 28127a848..2be6c9e40 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -871,29 +871,6 @@ class CashuWallet { }; return { payload, blindingData }; } - /** - * returns proofs that are already spent (use for keeping wallet state clean) - * @param proofs (only the `secret` field is required) - * @returns - */ - async checkProofsSpent(proofs: Array): Promise> { - const enc = new TextEncoder(); - const Ys = proofs.map((p: T) => hashToCurve(enc.encode(p.secret)).toHex(true)); - // TODO: Replace this with a value from the info endpoint of the mint eventually - const BATCH_SIZE = 100; - const states: Array = []; - for (let i = 0; i < Ys.length; i += BATCH_SIZE) { - const { states: batchStates } = await this.mint.check({ - Ys: Ys.slice(i, i + BATCH_SIZE) - }); - states.push(...batchStates); - } - - return proofs.filter((_: T, i: number) => { - const state = states.find((state: CheckStateEntry) => state.Y === Ys[i]); - return state && state.state === CheckStateEnum.SPENT; - }); - } /** * Get an array of the states of proofs from the mint (as an array of CheckStateEnum's) From 4e0afa12c4ce695dcb6a06261c814f7dd6435799 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 29 Oct 2024 22:16:11 +0100 Subject: [PATCH 139/246] added token to v2 migration --- migration-2.0.0.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index 36f7ebc3b..9bec1a6d2 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -48,3 +48,18 @@ export type OutputAmounts = { - `BlindedTransaction` has been removed - `BlindedMessageData` has been replaced by `BlindingData` - In `BlindingData` `rs` has been renamed to `blindingFactors` + +#### Token Types + +- The `Token` type no longer reassembles the token v3 structure, but instead is a simple object type: + +```ts +type Token = { + mint: string; + proofs: Array; + memo?: string; + unit?: string; +}; +``` + +- The old `Token` type got renamed to `DeprecatedToken` From 5f825fd3739b0c913fe9fc7a71657c3a2a779deb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 30 Oct 2024 01:04:16 +0100 Subject: [PATCH 140/246] return witnesses as well --- migration-2.0.0.md | 2 +- src/CashuWallet.ts | 19 +++++++++++++------ src/model/types/mint/responses.ts | 4 ++-- test/integration.test.ts | 12 ++++++++---- test/wallet.test.ts | 13 +++++++++++-- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index f81a1d7f5..6852df9c4 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -36,7 +36,7 @@ export type OutputAmounts = { #### `checkProofsStates` replaces `checkProofsSpent` -To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkProofsStates` now returns an array of `CheckStateEnum` for each proof provided. States are now `CheckStateEnum.SPENT`, `CheckStateEnum.UNSPENT`, and `CheckStateEnum.PENDING`. +To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkProofsStates` now returns an array of `ProofState`'s, one for each `Proof` provided. The spent states are in `ProofState.state` and can have the values `CheckStateEnum.SPENT`, `CheckStateEnum.UNSPENT`, and `CheckStateEnum.PENDING`. `ProofState` also contains a `witness` if present. #### renamed functions diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2be6c9e40..664833134 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -16,11 +16,10 @@ import { type SwapPayload, type Token, type TokenEntry, - CheckStateEnum, SerializedBlindedSignature, GetInfoResponse, OutputAmounts, - CheckStateEntry, + ProofState, BlindingData } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; @@ -877,17 +876,25 @@ class CashuWallet { * @param proofs (only the `secret` field is required) * @returns */ - async checkProofsStates(proofs: Array): Promise> { + async checkProofsStates(proofs: Array): Promise> { const enc = new TextEncoder(); const Ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); // TODO: Replace this with a value from the info endpoint of the mint eventually const BATCH_SIZE = 100; - const states: Array = []; + const states: Array = []; for (let i = 0; i < Ys.length; i += BATCH_SIZE) { + const YsSlice = Ys.slice(i, i + BATCH_SIZE); const { states: batchStates } = await this.mint.check({ - Ys: Ys.slice(i, i + BATCH_SIZE) + Ys: YsSlice }); - states.push(...batchStates.map((state: CheckStateEntry) => state.state)); + for (let j = 0; j < YsSlice.length; j++) { + const state = batchStates.find((s: ProofState) => s.Y === YsSlice[j]); + if (state) { + states.push(state); + } else { + throw new Error('Could not find state for proof with Y: ' + YsSlice[j]); + } + } } return states; } diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 9f42b9354..43a6ccd03 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -21,7 +21,7 @@ export type ApiError = { /** * Entries of CheckStateResponse with state of the proof */ -export type CheckStateEntry = { +export type ProofState = { Y: string; state: CheckStateEnum; witness: string | null; @@ -43,7 +43,7 @@ export type CheckStateResponse = { /** * */ - states: Array; + states: Array; } & ApiError; /** diff --git a/test/integration.test.ts b/test/integration.test.ts index 26499e640..4d650095e 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -104,13 +104,15 @@ describe('mint api', () => { expect(sentProofsStates).toBeDefined(); // expect that all proofs are spent, i.e. all are CheckStateEnum.SPENT sentProofsStates.forEach((state) => { - expect(state).toBe(CheckStateEnum.SPENT); + expect(state.state).toBe(CheckStateEnum.SPENT); + expect(state.witness).toBeNull(); }); // expect none of the sendResponse.keep to be spent const keepProofsStates = await wallet.checkProofsStates(sendResponse.keep); expect(keepProofsStates).toBeDefined(); keepProofsStates.forEach((state) => { - expect(state).toBe(CheckStateEnum.UNSPENT); + expect(state.state).toBe(CheckStateEnum.UNSPENT); + expect(state.witness).toBeNull(); }); }); test('pay external invoice', async () => { @@ -139,13 +141,15 @@ describe('mint api', () => { expect(sentProofsStates).toBeDefined(); // expect that all proofs are spent, i.e. all are CheckStateEnum.SPENT sentProofsStates.forEach((state) => { - expect(state).toBe(CheckStateEnum.SPENT); + expect(state.state).toBe(CheckStateEnum.SPENT); + expect(state.witness).toBeNull(); }); // expect none of the sendResponse.keep to be spent const keepProofsStates = await wallet.checkProofsStates(sendResponse.keep); expect(keepProofsStates).toBeDefined(); keepProofsStates.forEach((state) => { - expect(state).toBe(CheckStateEnum.UNSPENT); + expect(state.state).toBe(CheckStateEnum.UNSPENT); + expect(state.witness).toBeNull(); }); }); test('test send tokens exact without previous split', async () => { diff --git a/test/wallet.test.ts b/test/wallet.test.ts index d94c53a86..d66da7f7b 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -221,12 +221,21 @@ describe('checkProofsStates', () => { test('test checkProofsStates - get proofs that are NOT spendable', async () => { nock(mintUrl) .post('/v1/checkstate') - .reply(200, { states: [{ Y: 'asd', state: 'UNSPENT', witness: 'witness-asd' }] }); + .reply(200, { + states: [ + { + Y: '02d5dd71f59d917da3f73defe997928e9459e9d67d8bdb771e4989c2b5f50b2fff', + state: 'UNSPENT', + witness: 'witness-asd' + } + ] + }); const wallet = new CashuWallet(mint, { unit }); const result = await wallet.checkProofsStates(proofs); result.forEach((r) => { - expect(r).toEqual(CheckStateEnum.UNSPENT); + expect(r.state).toEqual(CheckStateEnum.UNSPENT); + expect(r.witness).toEqual('witness-asd'); }); }); }); From 75059d073a316af51665815a9fafa2ae7fc5ace3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 30 Oct 2024 01:08:31 +0100 Subject: [PATCH 141/246] npm run format --- test/wallet.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/wallet.test.ts b/test/wallet.test.ts index f96572386..093f35916 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,10 +1,7 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { - CheckStateEnum, - MeltQuoteResponse -} from '../src/model/types/index.js'; +import { CheckStateEnum, MeltQuoteResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; const dummyKeysResp = { From ec35ea68ca02cb5bd3b2914accfc313497b1bc45 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 30 Oct 2024 11:26:24 +0100 Subject: [PATCH 142/246] checkProofs: added stateMap --- src/CashuWallet.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index b9f092cbf..049061122 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -857,13 +857,16 @@ class CashuWallet { const { states: batchStates } = await this.mint.check({ Ys: YsSlice }); + const stateMap: { [y: string]: ProofState } = {}; + batchStates.forEach((s) => { + stateMap[s.Y] = s; + }); for (let j = 0; j < YsSlice.length; j++) { - const state = batchStates.find((s: ProofState) => s.Y === YsSlice[j]); - if (state) { - states.push(state); - } else { + const state = stateMap[YsSlice[j]]; + if (!state) { throw new Error('Could not find state for proof with Y: ' + YsSlice[j]); } + states.push(state); } } return states; From 102908f727dede701cf46129c50ab920f0419666 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 30 Oct 2024 18:06:22 +0100 Subject: [PATCH 143/246] add optional `dleqValid` to `Proof` type. It will be true if the mint returned a DLEQ and it was verified correctly. --- src/CashuWallet.ts | 37 ++++++--------------------------- src/model/types/wallet/index.ts | 4 ++++ 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 5bfee8c31..54e4fbd69 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -275,13 +275,11 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); - const requireDleq = options?.requireDLEQ; const freshProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keys, - requireDleq ?? false + keys ); return freshProofs; } @@ -463,7 +461,6 @@ class CashuWallet { * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param proofs secrets if set - * @param options.requireDLEQ? optionally require a DLEQ proof from the mint. * @returns promise of the change- and send-proofs */ async swap( @@ -477,7 +474,6 @@ class CashuWallet { privkey?: string; keysetId?: string; includeFees?: boolean; - requireDLEQ?: boolean; } ): Promise { if (!options) options = {}; @@ -550,13 +546,11 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); - const requireDleq = options?.requireDLEQ; const swapProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keyset, - requireDleq ?? false + keyset ); const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; @@ -580,15 +574,12 @@ class CashuWallet { * @param start set starting point for count (first cycle for each keyset should usually be 0) * @param count set number of blinded messages that should be generated * @param options.keysetId set a custom keysetId to restore from. keysetIds can be loaded with `CashuMint.getKeySets()` - * @param options.requireDLEQ require a DLEQ proof - * @returns proofs */ async restore( start: number, count: number, options?: { keysetId?: string; - requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keys = await this.getKeys(options?.keysetId); @@ -612,14 +603,12 @@ class CashuWallet { const validSecrets = secrets.filter((_: Uint8Array, i: number) => outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); - const requireDleq = options?.requireDLEQ; return { proofs: this.constructProofs( promises, validBlindingFactors, validSecrets, keys, - requireDleq ?? false ) }; } @@ -657,7 +646,6 @@ class CashuWallet { * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param options.requireDLEQ? optionally require a DLEQ proof. * @returns proofs */ async mintProofs( @@ -669,7 +657,6 @@ class CashuWallet { proofsWeHave?: Array; counter?: number; pubkey?: string; - requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keyset = await this.getKeys(options?.keysetId); @@ -697,14 +684,12 @@ class CashuWallet { quote: quote }; const { signatures } = await this.mint.mint(mintPayload); - const requireDleq = options?.requireDLEQ; return { proofs: this.constructProofs( signatures, blindingFactors, secrets, keyset, - requireDleq ?? false ) }; } @@ -750,7 +735,6 @@ class CashuWallet { keysetId?: string; counter?: number; privkey?: string; - requireDLEQ?: boolean; } ): Promise { const keys = await this.getKeys(options?.keysetId); @@ -779,14 +763,12 @@ class CashuWallet { }; const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; - const requireDleq = options?.requireDLEQ; if (meltResponse.change) { change = this.constructProofs( meltResponse.change, blindingFactors, secrets, keys, - requireDleq ?? false ); } return { @@ -1001,15 +983,13 @@ class CashuWallet { * @param rs arrays of binding factors * @param secrets array of secrets * @param keyset mint keyset - * @param verifyDLEQ require proof of same secret (DLEQ) * @returns array of serialized proofs */ private constructProofs( promises: Array, rs: Array, secrets: Array, - keyset: MintKeys, - verifyDLEQ: boolean + keyset: MintKeys ): Array { return promises.map((p: SerializedBlindedSignature, i: number) => { const dleq = @@ -1030,15 +1010,10 @@ class CashuWallet { const secret = secrets[i]; const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); - if (verifyDLEQ) { - if (dleq == undefined) { - throw new Error('DLEQ verification required, but none found'); - } - if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) { - throw new Error('DLEQ verification failed'); - } - } const serializedProof = serializeProof(proof) as Proof; + serializedProof.dleqValid = dleq == undefined + ? undefined + : verifyDLEQProof_reblind(secret, dleq, proof.C, A); serializedProof.dleq = dleq == undefined ? undefined : { diff --git a/src/model/types/wallet/index.ts b/src/model/types/wallet/index.ts index c65165739..641eac645 100644 --- a/src/model/types/wallet/index.ts +++ b/src/model/types/wallet/index.ts @@ -29,6 +29,10 @@ export type Proof = { * DLEQ proof */ dleq?: SerializedDLEQ; + /** + * Is the associated DLEQ proof valid? + */ + dleqValid?: boolean; }; /** From e4a59f6a2cc458dfde961bc4d4b85c920df0da7a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 30 Oct 2024 20:15:48 +0100 Subject: [PATCH 144/246] include dleq in encoded V4 token if provided --- src/model/types/wallet/tokens.ts | 19 +++++++++++++++++++ src/utils.ts | 24 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index 8d32662b2..bd0c95504 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -22,6 +22,21 @@ export type Token = { unit?: string; }; +export type V4DLEQTemplate = { + /** + * challenge + */ + e: Uint8Array; + /** + * response + */ + s: Uint8Array; + /** + * blinding factor + */ + r: Uint8Array; +} + /** * Template for a Proof inside a V4 Token */ @@ -38,6 +53,10 @@ export type V4ProofTemplate = { * Signature */ c: Uint8Array; + /** + * DLEQ + */ + d?: V4DLEQTemplate; }; /** diff --git a/src/utils.ts b/src/utils.ts index 4983db88d..3210f39ba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,7 @@ import { Proof, Token, TokenV4Template, + V4DLEQTemplate, V4InnerToken, V4ProofTemplate } from './model/types/index.js'; @@ -181,7 +182,15 @@ export function getEncodedToken(token: Token): string { * @param token to encode * @returns encoded token */ -export function getEncodedTokenV4(token: Token): string { +export function getEncodedTokenV4( + token: Token, +): string { + // Make sure each DLEQ has its blinding factor + token.proofs.forEach(p => { + if (p.dleq && p.dleq.r == undefined) { + throw new Error("Missing blinding factor in included DLEQ proof"); + } + }); const idMap: { [id: string]: Array } = {}; const mint = token.mint; for (let i = 0; i < token.proofs.length; i++) { @@ -199,7 +208,18 @@ export function getEncodedTokenV4(token: Token): string { (id: string): V4InnerToken => ({ i: hexToBytes(id), p: idMap[id].map( - (p: Proof): V4ProofTemplate => ({ a: p.amount, s: p.secret, c: hexToBytes(p.C) }) + (p: Proof): V4ProofTemplate => ({ + a: p.amount, + s: p.secret, + c: hexToBytes(p.C), + d: p.dleq == undefined + ? undefined + : { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? "00"), + } as V4DLEQTemplate + }) ) }) ) From 62c4c2f94035effcb981258dfb144c5e98d84760 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 24 Oct 2024 09:45:24 +0200 Subject: [PATCH 145/246] token: default to v4 --- src/utils.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 4983db88d..3df8650c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -150,13 +150,12 @@ export function hexToNumber(hex: string): bigint { return BigInt(`0x${hex}`); } -/** - * Helper function to stringify a bigint - * @param _key - * @param value to stringify - * @returns stringified bigint - */ -export function bigIntStringify(_key: unknown, value: T): string | T { +function isValidHex(str: string) { + return /^[a-f0-9]*$/i.test(str); +} + +//used for json serialization +function bigIntStringify(_key: unknown, value: T) { return typeof value === 'bigint' ? value.toString() : value; } @@ -177,11 +176,22 @@ export function getEncodedToken(token: Token): string { } /** - * Helper function to encode a v4 cashu token - * @param token to encode - * @returns encoded token + * Helper function to encode a cashu token (defaults to v4 if keyset id allows it) + * @param token + * @param [opts] */ -export function getEncodedTokenV4(token: Token): string { +function getEncodedToken(token: Token, opts?: { version: 3 | 4 }): string { + const hasNonHexId = token.proofs.some((p) => !isValidHex(p.id)); + if (hasNonHexId || opts?.version === 3) { + if (opts?.version === 4) { + throw new Error('can not encode to v4 token if proofs contain non-hex keyset id'); + } + return getEncodedTokenV3(token); + } + return getEncodedTokenV4(token); +} + +function getEncodedTokenV4(token: Token): string { const idMap: { [id: string]: Array } = {}; const mint = token.mint; for (let i = 0; i < token.proofs.length; i++) { From 03536720ddc1662055655c94a7ce2569e7e673b7 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 29 Oct 2024 10:29:24 +0100 Subject: [PATCH 146/246] fixed utils and tests --- src/utils.ts | 28 ++++++++++++++++++++++------ test/utils.test.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 3df8650c8..826e2705d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -154,8 +154,20 @@ function isValidHex(str: string) { return /^[a-f0-9]*$/i.test(str); } +/** + * Checks wether a proof or a list of proofs contains a non-hex id + * @param p Proof or list of proofs + * @returns boolean + */ +export function hasNonHexId(p: Proof | Array) { + if (Array.isArray(p)) { + return p.some((proof) => !isValidHex(proof.id)); + } + return isValidHex(p.id); +} + //used for json serialization -function bigIntStringify(_key: unknown, value: T) { +export function bigIntStringify(_key: unknown, value: T) { return typeof value === 'bigint' ? value.toString() : value; } @@ -164,7 +176,7 @@ function bigIntStringify(_key: unknown, value: T) { * @param token to encode * @returns encoded token */ -export function getEncodedToken(token: Token): string { +export function getEncodedTokenV3(token: Token): string { const v3TokenObj: DeprecatedToken = { token: [{ mint: token.mint, proofs: token.proofs }] }; if (token.unit) { v3TokenObj.unit = token.unit; @@ -180,9 +192,9 @@ export function getEncodedToken(token: Token): string { * @param token * @param [opts] */ -function getEncodedToken(token: Token, opts?: { version: 3 | 4 }): string { - const hasNonHexId = token.proofs.some((p) => !isValidHex(p.id)); - if (hasNonHexId || opts?.version === 3) { +export function getEncodedToken(token: Token, opts?: { version: 3 | 4 }): string { + const nonHex = hasNonHexId(token.proofs); + if (nonHex || opts?.version === 3) { if (opts?.version === 4) { throw new Error('can not encode to v4 token if proofs contain non-hex keyset id'); } @@ -191,7 +203,11 @@ function getEncodedToken(token: Token, opts?: { version: 3 | 4 }): string { return getEncodedTokenV4(token); } -function getEncodedTokenV4(token: Token): string { +export function getEncodedTokenV4(token: Token): string { + const nonHex = hasNonHexId(token.proofs); + if (nonHex) { + throw new Error('can not encode to v4 token if proofs contain non-hex keyset id'); + } const idMap: { [id: string]: Array } = {}; const mint = token.mint; for (let i = 0; i < token.proofs.length; i++) { diff --git a/test/utils.test.ts b/test/utils.test.ts index bff0d6bf4..778383936 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -86,7 +86,7 @@ describe('test split different key amount', () => { }); describe('test token v3 encoding', () => { - test('encode a v3 token', () => { + test('encode a v3 token with getEncodedToken', () => { const tokenObj = { token: [ { @@ -110,7 +110,44 @@ describe('test token v3 encoding', () => { unit: 'sat', memo: 'Thank you.' }; - const encoded = utils.getEncodedToken({ + const encoded = utils.getEncodedToken( + { + mint: tokenObj.token[0].mint, + memo: tokenObj.memo, + unit: tokenObj.unit, + proofs: tokenObj.token[0].proofs + }, + { version: 3 } + ); + expect(encoded).toBe( + 'cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9' + ); + }); + test('encode a v3 token with getEncodedTokenV3', () => { + const tokenObj = { + token: [ + { + mint: 'https://8333.space:3338', + proofs: [ + { + amount: 2, + id: '009a1f293253e41e', + secret: '407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837', + C: '02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea' + }, + { + amount: 8, + id: '009a1f293253e41e', + secret: 'fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be', + C: '029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059' + } + ] + } + ], + unit: 'sat', + memo: 'Thank you.' + }; + const encoded = utils.getEncodedTokenV3({ mint: tokenObj.token[0].mint, memo: tokenObj.memo, unit: tokenObj.unit, From a31f96550a11f6c5d9ae58ca3e110b72e0bed0fe Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 31 Oct 2024 08:33:04 +0100 Subject: [PATCH 147/246] run format --- src/CashuWallet.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 832da709f..1b1a1b8af 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,13 +21,7 @@ import { ProofState, BlindingData } from './model/types/index.js'; -import { - bytesToNumber, - getDecodedToken, - splitAmount, - sumProofs, - getKeepAmounts -} from './utils.js'; +import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { blindMessage, From 8bfc322d37ec790181de2dd3727293548315947c Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 31 Oct 2024 08:38:28 +0100 Subject: [PATCH 148/246] added migration to docs --- migration-2.0.0.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index f4c588e9c..5fc17b771 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -34,10 +34,22 @@ export type OutputAmounts = { }; ``` +--- + +#### CashuWallet can no longer be instantiated using BIP39 mnemonic! + +In order to reduce the bundle size, BIP39 has been removed as a dependency of cashu-ts. Therefore users that want to use deterministic secrets with a BIP39 menmonic will have to convert the wordlist into a seed manually and then instantiate the wallet using: + +```ts +const wallet = new CashuWallet(mint, { bip39seed: uint8ArrayOfSeed }); +``` + #### `checkProofsStates` replaces `checkProofsSpent` To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkProofsStates` now returns an array of `ProofState`'s, one for each `Proof` provided. The spent states are in `ProofState.state` and can have the values `CheckStateEnum.SPENT`, `CheckStateEnum.UNSPENT`, and `CheckStateEnum.PENDING`. `ProofState` also contains a `witness` if present. +--- + #### renamed functions - in `SendResponse`, `returnChange` is now called `keep` @@ -45,6 +57,8 @@ To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkPro - `CashuWallet.meltTokens()` is now called `CashuWallet.meltProofs()` - `CashuMint.split()` is now called `CashuMint.swap()` +--- + ### Type changes #### Wallet payload types @@ -53,6 +67,8 @@ To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkPro - `BlindedMessageData` has been replaced by `BlindingData` - In `BlindingData` `rs` has been renamed to `blindingFactors` +--- + #### Token Types - The `Token` type no longer reassembles the token v3 structure, but instead is a simple object type: @@ -67,3 +83,5 @@ type Token = { ``` - The old `Token` type got renamed to `DeprecatedToken` + +--- From 641609e0851c4a5b95056ab753c00ac16a3d723b Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 31 Oct 2024 08:45:04 +0100 Subject: [PATCH 149/246] 2.0.0-rc1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb0593c26..f1d6e7cc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cashu/cashu-ts", - "version": "1.1.0-3", + "version": "2.0.0-rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cashu/cashu-ts", - "version": "1.1.0-3", + "version": "2.0.0-rc1", "license": "MIT", "dependencies": { "@cashu/crypto": "^0.2.7", diff --git a/package.json b/package.json index 8cce56fef..b40a56801 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cashu/cashu-ts", - "version": "1.1.0-3", + "version": "2.0.0-rc1", "description": "cashu library for communicating with a cashu mint", "main": "dist/lib/es5/index.js", "module": "dist/lib/es6/index.js", From 72f4211f98d2ca4f4d5c9db312aa74d76ee18332 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 14:10:22 +0100 Subject: [PATCH 150/246] handleTokens include dleq if present. --- src/utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 3210f39ba..f1f36e251 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,7 @@ import { DeprecatedToken, Keys, Proof, + SerializedDLEQ, Token, TokenV4Template, V4DLEQTemplate, @@ -286,7 +287,14 @@ export function handleTokens(token: string): Token { secret: p.s, C: bytesToHex(p.c), amount: p.a, - id: bytesToHex(t.i) + id: bytesToHex(t.i), + dleq: p.d == undefined + ? undefined + : { + e: bytesToHex(p.d.e), + s: bytesToHex(p.d.s), + r: bytesToHex(p.d.r) + } as SerializedDLEQ, }); }) ); From e7c6501ac136409ca659801178a3e6fa43af6a03 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 14:37:01 +0100 Subject: [PATCH 151/246] fix tests accordingly --- test/utils.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index bff0d6bf4..127ce63f2 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -175,7 +175,8 @@ describe('test decode token', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ] }; @@ -195,19 +196,22 @@ describe('test decode token', () => { secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', - amount: 1 + amount: 1, + dleq: undefined, }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', - amount: 2 + amount: 2, + dleq: undefined, }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ] }; @@ -240,7 +244,8 @@ describe('test v4 encoding', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ], unit: 'sat' From 2379ed532e285f3bf7a7a9001e69baeadea7caaf Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 15:59:27 +0100 Subject: [PATCH 152/246] Carol receive from Alice --- src/CashuWallet.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 54e4fbd69..83af64a1a 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -22,7 +22,7 @@ import { BlindingData, SerializedDLEQ } from './model/types/index.js'; -import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; +import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts, hexToNumber } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -247,6 +247,7 @@ class CashuWallet { * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param token secrets if set + * @param options.requireDleq? will check each proof for DLEQ proofs. Reject the token if any one of them can't be verified. * @returns New token with newly created proofs, token entries that had errors */ async receive( @@ -258,12 +259,16 @@ class CashuWallet { counter?: number; pubkey?: string; privkey?: string; + requireDleq?: boolean; } ): Promise> { if (typeof token === 'string') { token = getDecodedToken(token); } const keys = await this.getKeys(options?.keysetId); + if (options?.requireDleq) { + this.requireDLEQ(token, keys); + } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); const { payload, blindingData } = this.createSwapPayload( amount, @@ -1024,6 +1029,34 @@ class CashuWallet { return serializedProof; }); } + + /** + * Checks that each proof in `token` has a valid DLEQ proof according to + * keyset `keys` + * @param token The token subject to the verification + * @param keys The Mint's keyset to be used for verification + */ + private requireDLEQ(token: Token, keys: MintKeys) { + token.proofs.forEach((p: Proof, i: number) => { + if (p.dleq == undefined) { + throw new Error(`${i}-th proof is missing DLEQ proof`); + } + const dleq = { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToNumber(p.dleq.r ?? "00"), + } as DLEQ; + const key = keys.keys[p.amount]; + if (!verifyDLEQProof_reblind( + new TextEncoder().encode(p.secret), + dleq, + pointFromHex(p.C), + pointFromHex(key) + )) { + throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); + } + }); + } } export { CashuWallet }; From 36a643e9ebaf31fb1736fe976079e03471a5e56b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 16:20:43 +0100 Subject: [PATCH 153/246] strip DLEQs when melting, swapping --- src/CashuWallet.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 83af64a1a..6b3fd22e7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -761,6 +761,11 @@ class CashuWallet { options.privkey ).map((p: NUT11Proof) => serializeProof(p)); } + // Strip DLEQs if any + proofsToSend.map((p: Proof) => { + p.dleq = undefined; + return p; + }); const meltPayload: MeltPayload = { quote: meltQuote.quote, inputs: proofsToSend, @@ -841,6 +846,12 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } + // Strip DLEQs if any + proofsToSend.map((p: Proof) => { + p.dleq = undefined; + return p; + }); + // join keepBlindedMessages and sendBlindedMessages const blindingData: BlindingData = { blindedMessages: [ From 87686d71acf3de9feb4c5036aff21776ba04be73 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 16:36:00 +0100 Subject: [PATCH 154/246] npm format --- src/CashuWallet.ts | 68 +++++++++++++++----------------- src/model/BlindedSignature.ts | 2 +- src/model/types/wallet/tokens.ts | 2 +- src/utils.ts | 38 +++++++++--------- test/utils.test.ts | 10 ++--- 5 files changed, 57 insertions(+), 63 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6b3fd22e7..f1fd943f0 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -22,7 +22,14 @@ import { BlindingData, SerializedDLEQ } from './model/types/index.js'; -import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts, hexToNumber } from './utils.js'; +import { + bytesToNumber, + getDecodedToken, + splitAmount, + sumProofs, + getKeepAmounts, + hexToNumber +} from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -609,12 +616,7 @@ class CashuWallet { outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); return { - proofs: this.constructProofs( - promises, - validBlindingFactors, - validSecrets, - keys, - ) + proofs: this.constructProofs(promises, validBlindingFactors, validSecrets, keys) }; } @@ -690,12 +692,7 @@ class CashuWallet { }; const { signatures } = await this.mint.mint(mintPayload); return { - proofs: this.constructProofs( - signatures, - blindingFactors, - secrets, - keyset, - ) + proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) }; } @@ -774,12 +771,7 @@ class CashuWallet { const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; if (meltResponse.change) { - change = this.constructProofs( - meltResponse.change, - blindingFactors, - secrets, - keys, - ); + change = this.constructProofs(meltResponse.change, blindingFactors, secrets, keys); } return { quote: meltResponse, @@ -1015,7 +1007,7 @@ class CashuWallet { s: hexToBytes(p.dleq.s), e: hexToBytes(p.dleq.e), r: rs[i] - } as DLEQ); + } as DLEQ); const blindSignature = { id: p.id, amount: p.amount, @@ -1027,16 +1019,16 @@ class CashuWallet { const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); const serializedProof = serializeProof(proof) as Proof; - serializedProof.dleqValid = dleq == undefined - ? undefined - : verifyDLEQProof_reblind(secret, dleq, proof.C, A); - serializedProof.dleq = dleq == undefined - ? undefined - : { - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) - } as SerializedDLEQ; + serializedProof.dleqValid = + dleq == undefined ? undefined : verifyDLEQProof_reblind(secret, dleq, proof.C, A); + serializedProof.dleq = + dleq == undefined + ? undefined + : ({ + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } as SerializedDLEQ); return serializedProof; }); } @@ -1055,15 +1047,17 @@ class CashuWallet { const dleq = { e: hexToBytes(p.dleq.e), s: hexToBytes(p.dleq.s), - r: hexToNumber(p.dleq.r ?? "00"), + r: hexToNumber(p.dleq.r ?? '00') } as DLEQ; const key = keys.keys[p.amount]; - if (!verifyDLEQProof_reblind( - new TextEncoder().encode(p.secret), - dleq, - pointFromHex(p.C), - pointFromHex(key) - )) { + if ( + !verifyDLEQProof_reblind( + new TextEncoder().encode(p.secret), + dleq, + pointFromHex(p.C), + pointFromHex(key) + ) + ) { throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); } }); diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 3dece8acf..85ca7f158 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -28,7 +28,7 @@ class BlindedSignature { s: bytesToHex(this.dleq.s), e: bytesToHex(this.dleq.e), r: this.dleq.r?.toString(16) - } + } }; } } diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index bd0c95504..011a8b4f0 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -35,7 +35,7 @@ export type V4DLEQTemplate = { * blinding factor */ r: Uint8Array; -} +}; /** * Template for a Proof inside a V4 Token diff --git a/src/utils.ts b/src/utils.ts index f1f36e251..4c1c5a1a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -183,13 +183,11 @@ export function getEncodedToken(token: Token): string { * @param token to encode * @returns encoded token */ -export function getEncodedTokenV4( - token: Token, -): string { +export function getEncodedTokenV4(token: Token): string { // Make sure each DLEQ has its blinding factor - token.proofs.forEach(p => { + token.proofs.forEach((p) => { if (p.dleq && p.dleq.r == undefined) { - throw new Error("Missing blinding factor in included DLEQ proof"); + throw new Error('Missing blinding factor in included DLEQ proof'); } }); const idMap: { [id: string]: Array } = {}; @@ -213,13 +211,14 @@ export function getEncodedTokenV4( a: p.amount, s: p.secret, c: hexToBytes(p.C), - d: p.dleq == undefined - ? undefined - : { - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToBytes(p.dleq.r ?? "00"), - } as V4DLEQTemplate + d: + p.dleq == undefined + ? undefined + : ({ + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? '00') + } as V4DLEQTemplate) }) ) }) @@ -288,13 +287,14 @@ export function handleTokens(token: string): Token { C: bytesToHex(p.c), amount: p.a, id: bytesToHex(t.i), - dleq: p.d == undefined - ? undefined - : { - e: bytesToHex(p.d.e), - s: bytesToHex(p.d.s), - r: bytesToHex(p.d.r) - } as SerializedDLEQ, + dleq: + p.d == undefined + ? undefined + : ({ + e: bytesToHex(p.d.e), + s: bytesToHex(p.d.s), + r: bytesToHex(p.d.r) + } as SerializedDLEQ) }); }) ); diff --git a/test/utils.test.ts b/test/utils.test.ts index 127ce63f2..7618bc738 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -176,7 +176,7 @@ describe('test decode token', () => { C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ] }; @@ -197,21 +197,21 @@ describe('test decode token', () => { C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', amount: 1, - dleq: undefined, + dleq: undefined }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', amount: 2, - dleq: undefined, + dleq: undefined }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ] }; @@ -245,7 +245,7 @@ describe('test v4 encoding', () => { C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ], unit: 'sat' From 714ac606209cf6a78852ac1b005e0014aeb74579 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 27 Mar 2024 17:51:04 +0100 Subject: [PATCH 155/246] added Queue --- src/utils.ts | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index 826e2705d..90f588138 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -360,3 +360,90 @@ export function sumProofs(proofs: Array) { export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } + +export class MessageNode { + private _value: string; + private _next: MessageNode | null; + + public get value(): string { + return this._value; + } + public set value(message: string) { + this._value = message; + } + public get next(): MessageNode | null { + return this._next; + } + public set next(node: MessageNode | null) { + this._next = node; + } + + constructor(message: string) { + this._value = message; + this._next = null; + } +} + +export class MessageQueue { + private _first: MessageNode | null; + private _last: MessageNode | null; + + public get first(): MessageNode | null { + return this._first; + } + public set first(messageNode: MessageNode | null) { + this._first = messageNode; + } + public get last(): MessageNode | null { + return this._last; + } + public set last(messageNode: MessageNode | null) { + this._last = messageNode; + } + private _size: number; + public get size(): number { + return this._size; + } + public set size(v: number) { + this._size = v; + } + + constructor() { + this._first = null; + this._last = null; + this._size = 0; + } + enqueue(message: string): boolean { + const newNode = new MessageNode(message); + if (this._size === 0 || !this._last) { + this._first = newNode; + this._last = newNode; + } else { + this._last.next = newNode; + this._last = newNode; + } + this._size++; + return true; + } + dequeue(): string | null { + if (this._size === 0 || !this._first) return null; + + const prev = this._first; + this._first = prev.next; + prev.next = null; + + this._size--; + return prev.value; + } +} + +export { + bigIntStringify, + bytesToNumber, + getDecodedToken, + getEncodedToken, + getEncodedTokenV4, + hexToNumber, + splitAmount, + getDefaultAmountPreference +}; From ac221bdd11f2e1ed12fd10a23514fc0690586eed Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 27 Mar 2024 17:51:13 +0100 Subject: [PATCH 156/246] started implementation --- src/WSConnection.ts | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/WSConnection.ts diff --git a/src/WSConnection.ts b/src/WSConnection.ts new file mode 100644 index 000000000..1707c2f9d --- /dev/null +++ b/src/WSConnection.ts @@ -0,0 +1,75 @@ +import { MessageQueue } from './utils'; + +let _WS: typeof WebSocket; + +if (WebSocket) { + _WS = WebSocket; +} + +export function injectWebSocketImpl(ws: any) { + _WS = ws; +} + +export class WSConnection { + public readonly url: URL; + private ws: WebSocket | undefined; + private listeners: { [reqId: string]: (e: any) => any } = {}; + private messageQueue: MessageQueue; + private handlingInterval?: NodeJS.Timer; + + constructor(url: string) { + this.url = new URL(url); + this.messageQueue = new MessageQueue(); + } + async connect() { + return new Promise((res, rej) => { + try { + this.ws = new _WS(this.url); + } catch (err) { + rej(err); + return; + } + this.ws.onopen = res; + this.ws.onerror = rej; + this.ws.onmessage = (e) => { + this.messageQueue.enqueue(e.data); + if (!this.handlingInterval) { + this.handlingInterval = setInterval(this.handleNextMesage, 0); + } + }; + }); + } + + handleNextMesage() { + if (this.messageQueue.size === 0) { + clearInterval(this.handlingInterval); + return; + } + const message = this.messageQueue.dequeue() as string; + let parsed; + try { + parsed = JSON.parse(message) as Array; + } catch (e) { + console.log(e); + return; + } + let subId: string; + let data: any; + switch (parsed.length) { + case 2: { + // Must be notice + // TODO: Implement NOTICED + return; + } + case 3: { + subId = parsed[1]; + data = parsed[3]; + break; + } + default: { + return; + } + } + const len = parsed.length; + } +} From fbf6fb0e6aa4cb8074b97e7d2a17acda9a135281 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 28 Mar 2024 10:28:09 +0100 Subject: [PATCH 157/246] added listeners --- src/WSConnection.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 1707c2f9d..b99878829 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,3 +1,4 @@ +import { listeners } from 'process'; import { MessageQueue } from './utils'; let _WS: typeof WebSocket; @@ -13,7 +14,7 @@ export function injectWebSocketImpl(ws: any) { export class WSConnection { public readonly url: URL; private ws: WebSocket | undefined; - private listeners: { [reqId: string]: (e: any) => any } = {}; + private listeners: { [reqId: string]: Array } = {}; private messageQueue: MessageQueue; private handlingInterval?: NodeJS.Timer; @@ -40,6 +41,14 @@ export class WSConnection { }); } + addListener(subId: string, callback: () => any) { + (this.listeners[subId] = this.listeners[subId] || []).push(callback); + } + + removeListener(subId: string, callback: () => any) { + (this.listeners[subId] = this.listeners[subId] || []).filter((fn) => fn !== callback); + } + handleNextMesage() { if (this.messageQueue.size === 0) { clearInterval(this.handlingInterval); @@ -58,12 +67,13 @@ export class WSConnection { switch (parsed.length) { case 2: { // Must be notice - // TODO: Implement NOTICED + // TODO: Implement NOTICE return; } case 3: { subId = parsed[1]; data = parsed[3]; + this.listeners[subId].forEach((cb) => cb(data)); break; } default: { From b61c9632e5d3cac614f59aa796ea7cd77dd55a30 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 28 Mar 2024 10:46:52 +0100 Subject: [PATCH 158/246] added subscription --- src/WSConnection.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index b99878829..a65f7ff06 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -11,6 +11,19 @@ export function injectWebSocketImpl(ws: any) { _WS = ws; } +class Subscription { + private connection: WSConnection; + private subId: string; + constructor(conn: WSConnection) { + // HACK: There might be way better ways to create an random string, but I want to create something without dependecies frist + this.subId = Math.random().toString(36).slice(-5); + this.connection = conn; + } + onmessage(cb: () => any) { + this.connection.addListener(this.subId, cb); + } +} + export class WSConnection { public readonly url: URL; private ws: WebSocket | undefined; @@ -49,6 +62,12 @@ export class WSConnection { (this.listeners[subId] = this.listeners[subId] || []).filter((fn) => fn !== callback); } + async ensureConenction() { + if (this.ws?.readyState !== 1) { + await this.connect(); + } + } + handleNextMesage() { if (this.messageQueue.size === 0) { clearInterval(this.handlingInterval); @@ -80,6 +99,9 @@ export class WSConnection { return; } } - const len = parsed.length; + } + + subscribe() { + return new Subscription(this); } } From 4234e7a63eadec27e45db8b4902cec1ee4d2edf0 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 28 Mar 2024 12:08:15 +0100 Subject: [PATCH 159/246] added dev test --- test/WSConnection.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/WSConnection.test.ts diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts new file mode 100644 index 000000000..a65192619 --- /dev/null +++ b/test/WSConnection.test.ts @@ -0,0 +1,17 @@ +import { WSConnection, injectWebSocketImpl } from '../src/WSConnection'; + +describe('testing WSConnection', () => { + test('connecting...', async () => { + injectWebSocketImpl(require('ws')); + const ws = new WSConnection('https://echo.websocket.org/'); + await ws.connect(); + const sub = ws.subscribe(); + await new Promise((res) => { + // @ts-ignore + sub.onmessage((e) => { + console.log(e); + res(e); + }); + }); + }); +}); From 28fb63f8809f30beb048b1823e9d1233e67d8a64 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 28 Mar 2024 12:08:41 +0100 Subject: [PATCH 160/246] fixed binding --- src/WSConnection.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index a65f7ff06..72eb3e80a 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -3,7 +3,7 @@ import { MessageQueue } from './utils'; let _WS: typeof WebSocket; -if (WebSocket) { +if (typeof WebSocket !== 'undefined') { _WS = WebSocket; } @@ -48,7 +48,7 @@ export class WSConnection { this.ws.onmessage = (e) => { this.messageQueue.enqueue(e.data); if (!this.handlingInterval) { - this.handlingInterval = setInterval(this.handleNextMesage, 0); + this.handlingInterval = setInterval(this.handleNextMesage.bind(this), 0); } }; }); @@ -71,6 +71,7 @@ export class WSConnection { handleNextMesage() { if (this.messageQueue.size === 0) { clearInterval(this.handlingInterval); + this.handlingInterval = undefined; return; } const message = this.messageQueue.dequeue() as string; From 1c68297a676401e0e5d26e2f1157809d8dc505ee Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 28 Mar 2024 12:18:03 +0100 Subject: [PATCH 161/246] added commands --- src/WSConnection.ts | 9 ++++++++- test/WSConnection.test.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 72eb3e80a..dd5f8de8c 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,6 +1,8 @@ import { listeners } from 'process'; import { MessageQueue } from './utils'; +type Command = 'check_quote' | 'check_proof'; + let _WS: typeof WebSocket; if (typeof WebSocket !== 'undefined') { @@ -18,6 +20,7 @@ class Subscription { // HACK: There might be way better ways to create an random string, but I want to create something without dependecies frist this.subId = Math.random().toString(36).slice(-5); this.connection = conn; + conn.sendCommand('check_proof', this.subId, { test: true }); } onmessage(cb: () => any) { this.connection.addListener(this.subId, cb); @@ -54,6 +57,10 @@ export class WSConnection { }); } + sendCommand(cmd: Command, subId: string, params: any) { + this.ws?.send(JSON.stringify(['REQ', subId, cmd, params])); + } + addListener(subId: string, callback: () => any) { (this.listeners[subId] = this.listeners[subId] || []).push(callback); } @@ -102,7 +109,7 @@ export class WSConnection { } } - subscribe() { + subscribe(cmd: 'check_proof' | 'check_quote') { return new Subscription(this); } } diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index a65192619..4beedbfbf 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -5,7 +5,7 @@ describe('testing WSConnection', () => { injectWebSocketImpl(require('ws')); const ws = new WSConnection('https://echo.websocket.org/'); await ws.connect(); - const sub = ws.subscribe(); + const sub = ws.subscribe('check_proof'); await new Promise((res) => { // @ts-ignore sub.onmessage((e) => { From 6efb1538ddf2ae0f2facd26ec5d4a0299aeee722 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 5 Apr 2024 09:22:15 +0200 Subject: [PATCH 162/246] added ws package --- package-lock.json | 31 ++++++++++++++++++++++++++++++- package.json | 3 ++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1d6e7cc3..8b92724d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "ts-jest-resolver": "^2.0.1", "ts-node": "^10.9.1", "typedoc": "^0.24.7", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "ws": "^8.16.0" } }, "node_modules/@ampproject/remapping": { @@ -6327,6 +6328,27 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10966,6 +10988,13 @@ "signal-exit": "^3.0.7" } }, + "ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "requires": {} + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b40a56801..61d4ef639 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "ts-jest-resolver": "^2.0.1", "ts-node": "^10.9.1", "typedoc": "^0.24.7", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "ws": "^8.16.0" }, "dependencies": { "@cashu/crypto": "^0.2.7", From 6f2116895deecf0cfff048faaefbc615091f83f4 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 5 Apr 2024 09:22:24 +0200 Subject: [PATCH 163/246] added unsub --- src/WSConnection.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index dd5f8de8c..09aeb0fe6 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -25,6 +25,7 @@ class Subscription { onmessage(cb: () => any) { this.connection.addListener(this.subId, cb); } + unsub() {} } export class WSConnection { @@ -61,6 +62,10 @@ export class WSConnection { this.ws?.send(JSON.stringify(['REQ', subId, cmd, params])); } + closeSubscription(subId: string) { + this.ws?.send(JSON.stringify(['CLOSE', subId])); + } + addListener(subId: string, callback: () => any) { (this.listeners[subId] = this.listeners[subId] || []).push(callback); } From c1f4ed5367e1a059f33feb8c8b8d5024fd3a023d Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 18 Apr 2024 15:10:03 +0200 Subject: [PATCH 164/246] added jsonrpc types --- src/model/types/index.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 8203aee60..392c2df00 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -17,3 +17,40 @@ export type InvoiceData = { memo?: string; expiry?: number; }; + +export type RpcSubId = string | number | null; + +type JsonRpcParams = any; + +type JsonRpcSuccess = { + jsonrpc: '2.0'; + result: T; + id: RpcSubId; +}; + +export type JsonRpcErrorObject = { + code: number; + message: string; + data?: any; +}; + +type JsonRpcError = { + jsonrpc: '2.0'; + error: JsonRpcErrorObject; + id: RpcSubId; +}; + +type JsonRpcRequest = { + jsonrpc: '2.0'; + method: string; + params?: JsonRpcParams; + id: Exclude; +}; + +type JsonRpcNotification = { + jsonrpc: '2.0'; + method: string; + params?: JsonRpcParams; +}; + +export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcSuccess | JsonRpcError; From f3f044bd4d82cb24e09e2dbd9e4a0029a64e5347 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 18 Apr 2024 15:10:29 +0200 Subject: [PATCH 165/246] moved to json rpc --- src/WSConnection.ts | 119 ++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 09aeb0fe6..de026e52d 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,5 +1,12 @@ import { listeners } from 'process'; import { MessageQueue } from './utils'; +import { + JsonRpcErrorObject, + JsonRpcMessage, + JsonRpcRequest, + JsonRpcResponse, + RpcSubId +} from './model/types'; type Command = 'check_quote' | 'check_proof'; @@ -13,33 +20,21 @@ export function injectWebSocketImpl(ws: any) { _WS = ws; } -class Subscription { - private connection: WSConnection; - private subId: string; - constructor(conn: WSConnection) { - // HACK: There might be way better ways to create an random string, but I want to create something without dependecies frist - this.subId = Math.random().toString(36).slice(-5); - this.connection = conn; - conn.sendCommand('check_proof', this.subId, { test: true }); - } - onmessage(cb: () => any) { - this.connection.addListener(this.subId, cb); - } - unsub() {} -} - export class WSConnection { public readonly url: URL; private ws: WebSocket | undefined; - private listeners: { [reqId: string]: Array } = {}; + private subListeners: { [subId: string]: Array } = {}; + private rpcListeners: { [rpsSubId: string]: any } = {}; private messageQueue: MessageQueue; private handlingInterval?: NodeJS.Timer; + private rpcId = 0; constructor(url: string) { this.url = new URL(url); this.messageQueue = new MessageQueue(); } - async connect() { + + connect() { return new Promise((res, rej) => { try { this.ws = new _WS(this.url); @@ -58,20 +53,34 @@ export class WSConnection { }); } - sendCommand(cmd: Command, subId: string, params: any) { - this.ws?.send(JSON.stringify(['REQ', subId, cmd, params])); + sendRequest(cmd: Command, subId: string, params: any) { + const id = this.rpcId; + this.rpcId++; + this.ws?.send(JSON.stringify({ jsonrpc: '2.0', method: cmd, params: { subId }, id: id })); } closeSubscription(subId: string) { this.ws?.send(JSON.stringify(['CLOSE', subId])); } - addListener(subId: string, callback: () => any) { - (this.listeners[subId] = this.listeners[subId] || []).push(callback); + addSubListener(subId: string, callback: () => any) { + (this.subListeners[subId] = this.subListeners[subId] || []).push(callback); + } + + addRpcListener( + callback: () => any, + errorCallback: (e: JsonRpcErrorObject) => any, + id: Exclude + ) { + this.rpcListeners[id] = { callback, errorCallback }; + } + + removeRpcListener(id: Exclude) { + delete this.rpcListeners[id]; } removeListener(subId: string, callback: () => any) { - (this.listeners[subId] = this.listeners[subId] || []).filter((fn) => fn !== callback); + (this.subListeners[subId] = this.subListeners[subId] || []).filter((fn) => fn !== callback); } async ensureConenction() { @@ -89,32 +98,56 @@ export class WSConnection { const message = this.messageQueue.dequeue() as string; let parsed; try { - parsed = JSON.parse(message) as Array; + parsed = JSON.parse(message) as JsonRpcMessage; + if ('result' in parsed && parsed.id != undefined) { + if (this.rpcListeners[parsed.id]) { + this.rpcListeners[parsed.id].callback(); + this.removeRpcListener(parsed.id); + } + } else if ('error' in parsed && parsed.id != undefined) { + if (this.rpcListeners[parsed.id]) { + this.rpcListeners[parsed.id].errorCallback(parsed.error); + this.removeRpcListener(parsed.id); + } + } else if ('method' in parsed) { + if ('id' in parsed) { + // This is a request + // Do nothing as mints should not send requests + } else { + const subId = parsed.params.subId; + if (!subId) { + return; + } + if (this.subListeners[subId].length > 0) { + this.subListeners[subId].forEach((cb) => cb()); + } + // This is a notification + } + } } catch (e) { console.log(e); return; } - let subId: string; - let data: any; - switch (parsed.length) { - case 2: { - // Must be notice - // TODO: Implement NOTICE - return; - } - case 3: { - subId = parsed[1]; - data = parsed[3]; - this.listeners[subId].forEach((cb) => cb(data)); - break; - } - default: { - return; - } - } } - subscribe(cmd: 'check_proof' | 'check_quote') { - return new Subscription(this); + createSubscription( + cmd: 'check_proof' | 'check_quote', + callback: () => any, + errorCallback: (e: Error) => any + ) { + if (this.ws?.readyState === 1) { + return errorCallback(new Error('Socket is not open')); + } + const subId = (Math.random() + 1).toString(36).substring(7); + this.addRpcListener( + () => { + this.addSubListener(subId, callback); + }, + (e: JsonRpcErrorObject) => { + errorCallback(new Error(e.message)); + }, + this.rpcId + ); + this.rpcId++; } } From 12045f9d2bae384f6c5bd86caaddbafdf4896456 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 18 Apr 2024 15:13:34 +0200 Subject: [PATCH 166/246] added send command --- src/WSConnection.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index de026e52d..df2d50b56 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,12 +1,5 @@ -import { listeners } from 'process'; import { MessageQueue } from './utils'; -import { - JsonRpcErrorObject, - JsonRpcMessage, - JsonRpcRequest, - JsonRpcResponse, - RpcSubId -} from './model/types'; +import { JsonRpcErrorObject, JsonRpcMessage, RpcSubId } from './model/types'; type Command = 'check_quote' | 'check_proof'; @@ -132,6 +125,7 @@ export class WSConnection { createSubscription( cmd: 'check_proof' | 'check_quote', + params: any, callback: () => any, errorCallback: (e: Error) => any ) { @@ -149,5 +143,6 @@ export class WSConnection { this.rpcId ); this.rpcId++; + this.sendRequest(cmd, subId, params); } } From 28e04912a938ba0346acc8f893d9982dbdd3829f Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 21 Apr 2024 09:35:27 +0200 Subject: [PATCH 167/246] calles format --- src/WSConnection.ts | 11 +++++------ src/model/types/index.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index df2d50b56..5233de0b1 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,5 +1,5 @@ import { MessageQueue } from './utils'; -import { JsonRpcErrorObject, JsonRpcMessage, RpcSubId } from './model/types'; +import { JsonRpcErrorObject, JsonRpcMessage, JsonRpcReqParams, RpcSubId } from './model/types'; type Command = 'check_quote' | 'check_proof'; @@ -46,10 +46,10 @@ export class WSConnection { }); } - sendRequest(cmd: Command, subId: string, params: any) { + sendRequest(params: JsonRpcReqParams) { const id = this.rpcId; this.rpcId++; - this.ws?.send(JSON.stringify({ jsonrpc: '2.0', method: cmd, params: { subId }, id: id })); + this.ws?.send(JSON.stringify({ jsonrpc: '2.0', method: 'sub', params, id: id })); } closeSubscription(subId: string) { @@ -124,8 +124,7 @@ export class WSConnection { } createSubscription( - cmd: 'check_proof' | 'check_quote', - params: any, + params: JsonRpcReqParams, callback: () => any, errorCallback: (e: Error) => any ) { @@ -143,6 +142,6 @@ export class WSConnection { this.rpcId ); this.rpcId++; - this.sendRequest(cmd, subId, params); + this.sendRequest(params); } } diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 392c2df00..c3b8b8974 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -18,10 +18,18 @@ export type InvoiceData = { expiry?: number; }; +type RpcSubKinds = 'bolt11_mint_quote' | 'bolt11_melt_quote' | 'proof_state'; + export type RpcSubId = string | number | null; type JsonRpcParams = any; +export type JsonRpcReqParams = { + kind: RpcSubKinds; + filter: Array; + subId: string; +}; + type JsonRpcSuccess = { jsonrpc: '2.0'; result: T; @@ -42,8 +50,8 @@ type JsonRpcError = { type JsonRpcRequest = { jsonrpc: '2.0'; - method: string; - params?: JsonRpcParams; + method: 'sub'; + params: JsonRpcReqParams; id: Exclude; }; From a1603693fb6ef3b8102c82739254121f7eb2d761 Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 21 Apr 2024 10:40:59 +0200 Subject: [PATCH 168/246] export fixes and logs --- src/WSConnection.ts | 9 +++++---- src/index.ts | 1 + src/model/types/index.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 5233de0b1..878ee116c 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,8 +1,6 @@ import { MessageQueue } from './utils'; import { JsonRpcErrorObject, JsonRpcMessage, JsonRpcReqParams, RpcSubId } from './model/types'; -type Command = 'check_quote' | 'check_proof'; - let _WS: typeof WebSocket; if (typeof WebSocket !== 'undefined') { @@ -49,7 +47,9 @@ export class WSConnection { sendRequest(params: JsonRpcReqParams) { const id = this.rpcId; this.rpcId++; - this.ws?.send(JSON.stringify({ jsonrpc: '2.0', method: 'sub', params, id: id })); + const message = JSON.stringify({ jsonrpc: '2.0', method: 'sub', params, id }); + console.log(message); + this.ws?.send(message); } closeSubscription(subId: string) { @@ -92,6 +92,7 @@ export class WSConnection { let parsed; try { parsed = JSON.parse(message) as JsonRpcMessage; + console.log(parsed); if ('result' in parsed && parsed.id != undefined) { if (this.rpcListeners[parsed.id]) { this.rpcListeners[parsed.id].callback(); @@ -128,7 +129,7 @@ export class WSConnection { callback: () => any, errorCallback: (e: Error) => any ) { - if (this.ws?.readyState === 1) { + if (this.ws?.readyState !== 1) { return errorCallback(new Error('Socket is not open')); } const subId = (Math.random() + 1).toString(36).substring(7); diff --git a/src/index.ts b/src/index.ts index fad66d8e7..cd9c6b3db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; import { PaymentRequest } from './model/PaymentRequest.js'; +import { WSConnection } from './WSConnection.js'; import { setGlobalRequestOptions } from './request.js'; import { getEncodedToken, diff --git a/src/model/types/index.ts b/src/model/types/index.ts index c3b8b8974..72cb6ceaf 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -26,7 +26,7 @@ type JsonRpcParams = any; export type JsonRpcReqParams = { kind: RpcSubKinds; - filter: Array; + filters: Array; subId: string; }; From c7676574185fcbec09b1e50185f11138d1dde262 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 06:40:11 +0200 Subject: [PATCH 169/246] fixed typo --- src/WSConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 878ee116c..1d4baa451 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -76,7 +76,7 @@ export class WSConnection { (this.subListeners[subId] = this.subListeners[subId] || []).filter((fn) => fn !== callback); } - async ensureConenction() { + async ensureConnection() { if (this.ws?.readyState !== 1) { await this.connect(); } From 72bda7dee27ca4a351e3e48dcee51cfee1309e8e Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 06:40:26 +0200 Subject: [PATCH 170/246] fixed connection types and params --- src/WSConnection.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 1d4baa451..0269a6f39 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -1,5 +1,11 @@ import { MessageQueue } from './utils'; -import { JsonRpcErrorObject, JsonRpcMessage, JsonRpcReqParams, RpcSubId } from './model/types'; +import { + JsonRpcErrorObject, + JsonRpcMessage, + JsonRpcNotification, + JsonRpcReqParams, + RpcSubId +} from './model/types'; let _WS: typeof WebSocket; @@ -92,7 +98,6 @@ export class WSConnection { let parsed; try { parsed = JSON.parse(message) as JsonRpcMessage; - console.log(parsed); if ('result' in parsed && parsed.id != undefined) { if (this.rpcListeners[parsed.id]) { this.rpcListeners[parsed.id].callback(); @@ -113,7 +118,8 @@ export class WSConnection { return; } if (this.subListeners[subId].length > 0) { - this.subListeners[subId].forEach((cb) => cb()); + const notification = parsed as JsonRpcNotification; + this.subListeners[subId].forEach((cb) => cb(notification.params.payload)); } // This is a notification } @@ -125,7 +131,7 @@ export class WSConnection { } createSubscription( - params: JsonRpcReqParams, + params: Omit, callback: () => any, errorCallback: (e: Error) => any ) { @@ -143,6 +149,6 @@ export class WSConnection { this.rpcId ); this.rpcId++; - this.sendRequest(params); + this.sendRequest({ ...params, subId }); } } From 703044fad4bc2b5f9fea0aa7ef3ac2e1b3de45ca Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 06:40:39 +0200 Subject: [PATCH 171/246] added connection to mint --- src/CashuMint.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index b86a07eba..482a3e513 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -1,3 +1,4 @@ +import { WSConnection } from './WSConnection.js'; import type { CheckStatePayload, CheckStateResponse, @@ -33,6 +34,7 @@ import { handleMintInfoContactFieldDeprecated } from './legacy/nut-06.js'; * Class represents Cashu Mint API. This class contains Lower level functions that are implemented by CashuWallet. */ class CashuMint { + private ws?: WSConnection; /** * @param _mintUrl requires mint URL to create this object * @param _customRequest if passed, use custom request implementation for network communication with the mint @@ -438,6 +440,21 @@ class CashuMint { }): Promise { return CashuMint.restore(this._mintUrl, restorePayload, this._customRequest); } + + async connectWebSocket() { + if (this.ws) { + await this.ws.ensureConnection(); + } else { + const mintUrl = new URL(this._mintUrl); + this.ws = new WSConnection( + `${mintUrl.protocol === 'https' ? 'wss' : 'ws'}://${mintUrl.host}/v1/ws` + ); + await this.ws.connect(); + } + } + get webSocketConnection() { + return this.ws; + } } export { CashuMint }; From e1780ff7eb2ede22511a610b88bda8b5f8ad62d4 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 06:42:40 +0200 Subject: [PATCH 172/246] more type fixes --- src/WSConnection.ts | 6 +++--- src/model/types/index.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 0269a6f39..b694bb658 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -20,7 +20,7 @@ export function injectWebSocketImpl(ws: any) { export class WSConnection { public readonly url: URL; private ws: WebSocket | undefined; - private subListeners: { [subId: string]: Array } = {}; + private subListeners: { [subId: string]: Array<(payload: any) => any> } = {}; private rpcListeners: { [rpsSubId: string]: any } = {}; private messageQueue: MessageQueue; private handlingInterval?: NodeJS.Timer; @@ -62,7 +62,7 @@ export class WSConnection { this.ws?.send(JSON.stringify(['CLOSE', subId])); } - addSubListener(subId: string, callback: () => any) { + addSubListener(subId: string, callback: (payload: any) => any) { (this.subListeners[subId] = this.subListeners[subId] || []).push(callback); } @@ -132,7 +132,7 @@ export class WSConnection { createSubscription( params: Omit, - callback: () => any, + callback: (payload: any) => any, errorCallback: (e: Error) => any ) { if (this.ws?.readyState !== 1) { diff --git a/src/model/types/index.ts b/src/model/types/index.ts index 72cb6ceaf..3f1f819ff 100644 --- a/src/model/types/index.ts +++ b/src/model/types/index.ts @@ -55,7 +55,7 @@ type JsonRpcRequest = { id: Exclude; }; -type JsonRpcNotification = { +export type JsonRpcNotification = { jsonrpc: '2.0'; method: string; params?: JsonRpcParams; From 24fb18e95a465f3c174546feb0b695f68f310c56 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 06:59:46 +0200 Subject: [PATCH 173/246] added waitOnQuotePaid API --- src/CashuWallet.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1b1a1b8af..be92bbb3e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -863,6 +863,21 @@ class CashuWallet { return states; } + async waitOnQuotePaid(quoteId: string, callback: (payload: any) => any) { + await this.mint.connectWebSocket(); + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + this.mint.webSocketConnection.createSubscription( + { kind: 'bolt11_mint_quote', filters: [quoteId] }, + callback, + (e) => { + throw new Error(e.message); + } + ); + //TODO: Return unsub function + } + /** * Creates blinded messages for a given amount * @param amount amount to create blinded messages for From d49db5f32e6e1886c6aa187e6420598747150e3b Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 10:05:44 +0200 Subject: [PATCH 174/246] added unsub method --- src/WSConnection.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index b694bb658..2426f4cbd 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -49,11 +49,12 @@ export class WSConnection { }; }); } - - sendRequest(params: JsonRpcReqParams) { + sendRequest(method: 'subscribe', params: JsonRpcReqParams): void; + sendRequest(method: 'unsubscribe', params: { subId: string }): void; + sendRequest(method: 'subscribe' | 'unsubscribe', params: Partial) { const id = this.rpcId; this.rpcId++; - const message = JSON.stringify({ jsonrpc: '2.0', method: 'sub', params, id }); + const message = JSON.stringify({ jsonrpc: '2.0', method, params, id }); console.log(message); this.ws?.send(message); } @@ -149,6 +150,17 @@ export class WSConnection { this.rpcId ); this.rpcId++; - this.sendRequest({ ...params, subId }); + this.sendRequest('subscribe', { ...params, subId }); + } + + cancelSubscription(subId: string, callback: () => any, errorCallback: (e: Error) => any) { + this.removeListener(subId, callback); + this.addRpcListener( + callback, + (e: JsonRpcErrorObject) => errorCallback(new Error(e.message)), + this.rpcId + ); + this.rpcId++; + this.sendRequest('unsubscribe', { subId }); } } From 7b351f22fd93b699e1fefe3a2d05747e43e35e60 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 18 Jun 2024 10:11:54 +0200 Subject: [PATCH 175/246] added unsub to wallet --- src/CashuWallet.ts | 6 ++++-- src/WSConnection.ts | 10 +++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index be92bbb3e..84ece60f7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -868,14 +868,16 @@ class CashuWallet { if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } - this.mint.webSocketConnection.createSubscription( + const subId = this.mint.webSocketConnection.createSubscription( { kind: 'bolt11_mint_quote', filters: [quoteId] }, callback, (e) => { throw new Error(e.message); } ); - //TODO: Return unsub function + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, callback); + }; } /** diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 2426f4cbd..cd0e5ca5e 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -79,7 +79,7 @@ export class WSConnection { delete this.rpcListeners[id]; } - removeListener(subId: string, callback: () => any) { + removeListener(subId: string, callback: (payload: any) => any) { (this.subListeners[subId] = this.subListeners[subId] || []).filter((fn) => fn !== callback); } @@ -151,15 +151,11 @@ export class WSConnection { ); this.rpcId++; this.sendRequest('subscribe', { ...params, subId }); + return subId; } - cancelSubscription(subId: string, callback: () => any, errorCallback: (e: Error) => any) { + cancelSubscription(subId: string, callback: (payload: any) => any) { this.removeListener(subId, callback); - this.addRpcListener( - callback, - (e: JsonRpcErrorObject) => errorCallback(new Error(e.message)), - this.rpcId - ); this.rpcId++; this.sendRequest('unsubscribe', { subId }); } From 03bf30b4c3fe04e9b7fc6b7552828dc9f42e3e4c Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 25 Jun 2024 20:49:36 +0200 Subject: [PATCH 176/246] clean up --- src/CashuWallet.ts | 6 ++++-- src/WSConnection.ts | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 84ece60f7..9973fb4bc 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -863,14 +863,16 @@ class CashuWallet { return states; } - async waitOnQuotePaid(quoteId: string, callback: (payload: any) => any) { + async onQuotePaid(quoteId: string, callback: (payload: any) => any) { await this.mint.connectWebSocket(); if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } const subId = this.mint.webSocketConnection.createSubscription( { kind: 'bolt11_mint_quote', filters: [quoteId] }, - callback, + (payload) => { + console.log(payload); + }, (e) => { throw new Error(e.message); } diff --git a/src/WSConnection.ts b/src/WSConnection.ts index cd0e5ca5e..7c2dd98d1 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -49,6 +49,7 @@ export class WSConnection { }; }); } + sendRequest(method: 'subscribe', params: JsonRpcReqParams): void; sendRequest(method: 'unsubscribe', params: { subId: string }): void; sendRequest(method: 'subscribe' | 'unsubscribe', params: Partial) { @@ -67,7 +68,8 @@ export class WSConnection { (this.subListeners[subId] = this.subListeners[subId] || []).push(callback); } - addRpcListener( + //TODO: Move to RPCManagerClass + private addRpcListener( callback: () => any, errorCallback: (e: JsonRpcErrorObject) => any, id: Exclude @@ -75,11 +77,12 @@ export class WSConnection { this.rpcListeners[id] = { callback, errorCallback }; } - removeRpcListener(id: Exclude) { + //TODO: Move to RPCManagerClass + private removeRpcListener(id: Exclude) { delete this.rpcListeners[id]; } - removeListener(subId: string, callback: (payload: any) => any) { + private removeListener(subId: string, callback: (payload: any) => any) { (this.subListeners[subId] = this.subListeners[subId] || []).filter((fn) => fn !== callback); } @@ -89,7 +92,7 @@ export class WSConnection { } } - handleNextMesage() { + private handleNextMesage() { if (this.messageQueue.size === 0) { clearInterval(this.handlingInterval); this.handlingInterval = undefined; @@ -111,7 +114,6 @@ export class WSConnection { } } else if ('method' in parsed) { if ('id' in parsed) { - // This is a request // Do nothing as mints should not send requests } else { const subId = parsed.params.subId; @@ -122,7 +124,6 @@ export class WSConnection { const notification = parsed as JsonRpcNotification; this.subListeners[subId].forEach((cb) => cb(notification.params.payload)); } - // This is a notification } } } catch (e) { From c965e9d5dbb2c50fb6f7c74bc31c67ab5fee0464 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 25 Jun 2024 20:49:45 +0200 Subject: [PATCH 177/246] began tests --- test/WSConnection.test.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index 4beedbfbf..d086f21f4 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -1,17 +1,11 @@ import { WSConnection, injectWebSocketImpl } from '../src/WSConnection'; +import { CashuMint, CashuWallet } from '../src/index'; describe('testing WSConnection', () => { test('connecting...', async () => { injectWebSocketImpl(require('ws')); - const ws = new WSConnection('https://echo.websocket.org/'); - await ws.connect(); - const sub = ws.subscribe('check_proof'); - await new Promise((res) => { - // @ts-ignore - sub.onmessage((e) => { - console.log(e); - res(e); - }); - }); + const wallet = new CashuWallet(new CashuMint('ws://localhost:3338')); + const quote = await wallet.getMintQuote(21); + console.log(quote); }); }); From 5a84753a94bc0610c96e1c4be8604f8bd4f202f0 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 26 Jun 2024 17:37:53 +0200 Subject: [PATCH 178/246] added error handling --- src/CashuMint.ts | 6 +++++- src/CashuWallet.ts | 32 ++++++++++++++++++++++++-------- src/WSConnection.ts | 7 ++++--- test/WSConnection.test.ts | 17 ++++++++++++++--- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 482a3e513..e51dcf0a1 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -449,7 +449,11 @@ class CashuMint { this.ws = new WSConnection( `${mintUrl.protocol === 'https' ? 'wss' : 'ws'}://${mintUrl.host}/v1/ws` ); - await this.ws.connect(); + try { + await this.ws.connect(); + } catch (e) { + throw new Error('Failed to connect to WebSocket...'); + } } } get webSocketConnection() { diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 9973fb4bc..2185de8ff 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -863,22 +863,38 @@ class CashuWallet { return states; } - async onQuotePaid(quoteId: string, callback: (payload: any) => any) { - await this.mint.connectWebSocket(); + async onQuotePaid( + quoteId: string, + callback: (payload: any) => any, + errorCallback: (e: Error) => void + ) { + try { + await this.mint.connectWebSocket(); + } catch (e) { + console.log('caught in quote paid'); + if (e instanceof Error) { + return errorCallback(e); + } else if (e) { + return errorCallback(new Error('Something went wrong')); + } + } if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } + const subCallback = (payload: any) => { + if (payload.paid) { + callback(payload); + } + }; const subId = this.mint.webSocketConnection.createSubscription( { kind: 'bolt11_mint_quote', filters: [quoteId] }, - (payload) => { - console.log(payload); - }, - (e) => { - throw new Error(e.message); + subCallback, + (e: Error) => { + errorCallback(e); } ); return () => { - this.mint.webSocketConnection?.cancelSubscription(subId, callback); + this.mint.webSocketConnection?.cancelSubscription(subId, subCallback); }; } diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 7c2dd98d1..fa6019787 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -33,9 +33,11 @@ export class WSConnection { connect() { return new Promise((res, rej) => { + console.log('running connect'); try { this.ws = new _WS(this.url); } catch (err) { + console.log(err); rej(err); return; } @@ -56,7 +58,6 @@ export class WSConnection { const id = this.rpcId; this.rpcId++; const message = JSON.stringify({ jsonrpc: '2.0', method, params, id }); - console.log(message); this.ws?.send(message); } @@ -120,7 +121,7 @@ export class WSConnection { if (!subId) { return; } - if (this.subListeners[subId].length > 0) { + if (this.subListeners[subId]?.length > 0) { const notification = parsed as JsonRpcNotification; this.subListeners[subId].forEach((cb) => cb(notification.params.payload)); } @@ -150,8 +151,8 @@ export class WSConnection { }, this.rpcId ); - this.rpcId++; this.sendRequest('subscribe', { ...params, subId }); + this.rpcId++; return subId; } diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index d086f21f4..f300348e4 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -4,8 +4,19 @@ import { CashuMint, CashuWallet } from '../src/index'; describe('testing WSConnection', () => { test('connecting...', async () => { injectWebSocketImpl(require('ws')); - const wallet = new CashuWallet(new CashuMint('ws://localhost:3338')); - const quote = await wallet.getMintQuote(21); - console.log(quote); + const wallet = new CashuWallet(new CashuMint('http://localhost:3338')); + await new Promise((res, rej) => { + const unsub = wallet.onQuotePaid( + 'XCV', + (pa) => { + console.log(pa); + res(pa); + }, + (e: Error) => { + rej(e); + } + ); + }); + console.log('Ended'); }); }); From ecfd530488f4b5252121183a7d7bcf1a862388b9 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 28 Jun 2024 09:12:51 +0200 Subject: [PATCH 179/246] improved error handling --- src/CashuWallet.ts | 1 - src/WSConnection.ts | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2185de8ff..1f633e65c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -871,7 +871,6 @@ class CashuWallet { try { await this.mint.connectWebSocket(); } catch (e) { - console.log('caught in quote paid'); if (e instanceof Error) { return errorCallback(e); } else if (e) { diff --git a/src/WSConnection.ts b/src/WSConnection.ts index fa6019787..ce0e3f296 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -33,16 +33,16 @@ export class WSConnection { connect() { return new Promise((res, rej) => { - console.log('running connect'); try { this.ws = new _WS(this.url); } catch (err) { - console.log(err); rej(err); return; } this.ws.onopen = res; - this.ws.onerror = rej; + this.ws.onerror = (e) => { + rej(e); + }; this.ws.onmessage = (e) => { this.messageQueue.enqueue(e.data); if (!this.handlingInterval) { @@ -55,6 +55,9 @@ export class WSConnection { sendRequest(method: 'subscribe', params: JsonRpcReqParams): void; sendRequest(method: 'unsubscribe', params: { subId: string }): void; sendRequest(method: 'subscribe' | 'unsubscribe', params: Partial) { + if (this.ws?.readyState !== 1) { + throw new Error('Socket not open...'); + } const id = this.rpcId; this.rpcId++; const message = JSON.stringify({ jsonrpc: '2.0', method, params, id }); From d636becf3df3ba89dbe2eb76819b59bc540aacc2 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 28 Jun 2024 09:13:00 +0200 Subject: [PATCH 180/246] added some tests --- package-lock.json | 35 ++++++++++++++++++ package.json | 2 + test/WSConnection.test.ts | 77 ++++++++++++++++++++++++++++++++------- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b92724d1..8b85330f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "devDependencies": { "@types/jest": "^29.5.1", "@types/node-fetch": "^2.6.4", + "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", "eslint": "^8.39.0", @@ -26,6 +27,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.5.0", + "mock-socket": "^9.3.1", "nock": "^13.3.3", "node-fetch": "^2.7.0", "prettier": "^2.8.8", @@ -1429,6 +1431,15 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.17", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", @@ -4967,6 +4978,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7512,6 +7532,15 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.17", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", @@ -10044,6 +10073,12 @@ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", "dev": true }, + "mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 61d4ef639..b5b877c0c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@types/jest": "^29.5.1", "@types/node-fetch": "^2.6.4", + "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", "eslint": "^8.39.0", @@ -37,6 +38,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.5.0", + "mock-socket": "^9.3.1", "nock": "^13.3.3", "node-fetch": "^2.7.0", "prettier": "^2.8.8", diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index f300348e4..f69b26a8c 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -1,22 +1,71 @@ import { WSConnection, injectWebSocketImpl } from '../src/WSConnection'; -import { CashuMint, CashuWallet } from '../src/index'; +import { Server, WebSocket } from 'mock-socket'; + +injectWebSocketImpl(WebSocket); describe('testing WSConnection', () => { test('connecting...', async () => { - injectWebSocketImpl(require('ws')); - const wallet = new CashuWallet(new CashuMint('http://localhost:3338')); - await new Promise((res, rej) => { - const unsub = wallet.onQuotePaid( - 'XCV', - (pa) => { - console.log(pa); - res(pa); - }, - (e: Error) => { - rej(e); - } + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + const connectionSpy = jest.fn(); + server.on('connection', connectionSpy); + const conn = new WSConnection(fakeUrl); + await conn.connect(); + expect(connectionSpy).toHaveBeenCalled(); + server.stop(); + }); + test('requesting subscription', async () => { + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + 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 = jest.fn(); + const errorCallback = jest.fn(); + conn.createSubscription( + { kind: 'bolt11_mint_quote', filters: ['12345'] }, + callback, + errorCallback + ); + })) as string; + expect(JSON.parse(message)).toMatchObject({ + jsonrpc: '2.0', + method: 'subscribe', + params: { kind: 'bolt11_mint_quote', filters: ['12345'] } + }); + server.stop(); + }); + test('unsubscribing', async () => { + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + const message = await new Promise(async (res) => { + server.on('connection', (socket) => { + socket.on('message', (m) => { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'unsubscribe') res(parsed); + }); + }); + const conn = new WSConnection(fakeUrl); + await conn.connect(); + + conn.sendRequest('subscribe', { + subId: '12345', + kind: 'bolt11_mint_quote', + filters: ['12345'] + }); + const callback = jest.fn(); + const errorCallback = jest.fn(); + conn.createSubscription( + { kind: 'bolt11_mint_quote', filters: ['123'] }, + callback, + errorCallback ); }); - console.log('Ended'); }); }); From cf60dedde106d1c3408e24b4935f0889ac528bf5 Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 30 Jun 2024 06:02:42 +0200 Subject: [PATCH 181/246] more tests --- test/WSConnection.test.ts | 50 ++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index f69b26a8c..1bc52af23 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -54,14 +54,52 @@ describe('testing WSConnection', () => { const conn = new WSConnection(fakeUrl); await conn.connect(); - conn.sendRequest('subscribe', { - subId: '12345', - kind: 'bolt11_mint_quote', - filters: ['12345'] - }); const callback = jest.fn(); const errorCallback = jest.fn(); - conn.createSubscription( + const subId = conn.createSubscription( + { kind: 'bolt11_mint_quote', filters: ['123'] }, + callback, + errorCallback + ); + //TODO: Add assertion for subListenerLength once SubscriptionManager is modularised + conn.cancelSubscription(subId, callback); + }); + expect(message).toMatchObject({ jsonrpc: '2.0', method: 'unsubscribe' }); + server.stop(); + }); + test('handing a notification', async () => { + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + server.on('connection', (socket) => { + socket.on('message', (m) => { + console.log(m); + try { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'subscribe') { + const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}", "id": ${parsed.id}}}`; + console.log(message); + socket.send(message); + setTimeout(() => { + const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "paid": true, "expiry": 123}}}`; + console.log(message); + socket.send(message); + }, 500); + } + } catch { + console.log('Server parsing failed...'); + } + }); + }); + const conn = new WSConnection(fakeUrl); + await conn.connect(); + + await new Promise((res) => { + const callback = jest.fn((p) => { + console.log('Payload received! ', p); + res(p); + }); + const errorCallback = jest.fn(); + const subId = conn.createSubscription( { kind: 'bolt11_mint_quote', filters: ['123'] }, callback, errorCallback From ac5173285377ac89410b202f9b6619fa6de0e880 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 11 Jul 2024 17:11:20 +0200 Subject: [PATCH 182/246] fixed notification test --- test/WSConnection.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index 1bc52af23..cf1981d4a 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -72,16 +72,13 @@ describe('testing WSConnection', () => { const server = new Server(fakeUrl, { mock: false }); server.on('connection', (socket) => { socket.on('message', (m) => { - console.log(m); try { const parsed = JSON.parse(m.toString()); if (parsed.method === 'subscribe') { - const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}", "id": ${parsed.id}}}`; - console.log(message); + const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}"}, "id": ${parsed.id}}`; socket.send(message); setTimeout(() => { const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "paid": true, "expiry": 123}}}`; - console.log(message); socket.send(message); }, 500); } @@ -93,17 +90,17 @@ describe('testing WSConnection', () => { const conn = new WSConnection(fakeUrl); await conn.connect(); - await new Promise((res) => { - const callback = jest.fn((p) => { - console.log('Payload received! ', p); + const payload = await new Promise((res) => { + const callback = jest.fn((p: any) => { res(p); }); const errorCallback = jest.fn(); - const subId = conn.createSubscription( + conn.createSubscription( { kind: 'bolt11_mint_quote', filters: ['123'] }, callback, errorCallback ); }); + expect(payload).toMatchObject({ quote: '123', request: '456', paid: true, expiry: 123 }); }); }); From 9c6992e4aa592dbb0706b5235dda25ec7213019f Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 15 Jul 2024 10:29:30 +0200 Subject: [PATCH 183/246] added onMintPaid test --- test/WSConnection.test.ts | 1 + test/wallet.test.ts | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index cf1981d4a..9c5b183dc 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -102,5 +102,6 @@ describe('testing WSConnection', () => { ); }); expect(payload).toMatchObject({ quote: '123', request: '456', paid: true, expiry: 123 }); + server.stop(); }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7ca5c27b2..e29251b07 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -3,6 +3,11 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import { CheckStateEnum, MeltQuoteResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; +import { Proof } from '@cashu/crypto/modules/common'; +import { injectWebSocketImpl } from '../src/WSConnection.js'; +import { Server, WebSocket } from 'mock-socket'; + +injectWebSocketImpl(WebSocket); const dummyKeysResp = { keysets: [ @@ -550,3 +555,38 @@ describe('deterministic', () => { ); }); }); + +describe('WebSocket Updates', () => { + test('mint update', async () => { + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + server.on('connection', (socket) => { + socket.on('message', (m) => { + console.log(m); + try { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'subscribe') { + const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}"}, "id": ${parsed.id}}`; + socket.send(message); + setTimeout(() => { + const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "paid": true, "expiry": 123}}}`; + socket.send(message); + }, 500); + } + } catch { + console.log('Server parsing failed...'); + } + }); + }); + const wallet = new CashuWallet(mint); + await new Promise((res) => { + const callback = (p: any) => { + console.log(p); + res(p); + }; + const test = wallet.onQuotePaid('123', callback, () => { + console.log('error'); + }); + }); + }); +}); From 7f0fe81d1eb72afffe4ebc1ae7a5018e8785f99f Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 17 Jul 2024 09:36:17 +0200 Subject: [PATCH 184/246] updated naming --- src/CashuWallet.ts | 2 +- test/wallet.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1f633e65c..a89ef0606 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -863,7 +863,7 @@ class CashuWallet { return states; } - async onQuotePaid( + async onMintQuotePaid( quoteId: string, callback: (payload: any) => any, errorCallback: (e: Error) => void diff --git a/test/wallet.test.ts b/test/wallet.test.ts index e29251b07..7fd9b0af8 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -584,7 +584,7 @@ describe('WebSocket Updates', () => { console.log(p); res(p); }; - const test = wallet.onQuotePaid('123', callback, () => { + const test = wallet.onMintQuotePaid('123', callback, () => { console.log('error'); }); }); From 2fbf05cafbc6f6c6dbb0d235c5318ac0d5504eca Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 17 Jul 2024 09:44:55 +0200 Subject: [PATCH 185/246] added onMeltQuote --- src/CashuWallet.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index a89ef0606..01060c8ab 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -897,6 +897,40 @@ class CashuWallet { }; } + async onMeltQuotePaid( + quoteId: string, + callback: (payload: MeltQuoteResponse) => any, + errorCallback: (e: Error) => void + ) { + try { + await this.mint.connectWebSocket(); + } catch (e) { + if (e instanceof Error) { + return errorCallback(e); + } else if (e) { + return errorCallback(new Error('Something went wrong')); + } + } + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + const subCallback = (payload: MeltQuoteResponse) => { + if (payload.state === 'PAID') { + callback(payload); + } + }; + const subId = this.mint.webSocketConnection.createSubscription( + { kind: 'bolt11_melt_quote', filters: [quoteId] }, + subCallback, + (e: Error) => { + errorCallback(e); + } + ); + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, subCallback); + }; + } + /** * Creates blinded messages for a given amount * @param amount amount to create blinded messages for From 387867dd658d5b1d2c5c461f3be05cdbcc158704 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 17 Jul 2024 10:42:00 +0200 Subject: [PATCH 186/246] updated types --- src/CashuWallet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 01060c8ab..993d22ac5 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -865,7 +865,7 @@ class CashuWallet { async onMintQuotePaid( quoteId: string, - callback: (payload: any) => any, + callback: (payload: MintQuoteResponse) => any, errorCallback: (e: Error) => void ) { try { @@ -880,8 +880,8 @@ class CashuWallet { if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } - const subCallback = (payload: any) => { - if (payload.paid) { + const subCallback = (payload: MintQuoteResponse) => { + if (payload.state === 'PAID') { callback(payload); } }; From 5f8d159122cdd394de69ca76ee285325ef32bc29 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 9 Sep 2024 14:37:12 +0200 Subject: [PATCH 187/246] ws: added disconnect method --- src/CashuMint.ts | 8 ++++++++ src/WSConnection.ts | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index e51dcf0a1..800eb0d5e 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -452,10 +452,18 @@ class CashuMint { try { await this.ws.connect(); } catch (e) { + console.log(e); throw new Error('Failed to connect to WebSocket...'); } } } + + disconnectWebSocket() { + if (this.ws) { + this.ws.close(); + } + } + get webSocketConnection() { return this.ws; } diff --git a/src/WSConnection.ts b/src/WSConnection.ts index ce0e3f296..e2273ab4e 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -164,4 +164,10 @@ export class WSConnection { this.rpcId++; this.sendRequest('unsubscribe', { subId }); } + + close() { + if (this.ws) { + this.ws?.close(); + } + } } From 2a3589a8cfca90fee5dec561348d6934da1af943 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 9 Sep 2024 14:37:25 +0200 Subject: [PATCH 188/246] ws: updated test --- test/wallet.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7fd9b0af8..802f92fa9 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -569,7 +569,7 @@ describe('WebSocket Updates', () => { const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}"}, "id": ${parsed.id}}`; socket.send(message); setTimeout(() => { - const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "paid": true, "expiry": 123}}}`; + const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "state": "PAID", "paid": true, "expiry": 123}}}`; socket.send(message); }, 500); } @@ -579,12 +579,13 @@ describe('WebSocket Updates', () => { }); }); const wallet = new CashuWallet(mint); - await new Promise((res) => { + await new Promise((res, rej) => { const callback = (p: any) => { console.log(p); res(p); }; const test = wallet.onMintQuotePaid('123', callback, () => { + rej(); console.log('error'); }); }); From f4bf0629725fb2e479181b0e3eb9205ede10f5cc Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 9 Sep 2024 14:37:30 +0200 Subject: [PATCH 189/246] added integration test --- test/integration.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/integration.test.ts b/test/integration.test.ts index d6528d82b..a105c33a3 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -6,6 +6,8 @@ import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; +import { injectWebSocketImpl } from '../src/WSConnection.js'; +import ws from 'ws'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = @@ -15,6 +17,8 @@ let request: Record | undefined; const mintUrl = 'http://localhost:3338'; const unit = 'sat'; +injectWebSocketImpl(ws); + describe('mint api', () => { test('get keys', async () => { const mint = new CashuMint(mintUrl); @@ -253,4 +257,27 @@ describe('mint api', () => { expect(response).toBeDefined(); expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); + test('websocket updates', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const mintQuote = await wallet.createMintQuote(21); + const callback = jest.fn(); + const res = await new Promise((res, rej) => { + wallet.onMintQuotePaid( + mintQuote.quote, + () => { + callback(); + res(1); + }, + (e) => { + console.log(e); + rej(e); + } + ); + }); + mint.disconnectWebSocket(); + expect(res).toBe(1); + expect(callback).toBeCalled(); + }); }); From 38c9dcb1a1c62ecde8a9b0f769a5ed927a8ef005 Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 29 Sep 2024 11:46:19 +0200 Subject: [PATCH 190/246] added connection promise --- src/WSConnection.ts | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index e2273ab4e..9e94a0d3d 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -6,6 +6,7 @@ import { JsonRpcReqParams, RpcSubId } from './model/types'; +import { OnOpenError, OnOpenSuccess } from './model/types/wallet/websocket'; let _WS: typeof WebSocket; @@ -20,6 +21,7 @@ export function injectWebSocketImpl(ws: any) { export class WSConnection { public readonly url: URL; private ws: WebSocket | undefined; + private connectionPromise: Promise | undefined; private subListeners: { [subId: string]: Array<(payload: any) => any> } = {}; private rpcListeners: { [rpsSubId: string]: any } = {}; private messageQueue: MessageQueue; @@ -32,24 +34,29 @@ export class WSConnection { } connect() { - return new Promise((res, rej) => { - try { - this.ws = new _WS(this.url); - } catch (err) { - rej(err); - return; - } - this.ws.onopen = res; - this.ws.onerror = (e) => { - rej(e); - }; - this.ws.onmessage = (e) => { - this.messageQueue.enqueue(e.data); - if (!this.handlingInterval) { - this.handlingInterval = setInterval(this.handleNextMesage.bind(this), 0); + if (!this.connectionPromise) { + this.connectionPromise = new Promise((res: OnOpenSuccess, rej: OnOpenError) => { + try { + this.ws = new _WS(this.url); + } catch (err) { + rej(err); + return; } - }; - }); + this.ws.onopen = () => { + res(); + }; + this.ws.onerror = () => { + rej(new Error('Failed to open WebSocket')); + }; + this.ws.onmessage = (e: MessageEvent) => { + this.messageQueue.enqueue(e.data); + if (!this.handlingInterval) { + this.handlingInterval = setInterval(this.handleNextMesage.bind(this), 0); + } + }; + }); + } + return this.connectionPromise; } sendRequest(method: 'subscribe', params: JsonRpcReqParams): void; From aeb7c3909c6ff160c12edbb1f82c480c5cb1a934 Mon Sep 17 00:00:00 2001 From: Egge Date: Sun, 29 Sep 2024 11:46:27 +0200 Subject: [PATCH 191/246] typed API --- src/CashuWallet.ts | 12 ++++++++---- src/model/types/wallet/websocket.ts | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/model/types/wallet/websocket.ts diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 993d22ac5..ebaabb453 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,6 +20,9 @@ import { OutputAmounts, ProofState, BlindingData + MeltQuoteState, + CheckStateEntry, + MintQuoteResponse } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -31,6 +34,7 @@ import { import { deriveBlindingFactor, deriveSecret } from '@cashu/crypto/modules/client/NUT09'; import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; +import { SubscriptionCanceller } from './model/types/wallet/websocket.js'; /** * The default number of proofs per denomination to keep in a wallet. @@ -865,16 +869,16 @@ class CashuWallet { async onMintQuotePaid( quoteId: string, - callback: (payload: MintQuoteResponse) => any, + callback: (payload: MintQuoteResponse) => void, errorCallback: (e: Error) => void - ) { + ): Promise { try { await this.mint.connectWebSocket(); } catch (e) { if (e instanceof Error) { - return errorCallback(e); + throw e; } else if (e) { - return errorCallback(new Error('Something went wrong')); + throw new Error('Something went wrong'); } } if (!this.mint.webSocketConnection) { diff --git a/src/model/types/wallet/websocket.ts b/src/model/types/wallet/websocket.ts new file mode 100644 index 000000000..a9249fcd8 --- /dev/null +++ b/src/model/types/wallet/websocket.ts @@ -0,0 +1,5 @@ +export type OnOpenSuccess = () => void; + +export type OnOpenError = (err: unknown) => void; + +export type SubscriptionCanceller = () => void; From 49e5beb6fc20fd68b4bc87688da75fe8ab5c1ecb Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 7 Oct 2024 10:04:20 +0200 Subject: [PATCH 192/246] remove subid key if no listener is left --- src/WSConnection.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 9e94a0d3d..bf0c90dec 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -94,7 +94,11 @@ export class WSConnection { } private removeListener(subId: string, callback: (payload: any) => any) { - (this.subListeners[subId] = this.subListeners[subId] || []).filter((fn) => fn !== callback); + if (this.subListeners[subId].length === 1) { + delete this.subListeners[subId]; + return; + } + this.subListeners[subId] = this.subListeners[subId].filter((fn: any) => fn !== callback); } async ensureConnection() { @@ -172,6 +176,10 @@ export class WSConnection { this.sendRequest('unsubscribe', { subId }); } + get activeSubscriptions() { + return Object.keys(this.subListeners); + } + close() { if (this.ws) { this.ws?.close(); From b010897a3ef40779b6b4903e9f070167efe6a806 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 7 Oct 2024 10:04:52 +0200 Subject: [PATCH 193/246] added quote update method --- src/CashuWallet.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index ebaabb453..2b64ef59e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -867,20 +867,31 @@ class CashuWallet { return states; } + async onMintQuoteUpdates( + quoteIds: Array, + callback: (payload: MintQuoteResponse) => void, + errorCallback: (e: Error) => void + ): Promise { + await this.mint.connectWebSocket(); + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + const subId = this.mint.webSocketConnection.createSubscription( + { kind: 'bolt11_mint_quote', filters: quoteIds }, + callback, + errorCallback + ); + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, callback); + }; + } + async onMintQuotePaid( quoteId: string, callback: (payload: MintQuoteResponse) => void, errorCallback: (e: Error) => void ): Promise { - try { - await this.mint.connectWebSocket(); - } catch (e) { - if (e instanceof Error) { - throw e; - } else if (e) { - throw new Error('Something went wrong'); - } - } + await this.mint.connectWebSocket(); if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } @@ -892,9 +903,7 @@ class CashuWallet { const subId = this.mint.webSocketConnection.createSubscription( { kind: 'bolt11_mint_quote', filters: [quoteId] }, subCallback, - (e: Error) => { - errorCallback(e); - } + errorCallback ); return () => { this.mint.webSocketConnection?.cancelSubscription(subId, subCallback); From 33221be75b8aaf3c5ac8db4b7e85f08081e174b9 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 7 Oct 2024 10:05:07 +0200 Subject: [PATCH 194/246] multiple ids integration test --- test/integration.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/integration.test.ts b/test/integration.test.ts index a105c33a3..76beabbe7 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -280,4 +280,40 @@ describe('mint api', () => { expect(res).toBe(1); expect(callback).toBeCalled(); }); + test('websocket mint quote updates on multiple ids', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const mintQuote1 = await wallet.createMintQuote(21); + const mintQuote2 = await wallet.createMintQuote(22); + + const callbackRef = jest.fn(); + const res = await new Promise(async (res, rej) => { + let counter = 0; + const unsub = await wallet.onMintQuoteUpdates( + [mintQuote1.quote, mintQuote2.quote], + (p) => { + console.log(p); + counter++; + callbackRef(); + if (counter === 4) { + unsub(); + res(1); + } + }, + (e) => { + counter++; + console.log(e); + if (counter === 4) { + unsub(); + rej(); + } + } + ); + }); + mint.disconnectWebSocket(); + expect(res).toBe(1); + expect(callbackRef).toHaveBeenCalledTimes(4); + expect(mint.webSocketConnection?.activeSubscriptions.length).toBe(0); + }); }); From 3339b7dcd3deb9904011e44d27326ad5ae8bf515 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 16 Oct 2024 06:11:15 +0200 Subject: [PATCH 195/246] moved ws injection --- src/WSConnection.ts | 15 ++++----------- src/ws.ts | 13 +++++++++++++ test/WSConnection.test.ts | 3 ++- test/wallet.test.ts | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 src/ws.ts diff --git a/src/WSConnection.ts b/src/WSConnection.ts index bf0c90dec..dca31a685 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -7,19 +7,11 @@ import { RpcSubId } from './model/types'; import { OnOpenError, OnOpenSuccess } from './model/types/wallet/websocket'; - -let _WS: typeof WebSocket; - -if (typeof WebSocket !== 'undefined') { - _WS = WebSocket; -} - -export function injectWebSocketImpl(ws: any) { - _WS = ws; -} +import { getWebSocketImpl } from './ws'; export class WSConnection { public readonly url: URL; + private readonly _WS: typeof WebSocket; private ws: WebSocket | undefined; private connectionPromise: Promise | undefined; private subListeners: { [subId: string]: Array<(payload: any) => any> } = {}; @@ -29,6 +21,7 @@ export class WSConnection { private rpcId = 0; constructor(url: string) { + this._WS = getWebSocketImpl(); this.url = new URL(url); this.messageQueue = new MessageQueue(); } @@ -37,7 +30,7 @@ export class WSConnection { if (!this.connectionPromise) { this.connectionPromise = new Promise((res: OnOpenSuccess, rej: OnOpenError) => { try { - this.ws = new _WS(this.url); + this.ws = new this._WS(this.url); } catch (err) { rej(err); return; diff --git a/src/ws.ts b/src/ws.ts new file mode 100644 index 000000000..f94d7b983 --- /dev/null +++ b/src/ws.ts @@ -0,0 +1,13 @@ +let _WS: typeof WebSocket; + +if (typeof WebSocket !== 'undefined') { + _WS = WebSocket; +} + +export function injectWebSocketImpl(ws: any) { + _WS = ws; +} + +export function getWebSocketImpl() { + return _WS; +} diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index 9c5b183dc..b564cf194 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -1,5 +1,6 @@ -import { WSConnection, injectWebSocketImpl } from '../src/WSConnection'; +import { WSConnection } from '../src/WSConnection'; import { Server, WebSocket } from 'mock-socket'; +import { injectWebSocketImpl } from '../src/ws'; injectWebSocketImpl(WebSocket); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 802f92fa9..7e17328ea 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -4,8 +4,8 @@ import { CashuWallet } from '../src/CashuWallet.js'; import { CheckStateEnum, MeltQuoteResponse } from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; import { Proof } from '@cashu/crypto/modules/common'; -import { injectWebSocketImpl } from '../src/WSConnection.js'; import { Server, WebSocket } from 'mock-socket'; +import { injectWebSocketImpl } from '../src/ws.js'; injectWebSocketImpl(WebSocket); From 0d25233e5004a7f966f857251d4138de4db20b84 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 13:25:03 +0000 Subject: [PATCH 196/246] tiny merge fixes --- src/CashuWallet.ts | 4 +--- src/utils.ts | 11 ----------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2b64ef59e..1eed65edb 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -19,9 +19,7 @@ import { GetInfoResponse, OutputAmounts, ProofState, - BlindingData - MeltQuoteState, - CheckStateEntry, + BlindingData, MintQuoteResponse } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; diff --git a/src/utils.ts b/src/utils.ts index 90f588138..db33e5614 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -436,14 +436,3 @@ export class MessageQueue { return prev.value; } } - -export { - bigIntStringify, - bytesToNumber, - getDecodedToken, - getEncodedToken, - getEncodedTokenV4, - hexToNumber, - splitAmount, - getDefaultAmountPreference -}; From b1fecea1a1547dcb56ef359de3e08057cd425951 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 14:20:54 +0000 Subject: [PATCH 197/246] fixed unsub test --- test/WSConnection.test.ts | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/test/WSConnection.test.ts b/test/WSConnection.test.ts index b564cf194..bb7eb8323 100644 --- a/test/WSConnection.test.ts +++ b/test/WSConnection.test.ts @@ -1,5 +1,5 @@ import { WSConnection } from '../src/WSConnection'; -import { Server, WebSocket } from 'mock-socket'; +import { Client, Server, WebSocket } from 'mock-socket'; import { injectWebSocketImpl } from '../src/ws'; injectWebSocketImpl(WebSocket); @@ -45,25 +45,40 @@ describe('testing WSConnection', () => { test('unsubscribing', async () => { const fakeUrl = 'ws://localhost:3338/v1/ws'; const server = new Server(fakeUrl, { mock: false }); - const message = await new Promise(async (res) => { + let wsSocket: Client; + let subId: string; + const conn = new WSConnection(fakeUrl); + await new Promise(async (res) => { server.on('connection', (socket) => { - socket.on('message', (m) => { - const parsed = JSON.parse(m.toString()); - if (parsed.method === 'unsubscribe') res(parsed); - }); + wsSocket = socket; + res(); }); - const conn = new WSConnection(fakeUrl); - await conn.connect(); - - const callback = jest.fn(); - const errorCallback = jest.fn(); - const subId = conn.createSubscription( + conn.connect(); + }); + const callback = jest.fn(); + const errorCallback = jest.fn(); + await new Promise((res) => { + wsSocket.on('message', (m) => { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'subscribe') { + const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}"}, "id": ${parsed.id}}`; + wsSocket.send(message); + setTimeout(res, 0); + } + }); + subId = conn.createSubscription( { kind: 'bolt11_mint_quote', filters: ['123'] }, callback, errorCallback ); - //TODO: Add assertion for subListenerLength once SubscriptionManager is modularised - conn.cancelSubscription(subId, callback); + }); + + const message = await new Promise(async (res) => { + wsSocket.on('message', (m) => { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'unsubscribe') res(parsed); + }); + conn.cancelSubscription(subId!, callback); }); expect(message).toMatchObject({ jsonrpc: '2.0', method: 'unsubscribe' }); server.stop(); From 888a4a76f739813f2f45749c3f58d215b86464db Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 14:52:46 +0000 Subject: [PATCH 198/246] fixed integration import --- test/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 76beabbe7..1f0656710 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -6,8 +6,8 @@ import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; -import { injectWebSocketImpl } from '../src/WSConnection.js'; import ws from 'ws'; +import { injectWebSocketImpl } from '../src/ws.js'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = From a86fab6206816ad447fdb508ae20d031dfa4f413 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 15:29:24 +0000 Subject: [PATCH 199/246] generalised methods --- src/CashuWallet.ts | 51 +++++---------------------------------- src/WSConnection.ts | 3 +++ test/wallet.test.ts | 58 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1eed65edb..d547aa1be 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -884,61 +884,22 @@ class CashuWallet { }; } - async onMintQuotePaid( - quoteId: string, - callback: (payload: MintQuoteResponse) => void, + async onMeltQuoteUpdates( + quoteIds: Array, + callback: (payload: MeltQuoteResponse) => void, errorCallback: (e: Error) => void ): Promise { await this.mint.connectWebSocket(); if (!this.mint.webSocketConnection) { throw new Error('failed to establish WebSocket connection.'); } - const subCallback = (payload: MintQuoteResponse) => { - if (payload.state === 'PAID') { - callback(payload); - } - }; const subId = this.mint.webSocketConnection.createSubscription( - { kind: 'bolt11_mint_quote', filters: [quoteId] }, - subCallback, + { kind: 'bolt11_melt_quote', filters: quoteIds }, + callback, errorCallback ); return () => { - this.mint.webSocketConnection?.cancelSubscription(subId, subCallback); - }; - } - - async onMeltQuotePaid( - quoteId: string, - callback: (payload: MeltQuoteResponse) => any, - errorCallback: (e: Error) => void - ) { - try { - await this.mint.connectWebSocket(); - } catch (e) { - if (e instanceof Error) { - return errorCallback(e); - } else if (e) { - return errorCallback(new Error('Something went wrong')); - } - } - if (!this.mint.webSocketConnection) { - throw new Error('failed to establish WebSocket connection.'); - } - const subCallback = (payload: MeltQuoteResponse) => { - if (payload.state === 'PAID') { - callback(payload); - } - }; - const subId = this.mint.webSocketConnection.createSubscription( - { kind: 'bolt11_melt_quote', filters: [quoteId] }, - subCallback, - (e: Error) => { - errorCallback(e); - } - ); - return () => { - this.mint.webSocketConnection?.cancelSubscription(subId, subCallback); + this.mint.webSocketConnection?.cancelSubscription(subId, callback); }; } diff --git a/src/WSConnection.ts b/src/WSConnection.ts index dca31a685..16c20c8a9 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -47,6 +47,9 @@ export class WSConnection { this.handlingInterval = setInterval(this.handleNextMesage.bind(this), 0); } }; + this.ws.onclose = () => { + this.connectionPromise = undefined; + }; }); } return this.connectionPromise; diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7e17328ea..48c6884ba 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -1,7 +1,13 @@ import nock from 'nock'; import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; -import { CheckStateEnum, MeltQuoteResponse } from '../src/model/types/index.js'; +import { + CheckStateEnum, + MeltQuoteResponse, + MeltQuoteState, + MintQuoteResponse, + MintQuoteState +} from '../src/model/types/index.js'; import { getDecodedToken } from '../src/utils.js'; import { Proof } from '@cashu/crypto/modules/common'; import { Server, WebSocket } from 'mock-socket'; @@ -579,15 +585,57 @@ describe('WebSocket Updates', () => { }); }); const wallet = new CashuWallet(mint); - await new Promise((res, rej) => { - const callback = (p: any) => { + const state = await new Promise(async (res, rej) => { + const callback = (p: MintQuoteResponse) => { + if (p.state === MintQuoteState.PAID) { + res(p); + } + }; + const test = await wallet.onMintQuoteUpdates(['123'], callback, () => { + rej(); + console.log('error'); + }); + }); + expect(state).toMatchObject({ quote: '123' }); + mint.disconnectWebSocket(); + server.close(); + }); + test('melt update', async () => { + const fakeUrl = 'ws://localhost:3338/v1/ws'; + const server = new Server(fakeUrl, { mock: false }); + server.on('connection', (socket) => { + socket.on('message', (m) => { + console.log(m); + try { + const parsed = JSON.parse(m.toString()); + if (parsed.method === 'subscribe') { + const message = `{"jsonrpc": "2.0", "result": {"status": "OK", "subId": "${parsed.params.subId}"}, "id": ${parsed.id}}`; + socket.send(message); + setTimeout(() => { + const message = `{"jsonrpc": "2.0", "method": "subscribe", "params": {"subId": "${parsed.params.subId}", "payload": {"quote": "123", "request": "456", "state": "PAID", "paid": true, "expiry": 123}}}`; + socket.send(message); + }, 500); + } + } catch { + console.log('Server parsing failed...'); + } + }); + }); + const wallet = new CashuWallet(mint); + const state = await new Promise(async (res, rej) => { + const callback = (p: MeltQuoteResponse) => { console.log(p); - res(p); + if (p.state === MeltQuoteState.PAID) { + res(p); + } }; - const test = wallet.onMintQuotePaid('123', callback, () => { + const test = await wallet.onMeltQuoteUpdates(['123'], callback, (e) => { + console.log(e); rej(); console.log('error'); }); }); + expect(state).toMatchObject({ quote: '123' }); + server.close(); }); }); From 4c315dfbd733a198188f94784c55175771c28e7b Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 15:34:33 +0000 Subject: [PATCH 200/246] updated integration test --- test/integration.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 1f0656710..601def521 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -5,7 +5,12 @@ import dns from 'node:dns'; import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; -import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; +import { + CheckStateEnum, + MeltQuoteState, + MintQuotePayload, + MintQuoteState +} from '../src/model/types/index.js'; import ws from 'ws'; import { injectWebSocketImpl } from '../src/ws.js'; dns.setDefaultResultOrder('ipv4first'); @@ -264,10 +269,10 @@ describe('mint api', () => { const mintQuote = await wallet.createMintQuote(21); const callback = jest.fn(); const res = await new Promise((res, rej) => { - wallet.onMintQuotePaid( - mintQuote.quote, - () => { - callback(); + wallet.onMintQuoteUpdates( + [mintQuote.quote], + (p) => { + if (p.state === MintQuoteState.PAID) callback(); res(1); }, (e) => { From 71e2e476dc40526a0c3994062b229aabadaab026 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 16:39:30 +0000 Subject: [PATCH 201/246] added on proofStateUpdates --- src/CashuWallet.ts | 21 +++++++++++++++++++++ test/integration.test.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index d547aa1be..c3b9a6307 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -903,6 +903,27 @@ class CashuWallet { }; } + async onProofStateUpdates( + proofs: Array, + callback: (payload: ProofState) => void, + errorCallback: (e: Error) => void + ): Promise { + await this.mint.connectWebSocket(); + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + const enc = new TextEncoder(); + const ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); + const subId = this.mint.webSocketConnection.createSubscription( + { kind: 'proof_state', filters: ys }, + callback, + errorCallback + ); + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, callback); + }; + } + /** * Creates blinded messages for a given amount * @param amount amount to create blinded messages for diff --git a/test/integration.test.ts b/test/integration.test.ts index 601def521..389c9a8ae 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -9,7 +9,9 @@ import { CheckStateEnum, MeltQuoteState, MintQuotePayload, - MintQuoteState + MintQuoteResponse, + MintQuoteState, + ProofState } from '../src/model/types/index.js'; import ws from 'ws'; import { injectWebSocketImpl } from '../src/ws.js'; @@ -321,4 +323,36 @@ describe('mint api', () => { expect(callbackRef).toHaveBeenCalledTimes(4); expect(mint.webSocketConnection?.activeSubscriptions.length).toBe(0); }); + test('websocket proof state + mint quote updates', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const quote = await wallet.createMintQuote(63); + await new Promise((res) => { + function handleUpdate(p: MintQuoteResponse) { + if (p.state === MintQuoteState.PAID) { + res(); + } + } + wallet.onMintQuoteUpdates([quote.quote], handleUpdate, (e) => { + console.log(e); + }); + }); + const { proofs } = await wallet.mintProofs(63, quote.quote); + const data = await new Promise((res) => { + wallet.onProofStateUpdates( + proofs, + (p) => { + if (p.state === CheckStateEnum.SPENT) { + res(p); + } + }, + (e) => { + console.log(e); + } + ); + wallet.swap(63, proofs); + }); + console.log('final ws boss', data); + }); }); From 406c54de75dc009e54bc94b408c93f8366ba4c7f Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 17:20:08 +0000 Subject: [PATCH 202/246] added proofmap to state updates --- src/CashuWallet.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index c3b9a6307..d781df934 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -905,7 +905,7 @@ class CashuWallet { async onProofStateUpdates( proofs: Array, - callback: (payload: ProofState) => void, + callback: (payload: ProofState & { proof: Proof }) => void, errorCallback: (e: Error) => void ): Promise { await this.mint.connectWebSocket(); @@ -913,10 +913,17 @@ class CashuWallet { throw new Error('failed to establish WebSocket connection.'); } const enc = new TextEncoder(); - const ys = proofs.map((p: Proof) => hashToCurve(enc.encode(p.secret)).toHex(true)); + const proofMap: { [y: string]: Proof } = {}; + for (let i = 0; i < proofs.length; i++) { + const y = hashToCurve(enc.encode(proofs[i].secret)).toHex(true); + proofMap[y] = proofs[i]; + } + const ys = Object.keys(proofMap); const subId = this.mint.webSocketConnection.createSubscription( { kind: 'proof_state', filters: ys }, - callback, + (p: ProofState) => { + callback({ ...p, proof: proofMap[p.Y] }); + }, errorCallback ); return () => { From 589a6ac8e8e58ef2e4418f70dbcb3bedf34db5a2 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 17:20:17 +0000 Subject: [PATCH 203/246] fixed integration test (again) --- test/integration.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 389c9a8ae..edf3a57c1 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -274,8 +274,10 @@ describe('mint api', () => { wallet.onMintQuoteUpdates( [mintQuote.quote], (p) => { - if (p.state === MintQuoteState.PAID) callback(); - res(1); + if (p.state === MintQuoteState.PAID) { + callback(); + res(1); + } }, (e) => { console.log(e); @@ -299,8 +301,7 @@ describe('mint api', () => { let counter = 0; const unsub = await wallet.onMintQuoteUpdates( [mintQuote1.quote, mintQuote2.quote], - (p) => { - console.log(p); + () => { counter++; callbackRef(); if (counter === 4) { @@ -308,9 +309,8 @@ describe('mint api', () => { res(1); } }, - (e) => { + () => { counter++; - console.log(e); if (counter === 4) { unsub(); rej(); @@ -353,6 +353,6 @@ describe('mint api', () => { ); wallet.swap(63, proofs); }); - console.log('final ws boss', data); - }); + mint.disconnectWebSocket(); + }, 10000); }); From 181a7c4c12743bcab869f6d4e23ee17644419909 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 17:49:37 +0000 Subject: [PATCH 204/246] bumped nutshell integration version --- .github/workflows/nutshell-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nutshell-integration.yml b/.github/workflows/nutshell-integration.yml index 3e881511c..ef25b0c1d 100644 --- a/.github/workflows/nutshell-integration.yml +++ b/.github/workflows/nutshell-integration.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Pull and start mint run: | - docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint + docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.2 poetry run mint - name: Check running containers run: docker ps From f9b3fd7d1b385573c88b3e4e6ae00ac338753243 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 17:52:37 +0000 Subject: [PATCH 205/246] fixed integration test (rly...) --- test/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index edf3a57c1..d8a7522d5 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -351,7 +351,7 @@ describe('mint api', () => { console.log(e); } ); - wallet.swap(63, proofs); + wallet.swap(21, proofs); }); mint.disconnectWebSocket(); }, 10000); From fc94cb265adb9525828b215963e507949a49409d Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 19:24:15 +0000 Subject: [PATCH 206/246] added onMintQuotePaid helper --- src/CashuWallet.ts | 26 +++++++++++++++++++++++++- test/integration.test.ts | 11 ++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index d781df934..77f320d8a 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -20,7 +20,8 @@ import { OutputAmounts, ProofState, BlindingData, - MintQuoteResponse + MintQuoteResponse, + MintQuoteState } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -884,6 +885,29 @@ class CashuWallet { }; } + async onMintQuotePaid( + quoteId: string, + callback: (payload: MintQuoteResponse) => void, + errorCallback: (e: Error) => void + ): Promise { + await this.mint.connectWebSocket(); + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + const subId = this.mint.webSocketConnection.createSubscription( + { kind: 'bolt11_mint_quote', filters: [quoteId] }, + (p: MintQuoteResponse) => { + if (p.state === MintQuoteState.PAID) { + callback(p); + } + }, + errorCallback + ); + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, callback); + }; + } + async onMeltQuoteUpdates( quoteIds: Array, callback: (payload: MeltQuoteResponse) => void, diff --git a/test/integration.test.ts b/test/integration.test.ts index d8a7522d5..508371add 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -328,15 +328,8 @@ describe('mint api', () => { const wallet = new CashuWallet(mint); const quote = await wallet.createMintQuote(63); - await new Promise((res) => { - function handleUpdate(p: MintQuoteResponse) { - if (p.state === MintQuoteState.PAID) { - res(); - } - } - wallet.onMintQuoteUpdates([quote.quote], handleUpdate, (e) => { - console.log(e); - }); + await new Promise((res, rej) => { + wallet.onMintQuotePaid(quote.quote, res, rej); }); const { proofs } = await wallet.mintProofs(63, quote.quote); const data = await new Promise((res) => { From 5d663bfcfac8e8af22867c042d0a4519d6a39c57 Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 19:26:03 +0000 Subject: [PATCH 207/246] added onMeltQuotePaid helper --- src/CashuWallet.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 77f320d8a..7615a27f1 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,7 +21,8 @@ import { ProofState, BlindingData, MintQuoteResponse, - MintQuoteState + MintQuoteState, + MeltQuoteState } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -885,6 +886,29 @@ class CashuWallet { }; } + async onMeltQuotePaid( + quoteId: string, + callback: (payload: MeltQuoteResponse) => void, + errorCallback: (e: Error) => void + ): Promise { + await this.mint.connectWebSocket(); + if (!this.mint.webSocketConnection) { + throw new Error('failed to establish WebSocket connection.'); + } + const subId = this.mint.webSocketConnection.createSubscription( + { kind: 'bolt11_melt_quote', filters: [quoteId] }, + (p: MeltQuoteResponse) => { + if (p.state === MeltQuoteState.PAID) { + callback(p); + } + }, + errorCallback + ); + return () => { + this.mint.webSocketConnection?.cancelSubscription(subId, callback); + }; + } + async onMintQuotePaid( quoteId: string, callback: (payload: MintQuoteResponse) => void, From 681a6f6dfd60520da799db77f5aac90cbc8060fe Mon Sep 17 00:00:00 2001 From: Egge Date: Mon, 4 Nov 2024 19:34:33 +0000 Subject: [PATCH 208/246] added jsdocs --- src/CashuMint.ts | 6 ++++++ src/CashuWallet.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 800eb0d5e..6f02881ef 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -441,6 +441,9 @@ class CashuMint { return CashuMint.restore(this._mintUrl, restorePayload, this._customRequest); } + /** + * Tries to establish a websocket connection with the websocket mint url according to NUT-17 + */ async connectWebSocket() { if (this.ws) { await this.ws.ensureConnection(); @@ -458,6 +461,9 @@ class CashuMint { } } + /** + * Closes a websocket connection + */ disconnectWebSocket() { if (this.ws) { this.ws.close(); diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 7615a27f1..8280783ad 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -867,6 +867,13 @@ class CashuWallet { return states; } + /** + * Register a callback to be called whenever a mint quote's state changes + * @param quoteIds List of mint quote IDs that should be subscribed to + * @param callback Callback function that will be called whenever a mint quote state changes + * @param errorCallback + * @returns + */ async onMintQuoteUpdates( quoteIds: Array, callback: (payload: MintQuoteResponse) => void, @@ -886,6 +893,13 @@ class CashuWallet { }; } + /** + * Register a callback to be called whenever a melt quote's state changes + * @param quoteIds List of melt quote IDs that should be subscribed to + * @param callback Callback function that will be called whenever a melt quote state changes + * @param errorCallback + * @returns + */ async onMeltQuotePaid( quoteId: string, callback: (payload: MeltQuoteResponse) => void, @@ -909,6 +923,13 @@ class CashuWallet { }; } + /** + * Register a callback to be called when a single mint quote gets paid + * @param quoteId Mint quote id that should be subscribed to + * @param callback Callback function that will be called when this mint quote gets paid + * @param errorCallback + * @returns + */ async onMintQuotePaid( quoteId: string, callback: (payload: MintQuoteResponse) => void, @@ -932,6 +953,13 @@ class CashuWallet { }; } + /** + * Register a callback to be called when a single melt quote gets paid + * @param quoteId Melt quote id that should be subscribed to + * @param callback Callback function that will be called when this melt quote gets paid + * @param errorCallback + * @returns + */ async onMeltQuoteUpdates( quoteIds: Array, callback: (payload: MeltQuoteResponse) => void, @@ -951,6 +979,13 @@ class CashuWallet { }; } + /** + * Register a callback to be called whenever a subscribed proof state changes + * @param proofs List of proofs that should be subscribed to + * @param callback Callback function that will be called whenever a proof's state changes + * @param errorCallback + * @returns + */ async onProofStateUpdates( proofs: Array, callback: (payload: ProofState & { proof: Proof }) => void, From acf3d9efd607da3cd7e82bdf09852dc31893903d Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 5 Nov 2024 14:20:24 +0000 Subject: [PATCH 209/246] refactor onPaid handlers --- src/CashuWallet.ts | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 8280783ad..ad68a2544 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -905,22 +905,15 @@ class CashuWallet { callback: (payload: MeltQuoteResponse) => void, errorCallback: (e: Error) => void ): Promise { - await this.mint.connectWebSocket(); - if (!this.mint.webSocketConnection) { - throw new Error('failed to establish WebSocket connection.'); - } - const subId = this.mint.webSocketConnection.createSubscription( - { kind: 'bolt11_melt_quote', filters: [quoteId] }, - (p: MeltQuoteResponse) => { + return this.onMeltQuoteUpdates( + [quoteId], + (p) => { if (p.state === MeltQuoteState.PAID) { callback(p); } }, errorCallback ); - return () => { - this.mint.webSocketConnection?.cancelSubscription(subId, callback); - }; } /** @@ -935,22 +928,15 @@ class CashuWallet { callback: (payload: MintQuoteResponse) => void, errorCallback: (e: Error) => void ): Promise { - await this.mint.connectWebSocket(); - if (!this.mint.webSocketConnection) { - throw new Error('failed to establish WebSocket connection.'); - } - const subId = this.mint.webSocketConnection.createSubscription( - { kind: 'bolt11_mint_quote', filters: [quoteId] }, - (p: MintQuoteResponse) => { + return this.onMintQuoteUpdates( + [quoteId], + (p) => { if (p.state === MintQuoteState.PAID) { callback(p); } }, errorCallback ); - return () => { - this.mint.webSocketConnection?.cancelSubscription(subId, callback); - }; } /** From 1a8cb47b228db17ce79b28a4927f5499e737e25c Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 5 Nov 2024 15:15:45 +0000 Subject: [PATCH 210/246] fixed mint url --- src/CashuMint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 6f02881ef..12b9511ed 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -450,7 +450,7 @@ class CashuMint { } else { const mintUrl = new URL(this._mintUrl); this.ws = new WSConnection( - `${mintUrl.protocol === 'https' ? 'wss' : 'ws'}://${mintUrl.host}/v1/ws` + `${mintUrl.protocol === 'https:' ? 'wss' : 'ws'}://${mintUrl.host}/v1/ws` ); try { await this.ws.connect(); From f8e66e73f5087155f3c970d5415e9c538fa99855 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 5 Nov 2024 17:02:04 +0100 Subject: [PATCH 211/246] remove unused imports + update cashu/crypto dependency to latest release. --- package-lock.json | 14 +++++++------- package.json | 2 +- src/CashuWallet.ts | 2 -- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f6d05b67..67df9f205 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0.0-rc1", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.3.3", + "@cashu/crypto": "^0.3.4", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -620,9 +620,9 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", - "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", @@ -6834,9 +6834,9 @@ "dev": true }, "@cashu/crypto": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", - "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", "requires": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", diff --git a/package.json b/package.json index 613c6a1b0..747cb8fbc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.3.3", + "@cashu/crypto": "^0.3.4", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bfe9df361..fae3211cd 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -30,8 +30,6 @@ import { getKeepAmounts, hexToNumber } from './utils.js'; -import { validateMnemonic } from '@scure/bip39'; -import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { blindMessage, From 6b8c7c73f0ad07cec43ad8bb40db673b39c96c50 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 15:51:12 +0100 Subject: [PATCH 212/246] integration tests for verify and includeDleq in token + some fixes --- src/CashuWallet.ts | 35 ++++++++++++++++++++++++++------- test/integration.test.ts | 42 ++++++++++++++++++++++++++++++++++++++-- test/wallet.test.ts | 7 +++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index fae3211cd..886f391c8 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -299,6 +299,7 @@ class CashuWallet { * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint * @param options.offline? optionally send proofs offline. * @param options.includeFees? optionally include fees in the response. + * @param options.includeDleq? optionally include DLEQ proof in the proofs to send. * @returns {SendResponse} */ async send( @@ -313,6 +314,7 @@ class CashuWallet { keysetId?: string; offline?: boolean; includeFees?: boolean; + includeDleq?: boolean; } ): Promise { if (sumProofs(proofs) < amount) { @@ -321,7 +323,8 @@ class CashuWallet { const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, amount, - options?.includeFees + options?.includeFees, + options?.includeDleq, ); const expectedFee = options?.includeFees ? this.getFeesForProofs(sendProofOffline) : 0; if ( @@ -337,7 +340,8 @@ class CashuWallet { const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, - true + true, + options?.includeDleq, ); options?.proofsWeHave?.push(...keepProofsSelect); @@ -356,8 +360,13 @@ class CashuWallet { selectProofsToSend( proofs: Array, amountToSend: number, - includeFees?: boolean + includeFees?: boolean, + includeDleq?: boolean, ): SendResponse { + if (includeDleq ?? false) { + // only pick the ones with a DLEQ proof + proofs = proofs.filter((p: Proof) => p.dleq != undefined); + } const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -386,7 +395,8 @@ class CashuWallet { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), remainder, - includeFees + includeFees, + includeDleq, ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -396,8 +406,19 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } + + const keepProofs = proofs.filter((p: Proof) => !selectedProofs.includes(p)); + + // if explicitly told to, strip DLEQ + if (!includeDleq) { + selectedProofs = selectedProofs.map((p: Proof) => { + p.dleq = undefined; + return p; + }); + } + return { - keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), + keep: keepProofs, send: selectedProofs }; } @@ -750,7 +771,7 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } // Strip DLEQs if any - proofsToSend.map((p: Proof) => { + proofsToSend = proofsToSend.map((p: Proof) => { p.dleq = undefined; return p; }); @@ -830,7 +851,7 @@ class CashuWallet { } // Strip DLEQs if any - proofsToSend.map((p: Proof) => { + proofsToSend = proofsToSend.map((p: Proof) => { p.dleq = undefined; return p; }); diff --git a/test/integration.test.ts b/test/integration.test.ts index d6528d82b..8ff2591d8 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,10 +2,10 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; +import { deriveKeysetId, getEncodedToken, getEncodedTokenV4, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; -import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; +import { CheckStateEnum, MeltQuoteState, Token } from '../src/model/types/index.js'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = @@ -253,4 +253,42 @@ describe('mint api', () => { expect(response).toBeDefined(); expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); + test('mint and check dleq', async () => { + const mint = new CashuMint(mintUrl); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + const wallet = new CashuWallet(mint); + + const mintRequest = await wallet.createMintQuote(3000); + const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + + proofs.forEach(p => { + expect(p).toHaveProperty('dleq'); + expect(p.dleq).toHaveProperty('s'); + expect(p.dleq).toHaveProperty('e'); + expect(p.dleq).toHaveProperty('r'); + expect(p).toHaveProperty('dleqValid', true); + }); + }); + test('send and receive token with dleq', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const mintRequest = await wallet.createMintQuote(3000); + const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + + const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); + + send.forEach(p => {expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined()}); + const token = { + mint: mint.mintUrl, + proofs: send + } as Token; + const encodedToken = getEncodedTokenV4(token); + const newProofs = await wallet.receive(encodedToken, { requireDleq: true }) + console.log(getEncodedTokenV4(token)); + expect(newProofs).toBeDefined(); + }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7ca5c27b2..129dbf04d 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -199,6 +199,13 @@ describe('receive', () => { const result = await wallet.receive(tokenInput).catch((e) => e); expect(result).toEqual(new Error('could not verify proofs.')); }); + + /* + test('test receive with dleq', async() => { + nock(mintUrl).post('/v1/swap').reply(200, {}); + const wallet = new CashuWallet(mint, { unit }); + }) + */ }); describe('checkProofsStates', () => { From 8c12a87fdcf5e0392f37ef26f4493895e25877e3 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 18:21:20 +0100 Subject: [PATCH 213/246] sure-fire pad to 64 characters for bigint --- src/CashuWallet.ts | 6 +++--- src/utils.ts | 9 +++++++++ test/integration.test.ts | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 886f391c8..e4faa55f3 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -28,7 +28,8 @@ import { splitAmount, sumProofs, getKeepAmounts, - hexToNumber + hexToNumber, + numberToHexPadded64 } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -40,7 +41,6 @@ import { deriveBlindingFactor, deriveSecret } from '@cashu/crypto/modules/client import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof, DLEQ } from '@cashu/crypto/modules/common/index'; import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; - /** * The default number of proofs per denomination to keep in a wallet. */ @@ -1039,7 +1039,7 @@ class CashuWallet { : ({ s: bytesToHex(dleq.s), e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) + r: numberToHexPadded64(dleq.r ?? BigInt(0)), } as SerializedDLEQ); return serializedProof; }); diff --git a/src/utils.ts b/src/utils.ts index fb7f1afea..471e7d284 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,6 +152,15 @@ export function hexToNumber(hex: string): bigint { return BigInt(`0x${hex}`); } +/** + * + * @param number (bigint) to conver to hex + * @returns hex string start-padded to 64 characters + */ +export function numberToHexPadded64(number: bigint): string { + return number.toString(16).padStart(64, '0'); +} + function isValidHex(str: string) { return /^[a-f0-9]*$/i.test(str); } diff --git a/test/integration.test.ts b/test/integration.test.ts index 8ff2591d8..f35470827 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -281,7 +281,11 @@ describe('mint api', () => { const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); - send.forEach(p => {expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined()}); + send.forEach(p => { + expect(p.dleq).toBeDefined(); + expect(p.dleq?.r).toBeDefined(); + }); + const token = { mint: mint.mintUrl, proofs: send From 5cb0d078951e88d524b93f75cf1bae5b7e3cc51b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 19:33:43 +0100 Subject: [PATCH 214/246] check pubkey is not undefined --- src/CashuWallet.ts | 3 +++ test/wallet.test.ts | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e4faa55f3..871088824 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -1062,6 +1062,9 @@ class CashuWallet { r: hexToNumber(p.dleq.r ?? '00') } as DLEQ; const key = keys.keys[p.amount]; + if (key == undefined) { + throw new Error(`undefined key for amount ${p.amount}`); + } if ( !verifyDLEQProof_reblind( new TextEncoder().encode(p.secret), diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 129dbf04d..7ca5c27b2 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -199,13 +199,6 @@ describe('receive', () => { const result = await wallet.receive(tokenInput).catch((e) => e); expect(result).toEqual(new Error('could not verify proofs.')); }); - - /* - test('test receive with dleq', async() => { - nock(mintUrl).post('/v1/swap').reply(200, {}); - const wallet = new CashuWallet(mint, { unit }); - }) - */ }); describe('checkProofsStates', () => { From 8aea25fd3e7782f41b1eb55d01baabc6febfeee7 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 08:37:09 +0100 Subject: [PATCH 215/246] fix dleq strip --- src/CashuWallet.ts | 57 ++++++++++++++++++++-------------------- test/integration.test.ts | 32 +++++++++++++++++++--- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 871088824..6f117495b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -317,14 +317,17 @@ class CashuWallet { includeDleq?: boolean; } ): Promise { + if (options?.includeDleq ?? false) { + // only pick the ones with a DLEQ proof + proofs = proofs.filter((p: Proof) => p.dleq != undefined); + } if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, amount, - options?.includeFees, - options?.includeDleq, + options?.includeFees ); const expectedFee = options?.includeFees ? this.getFeesForProofs(sendProofOffline) : 0; if ( @@ -340,33 +343,42 @@ class CashuWallet { const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, - true, - options?.includeDleq, + true ); options?.proofsWeHave?.push(...keepProofsSelect); - const { keep, send } = await this.swap(amount, sendProofs, options); - const keepProofs = keepProofsSelect.concat(keep); - return { keep: keepProofs, send }; + let { keep, send } = await this.swap(amount, sendProofs, options); + keep = keepProofsSelect.concat(keep); + + // strip dleq if explicitly told so + if (!options?.includeDleq) { + send = send.map((p: Proof) => { + return {...p, dleq: undefined }; + }); + } + + return { keep, send }; } if (sumProofs(sendProofOffline) < amount + expectedFee) { throw new Error('Not enough funds available to send'); } + // strip dleq if explicitly told so + if (!options?.includeDleq) { + sendProofOffline.forEach((p: Proof) => { + p.dleq = undefined; + }); + } + return { keep: keepProofsOffline, send: sendProofOffline }; } selectProofsToSend( proofs: Array, amountToSend: number, - includeFees?: boolean, - includeDleq?: boolean, + includeFees?: boolean ): SendResponse { - if (includeDleq ?? false) { - // only pick the ones with a DLEQ proof - proofs = proofs.filter((p: Proof) => p.dleq != undefined); - } const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -396,7 +408,6 @@ class CashuWallet { smallerProofs.slice(1), remainder, includeFees, - includeDleq, ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -406,19 +417,9 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } - - const keepProofs = proofs.filter((p: Proof) => !selectedProofs.includes(p)); - - // if explicitly told to, strip DLEQ - if (!includeDleq) { - selectedProofs = selectedProofs.map((p: Proof) => { - p.dleq = undefined; - return p; - }); - } return { - keep: keepProofs, + keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs }; } @@ -772,8 +773,7 @@ class CashuWallet { } // Strip DLEQs if any proofsToSend = proofsToSend.map((p: Proof) => { - p.dleq = undefined; - return p; + return { ...p, dleq: undefined }; }); const meltPayload: MeltPayload = { quote: meltQuote.quote, @@ -852,8 +852,7 @@ class CashuWallet { // Strip DLEQs if any proofsToSend = proofsToSend.map((p: Proof) => { - p.dleq = undefined; - return p; + return { ...p, dleq: undefined }; }); // join keepBlindedMessages and sendBlindedMessages diff --git a/test/integration.test.ts b/test/integration.test.ts index f35470827..6353ef542 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -253,6 +253,8 @@ describe('mint api', () => { expect(response).toBeDefined(); expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); +}); +describe('dleq', () => { test('mint and check dleq', async () => { const mint = new CashuMint(mintUrl); const NUT12 = (await mint.getInfo()).nuts['12']; @@ -275,11 +277,15 @@ describe('mint api', () => { test('send and receive token with dleq', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } - const mintRequest = await wallet.createMintQuote(3000); - const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + const mintRequest = await wallet.createMintQuote(8); + const { proofs } = await wallet.mintProofs(8, mintRequest.quote); - const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); + const { keep, send } = await wallet.send(4, proofs, { includeDleq: true }); send.forEach(p => { expect(p.dleq).toBeDefined(); @@ -295,4 +301,24 @@ describe('mint api', () => { console.log(getEncodedTokenV4(token)); expect(newProofs).toBeDefined(); }); + test('send strip dleq', async() => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + + const mintRequest = await wallet.createMintQuote(8); + const { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + const { keep, send } = await wallet.send(4, proofs, { includeDleq: false }); + send.forEach(p => { + expect(p.dleq).toBeUndefined(); + }); + keep.forEach(p => { + expect(p.dleq).toBeDefined(); + expect(p.dleq?.r).toBeDefined(); + }); + }); }); From 582172f0ee87fbe81b56a0475e0248ff31921ee0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 08:55:43 +0100 Subject: [PATCH 216/246] test not enough funds when dleq missing and includeDleq true --- test/integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/integration.test.ts b/test/integration.test.ts index 6353ef542..28b13a9a4 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -321,4 +321,23 @@ describe('dleq', () => { expect(p.dleq?.r).toBeDefined(); }); }); + test('send not enough proofs when dleq is required', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + + const mintRequest = await wallet.createMintQuote(8); + let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + // strip dleq + proofs = proofs.map(p => { + return { ...p, dleq: undefined }; + }); + + const exc = await wallet.send(4, proofs, { includeDleq: true }).catch(e => e); + expect(exc).toEqual(new Error("Not enough funds available to send")); + }); }); From 2bfb5a01e45768154a8696c45e9eb9403359c73f Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 11:22:20 +0100 Subject: [PATCH 217/246] npm run format --- src/CashuWallet.ts | 8 ++++---- src/utils.ts | 4 ++-- test/integration.test.ts | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6f117495b..6ba1621c5 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -353,7 +353,7 @@ class CashuWallet { // strip dleq if explicitly told so if (!options?.includeDleq) { send = send.map((p: Proof) => { - return {...p, dleq: undefined }; + return { ...p, dleq: undefined }; }); } @@ -407,7 +407,7 @@ class CashuWallet { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), remainder, - includeFees, + includeFees ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -417,7 +417,7 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } - + return { keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs @@ -1038,7 +1038,7 @@ class CashuWallet { : ({ s: bytesToHex(dleq.s), e: bytesToHex(dleq.e), - r: numberToHexPadded64(dleq.r ?? BigInt(0)), + r: numberToHexPadded64(dleq.r ?? BigInt(0)) } as SerializedDLEQ); return serializedProof; }); diff --git a/src/utils.ts b/src/utils.ts index 471e7d284..0ac43cc32 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -153,8 +153,8 @@ export function hexToNumber(hex: string): bigint { } /** - * - * @param number (bigint) to conver to hex + * Converts a number to a hex string of 64 characters. + * @param number (bigint) to conver to hex * @returns hex string start-padded to 64 characters */ export function numberToHexPadded64(number: bigint): string { diff --git a/test/integration.test.ts b/test/integration.test.ts index 28b13a9a4..0597dd55f 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -259,14 +259,14 @@ describe('dleq', () => { const mint = new CashuMint(mintUrl); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const wallet = new CashuWallet(mint); const mintRequest = await wallet.createMintQuote(3000); const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); - proofs.forEach(p => { + proofs.forEach((p) => { expect(p).toHaveProperty('dleq'); expect(p.dleq).toHaveProperty('s'); expect(p.dleq).toHaveProperty('e'); @@ -279,7 +279,7 @@ describe('dleq', () => { const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); @@ -287,36 +287,36 @@ describe('dleq', () => { const { keep, send } = await wallet.send(4, proofs, { includeDleq: true }); - send.forEach(p => { + send.forEach((p) => { expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined(); }); - + const token = { mint: mint.mintUrl, proofs: send } as Token; const encodedToken = getEncodedTokenV4(token); - const newProofs = await wallet.receive(encodedToken, { requireDleq: true }) + const newProofs = await wallet.receive(encodedToken, { requireDleq: true }); console.log(getEncodedTokenV4(token)); expect(newProofs).toBeDefined(); - }); - test('send strip dleq', async() => { + }); + test('send strip dleq', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); const { proofs } = await wallet.mintProofs(8, mintRequest.quote); const { keep, send } = await wallet.send(4, proofs, { includeDleq: false }); - send.forEach(p => { + send.forEach((p) => { expect(p.dleq).toBeUndefined(); }); - keep.forEach(p => { + keep.forEach((p) => { expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined(); }); @@ -326,18 +326,18 @@ describe('dleq', () => { const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); let { proofs } = await wallet.mintProofs(8, mintRequest.quote); - // strip dleq - proofs = proofs.map(p => { + // strip dleq + proofs = proofs.map((p) => { return { ...p, dleq: undefined }; }); - const exc = await wallet.send(4, proofs, { includeDleq: true }).catch(e => e); - expect(exc).toEqual(new Error("Not enough funds available to send")); + const exc = await wallet.send(4, proofs, { includeDleq: true }).catch((e) => e); + expect(exc).toEqual(new Error('Not enough funds available to send')); }); }); From 8e7a58d1d57435074f2348d02db0833bc92cd2ba Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 12:24:26 +0100 Subject: [PATCH 218/246] test receive with invalid dleq --- test/integration.test.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 0597dd55f..04f246618 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,7 +2,14 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken, getEncodedTokenV4, sumProofs } from '../src/utils.js'; +import { + deriveKeysetId, + getEncodedToken, + getEncodedTokenV4, + hexToNumber, + numberToHexPadded64, + sumProofs +} from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { CheckStateEnum, MeltQuoteState, Token } from '../src/model/types/index.js'; @@ -340,4 +347,33 @@ describe('dleq', () => { const exc = await wallet.send(4, proofs, { includeDleq: true }).catch((e) => e); expect(exc).toEqual(new Error('Not enough funds available to send')); }); + test('receive with invalid dleq', async () => { + const mint = new CashuMint(mintUrl); + const keys = await mint.getKeys(); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error('Cannot run this test: mint does not support NUT12'); + } + + const mintRequest = await wallet.createMintQuote(8); + let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + // alter dleq signature + proofs.forEach((p) => { + if (p.dleq != undefined) { + const s = hexToNumber(p.dleq.s) + BigInt(1); + p.dleq.s = numberToHexPadded64(s); + } + }); + + const token = { + mint: mint.mintUrl, + proofs: proofs + } as Token; + + const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; + const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); + expect(exc).toEqual(new Error(`0-th DLEQ proof is invalid for key ${key}`)); + }); }); From 6c83a5aa427082231b7e2905808776c4226614ba Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 8 Nov 2024 15:21:38 +0000 Subject: [PATCH 219/246] handle mint urls with base path --- src/CashuMint.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 12b9511ed..0434aa344 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -449,8 +449,16 @@ class CashuMint { await this.ws.ensureConnection(); } else { const mintUrl = new URL(this._mintUrl); + const wsSegment = 'v1/ws'; + if (mintUrl.pathname) { + if (mintUrl.pathname.endsWith('/')) { + mintUrl.pathname += wsSegment; + } else { + mintUrl.pathname += '/' + wsSegment; + } + } this.ws = new WSConnection( - `${mintUrl.protocol === 'https:' ? 'wss' : 'ws'}://${mintUrl.host}/v1/ws` + `${mintUrl.protocol === 'https:' ? 'wss' : 'ws'}://${mintUrl.host}${mintUrl.pathname}` ); try { await this.ws.connect(); From c70b2c6ddc9306a40941d6625a4215c78c096408 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 16:58:16 +0100 Subject: [PATCH 220/246] requested changes --- src/CashuWallet.ts | 69 ++++++++++------------------------- src/model/BlindedSignature.ts | 2 +- src/utils.ts | 68 ++++++++++++++++++++++++++-------- test/integration.test.ts | 2 +- 4 files changed, 74 insertions(+), 67 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6ba1621c5..58550197c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -28,8 +28,8 @@ import { splitAmount, sumProofs, getKeepAmounts, - hexToNumber, - numberToHexPadded64 + numberToHexPadded64, + hasValidDleq } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -265,7 +265,9 @@ class CashuWallet { } const keys = await this.getKeys(options?.keysetId); if (options?.requireDleq) { - this.requireDLEQ(token, keys); + if (token.proofs.some((p: Proof) => !hasValidDleq(p, keys))) { + throw new Error("Token contains proofs with invalid DLEQ") + } } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); const { payload, blindingData } = this.createSwapPayload( @@ -317,7 +319,7 @@ class CashuWallet { includeDleq?: boolean; } ): Promise { - if (options?.includeDleq ?? false) { + if (options?.includeDleq) { // only pick the ones with a DLEQ proof proofs = proofs.filter((p: Proof) => p.dleq != undefined); } @@ -351,7 +353,7 @@ class CashuWallet { keep = keepProofsSelect.concat(keep); // strip dleq if explicitly told so - if (!options?.includeDleq) { + if (options?.includeDleq === false) { send = send.map((p: Proof) => { return { ...p, dleq: undefined }; }); @@ -1029,53 +1031,22 @@ class CashuWallet { const secret = secrets[i]; const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); - const serializedProof = serializeProof(proof) as Proof; - serializedProof.dleqValid = - dleq == undefined ? undefined : verifyDLEQProof_reblind(secret, dleq, proof.C, A); - serializedProof.dleq = - dleq == undefined - ? undefined - : ({ - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: numberToHexPadded64(dleq.r ?? BigInt(0)) - } as SerializedDLEQ); + const serializedProof = { + ...serializeProof(proof), + ...(dleq && { + dleqValid: verifyDLEQProof_reblind(secret, dleq, proof.C, A) + }), + ...(dleq && { + dleq: { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: numberToHexPadded64(dleq.r ?? BigInt(0)) + } as SerializedDLEQ + }) + } as Proof; return serializedProof; }); } - - /** - * Checks that each proof in `token` has a valid DLEQ proof according to - * keyset `keys` - * @param token The token subject to the verification - * @param keys The Mint's keyset to be used for verification - */ - private requireDLEQ(token: Token, keys: MintKeys) { - token.proofs.forEach((p: Proof, i: number) => { - if (p.dleq == undefined) { - throw new Error(`${i}-th proof is missing DLEQ proof`); - } - const dleq = { - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToNumber(p.dleq.r ?? '00') - } as DLEQ; - const key = keys.keys[p.amount]; - if (key == undefined) { - throw new Error(`undefined key for amount ${p.amount}`); - } - if ( - !verifyDLEQProof_reblind( - new TextEncoder().encode(p.secret), - dleq, - pointFromHex(p.C), - pointFromHex(key) - ) - ) { - throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); - } - }); - } } export { CashuWallet }; diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 85ca7f158..e50116774 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -9,7 +9,7 @@ class BlindedSignature { C_: ProjPointType; dleq?: DLEQ; - constructor(id: string, amount: number, C_: ProjPointType, dleq: DLEQ) { + constructor(id: string, amount: number, C_: ProjPointType, dleq?: DLEQ) { this.id = id; this.amount = amount; this.C_ = C_; diff --git a/src/utils.ts b/src/utils.ts index 0ac43cc32..c388ecf0c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,7 @@ import { import { DeprecatedToken, Keys, + MintKeys, Proof, SerializedDLEQ, Token, @@ -20,6 +21,8 @@ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; import { PaymentRequest } from './model/PaymentRequest.js'; +import { DLEQ, pointFromHex } from '@cashu/crypto/modules/common'; +import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; /** * Splits the amount into denominations of the provided @param keyset @@ -246,14 +249,13 @@ export function getEncodedTokenV4(token: Token): string { a: p.amount, s: p.secret, c: hexToBytes(p.C), - d: - p.dleq == undefined - ? undefined - : ({ - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToBytes(p.dleq.r ?? '00') - } as V4DLEQTemplate) + ...(p.dleq && { + d: { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? '00') + } as V4DLEQTemplate + }), }) ) }) @@ -322,14 +324,13 @@ export function handleTokens(token: string): Token { C: bytesToHex(p.c), amount: p.a, id: bytesToHex(t.i), - dleq: - p.d == undefined - ? undefined - : ({ - e: bytesToHex(p.d.e), - s: bytesToHex(p.d.s), - r: bytesToHex(p.d.r) - } as SerializedDLEQ) + ...(p.d && { + dleq: { + r: bytesToHex(p.d.r), + s: bytesToHex(p.d.s), + e: bytesToHex(p.d.e), + } as SerializedDLEQ + }) }); }) ); @@ -397,3 +398,38 @@ export function sumProofs(proofs: Array) { export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } + +/** + * Checks that the proof has a valid DLEQ proof according to + * keyset `keys` + * @param proof The proof subject to verification + * @param keyset The Mint's keyset to be used for verification + * @returns true if verification succeeded, false otherwise + * @throws Error if @param proof does not match any key in @param keyset + */ +export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { + if (proof.dleq == undefined) { + return false; + } + const dleq = { + e: hexToBytes(proof.dleq.e), + s: hexToBytes(proof.dleq.s), + r: hexToNumber(proof.dleq.r ?? '00') + } as DLEQ; + const key = keyset.keys[proof.amount]; + if (key == undefined) { + throw new Error(`undefined key for amount ${proof.amount}`); + } + if ( + !verifyDLEQProof_reblind( + new TextEncoder().encode(proof.secret), + dleq, + pointFromHex(proof.C), + pointFromHex(key) + ) + ) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/test/integration.test.ts b/test/integration.test.ts index 04f246618..2b87932ad 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -374,6 +374,6 @@ describe('dleq', () => { const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); - expect(exc).toEqual(new Error(`0-th DLEQ proof is invalid for key ${key}`)); + expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); }); }); From d2da84204b546ff0ee1243f09fe0aaaaf194b9a0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 17:03:07 +0100 Subject: [PATCH 221/246] clean tests --- test/integration.test.ts | 1 - test/utils.test.ts | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 2b87932ad..37d2fc4db 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -372,7 +372,6 @@ describe('dleq', () => { proofs: proofs } as Token; - const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 640179545..778383936 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -212,8 +212,7 @@ describe('test decode token', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ] }; @@ -233,22 +232,19 @@ describe('test decode token', () => { secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', - amount: 1, - dleq: undefined + amount: 1 }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', - amount: 2, - dleq: undefined + amount: 2 }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ] }; @@ -281,8 +277,7 @@ describe('test v4 encoding', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ], unit: 'sat' From 266c8aa789df2b429e21520fd3caf2f14d0dcee8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:17:48 +0100 Subject: [PATCH 222/246] test for `hasValidDleq` in utils tests. --- src/utils.ts | 4 ++-- test/utils.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index c388ecf0c..ccd0b62da 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -416,10 +416,10 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { s: hexToBytes(proof.dleq.s), r: hexToNumber(proof.dleq.r ?? '00') } as DLEQ; - const key = keyset.keys[proof.amount]; - if (key == undefined) { + if (!hasCorrespondingKey(proof.amount, keyset.keys)) { throw new Error(`undefined key for amount ${proof.amount}`); } + const key = keyset.keys[proof.amount]; if ( !verifyDLEQProof_reblind( new TextEncoder().encode(proof.secret), diff --git a/test/utils.test.ts b/test/utils.test.ts index 778383936..7596d1ad1 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,6 +1,12 @@ -import { Token, Keys, Proof } from '../src/model/types/index.js'; +import { blindMessage, constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; +import { Keys, Proof } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; +import { createDLEQProof } from '@cashu/crypto/modules/mint/NUT12'; +import { hasValidDleq, hexToNumber, numberToHexPadded64 } from '../src/utils.js'; +import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; +import { createBlindSignature, getPubKeyFromPrivKey } from '@cashu/crypto/modules/mint'; +import { pointFromBytes } from '@cashu/crypto/modules/common'; const keys: Keys = {}; for (let i = 1; i <= 2048; i *= 2) { @@ -353,3 +359,40 @@ describe('test output selection', () => { expect(amountsToKeep).toEqual([1, 1, 2, 2, 8, 8]); }); }); +describe('test zero-knowledge utilities', () => { + test('has valid dleq', () => { + // create private public key pair + const privkey = hexToBytes('1'.padStart(64, '0')); + const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + + // make up a secret + const fakeSecret = new TextEncoder().encode('fakeSecret'); + // make up blinding factor + const r = hexToNumber('123456'.padStart(64, '0')); + // blind secret + const fakeBlindedMessage = blindMessage(fakeSecret, r) + // construct DLEQ + const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); + // blind signature + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00') + // unblind + const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); + // serialize + const serializedProof = { + ...serializeProof(proof), + dleq: { + r: numberToHexPadded64(r), + e: bytesToHex(fakeDleq.e), + s: bytesToHex(fakeDleq.s), + } + } as Proof; + // use hasValidDleq to verify DLEQ + const keyset = { + id: '00', + unit: 'sat', + keys: {[1]: pubkey.toHex(true)}, + }; + const validDleq = hasValidDleq(serializedProof, keyset); + expect(validDleq).toBe(true); + }) +}); From efc73ec9e5566b0b6a0509cbe819c0be71ba28af Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:18:13 +0100 Subject: [PATCH 223/246] npm run format --- src/CashuWallet.ts | 2 +- src/utils.ts | 8 ++++---- test/integration.test.ts | 2 +- test/utils.test.ts | 16 ++++++++++------ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 58550197c..1e3cfb2b3 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -266,7 +266,7 @@ class CashuWallet { const keys = await this.getKeys(options?.keysetId); if (options?.requireDleq) { if (token.proofs.some((p: Proof) => !hasValidDleq(p, keys))) { - throw new Error("Token contains proofs with invalid DLEQ") + throw new Error('Token contains proofs with invalid DLEQ'); } } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); diff --git a/src/utils.ts b/src/utils.ts index ccd0b62da..7c2f31378 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -255,7 +255,7 @@ export function getEncodedTokenV4(token: Token): string { s: hexToBytes(p.dleq.s), r: hexToBytes(p.dleq.r ?? '00') } as V4DLEQTemplate - }), + }) }) ) }) @@ -328,7 +328,7 @@ export function handleTokens(token: string): Token { dleq: { r: bytesToHex(p.d.r), s: bytesToHex(p.d.s), - e: bytesToHex(p.d.e), + e: bytesToHex(p.d.e) } as SerializedDLEQ }) }); @@ -405,7 +405,7 @@ export function decodePaymentRequest(paymentRequest: string) { * @param proof The proof subject to verification * @param keyset The Mint's keyset to be used for verification * @returns true if verification succeeded, false otherwise - * @throws Error if @param proof does not match any key in @param keyset + * @throws Error if @param proof does not match any key in @param keyset */ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { if (proof.dleq == undefined) { @@ -432,4 +432,4 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { } return true; -} \ No newline at end of file +} diff --git a/test/integration.test.ts b/test/integration.test.ts index 37d2fc4db..e0b0d9a5e 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -373,6 +373,6 @@ describe('dleq', () => { } as Token; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); - expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); + expect(exc).toEqual(new Error('Token contains proofs with invalid DLEQ')); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 7596d1ad1..cbdfed480 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,4 +1,8 @@ -import { blindMessage, constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; +import { + blindMessage, + constructProofFromPromise, + serializeProof +} from '@cashu/crypto/modules/client'; import { Keys, Proof } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; @@ -370,11 +374,11 @@ describe('test zero-knowledge utilities', () => { // make up blinding factor const r = hexToNumber('123456'.padStart(64, '0')); // blind secret - const fakeBlindedMessage = blindMessage(fakeSecret, r) + const fakeBlindedMessage = blindMessage(fakeSecret, r); // construct DLEQ const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); // blind signature - const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00') + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); // unblind const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); // serialize @@ -383,16 +387,16 @@ describe('test zero-knowledge utilities', () => { dleq: { r: numberToHexPadded64(r), e: bytesToHex(fakeDleq.e), - s: bytesToHex(fakeDleq.s), + s: bytesToHex(fakeDleq.s) } } as Proof; // use hasValidDleq to verify DLEQ const keyset = { id: '00', unit: 'sat', - keys: {[1]: pubkey.toHex(true)}, + keys: { [1]: pubkey.toHex(true) } }; const validDleq = hasValidDleq(serializedProof, keyset); expect(validDleq).toBe(true); - }) + }); }); From f26bd150ddd8310cfccbb3bfd8a4ccd9b511c25c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:29:40 +0100 Subject: [PATCH 224/246] test no matching key --- test/utils.test.ts | 66 ++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index cbdfed480..cafa3de0a 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -364,33 +364,33 @@ describe('test output selection', () => { }); }); describe('test zero-knowledge utilities', () => { - test('has valid dleq', () => { - // create private public key pair - const privkey = hexToBytes('1'.padStart(64, '0')); - const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + // create private public key pair + const privkey = hexToBytes('1'.padStart(64, '0')); + const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + + // make up a secret + const fakeSecret = new TextEncoder().encode('fakeSecret'); + // make up blinding factor + const r = hexToNumber('123456'.padStart(64, '0')); + // blind secret + const fakeBlindedMessage = blindMessage(fakeSecret, r); + // construct DLEQ + const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); + // blind signature + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); + // unblind + const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); + // serialize + const serializedProof = { + ...serializeProof(proof), + dleq: { + r: numberToHexPadded64(r), + e: bytesToHex(fakeDleq.e), + s: bytesToHex(fakeDleq.s) + } + } as Proof; - // make up a secret - const fakeSecret = new TextEncoder().encode('fakeSecret'); - // make up blinding factor - const r = hexToNumber('123456'.padStart(64, '0')); - // blind secret - const fakeBlindedMessage = blindMessage(fakeSecret, r); - // construct DLEQ - const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); - // blind signature - const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); - // unblind - const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); - // serialize - const serializedProof = { - ...serializeProof(proof), - dleq: { - r: numberToHexPadded64(r), - e: bytesToHex(fakeDleq.e), - s: bytesToHex(fakeDleq.s) - } - } as Proof; - // use hasValidDleq to verify DLEQ + test('has valid dleq', () => { const keyset = { id: '00', unit: 'sat', @@ -399,4 +399,18 @@ describe('test zero-knowledge utilities', () => { const validDleq = hasValidDleq(serializedProof, keyset); expect(validDleq).toBe(true); }); + test('has valid dleq with no matching key', () => { + const keyset = { + id: '00', + unit: 'sat', + keys: { [2]: pubkey.toHex(true) } + }; + let exc; + try { + hasValidDleq(serializedProof, keyset); + } catch (e) { + exc = e; + } + expect(exc).toEqual(new Error('undefined key for amount 1')); + }); }); From 84c0ccc927e43fe08f6284b2e8ebb490191a98af Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sat, 9 Nov 2024 10:11:54 -0800 Subject: [PATCH 225/246] set mintInfo on CashuWallet if passed into the constructor --- src/CashuWallet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1b1a1b8af..f65512cf8 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -88,6 +88,7 @@ class CashuWallet { if (keys) keys.forEach((key: MintKeys) => this._keys.set(key.id, key)); if (options?.unit) this._unit = options?.unit; if (options?.keysets) this._keysets = options.keysets; + if (options?.mintInfo) this._mintInfo = options.mintInfo; if (options?.denominationTarget) { this._denominationTarget = options.denominationTarget; } From 06fdaab06c8600128060a064f625ce8543f5fee5 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Nov 2024 10:37:12 +0100 Subject: [PATCH 226/246] use new syntax for serializedDLEQ --- src/model/BlindedSignature.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index e50116774..684fa50f4 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -2,6 +2,7 @@ import { ProjPointType } from '@noble/curves/abstract/weierstrass'; import { SerializedBlindedSignature } from './types/index.js'; import { DLEQ } from '@cashu/crypto/modules/common'; import { bytesToHex } from '@noble/hashes/utils.js'; +import { numberToHexPadded64 } from '../utils.js'; class BlindedSignature { id: string; @@ -21,14 +22,13 @@ class BlindedSignature { id: this.id, amount: this.amount, C_: this.C_.toHex(true), - dleq: - this.dleq == undefined - ? undefined - : { - s: bytesToHex(this.dleq.s), - e: bytesToHex(this.dleq.e), - r: this.dleq.r?.toString(16) - } + ...(this.dleq && { + dleq: { + s: bytesToHex(this.dleq.s), + e: bytesToHex(this.dleq.e), + r: numberToHexPadded64(this.dleq.r ?? BigInt(0)) + } + }) }; } } From af68d5766a0ed8e8fe561918ce5e62c1c3b46e0c Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 11:55:37 +0000 Subject: [PATCH 227/246] abstracted strip util + changed conditions --- src/CashuWallet.ts | 16 +++++----------- src/utils.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1e3cfb2b3..e436e3360 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -29,7 +29,8 @@ import { sumProofs, getKeepAmounts, numberToHexPadded64, - hasValidDleq + hasValidDleq, + stripDleq } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -320,7 +321,6 @@ class CashuWallet { } ): Promise { if (options?.includeDleq) { - // only pick the ones with a DLEQ proof proofs = proofs.filter((p: Proof) => p.dleq != undefined); } if (sumProofs(proofs) < amount) { @@ -352,11 +352,8 @@ class CashuWallet { let { keep, send } = await this.swap(amount, sendProofs, options); keep = keepProofsSelect.concat(keep); - // strip dleq if explicitly told so - if (options?.includeDleq === false) { - send = send.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + if (!options?.includeDleq) { + send = stripDleq(send); } return { keep, send }; @@ -366,11 +363,8 @@ class CashuWallet { throw new Error('Not enough funds available to send'); } - // strip dleq if explicitly told so if (!options?.includeDleq) { - sendProofOffline.forEach((p: Proof) => { - p.dleq = undefined; - }); + return { keep: keepProofsOffline, send: stripDleq(sendProofOffline) }; } return { keep: keepProofsOffline, send: sendProofOffline }; diff --git a/src/utils.ts b/src/utils.ts index 7c2f31378..32f5a89b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -399,6 +399,19 @@ export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } +/** + * Removes all traces of DLEQs from a list of proofs + * @param proofs The list of proofs that dleq should be stripped from + */ +export function stripDleq(proofs: Array): Array> { + return proofs.map((p) => { + const newP = { ...p }; + delete newP['dleq']; + delete newP['dleqValid']; + return newP; + }); +} + /** * Checks that the proof has a valid DLEQ proof according to * keyset `keys` From 31d383259dae7139dc511be6d9c674809d1900cb Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 12:10:20 +0000 Subject: [PATCH 228/246] use stripDleq everywhere --- src/CashuWallet.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e436e3360..54bcb0cd7 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -767,10 +767,9 @@ class CashuWallet { options.privkey ).map((p: NUT11Proof) => serializeProof(p)); } - // Strip DLEQs if any - proofsToSend = proofsToSend.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + + proofsToSend = stripDleq(proofsToSend); + const meltPayload: MeltPayload = { quote: meltQuote.quote, inputs: proofsToSend, @@ -846,10 +845,7 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } - // Strip DLEQs if any - proofsToSend = proofsToSend.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + proofsToSend = stripDleq(proofsToSend); // join keepBlindedMessages and sendBlindedMessages const blindingData: BlindingData = { From 2e531436dc659b944f7111b8c5b8712cc1f69165 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 12:16:01 +0000 Subject: [PATCH 229/246] mintProofs returns proofs --- migration-2.0.0.md | 2 +- src/CashuWallet.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/migration-2.0.0.md b/migration-2.0.0.md index 5fc17b771..69b1d81c0 100644 --- a/migration-2.0.0.md +++ b/migration-2.0.0.md @@ -53,7 +53,7 @@ To check the state of a `Proof`, call `CashuWallet.checkProofsStates`. `checkPro #### renamed functions - in `SendResponse`, `returnChange` is now called `keep` -- `CashuWallet.mintTokens()` is now called `CashuWallet.mintProofs()` +- `CashuWallet.mintTokens()` is now called `CashuWallet.mintProofs()` and returns > instead of Promise<{proofs: Array}> - `CashuWallet.meltTokens()` is now called `CashuWallet.meltProofs()` - `CashuMint.split()` is now called `CashuMint.swap()` diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 45901cd19..a215f8070 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -675,7 +675,7 @@ class CashuWallet { counter?: number; pubkey?: string; } - ): Promise<{ proofs: Array }> { + ): Promise> { const keyset = await this.getKeys(options?.keysetId); if (!options?.outputAmounts && options?.proofsWeHave) { options.outputAmounts = { @@ -701,9 +701,7 @@ class CashuWallet { quote: quote }; const { signatures } = await this.mint.mint(mintPayload); - return { - proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) - }; + return this.constructProofs(signatures, blindingFactors, secrets, keyset); } /** From dc3f50791a06351364d8cf61d886fec600405b01 Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 12:20:46 +0000 Subject: [PATCH 230/246] adjusted tests --- test/integration.test.ts | 46 ++++++++++++++++++++-------------------- test/wallet.test.ts | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index e0b0d9a5e..c8f3df36f 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -55,10 +55,10 @@ describe('mint api', () => { const request = await wallet.createMintQuote(1337); expect(request).toBeDefined(); expect(request.request).toContain('lnbc1337'); - const tokens = await wallet.mintProofs(1337, request.quote); - expect(tokens).toBeDefined(); + const proofs = await wallet.mintProofs(1337, request.quote); + expect(proofs).toBeDefined(); // expect that the sum of all tokens.proofs.amount is equal to the requested amount - expect(tokens.proofs.reduce((a, b) => a + b.amount, 0)).toBe(1337); + expect(sumProofs(proofs)).toBe(1337); }); test('get fee for local invoice', async () => { const mint = new CashuMint(mintUrl); @@ -88,7 +88,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintProofs(100, request.quote); + const proofs = await wallet.mintProofs(100, request.quote); // expect no fee because local invoice const mintQuote = await wallet.createMintQuote(10); @@ -100,7 +100,7 @@ describe('mint api', () => { const quote_ = await wallet.checkMeltQuote(quote.quote); expect(quote_).toBeDefined(); - const sendResponse = await wallet.send(10, tokens.proofs, { includeFees: true }); + const sendResponse = await wallet.send(10, proofs, { includeFees: true }); const response = await wallet.meltProofs(quote, sendResponse.send); expect(response).toBeDefined(); // expect that we have received the fee back, since it was internal @@ -126,7 +126,7 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(3000); - const tokens = await wallet.mintProofs(3000, request.quote); + const proofs = await wallet.mintProofs(3000, request.quote); const meltQuote = await wallet.createMeltQuote(externalInvoice); const fee = meltQuote.fee_reserve; @@ -136,7 +136,7 @@ describe('mint api', () => { const quote_ = await wallet.checkMeltQuote(meltQuote.quote); expect(quote_).toBeDefined(); - const sendResponse = await wallet.send(2000 + fee, tokens.proofs, { includeFees: true }); + const sendResponse = await wallet.send(2000 + fee, proofs, { includeFees: true }); const response = await wallet.meltProofs(meltQuote, sendResponse.send); expect(response).toBeDefined(); @@ -163,9 +163,9 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintProofs(64, request.quote); + const proofs = await wallet.mintProofs(64, request.quote); - const sendResponse = await wallet.send(64, tokens.proofs); + const sendResponse = await wallet.send(64, proofs); expect(sendResponse).toBeDefined(); expect(sendResponse.send).toBeDefined(); expect(sendResponse.keep).toBeDefined(); @@ -177,9 +177,9 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintProofs(100, request.quote); + const proofs = await wallet.mintProofs(100, request.quote); - const sendResponse = await wallet.send(10, tokens.proofs, { includeFees: false }); + const sendResponse = await wallet.send(10, proofs, { includeFees: false }); expect(sendResponse).toBeDefined(); expect(sendResponse.send).toBeDefined(); expect(sendResponse.keep).toBeDefined(); @@ -192,9 +192,9 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(100); - const tokens = await wallet.mintProofs(100, request.quote); + const proofs = await wallet.mintProofs(100, request.quote); - const sendResponse = await wallet.send(10, tokens.proofs); + const sendResponse = await wallet.send(10, proofs); const encoded = getEncodedToken({ mint: mintUrl, proofs: sendResponse.send }); const response = await wallet.receive(encoded); expect(response).toBeDefined(); @@ -203,8 +203,8 @@ describe('mint api', () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint, { unit }); const request = await wallet.createMintQuote(64); - const tokens = await wallet.mintProofs(64, request.quote); - const encoded = getEncodedToken({ mint: mintUrl, proofs: tokens.proofs }); + const proofs = await wallet.mintProofs(64, request.quote); + const encoded = getEncodedToken({ mint: mintUrl, proofs: proofs }); const response = await wallet.receive(encoded); expect(response).toBeDefined(); }); @@ -219,9 +219,9 @@ describe('mint api', () => { const pubKeyBob = secp256k1.getPublicKey(privKeyBob); const request = await wallet.createMintQuote(128); - const tokens = await wallet.mintProofs(128, request.quote); + const mintedProofs = await wallet.mintProofs(128, request.quote); - const { send } = await wallet.send(64, tokens.proofs, { pubkey: bytesToHex(pubKeyBob) }); + const { send } = await wallet.send(64, mintedProofs, { pubkey: bytesToHex(pubKeyBob) }); const encoded = getEncodedToken({ mint: mintUrl, proofs: send }); const result = await wallet @@ -254,7 +254,7 @@ describe('mint api', () => { const meltRequest = await wallet.createMeltQuote(externalInvoice); const fee = meltRequest.fee_reserve; expect(fee).toBeGreaterThan(0); - const response = await wallet.meltProofs(meltRequest, proofs.proofs, { + const response = await wallet.meltProofs(meltRequest, proofs, { privkey: bytesToHex(privKeyBob) }); expect(response).toBeDefined(); @@ -271,7 +271,7 @@ describe('dleq', () => { const wallet = new CashuWallet(mint); const mintRequest = await wallet.createMintQuote(3000); - const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + const proofs = await wallet.mintProofs(3000, mintRequest.quote); proofs.forEach((p) => { expect(p).toHaveProperty('dleq'); @@ -290,7 +290,7 @@ describe('dleq', () => { } const mintRequest = await wallet.createMintQuote(8); - const { proofs } = await wallet.mintProofs(8, mintRequest.quote); + const proofs = await wallet.mintProofs(8, mintRequest.quote); const { keep, send } = await wallet.send(4, proofs, { includeDleq: true }); @@ -317,7 +317,7 @@ describe('dleq', () => { } const mintRequest = await wallet.createMintQuote(8); - const { proofs } = await wallet.mintProofs(8, mintRequest.quote); + const proofs = await wallet.mintProofs(8, mintRequest.quote); const { keep, send } = await wallet.send(4, proofs, { includeDleq: false }); send.forEach((p) => { @@ -337,7 +337,7 @@ describe('dleq', () => { } const mintRequest = await wallet.createMintQuote(8); - let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + let proofs = await wallet.mintProofs(8, mintRequest.quote); // strip dleq proofs = proofs.map((p) => { @@ -357,7 +357,7 @@ describe('dleq', () => { } const mintRequest = await wallet.createMintQuote(8); - let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + let proofs = await wallet.mintProofs(8, mintRequest.quote); // alter dleq signature proofs.forEach((p) => { diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7ca5c27b2..f8c4f4ac0 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -247,7 +247,7 @@ describe('requestTokens', () => { }); const wallet = new CashuWallet(mint, { unit }); - const { proofs } = await wallet.mintProofs(1, ''); + const proofs = await wallet.mintProofs(1, ''); expect(proofs).toHaveLength(1); expect(proofs[0]).toMatchObject({ amount: 1, id: '009a1f293253e41e' }); From 0a1ec50cba492c32d1aedb6eefe14cbe2ea1d183 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 10:42:07 +0000 Subject: [PATCH 231/246] added ConnectionManager --- src/WSConnection.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index 16c20c8a9..ec2ff5db2 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -9,6 +9,27 @@ import { import { OnOpenError, OnOpenSuccess } from './model/types/wallet/websocket'; import { getWebSocketImpl } from './ws'; +export class ConnectionManager { + static instace: ConnectionManager; + private connectionMap: Map = new Map(); + + static getInstance() { + if (!ConnectionManager.instace) { + ConnectionManager.instace = new ConnectionManager(); + } + return ConnectionManager.instace; + } + + getConnection(url: string): WSConnection { + if (this.connectionMap.has(url)) { + return this.connectionMap.get(url) as WSConnection; + } + const newConn = new WSConnection(url); + this.connectionMap.set(url, newConn); + return newConn; + } +} + export class WSConnection { public readonly url: URL; private readonly _WS: typeof WebSocket; From 338759056e2b197eee6cd3812153e88ba0a19f9a Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 10:44:04 +0000 Subject: [PATCH 232/246] added ConnectionManager to CashuMint --- src/CashuMint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CashuMint.ts b/src/CashuMint.ts index 0434aa344..7cb4f2cf6 100644 --- a/src/CashuMint.ts +++ b/src/CashuMint.ts @@ -1,4 +1,4 @@ -import { WSConnection } from './WSConnection.js'; +import { ConnectionManager, WSConnection } from './WSConnection.js'; import type { CheckStatePayload, CheckStateResponse, @@ -457,7 +457,7 @@ class CashuMint { mintUrl.pathname += '/' + wsSegment; } } - this.ws = new WSConnection( + this.ws = ConnectionManager.getInstance().getConnection( `${mintUrl.protocol === 'https:' ? 'wss' : 'ws'}://${mintUrl.host}${mintUrl.pathname}` ); try { From dfe515ccb8a170b30e40e5657ae3eae3f7d9333e Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 10:54:14 +0000 Subject: [PATCH 233/246] adjusted integraton test --- test/integration.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 508371add..096766531 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,14 +2,12 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; +import { getEncodedToken, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { CheckStateEnum, MeltQuoteState, - MintQuotePayload, - MintQuoteResponse, MintQuoteState, ProofState } from '../src/model/types/index.js'; @@ -270,18 +268,20 @@ describe('mint api', () => { const mintQuote = await wallet.createMintQuote(21); const callback = jest.fn(); - const res = await new Promise((res, rej) => { - wallet.onMintQuoteUpdates( + const res = await new Promise(async (res, rej) => { + const unsub = await wallet.onMintQuoteUpdates( [mintQuote.quote], (p) => { if (p.state === MintQuoteState.PAID) { callback(); res(1); + unsub(); } }, (e) => { console.log(e); rej(e); + unsub(); } ); }); From dde08a1d9cb6239446c512ed6b96376e0f4ee1af Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 13:03:42 +0000 Subject: [PATCH 234/246] typos --- src/WSConnection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WSConnection.ts b/src/WSConnection.ts index ec2ff5db2..a6a5b0e02 100644 --- a/src/WSConnection.ts +++ b/src/WSConnection.ts @@ -36,7 +36,7 @@ export class WSConnection { private ws: WebSocket | undefined; private connectionPromise: Promise | undefined; private subListeners: { [subId: string]: Array<(payload: any) => any> } = {}; - private rpcListeners: { [rpsSubId: string]: any } = {}; + private rpcListeners: { [rpcSubId: string]: any } = {}; private messageQueue: MessageQueue; private handlingInterval?: NodeJS.Timer; private rpcId = 0; @@ -159,7 +159,7 @@ export class WSConnection { } } } catch (e) { - console.log(e); + console.error(e); return; } } From aa5d3ee332b38935b56dd8bb9adc89b7a229cae6 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 13:53:29 +0000 Subject: [PATCH 235/246] fixed integration test --- test/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 821e7c147..982be47fa 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -339,7 +339,7 @@ describe('mint api', () => { await new Promise((res, rej) => { wallet.onMintQuotePaid(quote.quote, res, rej); }); - const { proofs } = await wallet.mintProofs(63, quote.quote); + const proofs = await wallet.mintProofs(63, quote.quote); const data = await new Promise((res) => { wallet.onProofStateUpdates( proofs, From 1c04c23812bb22bc7a32dc60f962737af0902f1f Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 14:02:10 +0000 Subject: [PATCH 236/246] 2.0.0-rc2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 609d2862a..56e6c7dbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc1", + "version": "2.0.0-rc2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc1", + "version": "2.0.0-rc2", "license": "MIT", "dependencies": { "@cashu/crypto": "^0.3.4", diff --git a/package.json b/package.json index e7fac9e5d..0c4783e53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc1", + "version": "2.0.0-rc2", "description": "cashu library for communicating with a cashu mint", "main": "dist/lib/es5/index.js", "module": "dist/lib/es6/index.js", From c7d1aee7d3bdebed7ba54bf16268e88a4e444e43 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:53:01 +0100 Subject: [PATCH 237/246] nut-17: read support flags from info endpoint --- src/model/types/mint/responses.ts | 33 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 43a6ccd03..f53ddd5fe 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -57,35 +57,41 @@ export type GetInfoResponse = { description_long?: string; contact: Array; nuts: { - '4': { + '4': { // Minting methods: Array; disabled: boolean; }; - '5': { + '5': { // Melting methods: Array; disabled: boolean; }; - '7'?: { + '7'?: { // Token state check supported: boolean; }; - '8'?: { + '8'?: { // Overpaid melt fees supported: boolean; }; - '9'?: { + '9'?: { // Restore supported: boolean; }; - '10'?: { + '10'?: { // Spending conditions supported: boolean; }; - '11'?: { + '11'?: { // P2PK supported: boolean; }; - '12'?: { + '12'?: { // DLEQ supported: boolean; }; - '13'?: { + '14'?: { // HTLCs supported: boolean; }; + '15'?: { // MPP + supported: boolean; + }; + '17'?: { // WebSockets + supported: Array; + }; }; motd?: string; }; @@ -215,3 +221,12 @@ export type SwapResponse = { */ signatures: Array; } & ApiError; + +/** + * WebSocket supported methods + */ +export type WebSocketSupport = { + method: string; + unit: string; + commands: Array; +}; \ No newline at end of file From f3bfe8c4e0034eee9bca87c5115c960ae6c2c813 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:31:55 +0100 Subject: [PATCH 238/246] add missing settings --- src/model/types/mint/responses.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index f53ddd5fe..90824931f 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -87,7 +87,7 @@ export type GetInfoResponse = { supported: boolean; }; '15'?: { // MPP - supported: boolean; + methods: Array; }; '17'?: { // WebSockets supported: Array; @@ -222,6 +222,15 @@ export type SwapResponse = { signatures: Array; } & ApiError; +/** + * MPP supported methods + */ +export type MPPMethod = { + method: string; + unit: string; + mpp: boolean; +}; + /** * WebSocket supported methods */ From 349e5fe56a764ee726f70c5df59a15b1c1ff09ae Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:33:10 +0100 Subject: [PATCH 239/246] format --- src/model/types/mint/responses.ts | 35 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 90824931f..88ac36c58 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -57,39 +57,50 @@ export type GetInfoResponse = { description_long?: string; contact: Array; nuts: { - '4': { // Minting + '4': { + // Minting methods: Array; disabled: boolean; }; - '5': { // Melting + '5': { + // Melting methods: Array; disabled: boolean; }; - '7'?: { // Token state check + '7'?: { + // Token state check supported: boolean; }; - '8'?: { // Overpaid melt fees + '8'?: { + // Overpaid melt fees supported: boolean; }; - '9'?: { // Restore + '9'?: { + // Restore supported: boolean; }; - '10'?: { // Spending conditions + '10'?: { + // Spending conditions supported: boolean; }; - '11'?: { // P2PK + '11'?: { + // P2PK supported: boolean; }; - '12'?: { // DLEQ + '12'?: { + // DLEQ supported: boolean; }; - '14'?: { // HTLCs + '14'?: { + // HTLCs supported: boolean; }; - '15'?: { // MPP + '15'?: { + // MPP methods: Array; }; - '17'?: { // WebSockets + '17'?: { + // WebSockets supported: Array; }; }; @@ -238,4 +249,4 @@ export type WebSocketSupport = { method: string; unit: string; commands: Array; -}; \ No newline at end of file +}; From c408589b7fc0a1e0b4d10551499689d824197ed6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:40:32 +0100 Subject: [PATCH 240/246] add test --- test/wallet.test.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7ca5c27b2..4962a03a2 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -45,7 +45,7 @@ beforeEach(() => { describe('test info', () => { const mintInfoResp = JSON.parse( - '{"name":"Testnut mint","pubkey":"0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679","version":"Nutshell/0.16.0","description":"Mint for testing Cashu wallets","description_long":"This mint usually runs the latest main branch of the nutshell repository. 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":"npub..."}],"motd":"This is a message of the day field. You should display this field to your users if the content changes!","nuts":{"4":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"7":{"supported":true},"8":{"supported":true},"9":{"supported":true},"10":{"supported":true},"11":{"supported":true},"12":{"supported":true},"17":[{"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"]}]}}' + '{"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","description":true},{"method":"bolt11","unit":"usd","description":true},{"method":"bolt11","unit":"eur","description":true}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"},{"method":"bolt11","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"]}]}}}' ); test('test info', async () => { nock(mintUrl).get('/v1/info').reply(200, mintInfoResp); @@ -55,14 +55,33 @@ describe('test info', () => { expect(info.contact).toEqual([ { method: 'email', info: 'contact@me.com' }, { method: 'twitter', info: '@me' }, - { method: 'nostr', info: 'npub...' } + { method: 'nostr', info: 'npub1337' } ]); + expect(info.nuts?.['17']).toEqual({ + 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'] + } + ] + }); expect(info).toEqual(mintInfoResp); }); test('test info with deprecated contact field', async () => { // mintInfoRespDeprecated is the same as mintInfoResp but with the contact field in the old format const mintInfoRespDeprecated = JSON.parse( - '{"name":"Testnut mint","pubkey":"0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679","version":"Nutshell/0.16.0","description":"Mint for testing Cashu wallets","description_long":"This mint usually runs the latest main branch of the nutshell repository. All your Lightning invoices will always be marked paid so that you can test minting and melting ecash via Lightning.","contact":[["email","contact@me.com"],["twitter","@me"],["nostr","npub..."]],"motd":"This is a message of the day field. You should display this field to your users if the content changes!","nuts":{"4":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"7":{"supported":true},"8":{"supported":true},"9":{"supported":true},"10":{"supported":true},"11":{"supported":true},"12":{"supported":true},"17":[{"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"]}]}}' + '{"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. All your Lightning invoices will always be marked paid so that you can test minting and melting ecash via Lightning.","contact":[["email","contact@me.com"],["twitter","@me"],["nostr","npub1337"]],"motd":"This is a message of the day field. You should display this field to your users if the content changes!","nuts":{"4":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"5":{"methods":[{"method":"bolt11","unit":"sat"},{"method":"bolt11","unit":"usd"}],"disabled":false},"7":{"supported":true},"8":{"supported":true},"9":{"supported":true},"10":{"supported":true},"11":{"supported":true},"12":{"supported":true},"17":[{"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"]}]}}' ); nock(mintUrl).get('/v1/info').reply(200, mintInfoRespDeprecated); const wallet = new CashuWallet(mint, { unit }); @@ -70,9 +89,8 @@ describe('test info', () => { expect(info.contact).toEqual([ { method: 'email', info: 'contact@me.com' }, { method: 'twitter', info: '@me' }, - { method: 'nostr', info: 'npub...' } + { method: 'nostr', info: 'npub1337' } ]); - expect(info).toEqual(mintInfoResp); }); }); From 211c0a216364d0200ff5a6eccf31ad5723499fa5 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 16:40:14 +0000 Subject: [PATCH 241/246] removed mpp bool --- src/model/types/mint/responses.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 88ac36c58..78de40c49 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -239,7 +239,6 @@ export type SwapResponse = { export type MPPMethod = { method: string; unit: string; - mpp: boolean; }; /** From 4ac6b3c05b70244546d52aeaa29e0bfa1a8ce688 Mon Sep 17 00:00:00 2001 From: Egge Date: Fri, 15 Nov 2024 16:42:10 +0000 Subject: [PATCH 242/246] 2.0.0-rc3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56e6c7dbc..563450f0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc2", + "version": "2.0.0-rc3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc2", + "version": "2.0.0-rc3", "license": "MIT", "dependencies": { "@cashu/crypto": "^0.3.4", diff --git a/package.json b/package.json index 0c4783e53..941d26372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc2", + "version": "2.0.0-rc3", "description": "cashu library for communicating with a cashu mint", "main": "dist/lib/es5/index.js", "module": "dist/lib/es6/index.js", From 8101a8e76534cf5f1b903d947ad2f0d9d211b903 Mon Sep 17 00:00:00 2001 From: gandlaf21 Date: Sat, 16 Nov 2024 09:22:59 +0900 Subject: [PATCH 243/246] update wallet example --- examples/simpleWallet_example.ts | 235 +++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 examples/simpleWallet_example.ts diff --git a/examples/simpleWallet_example.ts b/examples/simpleWallet_example.ts new file mode 100644 index 000000000..ea0678560 --- /dev/null +++ b/examples/simpleWallet_example.ts @@ -0,0 +1,235 @@ +import { CashuMint } from '../src/CashuMint.js'; +import { CashuWallet } from '../src/CashuWallet.js'; + +import dns from 'node:dns'; +import { + MeltQuoteResponse, + MeltQuoteState, + MintQuoteResponse, + MintQuoteState, + Proof, + Token +} from '../src/model/types/index.js'; +import { getEncodedTokenV4, sumProofs } from '../src/utils.js'; +dns.setDefaultResultOrder('ipv4first'); + +const externalInvoice = + 'lnbc20u1p3u27nppp5pm074ffk6m42lvae8c6847z7xuvhyknwgkk7pzdce47grf2ksqwsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzpgxqyz5vqsp5sw6n7cztudpl5m5jv3z6dtqpt2zhd3q6dwgftey9qxv09w82rgjq9qyyssqhtfl8wv7scwp5flqvmgjjh20nf6utvv5daw5h43h69yqfwjch7wnra3cn94qkscgewa33wvfh7guz76rzsfg9pwlk8mqd27wavf2udsq3yeuju'; + +// const mintUrl = 'https://testnut.cashu.space'; +const mintUrl = 'http://localhost:3338'; + +// +++++++++++++++++++++ Example of a simple wallet implementation ++++++++++++++++++ +// run the example with the following command: `npx examples/_simpleWallet.ts` +// a local mint instance should be running on port 3338. Startup command: +// docker run -d -p 3338:3338 --name nutshell -e MINT_LIGHTNING_BACKEND=FakeWallet -e MINT_INPUT_FEE_PPK=100 -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint + +const mintAmount = 2050; + +const runWalletExample = async () => { + try { + // instantiate a mint. It is used later by the CashuWallet to create api calls to the mint + const mint = new CashuMint(mintUrl); + + // create a wallet with the keys loaded from the mint. + // The wallet is used as an interface for all cashu specific interactions + const wallet = new CashuWallet(mint); + + // In order to load mint keys and other information about the mint, we should call this method + await wallet.loadMint(); + + //we can store the mint information, in case we later want to initialize the wallet without requesting the mint info again. + const mintInfo = wallet.mintInfo; + const keys = wallet.keys; + const keysets = wallet.keysets; + + // ++++++++ Minting some ecash +++++++++++++ + + //First, let's decide on the amount of ecash we want to mint. + + // next we create a place to store the ecash after it has been minted. + // Usually, this would be done in a database or somewhere where persistence is guaranteed. + // in this example, we will store the ecash in memory to make the example easier to understand. + let proofs: Proof[] = []; + + // It's also a good idea to store proofs we've sent: in case the receiver does not claim them, we can receive them back later + let sentProofs: Proof[] = []; + + // let's start with minting some ecash! + const mintEcash = async function () { + // with this command, we can initiate the creation of some ecash. + // The mint will return a request, that we have to fullfil in order for the ecash to be issued. + // (in most cases this will be a lightning invoice that needs to be paid) + console.log('Requesting a mint quote for' + mintAmount + 'satoshis.'); + const quote = await wallet.createMintQuote(mintAmount); + + console.log('Invoice to pay, in order to fullfill the quote: ' + quote.request); + + //check if an error occurred in the creation of the quote + if (quote.error) { + console.error(quote.error, quote.code, quote.detail); + return; + } + + // After some time of waiting, let's ask the mint if the request has been fullfilled. + setTimeout(async () => await checkMintQuote(quote), 1000); + + const checkMintQuote = async (q: MintQuoteResponse) => { + // with this call, we can check the current status of a given quote + console.log('Checking the status of the quote: ' + q.quote); + const quote = await wallet.checkMintQuote(q.quote); + if (quote.error) { + console.error(quote.error, quote.code, quote.detail); + return; + } + if (quote.state === MintQuoteState.PAID) { + //if the quote was paid, we can ask the mint to issue the signatures for the ecash + const response = await wallet.mintProofs(mintAmount, quote.quote); + console.log(`minted proofs: ${response.map((p) => p.amount).join(', ')} sats`); + + // let's store the proofs in the storage we previously created + proofs = response; + + // after successfull minting, let's try to send some ecash + sendEcash(10); + } else if (quote.state === MintQuoteState.ISSUED) { + // if the quote has already been issued, we will receive an error if we try to mint again + console.error('Quote has already been issued'); + return; + } else { + // if the quote has not yet been paid, we will wait some more to get the status of the quote again + setTimeout(async () => await checkMintQuote(q), 1000); + } + }; + }; + await mintEcash(); + + // ++++++++ Sending some ecash +++++++++++++ + + const sendEcash = async (amount: number) => { + // to send some ecash, we will call the `send` function + // e can provide the proofs we created in the previous step. + // If we provide too many proofs, they will be returned by the `keep` array + // after that, + // If the amount of the accumulated proofs we provide do not match exactly the amount we want to send, + // a split will have to be performed. + // this will burn the current proofs at the mint, and return a fresh set of proofs, matching the amount we want to send + const { keep, send } = await wallet.send(amount, proofs, { includeFees: true }); + + console.log( + `sending ${send.reduce((a, b) => a + b.amount, 0)} keeping ${keep.reduce( + (a, b) => a + b.amount, + 0 + )}` + ); + // first, let's update our store with the new proofs + proofs = keep; + + sentProofs.push(...send); + + // and now, let's prepare the ecash we want to send as a cashu string + // For this, we can use the `Token` type + // In there, we set the mint url, and proof we want to send + const token: Token = { + mint: mintUrl, + proofs: send + }; + // and finally, we can encode the token as a cashu string + const cashuString = getEncodedTokenV4(token); + + // we can now send the cashu string to someone, and they can receive the ecash! let's try that next + console.log(cashuString); + + // let's try to receive the cashu string back to ourselves + await receiveEcash(cashuString); + }; + + // ++++++++ Receiving some ecash +++++++++++++ + + const receiveEcash = async (cashuString: string) => { + // we can receive a cashu string back with the `receive` method + // this step is crucial. It will burn the received proofs, and create new ones, + // making sure the sender cannot try to double spend them. + const received = await wallet.receive(cashuString); + console.log('Received proofs:' + received.reduce((acc, proof) => acc + proof.amount, 0)); + + // after receiving, let's not forget to add the proofs back to our storage + proofs.push(...received); + + // After receiving back our ecash, let's try to melt it. + // Melting ecash means, exchanging it back to the medium it was issued for. + // in most cases that would be lightning sats + await meltEcash(); + }; + + // ++++++++ Melting some ecash +++++++++++++ + + const meltEcash = async () => { + // Similar to the minting process, we need to create a melt quote first. + // For this, we let the mint know what kind of request we want to be fulfilled. + // Usually this would be the payment of a lightning invoice. + const quote = await wallet.createMeltQuote(externalInvoice); + + // After creating the melt quote, we can initiate the melting process. + const amountToMelt = quote.amount + quote.fee_reserve; + + console.log(`quote amount: ${quote.amount}`); + console.log(`fee reserve proofs: ${quote.fee_reserve}`); + console.log(`Total quote amount: ${amountToMelt}`); + + // in order to get the correct amount of proofs for the melt request, we can use the `send` function we used before + const { keep, send } = await wallet.send(amountToMelt, proofs, { includeFees: true }); + + // once again, we update the proofs we have to keep. + proofs = keep; + + sentProofs.push(...send); + + // and initiate the melting process with the prepared proofs. + const { change } = await wallet.meltProofs(quote, send); + + //in case we overpaid for lightning fees, the mint will return the owed amount in ecash + proofs.push(...change); + + if (quote.error) { + console.error(quote.error, quote.code, quote.detail); + return; + } + + // After giving the mint some time to fullfil the melt request, + // we can check on the status + setTimeout(async () => await checkMeltQuote(quote), 1000); + + const checkMeltQuote = async (q: MeltQuoteResponse) => { + // we can check on the status of the quote. + const quote = await wallet.checkMeltQuote(q.quote); + + if (quote.error) { + console.error(quote.error, quote.code, quote.detail); + return; + } + if (quote.state === MeltQuoteState.PAID) { + // if the request has succeeded, we should receive the preimage for the paid invoice. + console.log( + 'success! here is the payment preimage (if its null, the mints lightning backend did not forward the preimage): ', + quote.payment_preimage + ); + + console.log(`Ecash left: ${sumProofs(proofs)}`); + console.log(`Spent ecash notes: ${sumProofs(sentProofs)}`); + + // +++++++++++++++++++ THE END +++++++++++++++++++ + // There are more advanced features that were not touched on in this example. + // Take a look at the documentation to learn about features like seed recovery, locking ecash to pubkeys, etc. + } else { + // if the request has not succeeded, we will ask again + setTimeout(async () => await checkMeltQuote(quote), 1000); + } + }; + }; + } catch (error) { + console.error(error, 'u-oh something went wrong'); + } +}; + +runWalletExample(); From e31b556ed149152740f4e5947f0c5e9aed50e310 Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 21 Nov 2024 10:08:23 +0100 Subject: [PATCH 244/246] exported injectWebSocketImpl for nodejs --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index cd9c6b3db..09b59ccb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { CashuMint } from './CashuMint.js'; import { CashuWallet } from './CashuWallet.js'; import { PaymentRequest } from './model/PaymentRequest.js'; -import { WSConnection } from './WSConnection.js'; import { setGlobalRequestOptions } from './request.js'; import { getEncodedToken, @@ -24,3 +23,5 @@ export { deriveKeysetId, setGlobalRequestOptions }; + +export { injectWebSocketImpl } from './ws.js'; From 61078c8f58c6796ddfb6ee8ef85838c7c7d7820a Mon Sep 17 00:00:00 2001 From: Egge Date: Thu, 21 Nov 2024 09:35:32 +0000 Subject: [PATCH 245/246] 2.0.0-rc4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 563450f0b..f9791058b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc3", + "version": "2.0.0-rc4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc3", + "version": "2.0.0-rc4", "license": "MIT", "dependencies": { "@cashu/crypto": "^0.3.4", diff --git a/package.json b/package.json index 941d26372..b45a4792a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc3", + "version": "2.0.0-rc4", "description": "cashu library for communicating with a cashu mint", "main": "dist/lib/es5/index.js", "module": "dist/lib/es6/index.js", From 4a7a76b9fba20c70405676539c053fd0387e1f57 Mon Sep 17 00:00:00 2001 From: Egge Date: Tue, 26 Nov 2024 15:00:13 +0000 Subject: [PATCH 246/246] 2.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9791058b..2673c7449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc4", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc4", + "version": "2.0.0", "license": "MIT", "dependencies": { "@cashu/crypto": "^0.3.4", diff --git a/package.json b/package.json index b45a4792a..68ee3ae06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cashu/cashu-ts", - "version": "2.0.0-rc4", + "version": "2.0.0", "description": "cashu library for communicating with a cashu mint", "main": "dist/lib/es5/index.js", "module": "dist/lib/es6/index.js",