From 1554033a9ae30a81841132b680e72787d22064cb Mon Sep 17 00:00:00 2001 From: emjshrx Date: Sun, 18 Feb 2024 16:01:14 +0530 Subject: [PATCH 1/2] feat: add lookahead --- src/wallet/db/db.interface.ts | 3 ++- src/wallet/db/level/db.ts | 11 +++++++++-- src/wallet/db/level/layout.ts | 3 ++- src/wallet/wallet.ts | 20 ++++++++++++++++---- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/wallet/db/db.interface.ts b/src/wallet/db/db.interface.ts index d5800b2..b106dee 100644 --- a/src/wallet/db/db.interface.ts +++ b/src/wallet/db/db.interface.ts @@ -15,7 +15,8 @@ export type DbInterface = { encryptedChainCode: string, ): Promise; saveAddress(address: string, path: string): Promise; - getAddress(address: string): Promise; + getPathFromAddress(address: string): Promise; + getAddressFromPath(path: string): Promise; hasAddress(address: string): Promise; getReceiveDepth(): Promise; setReceiveDepth(depth: number): Promise; diff --git a/src/wallet/db/level/db.ts b/src/wallet/db/level/db.ts index a62a8ab..01d83fc 100644 --- a/src/wallet/db/level/db.ts +++ b/src/wallet/db/level/db.ts @@ -56,13 +56,20 @@ export class WalletDB implements DbInterface { } public async saveAddress(address: string, path: string): Promise { - await this.db.sublevel(wdb.A).put(address, path); + await Promise.all([ + this.db.sublevel(wdb.A).put(address, path), + this.db.sublevel(wdb.P).put(path, address), + ]); } - public async getAddress(address: string): Promise { + public async getPathFromAddress(address: string): Promise { return await this.db.sublevel(wdb.A).get(address); } + public async getAddressFromPath(path: string): Promise { + return await this.db.sublevel(wdb.P).get(path); + } + async hasAddress(address: string): Promise { return (await this.db.sublevel(wdb.A).get(address)) !== undefined; } diff --git a/src/wallet/db/level/layout.ts b/src/wallet/db/level/layout.ts index 3e6cd59..3f2ab1a 100644 --- a/src/wallet/db/level/layout.ts +++ b/src/wallet/db/level/layout.ts @@ -2,7 +2,8 @@ export const wdb = { V: 'V', // Version M: 'M', // Master key - A: 'A', // Address + A: 'A', // Address:Path + P: 'P', // Path:Address D: 'D', // Address depth C: 'C', // Coins SP: 'SP', // Silent payment address diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 1506ae3..320f7e4 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -20,9 +20,11 @@ const bip32 = BIP32Factory(ecc); export type WalletConfigOptions = { db: DbInterface; networkClient: NetworkInterface; + lookahead?: number; }; const DEFAULT_ENCRYPTION_PASSWORD = '12345678'; +const DEFAULT_LOOKAHEAD = 10; export class Wallet { private readonly db: DbInterface; @@ -30,10 +32,12 @@ export class Wallet { private masterKey: BIP32Interface; private receiveDepth: number = 0; private changeDepth: number = 0; + private lookahead: number; constructor(config: WalletConfigOptions) { this.db = config.db; this.network = config.networkClient; + this.lookahead = config.lookahead ?? DEFAULT_LOOKAHEAD; } async init(params?: { mnemonic?: string; password?: string }) { @@ -44,6 +48,9 @@ export class Wallet { const seed = mnemonicToSeedSync(mnemonic).toString('hex'); this.masterKey = bip32.fromSeed(Buffer.from(seed, 'hex')); this.setPassword(password ?? DEFAULT_ENCRYPTION_PASSWORD); + for (let i = 0; i < this.lookahead; i++) { + await this.deriveAddress(`m/84'/0'/0'/0/${i}`); + } } else { const { encryptedPrivateKey, encryptedChainCode } = await this.db.getMasterKey(); @@ -102,8 +109,11 @@ export class Wallet { } async deriveReceiveAddress(): Promise { - const path = `m/84'/0'/0'/0/${this.receiveDepth}`; - const address = await this.deriveAddress(path); + const nextPath = `m/84'/0'/0'/0/${this.receiveDepth + this.lookahead}`; + await this.deriveAddress(nextPath); + const address = await this.db.getAddressFromPath( + `m/84'/0'/0'/0/${this.receiveDepth}`, + ); this.receiveDepth++; return address; } @@ -133,7 +143,7 @@ export class Wallet { private async signTransaction(psbt: Psbt, coins: Coin[]): Promise { for (let index = 0; index < coins.length; index++) { - const path = await this.db.getAddress(coins[index].address); + const path = await this.db.getPathFromAddress(coins[index].address); const privateKey = this.masterKey.derivePath(path); psbt.signInput(index, privateKey); } @@ -237,7 +247,9 @@ export class Wallet { const privateKeys = ( await Promise.all( - selectedCoins.map((coin) => this.db.getAddress(coin.address)), + selectedCoins.map((coin) => + this.db.getPathFromAddress(coin.address), + ), ) ).map((path) => this.masterKey.derivePath(path)); const outpoints = selectedCoins.map((coin) => ({ From 96d3516a83f1c2bceb6a2c12dd9be89164165f7b Mon Sep 17 00:00:00 2001 From: emjshrx Date: Sun, 18 Feb 2024 16:40:05 +0530 Subject: [PATCH 2/2] test: lookahead test --- test/wallet.spec.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/wallet.spec.ts b/test/wallet.spec.ts index 8820718..90c047e 100644 --- a/test/wallet.spec.ts +++ b/test/wallet.spec.ts @@ -5,11 +5,12 @@ import { BitcoinRpcClient } from './helpers/bitcoin-rpc-client'; describe('Wallet', () => { let wallet: Wallet; let address: string; + let walletDB: WalletDB; let bitcoinRpcClient: BitcoinRpcClient; let silentPaymentAddress: string; beforeAll(async () => { - const walletDB = new WalletDB({ + walletDB = new WalletDB({ location: './test/wallet', }); @@ -31,11 +32,24 @@ describe('Wallet', () => { ); }); - it('should initialise the wallet', async () => { + it('should initialise the wallet and save lookahead addresses', async () => { await wallet.init({ mnemonic: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', }); + const allAddresses = await walletDB.getAllAddresses(); + expect(allAddresses).toStrictEqual([ + 'bcrt1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx', + 'bcrt1qgl5vlg0zdl7yvprgxj9fevsc6q6x5dmcvenxlt', + 'bcrt1qgswpjzsqgrm2qkfkf9kzqpw6642ptrgz4wwff7', + 'bcrt1qhxgzmkmwvrlwvlfn4qe57lx2qdfg8physujr0f', + 'bcrt1qm97vqzgj934vnaq9s53ynkyf9dgr05rat8p3ef', + 'bcrt1qncdts3qm2guw3hjstun7dd6t3689qg42eqsfxf', + 'bcrt1qnjg0jd8228aq7egyzacy8cys3knf9xvr3v5hfj', + 'bcrt1qnpzzqjzet8gd5gl8l6gzhuc4s9xv0djt8vazj8', + 'bcrt1qp59yckz4ae5c4efgw2s5wfyvrz0ala7rqr7utc', + 'bcrt1qtet8q6cd5vqm0zjfcfm8mfsydju0a29gq0p9sl', + ]); }); it('should set a new password, close and reopen the wallet with the same password', async () => { @@ -48,11 +62,15 @@ describe('Wallet', () => { it('should derive first receive address', async () => { address = await wallet.deriveReceiveAddress(); expect(address).toBe('bcrt1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx'); + const allAddresses = await walletDB.getAllAddresses(); + expect(allAddresses.length).toStrictEqual(11); }); it('should derive second receive address', async () => { const address = await wallet.deriveReceiveAddress(); expect(address).toBe('bcrt1qnjg0jd8228aq7egyzacy8cys3knf9xvr3v5hfj'); + const allAddresses = await walletDB.getAllAddresses(); + expect(allAddresses.length).toStrictEqual(12); }); it('should derive first change address', async () => {