diff --git a/.gitignore b/.gitignore index 0e7e1fe..2abd131 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ coverage # test test/wallet-db +test/wallet diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 4470800..347ed6c 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -1 +1,76 @@ -export class Wallet {} +import { DbInterface } from './db'; +import { NetworkInterface } from './network'; +import { mnemonicToSeedSync } from 'bip39'; +import { payments } from 'bitcoinjs-lib'; +import BIP32Factory from 'bip32'; +import * as ecc from 'tiny-secp256k1'; + +const bip32 = BIP32Factory(ecc); + +export type WalletConfigOptions = { + db: DbInterface; + networkClient: NetworkInterface; +}; + +export class Wallet { + private readonly db: DbInterface; + private readonly network: NetworkInterface; + private seed: string; + private receiveDepth: number = 0; + private changeDepth: number = 0; + + constructor(config: WalletConfigOptions) { + this.db = config.db; + this.network = config.networkClient; + this.seed = mnemonicToSeedSync( + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + ).toString('hex'); + } + + async init() { + await this.db.open(); + } + + async close() { + await this.db.setReceiveDepth(this.receiveDepth); + await this.db.setChangeDepth(this.changeDepth); + + await this.db.close(); + } + + async load(mnemonic?: string) { + if (mnemonic) { + this.seed = mnemonicToSeedSync(mnemonic).toString('hex'); + await this.db.setSeed(this.seed); + } else { + this.seed = await this.db.getSeed(); + } + } + + private deriveAddress(path: string): string { + const master = bip32.fromSeed(Buffer.from(this.seed, 'hex')); + const child = master.derivePath(path); + const { address } = payments.p2wpkh({ + pubkey: child.publicKey, + network: this.network.network, + }); + + return address!; + } + + async deriveReceiveAddress(): Promise { + const path = `m/84'/0'/0'/0/${this.receiveDepth}`; + const address = this.deriveAddress(path); + await this.db.saveAddress(address, path); + this.receiveDepth++; + return address; + } + + async deriveChangeAddress(): Promise { + const path = `m/84'/0'/0'/1/${this.changeDepth}`; + const address = this.deriveAddress(path); + await this.db.saveAddress(address, path); + this.changeDepth++; + return address; + } +} diff --git a/test/wallet.spec.ts b/test/wallet.spec.ts new file mode 100644 index 0000000..63ce524 --- /dev/null +++ b/test/wallet.spec.ts @@ -0,0 +1,49 @@ +import { EsploraClient, Wallet, WalletDB } from '../src/wallet'; + +describe('Wallet', () => { + let wallet: Wallet; + + beforeAll(async () => { + const walletDB = new WalletDB({ + location: './test/wallet', + }); + + wallet = new Wallet({ + db: walletDB, + networkClient: new EsploraClient({ + protocol: 'https', + host: 'blockstream.info', + network: 'main', + }), + }); + }); + + it('should initialise the wallet', async () => { + await wallet.init(); + }); + + it('should load the wallet', async () => { + await wallet.load( + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + ); + }); + + it('should derive first receive address', async () => { + const address = await wallet.deriveReceiveAddress(); + expect(address).toBe('bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'); + }); + + it('should derive second receive address', async () => { + const address = await wallet.deriveReceiveAddress(); + expect(address).toBe('bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g'); + }); + + it('should derive first change address', async () => { + const address = await wallet.deriveChangeAddress(); + expect(address).toBe('bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'); + }); + + afterAll(async () => { + await wallet.close(); + }); +});