From a17ee6eb5bce0acc2e7fa7b0f0ce1cfc67a77ac9 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sat, 21 Dec 2024 21:16:50 -0800 Subject: [PATCH 1/7] create cashu lib with bun tests and ci --- .github/workflows/ci.yml | 30 +++++++++ bun.lockb | Bin 364253 -> 370249 bytes lib/cashu/index.ts | 2 + lib/cashu/proof.ts | 20 ++++++ lib/cashu/secret.ts | 99 +++++++++++++++++++++++++++++ lib/cashu/types.ts | 127 ++++++++++++++++++++++++++++++++++++++ package.json | 3 + test/cashu/proof.test.ts | 33 ++++++++++ test/cashu/secret.test.ts | 19 ++++++ tsconfig.json | 2 +- 10 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 lib/cashu/index.ts create mode 100644 lib/cashu/proof.ts create mode 100644 lib/cashu/secret.ts create mode 100644 lib/cashu/types.ts create mode 100644 test/cashu/proof.test.ts create mode 100644 test/cashu/secret.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..07f2bf91 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + checks: write + pull-requests: write + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.1.34 + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 09633f5c0cd04ecb2b9fc0990a17e6078badf676..a2e527ace9b78cb344af4033dc0cfa9f380d74b4 100755 GIT binary patch delta 73482 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

