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

Wallet improvements #180

Merged
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
20 changes: 14 additions & 6 deletions src/wallet/hdnodewallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,21 @@ export class HDNodeWallet extends BaseWallet {
// - Mainnet: public=0x0488B21E, private=0x0488ADE4
// - Testnet: public=0x043587CF, private=0x04358394

assert(this.depth < 256, "Depth too deep", "UNSUPPORTED_OPERATION", { operation: "extendedKey" });
assert(this.depth < 256, 'Depth too deep', 'UNSUPPORTED_OPERATION', {
operation: 'extendedKey',
});

return encodeBase58Check(concat([
"0x0488ADE4", zpad(this.depth, 1), this.parentFingerprint,
zpad(this.index, 4), this.chainCode,
concat([ "0x00", this.privateKey ])
]));
const myKey = encodeBase58Check(
concat([
'0x0488ADE4',
'0x' + zpad(this.depth, 2),
this.parentFingerprint ?? '',
'0x' + zpad(this.index, 4),
this.chainCode,
concat(['0x00', this.privateKey]),
]),
);
return myKey;
}

/**
Expand Down
201 changes: 84 additions & 117 deletions src/wallet/hdwallet.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { HDNodeWallet } from './hdnodewallet.js';
import { Mnemonic } from './mnemonic.js';
import { LangEn } from '../wordlists/lang-en.js';
import type { Wordlist } from '../wordlists/index.js';
import { randomBytes } from '../crypto/index.js';
import { getZoneForAddress, isQiAddress } from '../utils/index.js';
import { ZoneData, ShardData } from '../constants/index.js';
import { HDNodeWallet } from "./hdnodewallet";
import { Mnemonic } from "./mnemonic.js";
import { LangEn } from "../wordlists/lang-en.js"
import type { Wordlist } from "../wordlists/index.js";
import { randomBytes } from "../crypto/index.js";
import { getZoneForAddress, isQiAddress } from "../utils/index.js";
import { Zone } from '../constants/index.js';
import { TransactionRequest, Provider, TransactionResponse } from '../providers/index.js';

export interface NeuteredAddressInfo {
pubKey: string;
address: string;
account: number;
index: number;
change: boolean;
zone: string;
pubKey: string;
address: string;
account: number;
index: number;
change: boolean;
zone: Zone;
}

// Constant to represent the maximum attempt to derive an address
const MAX_ADDRESS_DERIVATION_ATTEMPTS = 10000000;

export abstract class HDWallet {
protected static _coinType?: number = 969 || 994;
export abstract class AbstractHDWallet {
protected static _coinType?: number = 969 || 994;

// Map of account number to HDNodeWallet
protected _accounts: Map<number, HDNodeWallet> = new Map();
Expand All @@ -31,10 +31,7 @@ export abstract class HDWallet {
// Root node of the HD wallet
protected _root: HDNodeWallet;

// Wallet parent path
protected static _parentPath: string = '';

protected provider?: Provider;
protected provider?: Provider;

/**
* @private
Expand All @@ -44,57 +41,50 @@ export abstract class HDWallet {
this.provider = provider;
}

protected parentPath(): string {
return (this.constructor as typeof HDWallet)._parentPath;
}

protected coinType(): number {
return (this.constructor as typeof HDWallet)._coinType!;
}
protected static parentPath(coinType: number): string {
return `m/44'/${coinType}'`;
}
protected coinType(): number {
return (this.constructor as typeof AbstractHDWallet)._coinType!;
}

// helper methods that adds an account HD node to the HD wallet following the BIP-44 standard.
protected addAccount(accountIndex: number): void {
const newNode = this._root.deriveChild(accountIndex);
this._accounts.set(accountIndex, newNode);
}

protected deriveAddress(
account: number,
startingIndex: number,
zone: string,
isChange: boolean = false,
): HDNodeWallet {
protected deriveAddress(account: number, startingIndex: number, zone: Zone, isChange: boolean = false): HDNodeWallet {
this.validateZone(zone);
const isValidAddressForZone = (address: string) => {
const zoneByte = getZoneForAddress(address);
if (!zone) {
const addressZone = getZoneForAddress(address);
if (!addressZone) {
return false;
}
const shardNickname = ZoneData.find((zoneData) => zoneData.byte === zoneByte)?.nickname;
const isCorrectShard = shardNickname === zone.toLowerCase();
const isCorrectLedger = this.coinType() === 969 ? isQiAddress(address) : !isQiAddress(address);
return isCorrectShard && isCorrectLedger;
};
// derive the address node
const accountNode = this._accounts.get(account);
const changeIndex = isChange ? 1 : 0;
const changeNode = accountNode!.deriveChild(changeIndex);
let addrIndex: number = startingIndex;
let addressNode: HDNodeWallet;
do {
addressNode = changeNode.deriveChild(addrIndex);
addrIndex++;
// put a hard limit on the number of addresses to derive
if (addrIndex - startingIndex > MAX_ADDRESS_DERIVATION_ATTEMPTS) {
throw new Error(
`Failed to derive a valid address for the zone ${zone} after MAX_ADDRESS_DERIVATION_ATTEMPTS attempts.`,
);
}
} while (!isValidAddressForZone(addressNode.address));
const isCorrectShard = addressZone === zone;
const isCorrectLedger = (this.coinType() === 969) ? isQiAddress(address) : !isQiAddress(address);
return isCorrectShard && isCorrectLedger;
}
// derive the address node
const accountNode = this._accounts.get(account);
const changeIndex = isChange ? 1 : 0;
const changeNode = accountNode!.deriveChild(changeIndex);
let addrIndex: number = startingIndex;
let addressNode: HDNodeWallet;
do {
addressNode = changeNode.deriveChild(addrIndex);
addrIndex++;
// put a hard limit on the number of addresses to derive
if (addrIndex - startingIndex > MAX_ADDRESS_DERIVATION_ATTEMPTS) {
throw new Error(`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`);
}
} while (!isValidAddressForZone(addressNode.address));

return addressNode;
}

addAddress(account: number, zone: string, addressIndex: number): NeuteredAddressInfo {
public addAddress(account: number, addressIndex: number, zone: Zone): NeuteredAddressInfo {
if (!this._accounts.has(account)) {
this.addAccount(account);
}
Expand All @@ -121,9 +111,8 @@ export abstract class HDWallet {

return neuteredAddressInfo;
}

getNextAddress(accountIndex: number, zone: string): NeuteredAddressInfo {
if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`);
public getNextAddress(accountIndex: number, zone: Zone): NeuteredAddressInfo {
this.validateZone(zone);
if (!this._accounts.has(accountIndex)) {
this.addAccount(accountIndex);
}
Expand Down Expand Up @@ -151,82 +140,60 @@ export abstract class HDWallet {
return neuteredAddressInfo;
}

getAddressInfo(address: string): NeuteredAddressInfo | null {
public getAddressInfo(address: string): NeuteredAddressInfo | null {
const addressInfo = this._addresses.get(address);
if (!addressInfo) {
return null;
}
return addressInfo;
}

getAddressesForAccount(account: number): NeuteredAddressInfo[] {
public getAddressesForAccount(account: number): NeuteredAddressInfo[] {
const addresses = this._addresses.values();
return Array.from(addresses).filter((addressInfo) => addressInfo.account === account);
}

getAddressesForZone(zone: string): NeuteredAddressInfo[] {
if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`);
const addresses = this._addresses.values();
return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
}

protected static createInstance<T extends HDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any)._parentPath);
return new this(root);
}

static fromMnemonic<T extends HDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
return (this as any).createInstance(mnemonic);
}

static createRandom<T extends HDWallet>(
this: new (root: HDNodeWallet) => T,
password?: string,
wordlist?: Wordlist,
): T {
if (password == null) {
password = '';
}
if (wordlist == null) {
wordlist = LangEn.wordlist();
}
const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist);
return (this as any).createInstance(mnemonic);
}

static fromPhrase<T extends HDWallet>(
this: new (root: HDNodeWallet) => T,
phrase: string,
password?: string,
wordlist?: Wordlist,
): T {
if (password == null) {
password = '';
}
if (wordlist == null) {
wordlist = LangEn.wordlist();
}
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
return (this as any).createInstance(mnemonic);
}
public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
this.validateZone(zone);
const addresses = this._addresses.values();
return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
}

protected static createInstance<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
const coinType = (this as any)._coinType;
const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any).parentPath(coinType));
return new this(root);
}

static fromMnemonic<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
return (this as any).createInstance(mnemonic);
}

static createRandom<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, password?: string, wordlist?: Wordlist): T {
if (password == null) { password = ""; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist);
return (this as any).createInstance(mnemonic);
}

static fromPhrase<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, phrase: string, password?: string, wordlist?: Wordlist): T {
if (password == null) { password = ""; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
return (this as any).createInstance(mnemonic);
}

abstract signTransaction(tx: TransactionRequest): Promise<string>;

abstract sendTransaction(tx: TransactionRequest): Promise<TransactionResponse>;

connect(provider: Provider): void {
public connect(provider: Provider): void {
this.provider = provider;
}

// helper function to validate the zone
protected validateZone(zone: string): boolean {
zone = zone.toLowerCase();
const shard = ShardData.find(
(shard) =>
shard.name.toLowerCase() === zone ||
shard.nickname.toLowerCase() === zone ||
shard.byte.toLowerCase() === zone,
);
return shard !== undefined;
protected validateZone(zone: Zone): void {
if (!Object.values(Zone).includes(zone)) {
throw new Error(`Invalid zone: ${zone}`);
}
}
}
Loading
Loading