-
Notifications
You must be signed in to change notification settings - Fork 6
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
add new method: signTypedData() #207
Changes from 3 commits
1512954
5663247
340907a
3e4c9b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,8 +7,6 @@ import { getZoneForAddress, isQiAddress } from '../utils/index.js'; | |
import { Zone } from '../constants/index.js'; | ||
import { TransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; | ||
import { AllowedCoinType } from '../constants/index.js'; | ||
import { QiHDWallet } from './qi-hdwallet.js'; | ||
import { QuaiHDWallet } from './quai-hdwallet.js'; | ||
|
||
export interface NeuteredAddressInfo { | ||
pubKey: string; | ||
|
@@ -253,8 +251,19 @@ export abstract class AbstractHDWallet { | |
} | ||
} | ||
|
||
// Returns the HD node that derives the address. | ||
// If the address is not found in the wallet, an error is thrown. | ||
/** | ||
* Derives and returns the Hierarchical Deterministic (HD) node wallet associated with a given address. | ||
* | ||
* This method fetches the account and address information from the wallet's internal storage, derives the | ||
* appropriate change node based on whether the address is a change address, and further derives the final HD node | ||
* using the address index. | ||
* | ||
* @param {string} addr - The address for which to derive the HD node. | ||
* | ||
* @returns {HDNodeWallet} - The derived HD node wallet corresponding to the given address. | ||
* @throws {Error} If the given address is not known to the wallet. | ||
* @throws {Error} If the account associated with the address is not found. | ||
*/ | ||
protected _getHDNodeForAddress(addr: string): HDNodeWallet { | ||
const addressInfo = this._addresses.get(addr); | ||
if (!addressInfo) { | ||
|
@@ -283,31 +292,24 @@ export abstract class AbstractHDWallet { | |
}; | ||
} | ||
|
||
static async deserialize<T extends AbstractHDWallet>( | ||
this: new (root: HDNodeWallet, provider?: Provider) => T, | ||
serialized: SerializedHDWallet, | ||
): Promise<QuaiHDWallet | QiHDWallet> { | ||
// validate the version and coinType | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
public static async deserialize(_serialized: SerializedHDWallet): Promise<AbstractHDWallet> { | ||
throw new Error('deserialize method must be implemented in the subclass'); | ||
} | ||
|
||
// This method is used to validate the version and coinType of the serialized wallet. | ||
protected static validateSerializedWallet(serialized: SerializedHDWallet): void { | ||
if (serialized.version !== (this as any)._version) { | ||
throw new Error(`Invalid version ${serialized.version} for wallet (expected ${(this as any)._version})`); | ||
} | ||
if (serialized.coinType !== (this as any)._coinType) { | ||
throw new Error(`Invalid coinType ${serialized.coinType} for wallet (expected ${(this as any)._coinType})`); | ||
} | ||
// create the wallet instance | ||
const mnemonic = Mnemonic.fromPhrase(serialized.phrase); | ||
const path = (this as any).parentPath(serialized.coinType); | ||
const root = HDNodeWallet.fromMnemonic(mnemonic, path); | ||
const wallet = new this(root); | ||
|
||
// import the addresses | ||
wallet.importSerializedAddresses(wallet._addresses, serialized.addresses); | ||
|
||
return wallet as T; | ||
} | ||
|
||
// This method is used to import addresses from a serialized wallet. | ||
// It validates the addresses and adds them to the wallet. | ||
// This method is used to import addresses from a serialized wallet into the addresses map. | ||
// Before adding the addresses, a validation is performed to ensure the address, public key and zone | ||
// match the expected values. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going forward if we are adding comments to methods or classes to use JSDoc format like you did for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added a new commit with JSDOC for mostly all new methods added in this PR. Will continue adding more JSDOC documentation on subsequent PRs |
||
protected importSerializedAddresses( | ||
addressMap: Map<string, NeuteredAddressInfo>, | ||
addresses: NeuteredAddressInfo[], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,5 @@ | ||
|
||
|
||
import { AbstractHDWallet, NeuteredAddressInfo, SerializedHDWallet } from './hdwallet'; | ||
import { HDNodeWallet } from "./hdnodewallet"; | ||
import { HDNodeWallet } from './hdnodewallet'; | ||
import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; | ||
import { computeAddress } from '../address/index.js'; | ||
import { getBytes, hexlify } from '../utils/index.js'; | ||
|
@@ -13,6 +11,7 @@ import { musigCrypto } from '../crypto/index.js'; | |
import { Outpoint } from '../transaction/utxo.js'; | ||
import { getZoneForAddress } from '../utils/index.js'; | ||
import { AllowedCoinType, Zone } from '../constants/index.js'; | ||
import { Mnemonic } from './mnemonic.js'; | ||
|
||
type OutpointInfo = { | ||
outpoint: Outpoint; | ||
|
@@ -21,15 +20,14 @@ type OutpointInfo = { | |
account?: number; | ||
}; | ||
|
||
interface SerializedQiHDWallet extends SerializedHDWallet{ | ||
outpoints: OutpointInfo[]; | ||
changeAddresses: NeuteredAddressInfo[]; | ||
gapAddresses: NeuteredAddressInfo[]; | ||
gapChangeAddresses: NeuteredAddressInfo[]; | ||
interface SerializedQiHDWallet extends SerializedHDWallet { | ||
outpoints: OutpointInfo[]; | ||
changeAddresses: NeuteredAddressInfo[]; | ||
gapAddresses: NeuteredAddressInfo[]; | ||
gapChangeAddresses: NeuteredAddressInfo[]; | ||
} | ||
|
||
export class QiHDWallet extends AbstractHDWallet { | ||
|
||
protected static _version: number = 1; | ||
|
||
protected static _GAP_LIMIT: number = 20; | ||
|
@@ -81,10 +79,8 @@ export class QiHDWallet extends AbstractHDWallet { | |
} | ||
|
||
public importOutpoints(outpoints: OutpointInfo[]): void { | ||
outpoints.forEach((outpoint) => { | ||
this.validateZone(outpoint.zone); | ||
this._outpoints.push(outpoint); | ||
}); | ||
this.validateOutpointInfo(outpoints); | ||
this._outpoints.push(...outpoints); | ||
} | ||
|
||
public getOutpoints(zone: Zone): OutpointInfo[] { | ||
|
@@ -319,71 +315,83 @@ export class QiHDWallet extends AbstractHDWallet { | |
return gapAddresses; | ||
} | ||
|
||
public getGapChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] { | ||
this.validateZone(zone); | ||
const gapChangeAddresses = this._gapChangeAddresses.filter((addressInfo) => addressInfo.zone === zone); | ||
return gapChangeAddresses; | ||
} | ||
|
||
public async signMessage(address: string, message: string | Uint8Array): Promise<string> { | ||
const addrNode = this._getHDNodeForAddress(address); | ||
const privKey = addrNode.privateKey; | ||
const digest = keccak_256(message); | ||
const signature = schnorr.sign(digest, getBytes(privKey)); | ||
return hexlify(signature); | ||
} | ||
|
||
public async serialize(): Promise<SerializedQiHDWallet> { | ||
const hdwalletSerialized = await super.serialize(); | ||
return { | ||
outpoints: this._outpoints, | ||
changeAddresses: Array.from(this._changeAddresses.values()), | ||
gapAddresses: this._gapAddresses, | ||
gapChangeAddresses: this._gapChangeAddresses, | ||
...hdwalletSerialized, | ||
}; | ||
} | ||
|
||
public static async deserialize(serialized: SerializedQiHDWallet): Promise<QiHDWallet> { | ||
const wallet = await super.deserialize<QiHDWallet>(serialized) as QiHDWallet; | ||
// import the change addresses | ||
wallet.importSerializedAddresses(wallet._changeAddresses, serialized.changeAddresses); | ||
|
||
// import the gap addresses, verifying they exist in the wallet | ||
for (const gapAddressInfo of serialized.gapAddresses) { | ||
const gapAddress = gapAddressInfo.address; | ||
if (!wallet._addresses.has(gapAddress)) { | ||
throw new Error(`Address ${gapAddress} not found in wallet`); | ||
} | ||
wallet._gapAddresses.push(gapAddressInfo); | ||
|
||
} | ||
// import the gap change addresses, verifying they exist in the wallet | ||
for (const gapChangeAddressInfo of serialized.gapChangeAddresses) { | ||
const gapChangeAddress = gapChangeAddressInfo.address; | ||
if (!wallet._changeAddresses.has(gapChangeAddress)) { | ||
throw new Error(`Address ${gapChangeAddress} not found in wallet`); | ||
} | ||
wallet._gapChangeAddresses.push(gapChangeAddressInfo); | ||
} | ||
|
||
// validate the outpoints and import them | ||
for (const outpointInfo of serialized.outpoints) { | ||
// check the zone is valid | ||
wallet.validateZone(outpointInfo.zone); | ||
// check the outpoint address is known to the wallet | ||
if (!wallet._addresses.has(outpointInfo.address)) { | ||
throw new Error(`Address ${outpointInfo.address} not found in wallet`); | ||
} | ||
const outpoint = outpointInfo.outpoint; | ||
// TODO: implement a more robust check for Outpoint | ||
// check the Outpoint fields are not empty | ||
if (outpoint.Txhash == null || outpoint.Index == null || outpoint.Denomination == null) { | ||
throw new Error(`Invalid Outpoint: ${JSON.stringify(outpoint)} `); | ||
} | ||
wallet._outpoints.push(outpointInfo); | ||
} | ||
return wallet; | ||
|
||
} | ||
public getGapChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] { | ||
this.validateZone(zone); | ||
const gapChangeAddresses = this._gapChangeAddresses.filter((addressInfo) => addressInfo.zone === zone); | ||
return gapChangeAddresses; | ||
} | ||
|
||
public async signMessage(address: string, message: string | Uint8Array): Promise<string> { | ||
const addrNode = this._getHDNodeForAddress(address); | ||
const privKey = addrNode.privateKey; | ||
const digest = keccak_256(message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are some incompatibilites between the |
||
const signature = schnorr.sign(digest, getBytes(privKey)); | ||
return hexlify(signature); | ||
} | ||
|
||
public async serialize(): Promise<SerializedQiHDWallet> { | ||
const hdwalletSerialized = await super.serialize(); | ||
return { | ||
outpoints: this._outpoints, | ||
changeAddresses: Array.from(this._changeAddresses.values()), | ||
gapAddresses: this._gapAddresses, | ||
gapChangeAddresses: this._gapChangeAddresses, | ||
...hdwalletSerialized, | ||
}; | ||
} | ||
|
||
public static async deserialize(serialized: SerializedQiHDWallet): Promise<QiHDWallet> { | ||
super.validateSerializedWallet(serialized); | ||
// create the wallet instance | ||
const mnemonic = Mnemonic.fromPhrase(serialized.phrase); | ||
const path = (this as any).parentPath(serialized.coinType); | ||
const root = HDNodeWallet.fromMnemonic(mnemonic, path); | ||
const wallet = new this(root); | ||
|
||
// import the addresses | ||
wallet.importSerializedAddresses(wallet._addresses, serialized.addresses); | ||
// import the change addresses | ||
wallet.importSerializedAddresses(wallet._changeAddresses, serialized.changeAddresses); | ||
|
||
// import the gap addresses, verifying they already exist in the wallet | ||
for (const gapAddressInfo of serialized.gapAddresses) { | ||
const gapAddress = gapAddressInfo.address; | ||
if (!wallet._addresses.has(gapAddress)) { | ||
throw new Error(`Address ${gapAddress} not found in wallet`); | ||
} | ||
wallet._gapAddresses.push(gapAddressInfo); | ||
} | ||
// import the gap change addresses, verifying they already exist in the wallet | ||
for (const gapChangeAddressInfo of serialized.gapChangeAddresses) { | ||
const gapChangeAddress = gapChangeAddressInfo.address; | ||
if (!wallet._changeAddresses.has(gapChangeAddress)) { | ||
throw new Error(`Address ${gapChangeAddress} not found in wallet`); | ||
} | ||
wallet._gapChangeAddresses.push(gapChangeAddressInfo); | ||
} | ||
|
||
// validate the outpoints and import them | ||
wallet.validateOutpointInfo(serialized.outpoints); | ||
wallet._outpoints.push(...serialized.outpoints); | ||
return wallet; | ||
} | ||
|
||
private validateOutpointInfo(outpointInfo: OutpointInfo[]): void { | ||
outpointInfo.forEach((info) => { | ||
// validate zone | ||
this.validateZone(info.zone); | ||
// validate address | ||
if (!this._addresses.has(info.address)) { | ||
throw new Error(`Address ${info.address} not found in wallet`); | ||
} | ||
// validate account | ||
if (info.account && !this._accounts.has(info.account)) { | ||
throw new Error(`Account ${info.account} not found in wallet`); | ||
} | ||
// validate Outpoint | ||
if (info.outpoint.Txhash == null || info.outpoint.Index == null || info.outpoint.Denomination == null) { | ||
throw new Error(`Invalid Outpoint: ${JSON.stringify(info)} `); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file looks good. Only thing I noticed that needs to be changed is to remove
async
from theserialize
method signature since it is not async.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed
async
fromserialize