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

Various wallet improvements #210

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
2 changes: 1 addition & 1 deletion src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,7 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
address,
'latest',
);
return outpoints.map((outpoint: OutpointResponseParams) => ({
return (outpoints ?? []).map((outpoint: OutpointResponseParams) => ({
txhash: outpoint.Txhash,
index: outpoint.Index,
denomination: outpoint.Denomination,
Expand Down
208 changes: 131 additions & 77 deletions src/wallet/hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ export abstract class AbstractHDWallet {

protected static _coinType?: AllowedCoinType;

// Map of account number to HDNodeWallet
protected _accounts: Map<number, HDNodeWallet> = new Map();

// Map of addresses to address info
protected _addresses: Map<string, NeuteredAddressInfo> = new Map();

Expand All @@ -59,20 +56,32 @@ export abstract class AbstractHDWallet {
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(
/**
* Derives the next valid address node for a specified account, starting index, and zone. The method ensures the
* derived address belongs to the correct shard and ledger, as defined by the Quai blockchain specifications.
*
* @param {number} account - The account number from which to derive the address node.
* @param {number} startingIndex - The index from which to start deriving addresses.
* @param {Zone} zone - The zone (shard) for which the address should be valid.
* @param {boolean} [isChange=false] - Whether to derive a change address (default is false). Default is `false`
* Default is `false` Default is `false`
*
* @returns {HDNodeWallet} - The derived HD node wallet containing a valid address for the specified zone.
* @throws {Error} If a valid address for the specified zone cannot be derived within the allowed attempts.
*/
protected deriveNextAddressNode(
account: number,
startingIndex: number,
zone: Zone,
isChange: boolean = false,
): HDNodeWallet {
this.validateZone(zone);
const isValidAddressForZone = (address: string) => {
const changeIndex = isChange ? 1 : 0;
const changeNode = this._root.deriveChild(account).deriveChild(changeIndex);

let addrIndex = startingIndex;
let addressNode: HDNodeWallet;

const isValidAddressForZone = (address: string): boolean => {
const addressZone = getZoneForAddress(address);
if (!addressZone) {
return false;
Expand All @@ -81,24 +90,17 @@ export abstract class AbstractHDWallet {
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.`,
);

for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) {
addressNode = changeNode.deriveChild(addrIndex++);
if (isValidAddressForZone(addressNode.address)) {
return addressNode;
}
} while (!isValidAddressForZone(addressNode.address));
}

return addressNode;
throw new Error(
`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`,
);
}

public addAddress(account: number, addressIndex: number, isChange: boolean = false): NeuteredAddressInfo {
Expand All @@ -112,66 +114,57 @@ export abstract class AbstractHDWallet {
addressIndex: number,
isChange: boolean = false,
): NeuteredAddressInfo {
if (!this._accounts.has(account)) {
this.addAccount(account);
}
// check if address already exists for the index
this._addresses.forEach((addressInfo) => {
if (addressInfo.index === addressIndex) {
throw new Error(`Address for index ${addressIndex} already exists`);
}
});

// derive the address node
// derive the address node and validate the zone
const changeIndex = isChange ? 1 : 0;
const addressNode = this._root.deriveChild(account).deriveChild(changeIndex).deriveChild(addressIndex);
const zone = getZoneForAddress(addressNode.address);
if (!zone) {
throw new Error(`Failed to derive a valid address zone for the index ${addressIndex}`);
}

// create the NeuteredAddressInfo object and update the map
const neuteredAddressInfo = {
pubKey: addressNode.publicKey,
address: addressNode.address,
account: account,
index: addressNode.index,
change: isChange,
zone: zone,
};

addressMap.set(neuteredAddressInfo.address, neuteredAddressInfo);
return this.createAndStoreAddressInfo(addressNode, account, zone, isChange, addressMap);
}

return neuteredAddressInfo;
/**
* Retrieves the next address for the specified account and zone.
*
* @param {number} account - The index of the account for which to retrieve the next address.
* @param {Zone} zone - The zone in which to retrieve the next address.
*
* @returns {NeuteredAddressInfo} The next neutered address information.
*/
public getNextAddress(account: number, zone: Zone): NeuteredAddressInfo {
return this._getNextAddress(account, zone, false, this._addresses);
}

public getNextAddress(accountIndex: number, zone: Zone): NeuteredAddressInfo {
/**
* Derives and returns the next address information for the specified account and zone.
*
* @param {number} accountIndex - The index of the account for which the address is being generated.
* @param {Zone} zone - The zone in which the address is to be used.
* @param {boolean} isChange - A flag indicating whether the address is a change address.
* @param {Map<string, NeuteredAddressInfo>} addressMap - A map storing the neutered address information.
*
* @returns {NeuteredAddressInfo} The derived neutered address information.
* @throws {Error} If the zone is invalid.
*/
protected _getNextAddress(
accountIndex: number,
zone: Zone,
isChange: boolean,
addressMap: Map<string, NeuteredAddressInfo>,
): NeuteredAddressInfo {
this.validateZone(zone);
if (!this._accounts.has(accountIndex)) {
this.addAccount(accountIndex);
}

const filteredAccountInfos = Array.from(this._addresses.values()).filter(
(addressInfo) => addressInfo.account === accountIndex && addressInfo.zone === zone,
);
const lastIndex = filteredAccountInfos.reduce(
(maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index),
-1,
);
const addressNode = this.deriveAddress(accountIndex, lastIndex + 1, zone);

// create the NeuteredAddressInfo object and update the maps
const neuteredAddressInfo = {
pubKey: addressNode.publicKey,
address: addressNode.address,
account: accountIndex,
index: addressNode.index,
change: false,
zone: zone,
};
this._addresses.set(neuteredAddressInfo.address, neuteredAddressInfo);

return neuteredAddressInfo;
const lastIndex = this.getLastAddressIndex(addressMap, zone, accountIndex, isChange);
const addressNode = this.deriveNextAddressNode(accountIndex, lastIndex + 1, zone, isChange);
return this.createAndStoreAddressInfo(addressNode, accountIndex, zone, isChange, addressMap);
}

public getAddressInfo(address: string): NeuteredAddressInfo | null {
Expand Down Expand Up @@ -270,14 +263,8 @@ export abstract class AbstractHDWallet {
throw new Error(`Address ${addr} is not known to this wallet`);
}

// derive a HD node for the from address using the index
const accountNode = this._accounts.get(addressInfo.account);
if (!accountNode) {
throw new Error(`Account ${addressInfo.account} not found`);
}
const changeIndex = addressInfo.change ? 1 : 0;
const changeNode = accountNode.deriveChild(changeIndex);
return changeNode.deriveChild(addressInfo.index);
return this._root.deriveChild(addressInfo.account).deriveChild(changeIndex).deriveChild(addressInfo.index);
}

/**
Expand Down Expand Up @@ -371,4 +358,71 @@ export abstract class AbstractHDWallet {
}
}
}

/**
* Retrieves the highest address index from the given address map for a specified zone, account, and change type.
*
* This method filters the address map based on the provided zone, account, and change type, then determines the
* maximum address index from the filtered addresses.
*
* @param {Map<string, NeuteredAddressInfo>} addressMap - The map containing address information, where the key is
* an address string and the value is a NeuteredAddressInfo object.
* @param {Zone} zone - The specific zone to filter the addresses by.
* @param {number} account - The account number to filter the addresses by.
* @param {boolean} isChange - A boolean indicating whether to filter for change addresses (true) or receiving
* addresses (false).
*
* @returns {number} - The highest address index for the specified criteria, or -1 if no addresses match.
* @protected
*/
protected getLastAddressIndex(
addressMap: Map<string, NeuteredAddressInfo>,
zone: Zone,
account: number,
isChange: boolean,
): number {
const addresses = Array.from(addressMap.values()).filter(
(addressInfo) =>
addressInfo.account === account && addressInfo.zone === zone && addressInfo.change === isChange,
);
return addresses.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1);
}

/**
* Creates and stores address information in the address map for a specified account, zone, and change type.
*
* This method constructs a NeuteredAddressInfo object using the provided HDNodeWallet and other parameters, then
* stores this information in the provided address map.
*
* @param {HDNodeWallet} addressNode - The HDNodeWallet object containing the address and public key information.
* @param {number} account - The account number to associate with the address.
* @param {Zone} zone - The specific zone to associate with the address.
* @param {boolean} isChange - A boolean indicating whether the address is a change address (true) or a receiving
* address (false).
* @param {Map<string, NeuteredAddressInfo>} addressMap - The map to store the created NeuteredAddressInfo, with the
* address as the key.
*
* @returns {NeuteredAddressInfo} - The created NeuteredAddressInfo object.
* @protected
*/
protected createAndStoreAddressInfo(
addressNode: HDNodeWallet,
account: number,
zone: Zone,
isChange: boolean,
addressMap: Map<string, NeuteredAddressInfo>,
): NeuteredAddressInfo {
const neuteredAddressInfo: NeuteredAddressInfo = {
pubKey: addressNode.publicKey,
address: addressNode.address,
account,
index: addressNode.index,
change: isChange,
zone,
};

addressMap.set(neuteredAddressInfo.address, neuteredAddressInfo);

return neuteredAddressInfo;
}
}
Loading
Loading