Mt(f;B=Bru~wfDM>3F*CPD$W6n|#9qA< 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 diff --git a/lib/cashu/index.ts b/lib/cashu/index.ts new file mode 100644 index 00000000..a04b8d1a --- /dev/null +++ b/lib/cashu/index.ts @@ -0,0 +1,2 @@ +export * from './proof'; +export * from './secret'; diff --git a/lib/cashu/proof.ts b/lib/cashu/proof.ts new file mode 100644 index 00000000..afba6e89 --- /dev/null +++ b/lib/cashu/proof.ts @@ -0,0 +1,20 @@ +import type { Proof } from '@cashu/cashu-ts'; +import { getP2PKPubkeyFromSecret } from './secret'; + +/** + * Get the pubkey that a list of proofs are locked to. + * @param proofs - The list of proofs to get the pubkey from. + * @returns The pubkey in the P2PK secrets + * @throws Error if there are multiple pubkeys in the list or if the secret is not a P2PK secret. + */ +export const getP2PKPubkeyFromProofs = (proofs: Proof[]) => { + const pubkeys = [ + ...new Set( + proofs.map((p) => getP2PKPubkeyFromSecret(p.secret)).filter(Boolean), + ), + ]; + if (pubkeys.length > 1) { + throw new Error('Received a set of proofs with multiple pubkeys'); + } + return pubkeys[0] || null; +}; diff --git a/lib/cashu/secret.ts b/lib/cashu/secret.ts new file mode 100644 index 00000000..8caf229b --- /dev/null +++ b/lib/cashu/secret.ts @@ -0,0 +1,99 @@ +import { z } from 'zod'; +import { + type NUT10Secret, + type NUT10SecretData, + type NUT10SecretTag, + type P2PKSecret, + type ParsedNUT10Secret, + type PlainSecret, + type ProofSecret, + WELL_KNOWN_SECRET_KINDS, +} from './types'; + +const NUT10SecretTagSchema = z + .tuple([z.string(), z.string()]) + .rest(z.string()) satisfies z.ZodType; + +const NUT10SecretDataSchema = z.object({ + nonce: z.string(), + data: z.string(), + tags: z.array(NUT10SecretTagSchema).optional(), +}) satisfies z.ZodType; + +const WellKnownSecretKindSchema = z.enum(WELL_KNOWN_SECRET_KINDS); + +const NUT10SecretSchema = z.tuple([ + WellKnownSecretKindSchema, + NUT10SecretDataSchema, +]) satisfies z.ZodType; + +/** + * Type guard to check if asecret is a NUT-10 secret + */ +export const isNUT10Secret = (secret: ProofSecret): secret is NUT10Secret => { + return typeof secret !== 'string'; +}; + +/** + * Type guard to check if a secret is a P2PK secret + */ +export const isP2PKSecret = (secret: ProofSecret): secret is P2PKSecret => { + return ( + isNUT10Secret(secret) && + secret.kind === WELL_KNOWN_SECRET_KINDS.find((kind) => kind === 'P2PK') + ); +}; + +/** + * Type guard to check if a secret is a plain string secret + */ +export const isPlainSecret = (secret: ProofSecret): secret is PlainSecret => { + return typeof secret === 'string'; +}; + +/** + * Parse secret string from Proof.secret into a well-known secret [NUT-10](https://github.com/cashubtc/nuts/blob/main/10.md) + * or a string [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md) + * @param secret - The stringified secret to parse + * @returns The parsed secret as a NUT-10 secret or a string + * @throws Error if the secret is a NUT-10 secret with an invalid format + */ +export const parseSecret = (secret: string): ProofSecret => { + let parsed: unknown; + try { + parsed = JSON.parse(secret); + } catch { + // If JSON parsing fails, assume it's a plain string secret + // as defined in NUT-00 + return secret; + } + + try { + const validatedSecret = NUT10SecretSchema.parse(parsed); + const [kind, data] = validatedSecret; + + return { + kind, + ...data, + }; + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error('Invalid secret format'); + } + throw error; + } +}; + +/** + * Extract the public key from a P2PK secret + * @param secret - The stringified secret to parse + * @returns The public key stored in the secret's data field + * @throws Error if the secret is not a valid P2PK secret + */ +export const getP2PKPubkeyFromSecret = (secret: string): string => { + const parsedSecret = parseSecret(secret); + if (!isP2PKSecret(parsedSecret)) { + throw new Error('Secret is not a P2PK secret'); + } + return parsedSecret.data; +}; diff --git a/lib/cashu/types.ts b/lib/cashu/types.ts new file mode 100644 index 00000000..6fb0e00d --- /dev/null +++ b/lib/cashu/types.ts @@ -0,0 +1,127 @@ +/** + * Tags are part of the data in a NUT-10 secret and hold additional data committed to + * and can be used for feature extensions. + * + * Tags are arrays with two or more strings being `["key", "value1", "value2", ...]`. + * +Supported tags are: + + * - `sigflag`: determines whether outputs have to be signed as well + * - `n_sigs`: specifies the minimum number of valid signatures expected + * - `pubkeys`: are additional public keys that can provide signatures (allows multiple entries) + * - `locktime`: is the Unix timestamp of when the lock expires + * - `refund`: are optional refund public keys that can exclusively spend after locktime (allows multiple entries) + * + * @example + * ```typescript + * const tag: NUT10SecretTag = ["sigflag", "SIG_INPUTS"]; + * ``` + */ +export type NUT10SecretTag = [string, ...string[]]; + +/** + * CAUTION: If the mint does not support spending conditions or a specific kind + * of spending condition, proofs may be treated as a regular anyone-can-spend tokens. + * Applications need to make sure to check whether the mint supports a specific kind of + * spending condition by checking the mint's info endpoint. + */ +export const WELL_KNOWN_SECRET_KINDS = ['P2PK'] as const; + +/** + * the kind of the spending condition + */ +export type WellKnownSecretKind = (typeof WELL_KNOWN_SECRET_KINDS)[number]; + +/** + * The data from a parsed stringified NUT-10 secret + */ +export type NUT10SecretData = { + nonce: string; + data: string; + tags?: NUT10SecretTag[]; +}; + +/** + * The raw NUT-10 secret from parsing the proof secret that describes the spending conditions + * of the proof. + * @example + * ```json + * ["P2PK", { + * "nonce": "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f", + * "data": "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7", + * "tags": [["sigflag", "SIG_INPUTS"]] + * }] + * ``` + */ +export type ParsedNUT10Secret = [WellKnownSecretKind, NUT10SecretData]; + +/** + * A NUT-10 secret in a proof is stored as a JSON string of a tuple: + * [kind, {nonce, data, tags?}] + * + * When parsed, it is transformed into this object format. + * @example + * ```json + * { + * "secret": "[\"P2PK\", { + * \"nonce\": \"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\", + * \"data\": \"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\", + * \"tags\": [[\"sigflag\", \"SIG_INPUTS\"]] + * }]" + * } + * ``` + * + * Gets parsed into: + * ```json + * { + * "kind": "P2PK", + * "data": "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7", + * "nonce": "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f", + * "tags": [["sigflag", "SIG_INPUTS"]] + * } + * ``` + */ +export type NUT10Secret = { + /** + * well-known secret kind + * @example "P2PK" + */ + kind: WellKnownSecretKind; + /** + * Expresses the spending condition specific to each kind + * @example "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7" + */ + data: string; + /** + * A unique random string + * @example "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f" + */ + nonce: string; + /** + * Hold additional data committed to and can be used for feature extensions + * @example [["sigflag", "SIG_INPUTS"]] + */ + tags?: NUT10SecretTag[]; +}; + +/** + * A plain secret is a random string + * + * @see https://github.com/cashubtc/nuts/blob/main/00.md for plain string secret format + */ +export type PlainSecret = string; + +/** + * A proof secret can be either be a random string or a NUT-10 secret + * + * @see https://github.com/cashubtc/nuts/blob/main/10.md for NUT-10 secret format + * @see https://github.com/cashubtc/nuts/blob/main/00.md for plain string secret format + */ +export type ProofSecret = NUT10Secret | PlainSecret; + +/** + * A P2PK secret requires a valid signature for the given pubkey + * + * @see https://github.com/cashubtc/nuts/blob/main/11.md for Pay-to-Pub-Key (P2PK) spending condition + */ +export type P2PKSecret = NUT10Secret & { kind: 'P2PK' }; diff --git a/package.json b/package.json index c256ff75..e619c982 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "start": "We are running the build in node because atm we are using node as the build target." }, "dependencies": { + "@cashu/cashu-ts": "2.1.0", "@opensecret/react": "0.3.0", "@radix-ui/react-dialog": "1.1.4", "@radix-ui/react-dropdown-menu": "2.1.2", @@ -52,11 +53,13 @@ "tailwindcss-animate": "1.0.7", "use-dehydrated-state": "0.1.0", "vaul": "1.1.2", + "zod": "3.24.1", "zustand": "5.0.2" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@remix-run/dev": "2.15.0", + "@types/bun": "1.1.14", "@types/express": "5.0.0", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", diff --git a/test/cashu/proof.test.ts b/test/cashu/proof.test.ts new file mode 100644 index 00000000..8f8eb159 --- /dev/null +++ b/test/cashu/proof.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'bun:test'; +import type { Proof } from '@cashu/cashu-ts'; +import { getP2PKPubkeyFromProofs } from '../../lib/cashu'; + +describe('getP2PKPubkeyFromProofs', () => { + const proofWithP2PKSecret: Proof = { + amount: 1, + secret: + '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7","tags":[["sigflag","SIG_INPUTS"]]}]', + C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', + id: '009a1f293253e41e', + }; + + test('proof with P2PK secret should return the pubkey', () => { + expect(getP2PKPubkeyFromProofs([proofWithP2PKSecret])).toBe( + '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + ); + }); + + const proof2WithDifferentPubkey: Proof = { + amount: 1, + secret: + '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a8","tags":[["sigflag","SIG_INPUTS"]]}]', + C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', + id: '009a1f293253e41e', + }; + + test('proofs with different pubkeys should throw', () => { + expect(() => + getP2PKPubkeyFromProofs([proofWithP2PKSecret, proof2WithDifferentPubkey]), + ).toThrow(); + }); +}); diff --git a/test/cashu/secret.test.ts b/test/cashu/secret.test.ts new file mode 100644 index 00000000..bb37a294 --- /dev/null +++ b/test/cashu/secret.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from 'bun:test'; +import { parseSecret } from '../../lib/cashu/secret'; + +describe('parseSecret', () => { + test('should return a secret as described in NUT00', () => { + const s = parseSecret( + '859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f', + ); + expect(s).toBe( + '859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f', + ); + }); + + test('should throw if secret is not in WELL_KNOWN_SECRET_KINDS', () => { + expect(() => + parseSecret('["HTLC",{"nonce":"0","data":"0","tags":[]}]'), + ).toThrow(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 9d87dd37..064e82d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["@remix-run/node", "vite/client"], + "types": ["@remix-run/node", "vite/client", "bun-types"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", From 4e81dd885a38189dd19872dcabbf0d254489e987 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sat, 21 Dec 2024 21:23:51 -0800 Subject: [PATCH 2/7] move cashu lib into app/lib --- {lib => app/lib}/cashu/index.ts | 0 {lib => app/lib}/cashu/proof.ts | 0 {lib => app/lib}/cashu/secret.ts | 0 {lib => app/lib}/cashu/types.ts | 0 test/cashu/proof.test.ts | 2 +- test/cashu/secret.test.ts | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename {lib => app/lib}/cashu/index.ts (100%) rename {lib => app/lib}/cashu/proof.ts (100%) rename {lib => app/lib}/cashu/secret.ts (100%) rename {lib => app/lib}/cashu/types.ts (100%) diff --git a/lib/cashu/index.ts b/app/lib/cashu/index.ts similarity index 100% rename from lib/cashu/index.ts rename to app/lib/cashu/index.ts diff --git a/lib/cashu/proof.ts b/app/lib/cashu/proof.ts similarity index 100% rename from lib/cashu/proof.ts rename to app/lib/cashu/proof.ts diff --git a/lib/cashu/secret.ts b/app/lib/cashu/secret.ts similarity index 100% rename from lib/cashu/secret.ts rename to app/lib/cashu/secret.ts diff --git a/lib/cashu/types.ts b/app/lib/cashu/types.ts similarity index 100% rename from lib/cashu/types.ts rename to app/lib/cashu/types.ts diff --git a/test/cashu/proof.test.ts b/test/cashu/proof.test.ts index 8f8eb159..08bd8a28 100644 --- a/test/cashu/proof.test.ts +++ b/test/cashu/proof.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test'; import type { Proof } from '@cashu/cashu-ts'; -import { getP2PKPubkeyFromProofs } from '../../lib/cashu'; +import { getP2PKPubkeyFromProofs } from '~/lib/cashu'; describe('getP2PKPubkeyFromProofs', () => { const proofWithP2PKSecret: Proof = { diff --git a/test/cashu/secret.test.ts b/test/cashu/secret.test.ts index bb37a294..11dfc83d 100644 --- a/test/cashu/secret.test.ts +++ b/test/cashu/secret.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test'; -import { parseSecret } from '../../lib/cashu/secret'; +import { parseSecret } from '~/lib/cashu/secret'; describe('parseSecret', () => { test('should return a secret as described in NUT00', () => { From ae951a653fdf30417ecf28883e748f6b5e1d687e Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sat, 21 Dec 2024 21:54:34 -0800 Subject: [PATCH 3/7] annotate return type of getP2PKPubkeyFromProofs --- app/lib/cashu/proof.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/cashu/proof.ts b/app/lib/cashu/proof.ts index afba6e89..3d37ea9d 100644 --- a/app/lib/cashu/proof.ts +++ b/app/lib/cashu/proof.ts @@ -7,7 +7,7 @@ import { getP2PKPubkeyFromSecret } from './secret'; * @returns The pubkey in the P2PK secrets * @throws Error if there are multiple pubkeys in the list or if the secret is not a P2PK secret. */ -export const getP2PKPubkeyFromProofs = (proofs: Proof[]) => { +export const getP2PKPubkeyFromProofs = (proofs: Proof[]): string | null => { const pubkeys = [ ...new Set( proofs.map((p) => getP2PKPubkeyFromSecret(p.secret)).filter(Boolean), From c96c23861152dbfa18ebef0187ccd52a2b130779 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sun, 22 Dec 2024 18:33:11 -0800 Subject: [PATCH 4/7] chage `LongTimeout` type to be runtime agnositc --- app/lib/timeout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/timeout.ts b/app/lib/timeout.ts index 2638160c..2255011b 100644 --- a/app/lib/timeout.ts +++ b/app/lib/timeout.ts @@ -3,7 +3,7 @@ const maxSetTimeoutDelay = 2 ** 31 - 1; type LongTimeout = { - id: number | NodeJS.Timeout | null; // Tracks the latest timeout ID + id: ReturnType | null; // Tracks the latest timeout ID }; /** From 105c899fbf7005034f79cd75e7b2f8ccbb1056ae Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sun, 22 Dec 2024 19:33:48 -0800 Subject: [PATCH 5/7] add more tests --- test/cashu/proof.test.ts | 51 ++++++++++++++------- test/cashu/secret.test.ts | 93 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 22 deletions(-) diff --git a/test/cashu/proof.test.ts b/test/cashu/proof.test.ts index 08bd8a28..32d06bf3 100644 --- a/test/cashu/proof.test.ts +++ b/test/cashu/proof.test.ts @@ -2,32 +2,49 @@ import { describe, expect, test } from 'bun:test'; import type { Proof } from '@cashu/cashu-ts'; import { getP2PKPubkeyFromProofs } from '~/lib/cashu'; -describe('getP2PKPubkeyFromProofs', () => { - const proofWithP2PKSecret: Proof = { - amount: 1, - secret: - '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7","tags":[["sigflag","SIG_INPUTS"]]}]', - C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', - id: '009a1f293253e41e', - }; +const proofWithP2PKSecret: Proof = { + amount: 1, + secret: + '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7","tags":[["sigflag","SIG_INPUTS"]]}]', + C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', + id: '009a1f293253e41e', +}; + +const proof2WithDifferentPubkey: Proof = { + amount: 1, + secret: + '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a8","tags":[["sigflag","SIG_INPUTS"]]}]', + C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', + id: '009a1f293253e41e', +}; + +const proofWithPlainSecret: Proof = { + amount: 1, + secret: '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', + id: '009a1f293253e41e', +}; +describe('getP2PKPubkeyFromProofs', () => { test('proof with P2PK secret should return the pubkey', () => { expect(getP2PKPubkeyFromProofs([proofWithP2PKSecret])).toBe( '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', ); }); - const proof2WithDifferentPubkey: Proof = { - amount: 1, - secret: - '["P2PK",{"nonce":"0","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a8","tags":[["sigflag","SIG_INPUTS"]]}]', - C: '02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904', - id: '009a1f293253e41e', - }; - - test('proofs with different pubkeys should throw', () => { + test('proofs with multiple pubkeys should throw', () => { expect(() => getP2PKPubkeyFromProofs([proofWithP2PKSecret, proof2WithDifferentPubkey]), ).toThrow(); }); + + test('an empty array should return null', () => { + expect(getP2PKPubkeyFromProofs([])).toBeNull(); + }); + + test('should throw if any proofs have a plain secret', () => { + expect(() => + getP2PKPubkeyFromProofs([proofWithP2PKSecret, proofWithPlainSecret]), + ).toThrow('Secret is not a P2PK secret'); + }); }); diff --git a/test/cashu/secret.test.ts b/test/cashu/secret.test.ts index 11dfc83d..7d9982a9 100644 --- a/test/cashu/secret.test.ts +++ b/test/cashu/secret.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test'; -import { parseSecret } from '~/lib/cashu/secret'; +import { getP2PKPubkeyFromSecret, parseSecret } from '~/lib/cashu/secret'; describe('parseSecret', () => { test('should return a secret as described in NUT00', () => { @@ -11,9 +11,92 @@ describe('parseSecret', () => { ); }); - test('should throw if secret is not in WELL_KNOWN_SECRET_KINDS', () => { - expect(() => - parseSecret('["HTLC",{"nonce":"0","data":"0","tags":[]}]'), - ).toThrow(); + test('should successfully parse a valid NUT10 secret', () => { + const s = parseSecret( + '["P2PK",{"nonce":"abc123","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7","tags":[["sigflag","SIG_INPUTS"]]}]', + ); + expect(s).toEqual({ + kind: 'P2PK', + nonce: 'abc123', + data: '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + tags: [['sigflag', 'SIG_INPUTS']], + }); + }); + + describe('should not throw if', () => { + test('tags are not provided', () => { + const s = parseSecret( + '["P2PK",{"nonce":"abc123","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7"}]', + ); + expect(s).toEqual({ + kind: 'P2PK', + nonce: 'abc123', + data: '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + }); + }); + + test('a tag has multiple values', () => { + const s = parseSecret( + '["P2PK",{"nonce":"abc123","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7","tags":[["sigflag","a","b"]]}]', + ); + expect(s).toEqual({ + kind: 'P2PK', + nonce: 'abc123', + data: '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + tags: [['sigflag', 'a', 'b']], + }); + }); + }); + describe('should throw for invalid NUT10 secret if', () => { + test('secret is not in WELL_KNOWN_SECRET_KINDS', () => { + expect(() => + parseSecret('["HTLC",{"nonce":"0","data":"0","tags":[]}]'), + ).toThrow(); + }); + + test('nonce is not a string', () => { + expect(() => parseSecret('["P2PK",{"nonce":123,"data":"test"}]')).toThrow( + 'Invalid secret format', + ); + }); + + test('data is not a string', () => { + expect(() => + parseSecret('["P2PK",{"nonce":"test","data":false}]'), + ).toThrow('Invalid secret format'); + }); + + test('tags is not an array of string arrays', () => { + expect(() => + parseSecret( + '["P2PK",{"nonce":"test","data":"test","tags":["invalid"]}]', + ), + ).toThrow('Invalid secret format'); + }); + }); +}); + +describe('getP2PKPubkeyFromSecret', () => { + test('should return the public key from a P2PK secret', () => { + const pubkey = getP2PKPubkeyFromSecret( + '["P2PK",{"nonce":"abc123","data":"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7"}]', + ); + expect(pubkey).toBe( + '0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7', + ); + }); + + describe('should throw if', () => { + test('secret is not a P2PK secret', () => { + expect(() => getP2PKPubkeyFromSecret('not-a-p2pk-secret')).toThrow( + 'Secret is not a P2PK secret', + ); + }); + + test('secret is an invalid NUT10 secret', () => { + expect(() => + getP2PKPubkeyFromSecret('["P2PK",{"nonce":123,"data":"test"}]'), + ).toThrow('Invalid secret format'); + }); }); }); From e91f97dbe16605a7c7bd88f72c0f5750ac191cb4 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Sun, 22 Dec 2024 19:37:08 -0800 Subject: [PATCH 6/7] update tesdescription --- test/cashu/secret.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cashu/secret.test.ts b/test/cashu/secret.test.ts index 7d9982a9..1d9ee62e 100644 --- a/test/cashu/secret.test.ts +++ b/test/cashu/secret.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'; import { getP2PKPubkeyFromSecret, parseSecret } from '~/lib/cashu/secret'; describe('parseSecret', () => { - test('should return a secret as described in NUT00', () => { + test('should return the input value if input value is NUT00 plain secret', () => { const s = parseSecret( '859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f', ); From aa0cecccde75f33f26759b305cb8116d71de2e53 Mon Sep 17 00:00:00 2001 From: gudnuf <108303703+gudnuf@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:50:47 -0800 Subject: [PATCH 7/7] remove ParsedNUT10Secret and NUT10SecretData types (#233) * remove ParsedNUT10Secret and NUT10SecretData types * use safe json parsing * return success in safeJsonParse and use zod in guest-account-storage * move schemas to types and infer from zod * redefined types using the schemas --- app/features/user/guest-account-storage.ts | 45 +++----- app/lib/cashu/secret.ts | 51 ++------- app/lib/cashu/types.ts | 121 +++++++++++---------- app/lib/json.ts | 13 +++ 4 files changed, 106 insertions(+), 124 deletions(-) create mode 100644 app/lib/json.ts diff --git a/app/features/user/guest-account-storage.ts b/app/features/user/guest-account-storage.ts index 3b1c1656..cd0052db 100644 --- a/app/features/user/guest-account-storage.ts +++ b/app/features/user/guest-account-storage.ts @@ -1,44 +1,35 @@ +import { z } from 'zod'; +import { safeJsonParse } from '~/lib/json'; + const storageKey = 'guestAccount'; -type GuestAccountDetails = { - id: string; - password: string; -}; +const GuestAccountDetailsSchema = z.object({ + id: z.string(), + password: z.string(), +}); -const assertGuestAccountDetails = ( - value: unknown, -): value is GuestAccountDetails => { - return ( - value instanceof Object && - 'id' in value && - typeof value.id === 'string' && - 'password' in value && - typeof value.password === 'string' - ); -}; - -const safeJsonParse = (value: string): unknown | null => { - try { - return JSON.parse(value); - } catch { - return null; - } -}; +type GuestAccountDetails = z.infer; const getGuestAccount = (): GuestAccountDetails | null => { const dataString = localStorage.getItem(storageKey); if (!dataString) { return null; } - const dataObject = safeJsonParse(dataString); - if (!assertGuestAccountDetails(dataObject)) { + const parseResult = safeJsonParse(dataString); + if (!parseResult.success) { + return null; + } + const validationResult = GuestAccountDetailsSchema.safeParse( + parseResult.data, + ); + if (!validationResult.success) { console.error( 'Invalid guest account data found in the storage', - dataObject, + parseResult.data, ); return null; } - return dataObject; + return validationResult.data; }; const storeGuestAccount = (data: GuestAccountDetails) => { diff --git a/app/lib/cashu/secret.ts b/app/lib/cashu/secret.ts index 8caf229b..4fc692dc 100644 --- a/app/lib/cashu/secret.ts +++ b/app/lib/cashu/secret.ts @@ -1,32 +1,13 @@ -import { z } from 'zod'; +import { safeJsonParse } from '../json'; +import { RawNUT10SecretSchema } from './types'; import { type NUT10Secret, - type NUT10SecretData, - type NUT10SecretTag, type P2PKSecret, - type ParsedNUT10Secret, type PlainSecret, type ProofSecret, WELL_KNOWN_SECRET_KINDS, } from './types'; -const NUT10SecretTagSchema = z - .tuple([z.string(), z.string()]) - .rest(z.string()) satisfies z.ZodType; - -const NUT10SecretDataSchema = z.object({ - nonce: z.string(), - data: z.string(), - tags: z.array(NUT10SecretTagSchema).optional(), -}) satisfies z.ZodType; - -const WellKnownSecretKindSchema = z.enum(WELL_KNOWN_SECRET_KINDS); - -const NUT10SecretSchema = z.tuple([ - WellKnownSecretKindSchema, - NUT10SecretDataSchema, -]) satisfies z.ZodType; - /** * Type guard to check if asecret is a NUT-10 secret */ @@ -59,29 +40,21 @@ export const isPlainSecret = (secret: ProofSecret): secret is PlainSecret => { * @throws Error if the secret is a NUT-10 secret with an invalid format */ export const parseSecret = (secret: string): ProofSecret => { - let parsed: unknown; - try { - parsed = JSON.parse(secret); - } catch { - // If JSON parsing fails, assume it's a plain string secret + const parsed = safeJsonParse(secret); + if (!parsed.success) { + // if parsing fails, assume it's a plain string secret // as defined in NUT-00 return secret; } - try { - const validatedSecret = NUT10SecretSchema.parse(parsed); - const [kind, data] = validatedSecret; - - return { - kind, - ...data, - }; - } catch (error) { - if (error instanceof z.ZodError) { - throw new Error('Invalid secret format'); - } - throw error; + // if not a plain string, then,validate the parsed JSON is a valid NUT-10 secret + const validatedSecret = RawNUT10SecretSchema.safeParse(parsed.data); + if (!validatedSecret.success) { + throw new Error('Invalid secret format'); } + + const [kind, { nonce, data, tags }] = validatedSecret.data; + return { kind, nonce, data, tags }; }; /** diff --git a/app/lib/cashu/types.ts b/app/lib/cashu/types.ts index 6fb0e00d..7ef18bd6 100644 --- a/app/lib/cashu/types.ts +++ b/app/lib/cashu/types.ts @@ -1,3 +1,20 @@ +import { z } from 'zod'; + +/** + * CAUTION: If the mint does not support spending conditions or a specific kind + * of spending condition, proofs may be treated as a regular anyone-can-spend tokens. + * Applications need to make sure to check whether the mint supports a specific kind of + * spending condition by checking the mint's info endpoint. + */ +export const WELL_KNOWN_SECRET_KINDS = ['P2PK'] as const; + +const WellKnownSecretKindSchema = z.enum(WELL_KNOWN_SECRET_KINDS); + +export const NUT10SecretTagSchema = z + .array(z.string()) + .nonempty() + .refine((arr): arr is [string, ...string[]] => arr.length >= 1); + /** * Tags are part of the data in a NUT-10 secret and hold additional data committed to * and can be used for feature extensions. @@ -17,43 +34,30 @@ Supported tags are: * const tag: NUT10SecretTag = ["sigflag", "SIG_INPUTS"]; * ``` */ -export type NUT10SecretTag = [string, ...string[]]; - -/** - * CAUTION: If the mint does not support spending conditions or a specific kind - * of spending condition, proofs may be treated as a regular anyone-can-spend tokens. - * Applications need to make sure to check whether the mint supports a specific kind of - * spending condition by checking the mint's info endpoint. - */ -export const WELL_KNOWN_SECRET_KINDS = ['P2PK'] as const; +export type NUT10SecretTag = z.infer; -/** - * the kind of the spending condition - */ -export type WellKnownSecretKind = (typeof WELL_KNOWN_SECRET_KINDS)[number]; - -/** - * The data from a parsed stringified NUT-10 secret - */ -export type NUT10SecretData = { - nonce: string; - data: string; - tags?: NUT10SecretTag[]; -}; - -/** - * The raw NUT-10 secret from parsing the proof secret that describes the spending conditions - * of the proof. - * @example - * ```json - * ["P2PK", { - * "nonce": "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f", - * "data": "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7", - * "tags": [["sigflag", "SIG_INPUTS"]] - * }] - * ``` - */ -export type ParsedNUT10Secret = [WellKnownSecretKind, NUT10SecretData]; +export const NUT10SecretSchema = z.object({ + /** + * well-known secret kind + * @example "P2PK" + */ + kind: z.enum(WELL_KNOWN_SECRET_KINDS), + /** + * Expresses the spending condition specific to each kind + * @example "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7" + */ + data: z.string(), + /** + * A unique random string + * @example "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f" + */ + nonce: z.string(), + /** + * Hold additional data committed to and can be used for feature extensions + * @example [["sigflag", "SIG_INPUTS"]] + */ + tags: z.array(NUT10SecretTagSchema).optional(), +}); /** * A NUT-10 secret in a proof is stored as a JSON string of a tuple: @@ -81,28 +85,29 @@ export type ParsedNUT10Secret = [WellKnownSecretKind, NUT10SecretData]; * } * ``` */ -export type NUT10Secret = { - /** - * well-known secret kind - * @example "P2PK" - */ - kind: WellKnownSecretKind; - /** - * Expresses the spending condition specific to each kind - * @example "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7" - */ - data: string; - /** - * A unique random string - * @example "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f" - */ - nonce: string; - /** - * Hold additional data committed to and can be used for feature extensions - * @example [["sigflag", "SIG_INPUTS"]] - */ - tags?: NUT10SecretTag[]; -}; +export type NUT10Secret = z.infer; + +export const RawNUT10SecretSchema = z.tuple([ + WellKnownSecretKindSchema, + NUT10SecretSchema.omit({ kind: true }), +]); + +/** + * The raw data format of a NUT-10 secret as stored in a proof's secret field. + * JSON.parse(proof.secret) of a valid NUT-10 secret returns this type. + * @example + * ```json + * { + * "secret": "[\"P2PK\", {nonce: \"...", data: "...", tags: [["sigflag", "SIG_INPUTS"]]}] + * } + * ``` + * + * Gets parsed into: + * ```typescript + * const secret: RawNUT10Secret = ["P2PK", {nonce: "...", data: "...", tags: [["sigflag", "SIG_INPUTS"]]}] + * ``` + */ +export type RawNUT10Secret = z.infer; /** * A plain secret is a random string diff --git a/app/lib/json.ts b/app/lib/json.ts new file mode 100644 index 00000000..598ff8e3 --- /dev/null +++ b/app/lib/json.ts @@ -0,0 +1,13 @@ +/** + * Safely parse a JSON string. + * @returns Success and data if parsing is successful, or failure if it is not. + */ +export const safeJsonParse = ( + jsonString: string, +): { success: true; data: T } | { success: false } => { + try { + return { success: true, data: JSON.parse(jsonString) }; + } catch { + return { success: false }; + } +};