-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create cashu lib with bun tests and ci
- Loading branch information
Showing
10 changed files
with
334 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './proof'; | ||
export * from './secret'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<NUT10SecretTag>; | ||
|
||
const NUT10SecretDataSchema = z.object({ | ||
nonce: z.string(), | ||
data: z.string(), | ||
tags: z.array(NUT10SecretTagSchema).optional(), | ||
}) satisfies z.ZodType<NUT10SecretData>; | ||
|
||
const WellKnownSecretKindSchema = z.enum(WELL_KNOWN_SECRET_KINDS); | ||
|
||
const NUT10SecretSchema = z.tuple([ | ||
WellKnownSecretKindSchema, | ||
NUT10SecretDataSchema, | ||
]) satisfies z.ZodType<ParsedNUT10Secret>; | ||
|
||
/** | ||
* 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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`: <str> determines whether outputs have to be signed as well | ||
* - `n_sigs`: <int> specifies the minimum number of valid signatures expected | ||
* - `pubkeys`: <hex_str> are additional public keys that can provide signatures (allows multiple entries) | ||
* - `locktime`: <int> is the Unix timestamp of when the lock expires | ||
* - `refund`: <hex_str> 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' }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters