From 243fad1d257edf982c945020495bdcc04234da5d Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 22 Jan 2026 14:36:51 +0100 Subject: [PATCH 1/6] feat: follow deterministic derivation spec --- packages/core/services/KeyRingService.ts | 11 ++--- .../core/test/unit/KeyRingService.test.ts | 48 +++++++++++++++++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/core/services/KeyRingService.ts b/packages/core/services/KeyRingService.ts index bc9c54ca..f02bc890 100644 --- a/packages/core/services/KeyRingService.ts +++ b/packages/core/services/KeyRingService.ts @@ -2,7 +2,7 @@ import type { Proof } from '@cashu/cashu-ts'; import type { Logger } from '@core/logging'; import type { KeyRingRepository } from '@core/repositories'; import type { Keypair } from '@core/models/Keypair'; -import { schnorr } from '@noble/curves/secp256k1.js'; +import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'; import { bytesToHex } from '@noble/curves/utils.js'; import { sha256 } from '@noble/hashes/sha2.js'; import type { SeedService } from '@core/services/SeedService.ts'; @@ -29,7 +29,7 @@ export class KeyRingService { const nextDerivationIndex = lastDerivationIndex + 1; const seed = await this.seedService.getSeed(); const hdKey = HDKey.fromMasterSeed(seed); - const derivationPath = `m/129373'/10'/0'/0'/${nextDerivationIndex}`; + const derivationPath = `m/129372'/10'/0'/0'/${nextDerivationIndex}`; const { privateKey: secretKey } = hdKey.derive(derivationPath); if (!secretKey) { throw new Error('Failed to derive secret key'); @@ -102,11 +102,10 @@ export class KeyRingService { /** * Converts a secret key to its corresponding public key in SEC1 compressed format. - * Note: schnorr.getPublicKey() returns a 32-byte x-only public key (BIP340). - * We prepend '02' to create a 33-byte SEC1 compressed format as expected by Cashu. + * Note: secp256k1.getPublicKey() returns a 33-byte compressed public key by default. */ private getPublicKeyHex(secretKey: Uint8Array): string { - const publicKey = schnorr.getPublicKey(secretKey); - return '02' + bytesToHex(publicKey); + const publicKey = secp256k1.getPublicKey(secretKey); + return bytesToHex(publicKey); } } diff --git a/packages/core/test/unit/KeyRingService.test.ts b/packages/core/test/unit/KeyRingService.test.ts index a57366bb..c8314276 100644 --- a/packages/core/test/unit/KeyRingService.test.ts +++ b/packages/core/test/unit/KeyRingService.test.ts @@ -3,7 +3,7 @@ import { KeyRingService } from '../../services/KeyRingService.ts'; import { SeedService } from '../../services/SeedService.ts'; import { MemoryKeyRingRepository } from '../../repositories/memory/MemoryKeyRingRepository.ts'; import { bytesToHex } from '@noble/curves/utils.js'; -import { schnorr } from '@noble/curves/secp256k1.js'; +import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'; import type { Proof } from '@cashu/cashu-ts'; // Mock seed for deterministic testing @@ -29,7 +29,7 @@ describe('KeyRingService', () => { expect(result.publicKeyHex).toBeDefined(); expect(result.publicKeyHex.length).toBe(66); // 32 bytes * 2 for hex + '02' prefix - expect(result.publicKeyHex.startsWith('02')).toBe(true); + expect(result.publicKeyHex.startsWith('02') || result.publicKeyHex.startsWith('03')).toBe(true); expect('secretKey' in result).toBe(false); // Verify it was stored in the repository @@ -162,8 +162,8 @@ describe('KeyRingService', () => { const secretKey = schnorr.utils.randomSecretKey(); const result = await service.addKeyPair(secretKey); - // The public key should have '02' prefix for compressed format - const publicKeyHex = '02' + bytesToHex(schnorr.getPublicKey(secretKey)); + // The public key should be in compressed format (starts with 02 or 03) + const publicKeyHex = bytesToHex(secp256k1.getPublicKey(secretKey)); const stored = await repo.getPersistedKeyPair(publicKeyHex); expect(stored).not.toBeNull(); @@ -484,4 +484,44 @@ describe('KeyRingService', () => { ); }); }); + + describe('P2PK Test Vectors', () => { + // Mnemonic: half depart obvious quality work element tank gorilla view sugar picture humble + // Seed (calculated via PBKDF2): dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8 + const TEST_VECTOR_SEED_HEX = + 'dd44ee516b0647e80b488e8dcc56d736a148f15276bef588b37057476d4b2b25780d3688a32b37353d6995997842c0fd8b412475c891c16310471fbc86dcbda8'; + + // Convert hex seed to Uint8Array + const seedBytes = new Uint8Array( + TEST_VECTOR_SEED_HEX.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)), + ); + + const EXPECTED_PUBKEYS = [ + '03381fbf0996b81d49c35bae17a70d71db9a9e802b1af5c2516fc90381f4741e06', // index 0 + '039bbb7a9cd234da13a113cdd8e037a25c66bbf3a77139d652786a1d7e9d73e600', // index 1 + '02ffd52ed54761750d75b67342544cc8da8a0994f84c46d546e0ab574dd3651a29', // index 2 + '02751ab780960ff177c2300e440fddc0850238a78782a1cab7b0ae03c41978d92d', // index 3 + '0391a9ba1c3caf39ca0536d44419a6ceeda922ee61aa651a72a60171499c02b423', // index 4 + ]; + + let vectorService: KeyRingService; + let vectorRepo: MemoryKeyRingRepository; + + beforeEach(() => { + vectorRepo = new MemoryKeyRingRepository(); + const vectorSeedService = new SeedService(async () => seedBytes); + vectorService = new KeyRingService(vectorRepo, vectorSeedService); + }); + + it('generates correct public keys for test vectors', async () => { + for (let i = 0; i < EXPECTED_PUBKEYS.length; i++) { + const keyPair = await vectorService.generateNewKeyPair(); + expect(keyPair.publicKeyHex).toBe(EXPECTED_PUBKEYS[i]); + + // Also verify the derivation index is correct in storage + const stored = await vectorRepo.getPersistedKeyPair(keyPair.publicKeyHex); + expect(stored?.derivationIndex).toBe(i); + } + }); + }); }); From f8ec2fc8170494de685000457efee6ff061c0d9f Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 26 Jan 2026 15:45:15 +0100 Subject: [PATCH 2/6] migration for older keysets --- packages/adapter-tests/src/migrations.ts | 76 +++++++++++++++++++ packages/core/models/Keypair.ts | 1 + .../memory/MemoryKeyRingRepository.ts | 10 ++- packages/core/services/KeyRingService.ts | 1 + .../core/test/unit/KeyRingService.test.ts | 5 +- .../src/repositories/KeyRingRepository.ts | 27 +++++-- packages/expo-sqlite/src/schema.ts | 11 +++ packages/indexeddb/src/lib/schema.ts | 30 ++++++++ .../src/repositories/KeyRingRepository.ts | 13 +++- .../src/repositories/KeyRingRepository.ts | 27 +++++-- packages/sqlite3/src/schema.ts | 15 ++++ .../src/test/migrations.test.ts.disabled | 2 +- 12 files changed, 198 insertions(+), 20 deletions(-) diff --git a/packages/adapter-tests/src/migrations.ts b/packages/adapter-tests/src/migrations.ts index 583220cf..90306886 100644 --- a/packages/adapter-tests/src/migrations.ts +++ b/packages/adapter-tests/src/migrations.ts @@ -83,6 +83,13 @@ export type MigrationTestOptions { + it('should backfill derivationPath from derivationIndex', async () => { + const { dispose, runRemainingMigrations, rawInsert, rawQuery } = + await createRepositoriesAtMigration(keypairDerivationPathMigration); + + try { + await rawInsert('coco_cashu_keypairs', { + publicKey: 'pk-derivation-0', + secretKey: '00', + createdAt: 1000, + derivationIndex: 0, + }); + + await rawInsert('coco_cashu_keypairs', { + publicKey: 'pk-derivation-7', + secretKey: '01', + createdAt: 1001, + derivationIndex: 7, + }); + + await runRemainingMigrations(); + + const keypairs = await rawQuery<{ + publicKey: string; + derivationIndex: number | null; + derivationPath: string | null; + }>('coco_cashu_keypairs'); + + const keypair0 = keypairs.find((kp) => kp.publicKey === 'pk-derivation-0'); + const keypair7 = keypairs.find((kp) => kp.publicKey === 'pk-derivation-7'); + + expect(keypair0?.derivationPath).toBe("m/129373'/10'/0'/0'/0"); + expect(keypair7?.derivationPath).toBe("m/129373'/10'/0'/0'/7"); + } finally { + await dispose(); + } + }); + + it('should not override existing derivationPath', async () => { + const { dispose, runRemainingMigrations, rawInsert, rawQuery } = + await createRepositoriesAtMigration(keypairDerivationPathMigration); + + try { + await rawInsert('coco_cashu_keypairs', { + publicKey: 'pk-derivation-existing', + secretKey: '02', + createdAt: 1002, + derivationIndex: 3, + derivationPath: "m/129373'/10'/0'/0'/999", + }); + + await runRemainingMigrations(); + + const keypairs = await rawQuery<{ + publicKey: string; + derivationPath: string | null; + }>('coco_cashu_keypairs', { publicKey: 'pk-derivation-existing' }); + + expect(keypairs).toHaveLength(1); + expect(keypairs[0].derivationPath).toBe("m/129373'/10'/0'/0'/999"); + } finally { + await dispose(); + } + }); + }); + } }); } diff --git a/packages/core/models/Keypair.ts b/packages/core/models/Keypair.ts index 659708c1..c4876789 100644 --- a/packages/core/models/Keypair.ts +++ b/packages/core/models/Keypair.ts @@ -2,4 +2,5 @@ export type Keypair = { publicKeyHex: string; secretKey: Uint8Array; derivationIndex?: number; + derivationPath?: string; }; diff --git a/packages/core/repositories/memory/MemoryKeyRingRepository.ts b/packages/core/repositories/memory/MemoryKeyRingRepository.ts index d50d95dd..55fe3ff0 100644 --- a/packages/core/repositories/memory/MemoryKeyRingRepository.ts +++ b/packages/core/repositories/memory/MemoryKeyRingRepository.ts @@ -16,16 +16,22 @@ export class MemoryKeyRingRepository implements KeyRingRepository { // Preserve existing derivationIndex if new one is not provided let derivationIndex = keyPair.derivationIndex; - if (derivationIndex == null) { + let derivationPath = keyPair.derivationPath; + + if (derivationIndex == null || derivationPath == null) { const existing = this.keyPairs.get(keyPair.publicKeyHex); - if (existing?.derivationIndex != null) { + if (derivationIndex == null && existing?.derivationIndex != null) { derivationIndex = existing.derivationIndex; } + if (derivationPath == null && existing?.derivationPath != null) { + derivationPath = existing.derivationPath; + } } this.keyPairs.set(keyPair.publicKeyHex, { ...keyPair, derivationIndex, + derivationPath, }); } diff --git a/packages/core/services/KeyRingService.ts b/packages/core/services/KeyRingService.ts index f02bc890..296eb38a 100644 --- a/packages/core/services/KeyRingService.ts +++ b/packages/core/services/KeyRingService.ts @@ -39,6 +39,7 @@ export class KeyRingService { publicKeyHex, secretKey, derivationIndex: nextDerivationIndex, + derivationPath, }); this.logger?.debug('New key pair generated', { publicKeyHex }); if (options?.dumpSecretKey) { diff --git a/packages/core/test/unit/KeyRingService.test.ts b/packages/core/test/unit/KeyRingService.test.ts index c8314276..fc05ab06 100644 --- a/packages/core/test/unit/KeyRingService.test.ts +++ b/packages/core/test/unit/KeyRingService.test.ts @@ -28,7 +28,7 @@ describe('KeyRingService', () => { const result = await service.generateNewKeyPair(); expect(result.publicKeyHex).toBeDefined(); - expect(result.publicKeyHex.length).toBe(66); // 32 bytes * 2 for hex + '02' prefix + expect(result.publicKeyHex.length).toBe(66); // 33 bytes * 2 for hex expect(result.publicKeyHex.startsWith('02') || result.publicKeyHex.startsWith('03')).toBe(true); expect('secretKey' in result).toBe(false); @@ -80,6 +80,9 @@ describe('KeyRingService', () => { expect(stored1?.derivationIndex).toBe(0); expect(stored2?.derivationIndex).toBe(1); expect(stored3?.derivationIndex).toBe(2); + expect(stored1?.derivationPath).toBe("m/129372'/10'/0'/0'/0"); + expect(stored2?.derivationPath).toBe("m/129372'/10'/0'/0'/1"); + expect(stored3?.derivationPath).toBe("m/129372'/10'/0'/0'/2"); }); it('derives deterministic keys from the same seed', async () => { diff --git a/packages/expo-sqlite/src/repositories/KeyRingRepository.ts b/packages/expo-sqlite/src/repositories/KeyRingRepository.ts index 316eac61..afcbfd44 100644 --- a/packages/expo-sqlite/src/repositories/KeyRingRepository.ts +++ b/packages/expo-sqlite/src/repositories/KeyRingRepository.ts @@ -14,8 +14,9 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; + derivationPath: string | null; }>( - 'SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs WHERE publicKey = ? LIMIT 1', + 'SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs WHERE publicKey = ? LIMIT 1', [publicKey], ); if (!row) return null; @@ -26,6 +27,7 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( @@ -40,12 +42,19 @@ export class ExpoKeyRingRepository implements KeyRingRepository { const secretKeyHex = bytesToHex(keyPair.secretKey); await this.db.run( - `INSERT INTO coco_cashu_keypairs (publicKey, secretKey, createdAt, derivationIndex) - VALUES (?, ?, ?, ?) + `INSERT INTO coco_cashu_keypairs (publicKey, secretKey, createdAt, derivationIndex, derivationPath) + VALUES (?, ?, ?, ?, ?) ON CONFLICT(publicKey) DO UPDATE SET secretKey=excluded.secretKey, - derivationIndex=COALESCE(excluded.derivationIndex, coco_cashu_keypairs.derivationIndex)`, - [keyPair.publicKeyHex, secretKeyHex, Date.now(), keyPair.derivationIndex ?? null], + derivationIndex=COALESCE(excluded.derivationIndex, coco_cashu_keypairs.derivationIndex), + derivationPath=COALESCE(excluded.derivationPath, coco_cashu_keypairs.derivationPath)`, + [ + keyPair.publicKeyHex, + secretKeyHex, + Date.now(), + keyPair.derivationIndex ?? null, + keyPair.derivationPath ?? null, + ], ); } @@ -58,7 +67,8 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; - }>('SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs'); + derivationPath: string | null; + }>('SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs'); return rows.map((row) => { try { @@ -67,6 +77,7 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( @@ -83,8 +94,9 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; + derivationPath: string | null; }>( - 'SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs ORDER BY createdAt DESC LIMIT 1', + 'SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs ORDER BY createdAt DESC LIMIT 1', ); if (!row) return null; @@ -94,6 +106,7 @@ export class ExpoKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( diff --git a/packages/expo-sqlite/src/schema.ts b/packages/expo-sqlite/src/schema.ts index 1185bfab..9a6ceb2d 100644 --- a/packages/expo-sqlite/src/schema.ts +++ b/packages/expo-sqlite/src/schema.ts @@ -356,6 +356,17 @@ const MIGRATIONS: readonly Migration[] = [ ALTER TABLE coco_cashu_melt_operations ADD COLUMN effectiveFee INTEGER; `, }, + { + id: '017_keypair_derivation_path', + run: async (db: ExpoSqliteDb) => { + await db.exec('ALTER TABLE coco_cashu_keypairs ADD COLUMN derivationPath TEXT'); + await db.exec(` + UPDATE coco_cashu_keypairs + SET derivationPath = 'm/129373''/10''/0''/0''/' || derivationIndex + WHERE derivationIndex IS NOT NULL AND derivationPath IS NULL + `); + }, + }, ]; // Export for testing diff --git a/packages/indexeddb/src/lib/schema.ts b/packages/indexeddb/src/lib/schema.ts index 6f490cd4..f641855d 100644 --- a/packages/indexeddb/src/lib/schema.ts +++ b/packages/indexeddb/src/lib/schema.ts @@ -386,4 +386,34 @@ export async function ensureSchema(db: IdbDb): Promise { mint.updatedAt = 0; }); }); + + // Version 15: Add derivationPath to keypairs + db.version(15) + .stores({ + coco_cashu_mints: '&mintUrl, name, updatedAt, trusted', + coco_cashu_keysets: '&[mintUrl+id], mintUrl, id, updatedAt, unit', + coco_cashu_counters: '&[mintUrl+keysetId]', + coco_cashu_proofs: + '&[mintUrl+secret], [mintUrl+state], [mintUrl+id+state], state, mintUrl, id, usedByOperationId, createdByOperationId', + coco_cashu_mint_quotes: '&[mintUrl+quote], state, mintUrl', + coco_cashu_melt_quotes: '&[mintUrl+quote], state, mintUrl', + coco_cashu_history: + '++id, mintUrl, type, createdAt, [mintUrl+quoteId+type], [mintUrl+operationId]', + coco_cashu_keypairs: '&publicKey, createdAt, derivationIndex, derivationPath', + coco_cashu_send_operations: '&id, state, mintUrl', + coco_cashu_melt_operations: '&id, state, mintUrl, [mintUrl+quoteId]', + coco_cashu_receive_operations: '&id, state, mintUrl', + }) + .upgrade(async (tx) => { + const table = tx.table('coco_cashu_keypairs'); + const keypairs = await table.toArray(); + + for (const keypair of keypairs) { + if (keypair.derivationPath == null && keypair.derivationIndex != null) { + await table.update(keypair.publicKey, { + derivationPath: `m/129373'/10'/0'/0'/${keypair.derivationIndex}`, + }); + } + } + }); } diff --git a/packages/indexeddb/src/repositories/KeyRingRepository.ts b/packages/indexeddb/src/repositories/KeyRingRepository.ts index 9a1c254f..9d293f48 100644 --- a/packages/indexeddb/src/repositories/KeyRingRepository.ts +++ b/packages/indexeddb/src/repositories/KeyRingRepository.ts @@ -7,6 +7,7 @@ interface KeypairRow { secretKey: string; createdAt: number; derivationIndex?: number; + derivationPath?: string; } export class IdbKeyRingRepository implements KeyRingRepository { @@ -29,6 +30,7 @@ export class IdbKeyRingRepository implements KeyRingRepository { publicKeyHex: keypairRow.publicKey, secretKey: secretKeyBytes, derivationIndex: keypairRow.derivationIndex, + derivationPath: keypairRow.derivationPath, }; } @@ -38,11 +40,15 @@ export class IdbKeyRingRepository implements KeyRingRepository { // Preserve existing derivationIndex if new one is not provided let derivationIndex = keyPair.derivationIndex; - if (derivationIndex == null) { + let derivationPath = keyPair.derivationPath; + if (derivationIndex == null || derivationPath == null) { const existing = (await table.get(keyPair.publicKeyHex)) as KeypairRow | undefined; - if (existing?.derivationIndex != null) { + if (derivationIndex == null && existing?.derivationIndex != null) { derivationIndex = existing.derivationIndex; } + if (derivationPath == null && existing?.derivationPath != null) { + derivationPath = existing.derivationPath; + } } await table.put({ @@ -50,6 +56,7 @@ export class IdbKeyRingRepository implements KeyRingRepository { secretKey: secretKeyHex, createdAt: Date.now(), derivationIndex, + derivationPath, }); } @@ -66,6 +73,7 @@ export class IdbKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: hexToBytes(row.secretKey), derivationIndex: row.derivationIndex, + derivationPath: row.derivationPath, })); } @@ -79,6 +87,7 @@ export class IdbKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: hexToBytes(row.secretKey), derivationIndex: row.derivationIndex, + derivationPath: row.derivationPath, }; } diff --git a/packages/sqlite3/src/repositories/KeyRingRepository.ts b/packages/sqlite3/src/repositories/KeyRingRepository.ts index 7e7dcbb1..83c91744 100644 --- a/packages/sqlite3/src/repositories/KeyRingRepository.ts +++ b/packages/sqlite3/src/repositories/KeyRingRepository.ts @@ -14,8 +14,9 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; + derivationPath: string | null; }>( - 'SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs WHERE publicKey = ? LIMIT 1', + 'SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs WHERE publicKey = ? LIMIT 1', [publicKey], ); if (!row) return null; @@ -26,6 +27,7 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( @@ -40,12 +42,19 @@ export class SqliteKeyRingRepository implements KeyRingRepository { const secretKeyHex = bytesToHex(keyPair.secretKey); await this.db.run( - `INSERT INTO coco_cashu_keypairs (publicKey, secretKey, createdAt, derivationIndex) - VALUES (?, ?, ?, ?) + `INSERT INTO coco_cashu_keypairs (publicKey, secretKey, createdAt, derivationIndex, derivationPath) + VALUES (?, ?, ?, ?, ?) ON CONFLICT(publicKey) DO UPDATE SET secretKey=excluded.secretKey, - derivationIndex=COALESCE(excluded.derivationIndex, coco_cashu_keypairs.derivationIndex)`, - [keyPair.publicKeyHex, secretKeyHex, Date.now(), keyPair.derivationIndex ?? null], + derivationIndex=COALESCE(excluded.derivationIndex, coco_cashu_keypairs.derivationIndex), + derivationPath=COALESCE(excluded.derivationPath, coco_cashu_keypairs.derivationPath)`, + [ + keyPair.publicKeyHex, + secretKeyHex, + Date.now(), + keyPair.derivationIndex ?? null, + keyPair.derivationPath ?? null, + ], ); } @@ -58,7 +67,8 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; - }>('SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs'); + derivationPath: string | null; + }>('SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs'); return rows.map((row) => { try { @@ -67,6 +77,7 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( @@ -83,8 +94,9 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKey: string; secretKey: string; derivationIndex: number | null; + derivationPath: string | null; }>( - 'SELECT publicKey, secretKey, derivationIndex FROM coco_cashu_keypairs ORDER BY createdAt DESC LIMIT 1', + 'SELECT publicKey, secretKey, derivationIndex, derivationPath FROM coco_cashu_keypairs ORDER BY createdAt DESC LIMIT 1', ); if (!row) return null; @@ -94,6 +106,7 @@ export class SqliteKeyRingRepository implements KeyRingRepository { publicKeyHex: row.publicKey, secretKey: secretKeyBytes, derivationIndex: row.derivationIndex ?? undefined, + derivationPath: row.derivationPath ?? undefined, }; } catch (error) { throw new Error( diff --git a/packages/sqlite3/src/schema.ts b/packages/sqlite3/src/schema.ts index c34dedda..cc1c5c09 100644 --- a/packages/sqlite3/src/schema.ts +++ b/packages/sqlite3/src/schema.ts @@ -356,6 +356,21 @@ const MIGRATIONS: readonly Migration[] = [ ALTER TABLE coco_cashu_melt_operations ADD COLUMN effectiveFee INTEGER; `, }, + { + id: '017_keypair_derivation_path', + run: async (db: SqliteDb) => { + // Add derivationPath column + await db.exec('ALTER TABLE coco_cashu_keypairs ADD COLUMN derivationPath TEXT'); + + // Backfill derivationPath for existing derived keys with the legacy coin type (129373) + // We construct the path using string concatenation: 'm/129373\'/10\'/0\'/0\'/' || derivationIndex + await db.exec(` + UPDATE coco_cashu_keypairs + SET derivationPath = 'm/129373''/10''/0''/0''/' || derivationIndex + WHERE derivationIndex IS NOT NULL AND derivationPath IS NULL + `); + }, + }, ]; // Export for testing diff --git a/packages/sqlite3/src/test/migrations.test.ts.disabled b/packages/sqlite3/src/test/migrations.test.ts.disabled index fa173732..099ae253 100644 --- a/packages/sqlite3/src/test/migrations.test.ts.disabled +++ b/packages/sqlite3/src/test/migrations.test.ts.disabled @@ -70,6 +70,7 @@ runMigrationTests( createRepositories, createRepositoriesAtMigration, completedToFinalizedMigration: '010_rename_completed_to_finalized', + keypairDerivationPathMigration: '012_keypair_derivation_path', logger: { info: (message, data) => console.log(`[INFO] ${message}`, data ?? ''), error: (message, data) => console.error(`[ERROR] ${message}`, data ?? ''), @@ -78,4 +79,3 @@ runMigrationTests( { describe, it, expect, beforeEach, afterEach }, ); - From e3febf63c2e7fc01fdc6ca352452372ac549aa3d Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 26 Jan 2026 16:44:51 +0100 Subject: [PATCH 3/6] type check fix --- packages/adapter-tests/src/migrations.ts | 4 +++- packages/adapter-tests/tsconfig.json | 2 +- packages/core/test/unit/KeyRingService.test.ts | 6 ++++-- packages/expo-sqlite/tsconfig.json | 1 + packages/indexeddb/src/test/integration.test.ts | 6 ++++++ packages/indexeddb/vitest.config.ts | 2 ++ packages/sqlite3/tsconfig.json | 1 + 7 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/adapter-tests/src/migrations.ts b/packages/adapter-tests/src/migrations.ts index 90306886..65a162b0 100644 --- a/packages/adapter-tests/src/migrations.ts +++ b/packages/adapter-tests/src/migrations.ts @@ -631,7 +631,9 @@ export function runMigrationTests('coco_cashu_keypairs', { publicKey: 'pk-derivation-existing' }); expect(keypairs).toHaveLength(1); - expect(keypairs[0].derivationPath).toBe("m/129373'/10'/0'/0'/999"); + const keypair = keypairs[0]; + expect(keypair).toBeDefined(); + expect(keypair?.derivationPath).toBe("m/129373'/10'/0'/0'/999"); } finally { await dispose(); } diff --git a/packages/adapter-tests/tsconfig.json b/packages/adapter-tests/tsconfig.json index 112155a4..47a2cc4a 100644 --- a/packages/adapter-tests/tsconfig.json +++ b/packages/adapter-tests/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", diff --git a/packages/core/test/unit/KeyRingService.test.ts b/packages/core/test/unit/KeyRingService.test.ts index fc05ab06..7c64373f 100644 --- a/packages/core/test/unit/KeyRingService.test.ts +++ b/packages/core/test/unit/KeyRingService.test.ts @@ -517,13 +517,15 @@ describe('KeyRingService', () => { }); it('generates correct public keys for test vectors', async () => { - for (let i = 0; i < EXPECTED_PUBKEYS.length; i++) { + let i = 0; + for (const expected of EXPECTED_PUBKEYS) { const keyPair = await vectorService.generateNewKeyPair(); - expect(keyPair.publicKeyHex).toBe(EXPECTED_PUBKEYS[i]); + expect(keyPair.publicKeyHex).toBe(expected); // Also verify the derivation index is correct in storage const stored = await vectorRepo.getPersistedKeyPair(keyPair.publicKeyHex); expect(stored?.derivationIndex).toBe(i); + i++; } }); }); diff --git a/packages/expo-sqlite/tsconfig.json b/packages/expo-sqlite/tsconfig.json index d0a3fd2c..b712e984 100644 --- a/packages/expo-sqlite/tsconfig.json +++ b/packages/expo-sqlite/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "lib": ["ESNext"], + "types": ["bun-types"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", diff --git a/packages/indexeddb/src/test/integration.test.ts b/packages/indexeddb/src/test/integration.test.ts index 395a6333..cf05ca69 100644 --- a/packages/indexeddb/src/test/integration.test.ts +++ b/packages/indexeddb/src/test/integration.test.ts @@ -3,6 +3,12 @@ import { runIntegrationTests } from 'coco-cashu-adapter-tests'; import { IndexedDbRepositories } from '../index.ts'; import { ConsoleLogger, type Logger } from 'coco-cashu-core'; +declare global { + interface ImportMeta { + env: Record; + } +} + const mintUrl = import.meta.env.VITE_MINT_URL || 'http://localhost:3338'; if (!mintUrl) { diff --git a/packages/indexeddb/vitest.config.ts b/packages/indexeddb/vitest.config.ts index e4f8d5c9..48f2adce 100644 --- a/packages/indexeddb/vitest.config.ts +++ b/packages/indexeddb/vitest.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'vitest/config'; +declare const process: { env: Record }; + // Determine which browsers to test based on environment // In CI, test all browsers. Locally, default to just chromium for speed. const browsers = process.env.CI diff --git a/packages/sqlite3/tsconfig.json b/packages/sqlite3/tsconfig.json index 82fa2de3..42e0cc49 100644 --- a/packages/sqlite3/tsconfig.json +++ b/packages/sqlite3/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { // Environment setup & latest features "lib": ["ESNext"], + "types": ["bun-types"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", From a775ff5d48ba807eb5e5ca9269427def0e709388 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 26 Jan 2026 16:52:04 +0100 Subject: [PATCH 4/6] fix: unecesary types --- packages/expo-sqlite/tsconfig.json | 1 - packages/sqlite3/tsconfig.json | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/expo-sqlite/tsconfig.json b/packages/expo-sqlite/tsconfig.json index b712e984..d0a3fd2c 100644 --- a/packages/expo-sqlite/tsconfig.json +++ b/packages/expo-sqlite/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "lib": ["ESNext"], - "types": ["bun-types"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", diff --git a/packages/sqlite3/tsconfig.json b/packages/sqlite3/tsconfig.json index 42e0cc49..82fa2de3 100644 --- a/packages/sqlite3/tsconfig.json +++ b/packages/sqlite3/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { // Environment setup & latest features "lib": ["ESNext"], - "types": ["bun-types"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", From 9bfe985ccf85d494fec7b6a406918a50ae308415 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Tue, 27 Jan 2026 15:22:13 +0100 Subject: [PATCH 5/6] fix: typecheck --- packages/indexeddb/src/test/integration.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/indexeddb/src/test/integration.test.ts b/packages/indexeddb/src/test/integration.test.ts index cf05ca69..395a6333 100644 --- a/packages/indexeddb/src/test/integration.test.ts +++ b/packages/indexeddb/src/test/integration.test.ts @@ -3,12 +3,6 @@ import { runIntegrationTests } from 'coco-cashu-adapter-tests'; import { IndexedDbRepositories } from '../index.ts'; import { ConsoleLogger, type Logger } from 'coco-cashu-core'; -declare global { - interface ImportMeta { - env: Record; - } -} - const mintUrl = import.meta.env.VITE_MINT_URL || 'http://localhost:3338'; if (!mintUrl) { From 6883df6db1efa5e1d61397729ee2908fad721812 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Fri, 13 Mar 2026 12:26:06 +0100 Subject: [PATCH 6/6] change derivation --- packages/adapter-tests/src/migrations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adapter-tests/src/migrations.ts b/packages/adapter-tests/src/migrations.ts index 65a162b0..6cc7cdef 100644 --- a/packages/adapter-tests/src/migrations.ts +++ b/packages/adapter-tests/src/migrations.ts @@ -85,8 +85,8 @@ export type MigrationTestOptions