Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add lookahead #33

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/wallet/db/db.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export type DbInterface = {
encryptedChainCode: string,
): Promise<void>;
saveAddress(address: string, path: string): Promise<void>;
getAddress(address: string): Promise<string>;
getPathFromAddress(address: string): Promise<string>;
getAddressFromPath(path: string): Promise<string>;
hasAddress(address: string): Promise<boolean>;
getReceiveDepth(): Promise<number>;
setReceiveDepth(depth: number): Promise<void>;
Expand Down
11 changes: 9 additions & 2 deletions src/wallet/db/level/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,20 @@ export class WalletDB implements DbInterface {
}

public async saveAddress(address: string, path: string): Promise<void> {
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<string> {
public async getPathFromAddress(address: string): Promise<string> {
return await this.db.sublevel(wdb.A).get(address);
}

public async getAddressFromPath(path: string): Promise<string> {
return await this.db.sublevel(wdb.P).get(path);
}

async hasAddress(address: string): Promise<boolean> {
return (await this.db.sublevel(wdb.A).get(address)) !== undefined;
}
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/db/level/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,24 @@ 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;
private readonly network: NetworkInterface;
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 }) {
Expand All @@ -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();
Expand Down Expand Up @@ -102,8 +109,11 @@ export class Wallet {
}

async deriveReceiveAddress(): Promise<string> {
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;
}
Expand Down Expand Up @@ -133,7 +143,7 @@ export class Wallet {

private async signTransaction(psbt: Psbt, coins: Coin[]): Promise<void> {
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);
}
Expand Down Expand Up @@ -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) => ({
Expand Down
22 changes: 20 additions & 2 deletions test/wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
Loading