From 8c04a8f3d061f54a2daedbe49ffa4b81086d8ef2 Mon Sep 17 00:00:00 2001 From: robertlincecum Date: Fri, 7 Jun 2024 15:16:25 -0500 Subject: [PATCH] update block structure and formatters --- src/_tests/test-providers-data.ts | 86 +++-- src/providers/abstract-provider.ts | 4 +- src/providers/format.ts | 269 ++++++++----- src/providers/formatting.ts | 133 +++---- src/providers/provider.ts | 409 ++++++++++---------- src/quais.ts | 2 + src/signers/abstract-signer.ts | 2 +- src/wallet/hdwallet.ts | 388 ++++++++++--------- src/wallet/qi-hdwallet.ts | 589 ++++++++++++++--------------- src/wallet/quai-hdwallet.ts | 15 +- 10 files changed, 987 insertions(+), 910 deletions(-) diff --git a/src/_tests/test-providers-data.ts b/src/_tests/test-providers-data.ts index 467eff56..e3f735ba 100644 --- a/src/_tests/test-providers-data.ts +++ b/src/_tests/test-providers-data.ts @@ -206,35 +206,69 @@ describe('Test Provider Block operations', function () { before(async () => { const rpcBlock = await fetchRPCBlock('0xA'); block = { - hash: rpcBlock.hash, - number: rpcBlock.number.map((stringNumber: string) => Number(stringNumber)), - transactions: rpcBlock.transactions, - parentHash: rpcBlock.parentHash, - parentEntropy: rpcBlock.parentEntropy.map((entropy: string) => BigInt(entropy)), extTransactions: rpcBlock.extTransactions, - nonce: rpcBlock.nonce, - difficulty: BigInt(rpcBlock.difficulty), - gasLimit: BigInt(rpcBlock.gasLimit), - gasUsed: BigInt(rpcBlock.gasUsed), - miner: rpcBlock.miner, - extraData: rpcBlock.extraData, - transactionsRoot: rpcBlock.transactionsRoot, - evmRoot: rpcBlock.stateRoot, - utxoRoot: rpcBlock.utxoRoot, - receiptsRoot: rpcBlock.receiptsRoot, - baseFeePerGas: BigInt(rpcBlock.baseFeePerGas), - extRollupRoot: rpcBlock.extRollupRoot, - extTransactionsRoot: rpcBlock.extTransactionsRoot, - location: rpcBlock.location, - manifestHash: rpcBlock.manifestHash, - mixHash: rpcBlock.mixHash, + interlinkHashes: rpcBlock.interlinkHashes, order: rpcBlock.order, - parentDeltaS: rpcBlock.parentDeltaS.map((delta: string) => BigInt(delta)), - sha3Uncles: rpcBlock.sha3Uncles, size: BigInt(rpcBlock.size), - uncles: rpcBlock.uncles, subManifest: rpcBlock.subManifest, totalEntropy: BigInt(rpcBlock.totalEntropy), + transactions: rpcBlock.transactions, + uncles: rpcBlock.uncles, + woBody: { + extTransactions: rpcBlock.woBody.extTransactions, + header: { + baseFeePerGas: BigInt(rpcBlock.woBody.header.baseFeePerGas), + efficiencyScore: BigInt(rpcBlock.woBody.header.efficiencyScore), + etxEligibleSlices: rpcBlock.woBody.header.etxEligibleSlices, + etxSetRoot: rpcBlock.woBody.header.etxSetRoot, + evmRoot: rpcBlock.woBody.header.evmRoot, + expansionNumber: Number(rpcBlock.woBody.header.expansionNumber), + extRollupRoot: rpcBlock.woBody.header.extRollupRoot, + extTransactionsRoot: rpcBlock.woBody.header.extTransactionsRoot, + extraData: rpcBlock.woBody.header.extraData, + gasLimit: BigInt(rpcBlock.woBody.header.gasLimit), + gasUsed: BigInt(rpcBlock.woBody.header.gasUsed), + hash: rpcBlock.woBody.header.hash, + interlinkRootHash: rpcBlock.woBody.header.interlinkRootHash, + manifestHash: rpcBlock.woBody.header.manifestHash, + miner: rpcBlock.woBody.header.miner, + mixHash: rpcBlock.woBody.header.mixHash, + nonce: rpcBlock.woBody.header.nonce, + number: rpcBlock.woBody.header.number.map((stringNumber: string) => Number(stringNumber)), + parentDeltaS: rpcBlock.woBody.header.parentDeltaS.map((delta: string) => BigInt(delta)), + parentEntropy: rpcBlock.woBody.header.parentEntropy.map((entropy: string) => BigInt(entropy)), + parentHash: rpcBlock.woBody.header.parentHash, + parentUncledS: rpcBlock.woBody.header.parentUncledS.map((uncled: string | null) => + uncled ? BigInt(uncled) : null, + ), + parentUncledSubDeltaS: rpcBlock.woBody.header.parentUncledSubDeltaS.map((delta: string) => + BigInt(delta), + ), + primeTerminus: rpcBlock.woBody.header.primeTerminus, + receiptsRoot: rpcBlock.woBody.header.receiptsRoot, + sha3Uncles: rpcBlock.woBody.header.sha3Uncles, + size: BigInt(rpcBlock.woBody.header.size), + thresholdCount: BigInt(rpcBlock.woBody.header.thresholdCount), + transactionsRoot: rpcBlock.woBody.header.transactionsRoot, + uncledS: BigInt(rpcBlock.woBody.header.uncledS), + utxoRoot: rpcBlock.woBody.header.utxoRoot, + }, + interlinkHashes: rpcBlock.woBody.interlinkHashes, + manifest: rpcBlock.woBody.manifest, + transactions: rpcBlock.woBody.transactions, + uncles: rpcBlock.woBody.uncles, + }, + woHeader: { + difficulty: rpcBlock.woHeader.difficulty, + headerHash: rpcBlock.woHeader.headerHash, + location: rpcBlock.woHeader.location, + mixHash: rpcBlock.woHeader.mixHash, + nonce: rpcBlock.woHeader.nonce, + number: rpcBlock.woHeader.number, + parentHash: rpcBlock.woHeader.parentHash, + time: rpcBlock.woHeader.time, + txHash: rpcBlock.woHeader.txHash, + }, }; }); @@ -252,8 +286,8 @@ describe('Test Provider Block operations', function () { }); it('should fetch block by hash', async function () { - assert.ok(block.hash != null, 'block.hash != null'); - const responseBlock = (await providerC1.getBlock(Shard.Paxos2, block.hash)) as quais.Block; + assert.ok(block.woBody.header.hash != null, 'block.hash != null'); + const responseBlock = (await providerC1.getBlock(Shard.Paxos2, block.woBody.header.hash)) as quais.Block; assert.ok(responseBlock != null, 'block != null'); // TODO: `provider` is not used, remove? // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/providers/abstract-provider.ts b/src/providers/abstract-provider.ts index 708d1e8d..fdee90e5 100644 --- a/src/providers/abstract-provider.ts +++ b/src/providers/abstract-provider.ts @@ -821,7 +821,9 @@ export class AbstractProvider implements Provider { // eslint-disable-next-line @typescript-eslint/no-unused-vars _wrapBlock(value: BlockParams, network: Network): Block { // Handle known node by -> remove null values from the number array - value.number = Array.isArray(value.number) ? value.number.filter((n: any) => n != null) : value.number; + value.woBody.header.number = Array.isArray(value.woBody.header.number) + ? value.woBody.header.number.filter((n: any) => n != null) + : value.woBody.header.number; return new Block(formatBlock(value), this); } diff --git a/src/providers/format.ts b/src/providers/format.ts index abe43232..b4b8b4b4 100644 --- a/src/providers/format.ts +++ b/src/providers/format.ts @@ -1,7 +1,7 @@ /** * @ignore */ -import { getAddress, getCreateAddress } from '../address/index.js'; +import { getAddress } from '../address/index.js'; import { Signature } from '../crypto/index.js'; import { accessListify } from '../transaction/index.js'; import { @@ -21,6 +21,8 @@ import type { TransactionReceiptParams, TransactionResponseParams, EtxParams, + QiTransactionResponseParams, + QuaiTransactionResponseParams, } from './formatting.js'; const BN_0 = BigInt(0); @@ -89,7 +91,7 @@ export function formatBoolean(value: any): boolean { } export function formatData(value: string): string { - assertArgument(isHexString(value, true), 'invalid data', 'value', value); + assertArgument(isHexString(value), 'invalid data', 'value', value); return value; } @@ -140,30 +142,74 @@ export function formatLog(value: any): LogParams { return _formatLog(value); } -const _formatBlock = object({ - hash: allowNull(formatHash), - parentHash: arrayOf(formatHash), - number: arrayOf(getNumber), - nonce: allowNull(formatData), - gasLimit: getBigInt, - gasUsed: getBigInt, - miner: allowNull(getAddress), - extraData: formatData, - baseFeePerGas: allowNull(getBigInt), +const _formatWoBodyHeader = object({ + baseFeePerGas: getBigInt, + efficiencyScore: getBigInt, + etxEligibleSlices: formatHash, + etxSetRoot: formatHash, + evmRoot: formatHash, + expansionNumber: getNumber, extRollupRoot: formatHash, extTransactionsRoot: formatHash, - transactionsRoot: formatHash, + extraData: formatData, + gasLimit: getBigInt, + gasUsed: getBigInt, + hash: formatHash, + interlinkRootHash: formatHash, manifestHash: arrayOf(formatHash), + miner: allowNull(getAddress), + mixHash: formatHash, + nonce: formatData, + number: arrayOf(getNumber), parentDeltaS: arrayOf(getBigInt), parentEntropy: arrayOf(getBigInt), - subManifest: arrayOf(formatData), + parentHash: arrayOf(formatHash), + parentUncledS: arrayOf(allowNull(getBigInt)), + parentUncledSubDeltaS: arrayOf(getBigInt), + primeTerminus: formatHash, receiptsRoot: formatHash, sha3Uncles: formatHash, size: getBigInt, - evmRoot: formatHash, + thresholdCount: getBigInt, + transactionsRoot: formatHash, + uncledS: getBigInt, utxoRoot: formatHash, }); +const _formatWoBody = object({ + extTransactions: arrayOf(formatTransactionResponse), + header: _formatWoBodyHeader, + interlinkHashes: arrayOf(formatHash), + manifest: arrayOf(formatHash), + transactions: arrayOf(formatTransactionResponse), + uncles: arrayOf(formatHash), +}); + +const _formatWoHeader = object({ + difficulty: formatData, + headerHash: formatHash, + location: formatData, + mixHash: formatHash, + nonce: formatData, + number: formatData, + parentHash: formatHash, + time: formatData, + txHash: formatHash, +}); + +const _formatBlock = object({ + extTransactions: arrayOf(formatHash), + interlinkHashes: arrayOf(formatHash), + order: getNumber, + size: getBigInt, + subManifest: arrayOf(formatData), + totalEntropy: getBigInt, + transactions: arrayOf(formatHash), + uncles: arrayOf(formatHash), + woBody: _formatWoBody, + woHeader: _formatWoHeader, +}); + export function formatBlock(value: any): BlockParams { const result = _formatBlock(value); result.transactions = value.transactions.map((tx: string | TransactionResponseParams) => { @@ -226,26 +272,29 @@ export function formatEtx(value: any): EtxParams { return _formatEtx(value); } -const _formatTransactionReceipt = object({ - to: allowNull(getAddress, null), - from: allowNull(getAddress, null), - contractAddress: allowNull(getAddress, null), - index: getNumber, - gasUsed: getBigInt, - logsBloom: allowNull(formatData), - blockHash: formatHash, - hash: formatHash, - logs: arrayOf(formatReceiptLog), - blockNumber: getNumber, - cumulativeGasUsed: getBigInt, - effectiveGasPrice: allowNull(getBigInt), - status: allowNull(getNumber), - type: allowNull(getNumber, 0), - etxs: (value) => (value === null ? [] : arrayOf(formatEtx)(value)), -}, { - hash: ["transactionHash"], - index: ["transactionIndex"], -}); +const _formatTransactionReceipt = object( + { + to: allowNull(getAddress, null), + from: allowNull(getAddress, null), + contractAddress: allowNull(getAddress, null), + index: getNumber, + gasUsed: getBigInt, + logsBloom: allowNull(formatData), + blockHash: formatHash, + hash: formatHash, + logs: arrayOf(formatReceiptLog), + blockNumber: getNumber, + cumulativeGasUsed: getBigInt, + effectiveGasPrice: allowNull(getBigInt), + status: allowNull(getNumber), + type: allowNull(getNumber, 0), + etxs: (value) => (value === null ? [] : arrayOf(formatEtx)(value)), + }, + { + hash: ['transactionHash'], + index: ['transactionIndex'], + }, +); export function formatTransactionReceipt(value: any): TransactionReceiptParams { const result = _formatTransactionReceipt(value); @@ -253,78 +302,96 @@ export function formatTransactionReceipt(value: any): TransactionReceiptParams { } export function formatTransactionResponse(value: any): TransactionResponseParams { - // Some clients (TestRPC) do strange things like return 0x0 for the - // 0 address; correct this to be a real address - if (value.to && getBigInt(value.to) === BN_0) { - value.to = '0x0000000000000000000000000000000000000000'; - } - if (value.type === '0x1') value.from = value.sender; - - const result = object( - { - hash: formatHash, - - type: (value: any) => { - if (value === '0x' || value == null) { - return 0; - } - return getNumber(value); + // Determine if it is a Quai or Qi transaction based on the type + const transactionType = parseInt(value.type, 16); + + let result: TransactionResponseParams; + + if (transactionType === 0x1) { + // QuaiTransactionResponseParams + result = object( + { + hash: formatHash, + type: (value: any) => { + if (value === '0x' || value == null) { + return 0; + } + return parseInt(value, 16); + }, + accessList: allowNull(accessListify, null), + blockHash: allowNull(formatHash, null), + blockNumber: allowNull((value: any) => (value ? parseInt(value, 16) : null), null), + index: allowNull((value: any) => (value ? BigInt(value) : null), null), + from: getAddress, + maxPriorityFeePerGas: allowNull((value: any) => (value ? BigInt(value) : null)), + maxFeePerGas: allowNull((value: any) => (value ? BigInt(value) : null)), + gasLimit: allowNull((value: any) => (value ? BigInt(value) : null), null), + to: allowNull(getAddress, null), + value: allowNull((value: any) => (value ? BigInt(value) : null), null), + nonce: allowNull((value: any) => (value ? parseInt(value, 16) : null), null), + creates: allowNull(getAddress, null), + chainId: allowNull((value: any) => (value ? BigInt(value) : null), null), + data: (value: any) => value, }, - accessList: allowNull(accessListify, null), - - blockHash: allowNull(formatHash, null), - blockNumber: allowNull(getNumber, null), - index: allowNull(getNumber, null), - - from: getAddress, - - maxPriorityFeePerGas: allowNull(getBigInt), - maxFeePerGas: allowNull(getBigInt), - - gasLimit: getBigInt, - to: allowNull(getAddress, null), - value: getBigInt, - nonce: getNumber, - - creates: allowNull(getAddress, null), - - chainId: allowNull(getBigInt, null), - }, - { - data: ['input'], - gasLimit: ['gas'], - index: ['transactionIndex'], - }, - )(value); - - // If to and creates are empty, populate the creates from the value - if (result.to == null && result.creates == null) { - result.creates = getCreateAddress(result); - } + { + data: ['input'], + gasLimit: ['gas'], + index: ['transactionIndex'], + }, + )(value) as QuaiTransactionResponseParams; - // Add an access list to supported transaction types - if ((value.type === 1 || value.type === 2) && value.accessList == null) { - result.accessList = []; - } + // Add an access list to supported transaction types + if ((value.type === 1 || value.type === 2) && value.accessList == null) { + result.accessList = []; + } - // Compute the signature - if (value.signature) { - result.signature = Signature.from(value.signature); - } else { - result.signature = Signature.from(value); - } + // Compute the signature + if (value.signature) { + result.signature = Signature.from(value.signature); + } else { + result.signature = Signature.from(value); + } - // Some backends omit ChainId on legacy transactions, but we can compute it - if (result.chainId == null) { - const chainId = result.signature.legacyChainId; - if (chainId != null) { - result.chainId = chainId; + // Some backends omit ChainId on legacy transactions, but we can compute it + if (result.chainId == null) { + const chainId = result.signature.legacyChainId; + if (chainId != null) { + result.chainId = chainId; + } } - } - // 0x0000... should actually be null - if (result.blockHash && getBigInt(result.blockHash) === BN_0) { - result.blockHash = null; + // 0x0000... should actually be null + if (result.blockHash && getBigInt(result.blockHash) === BN_0) { + result.blockHash = null; + } + } else if (transactionType === 0x2) { + // QiTransactionResponseParams + result = object( + { + hash: formatHash, + type: (value: any) => { + if (value === '0x' || value == null) { + return 0; + } + return parseInt(value, 16); + }, + blockHash: allowNull(formatHash, null), + blockNumber: allowNull((value: any) => (value ? parseInt(value, 16) : null), null), + index: allowNull((value: any) => (value ? BigInt(value) : null), null), + chainId: allowNull((value: any) => (value ? BigInt(value) : null), null), + signature: (value: any) => value, + txInputs: allowNull((value: any) => value, null), + txOutputs: allowNull((value: any) => value, null), + }, + { + index: ['transactionIndex'], + signature: ['utxoSignature'], + txInputs: ['inputs'], + txOutputs: ['outputs'], + }, + )(value) as QiTransactionResponseParams; + } else { + throw new Error('Unknown transaction type'); } return result; diff --git a/src/providers/formatting.ts b/src/providers/formatting.ts index 1c7205a4..1a316ee7 100644 --- a/src/providers/formatting.ts +++ b/src/providers/formatting.ts @@ -16,98 +16,71 @@ import type { AccessList, TxInput, TxOutput } from '../transaction/index.js'; * @category Providers */ export interface BlockParams { - /** - * The block hash. - */ - hash?: null | string; - - /** - * The block number. - */ - number: Array | number; - - /** - * The hash of the previous block in the blockchain. The genesis block has the parentHash of the - * [ZeroHash](../variables/ZeroHash). - */ - parentHash: Array | string; - - /** - * A random sequence provided during the mining process for proof-of-work networks. - */ - nonce: string; + extTransactions: ReadonlyArray; + interlinkHashes: Array; // New parameter + order: number; + size: bigint; + subManifest: Array | null; + totalEntropy: bigint; + transactions: ReadonlyArray; + uncles: Array | null; + woBody: WoBodyParams; // New nested parameter structure + woHeader: WoHeaderParams; // New nested parameter structure +} - /** - * For proof-of-work networks, the difficulty target is used to adjust the difficulty in mining to ensure a expected - * block rate. - */ - difficulty: bigint; +export interface WoBodyParams { + extTransactions: Array; + header: WoBodyHeaderParams; + interlinkHashes: Array; + manifest: Array; + transactions: Array; + uncles: Array; +} - /** - * The maximum amount of gas a block can consume. - */ +export interface WoBodyHeaderParams { + baseFeePerGas: null | bigint; + efficiencyScore: bigint; + etxEligibleSlices: string; + etxSetRoot: string; + evmRoot: string; + expansionNumber: number; + extRollupRoot: string; + extTransactionsRoot: string; + extraData: string; gasLimit: bigint; - - /** - * The amount of gas a block consumed. - */ gasUsed: bigint; - - /** - * The miner (or author) of a block. - */ - miner: string; - - /** - * Additional data the miner choose to include. - */ - extraData: string; - - /** - * The protocol-defined base fee per gas in an [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) block. - */ - baseFeePerGas: null | bigint; - + hash: null | string; + interlinkRootHash: string; manifestHash: Array; - - location: bigint; - + miner: string; + mixHash: string; + nonce: string; + number: Array; parentDeltaS: Array; - parentEntropy: Array; - - order: number; - - subManifest: Array | null; - - totalEntropy: bigint; - - mixHash: string; - + parentHash: Array; + parentUncledS: Array; + parentUncledSubDeltaS: Array; + primeTerminus: string; receiptsRoot: string; - sha3Uncles: string; - size: bigint; - - evmRoot: string; - - utxoRoot: string; - - uncles: Array | null; - - /** - * The list of transactions in the block. - */ - transactions: ReadonlyArray; - + thresholdCount: bigint; transactionsRoot: string; + uncledS: bigint; + utxoRoot: string; +} - extRollupRoot: string; - - extTransactions: ReadonlyArray; - - extTransactionsRoot: string; +export interface WoHeaderParams { + difficulty: string; + headerHash: string; + location: string; + mixHash: string; + nonce: string; + number: string; + parentHash: string; + time: string; + txHash: string; } ////////////////////// diff --git a/src/providers/provider.ts b/src/providers/provider.ts index ce9333cf..0c0c1b7b 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -46,6 +46,9 @@ import { QiTransactionResponseParams, QuaiTransactionResponseParams, TransactionReceiptParams, + WoBodyHeaderParams, + WoBodyParams, + WoHeaderParams, } from './formatting.js'; import { WorkObjectLike } from '../transaction/work-object.js'; import { QiTransactionLike } from '../transaction/qi-transaction.js'; @@ -61,7 +64,7 @@ function getValue(value: undefined | null | T): null | T { return value; } -function toJson(value: null | bigint): null | string { +function toJson(value: null | bigint | string): null | string { if (value == null) { return null; } @@ -436,131 +439,134 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest * * @category Providers */ -export interface MinedBlock extends Block { - /** - * The block number also known as the block height. - */ - readonly number: number; - - /** - * The block hash. - */ - readonly hash: string; - - /** - * The block timestamp, in seconds from epoch. - */ - readonly timestamp: number; - - /** - * The block date, created from the {@link MinedBlock.timestamp | **timestamp**}. - */ - readonly date: Date; +export interface MinedBlock extends Block {} + +export class WoBody implements WoBodyParams { + readonly extTransactions!: Array; + readonly header: WoBodyHeader; + readonly interlinkHashes: Array; + readonly manifest: Array; + readonly transactions!: Array; + readonly uncles!: Array; + constructor(params: WoBodyParams) { + this.extTransactions = params.extTransactions; + this.header = new WoBodyHeader(params.header); + this.interlinkHashes = params.interlinkHashes; + this.manifest = params.manifest; + this.transactions = params.transactions; + this.uncles = params.uncles; + } +} - /** - * The miner of the block, also known as the `author` or block `producer`. - */ - readonly miner: string; +export class WoBodyHeader implements WoBodyHeaderParams { + readonly baseFeePerGas!: null | bigint; + readonly efficiencyScore: bigint; + readonly etxEligibleSlices: string; + readonly etxSetRoot: string; + readonly evmRoot!: string; + readonly expansionNumber: number; + readonly extRollupRoot!: string; + readonly extTransactionsRoot!: string; + readonly extraData!: string; + readonly gasLimit!: bigint; + readonly gasUsed!: bigint; + readonly hash!: null | string; + readonly interlinkRootHash: string; + readonly manifestHash!: Array; + readonly miner!: string; + readonly mixHash!: string; + readonly nonce!: string; + readonly number!: Array; + readonly parentDeltaS!: Array; + readonly parentEntropy!: Array; + readonly parentHash!: Array; + readonly parentUncledS: Array; + readonly parentUncledSubDeltaS: Array; + readonly primeTerminus: string; + readonly receiptsRoot!: string; + readonly sha3Uncles!: string; + readonly size!: bigint; + readonly thresholdCount: bigint; + readonly transactionsRoot!: string; + readonly uncledS: bigint; + readonly utxoRoot!: string; + constructor(params: WoBodyHeaderParams) { + this.baseFeePerGas = params.baseFeePerGas; + this.efficiencyScore = params.efficiencyScore; + this.etxEligibleSlices = params.etxEligibleSlices; + this.etxSetRoot = params.etxSetRoot; + this.evmRoot = params.evmRoot; + this.expansionNumber = params.expansionNumber; + this.extRollupRoot = params.extRollupRoot; + this.extTransactionsRoot = params.extTransactionsRoot; + this.extraData = params.extraData; + this.gasLimit = params.gasLimit; + this.gasUsed = params.gasUsed; + this.hash = params.hash; + this.interlinkRootHash = params.interlinkRootHash; + this.manifestHash = params.manifestHash; + this.miner = params.miner; + this.mixHash = params.mixHash; + this.nonce = params.nonce; + this.number = params.number; + this.parentDeltaS = params.parentDeltaS; + this.parentEntropy = params.parentEntropy; + this.parentHash = params.parentHash; + this.parentUncledS = params.parentUncledS; + this.parentUncledSubDeltaS = params.parentUncledSubDeltaS; + this.primeTerminus = params.primeTerminus; + this.receiptsRoot = params.receiptsRoot; + this.sha3Uncles = params.sha3Uncles; + this.size = params.size; + this.thresholdCount = params.thresholdCount; + this.transactionsRoot = params.transactionsRoot; + this.uncledS = params.uncledS; + this.utxoRoot = params.utxoRoot; + } } +export class WoHeader implements WoHeaderParams { + readonly difficulty!: string; + readonly headerHash: string; + readonly location!: string; + readonly mixHash!: string; + readonly nonce!: string; + readonly number!: string; + readonly parentHash!: string; + readonly time: string; + readonly txHash: string; + constructor(params: WoHeaderParams) { + this.difficulty = params.difficulty; + this.headerHash = params.headerHash; + this.location = params.location; + this.mixHash = params.mixHash; + this.nonce = params.nonce; + this.number = params.number; + this.parentHash = params.parentHash; + this.time = params.time; + this.txHash = params.txHash; + } +} /** * A **Block** represents the data associated with a full block on Ethereum. * * @category Providers */ export class Block implements BlockParams, Iterable { - /** - * The provider connected to the block used to fetch additional details if necessary. - */ - readonly provider!: Provider; - - /** - * The block number, sometimes called the block height. This is a sequential number that is one higher than the - * parent block. - */ - readonly number!: Array | number; - - /** - * The block hash. - * - * This hash includes all properties, so can be safely used to identify an exact set of block properties. - */ - readonly hash!: null | string; - - /** - * The timestamp for this block, which is the number of seconds since epoch that this block was included. - */ - readonly timestamp!: number; - - /** - * The block hash of the parent block. - */ - readonly parentHash!: Array | string; - - /** - * The nonce. - * - * On legacy networks, this is the random number inserted which permitted the difficulty target to be reached. - */ - readonly nonce!: string; - - /** - * The difficulty target. - * - * On legacy networks, this is the proof-of-work target required for a block to meet the protocol rules to be - * included. - * - * On modern networks, this is a random number arrived at using randao. @TODO: Find links? - */ - readonly difficulty!: bigint; - - /** - * The total gas limit for this block. - */ - readonly gasLimit!: bigint; - - /** - * The total gas used in this block. - */ - readonly gasUsed!: bigint; - - /** - * The miner coinbase address, wihch receives any subsidies for including this block. - */ - readonly miner!: string; - - /** - * Any extra data the validator wished to include. - */ - readonly extraData!: string; - - /** - * The base fee per gas that all transactions in this block were charged. - * - * This adjusts after each block, depending on how congested the network is. - */ - readonly baseFeePerGas!: null | bigint; - - readonly manifestHash!: Array; - readonly location!: bigint; - readonly parentDeltaS!: Array; - readonly parentEntropy!: Array; + readonly #extTransactions!: Array; + readonly interlinkHashes: Array; // New parameter readonly order!: number; + readonly size!: bigint; readonly subManifest!: Array | null; readonly totalEntropy!: bigint; - readonly mixHash!: string; - readonly receiptsRoot!: string; - readonly sha3Uncles!: string; - readonly size!: bigint; - readonly evmRoot!: string; - readonly utxoRoot!: string; + readonly #transactions!: Array; readonly uncles!: Array | null; - - readonly #transactions: Array; - readonly transactionsRoot: string; - readonly extRollupRoot: string; - readonly #extTransactions: Array; - readonly extTransactionsRoot: string; + readonly woBody: WoBody; // New nested parameter structure + readonly woHeader: WoHeader; // New nested parameter structure + /** + * The provider connected to the block used to fetch additional details if necessary. + */ + readonly provider!: Provider; /** * Create a new **Block** object. @@ -582,49 +588,15 @@ export class Block implements BlockParams, Iterable { return tx; }); - this.transactionsRoot = block.transactionsRoot; - - this.extRollupRoot = block.extRollupRoot; - - this.extTransactionsRoot = block.extTransactionsRoot; - - defineProperties(this, { - provider, - - hash: getValue(block.hash), - - number: block.number, - - parentHash: block.parentHash, - - nonce: block.nonce, - difficulty: block.difficulty, - - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - miner: block.miner, - extraData: block.extraData, - - baseFeePerGas: getValue(block.baseFeePerGas), - - manifestHash: block.manifestHash, - location: block.location, - parentDeltaS: block.parentDeltaS, - parentEntropy: block.parentEntropy, - order: block.order, - subManifest: block.subManifest, - totalEntropy: block.totalEntropy, - mixHash: block.mixHash, - receiptsRoot: block.receiptsRoot, - sha3Uncles: block.sha3Uncles, - size: block.size, - evmRoot: block.evmRoot, - utxoRoot: block.utxoRoot, - uncles: block.uncles, - transactionsRoot: block.transactionsRoot, - extRollupRoot: block.extRollupRoot, - extTransactionsRoot: block.extTransactionsRoot, - }); + this.interlinkHashes = block.interlinkHashes; + this.order = block.order; + this.size = block.size; + this.subManifest = block.subManifest; + this.totalEntropy = block.totalEntropy; + this.uncles = block.uncles; + this.woBody = new WoBody(block.woBody); + this.woHeader = new WoHeader(block.woHeader); + this.provider = provider; } /** @@ -700,36 +672,7 @@ export class Block implements BlockParams, Iterable { * Returns a JSON-friendly value. */ toJSON(): any { - const { - baseFeePerGas, - difficulty, - extraData, - gasLimit, - gasUsed, - hash, - miner, - nonce, - number, - parentHash, - timestamp, - manifestHash, - location, - parentDeltaS, - parentEntropy, - order, - subManifest, - totalEntropy, - mixHash, - receiptsRoot, - sha3Uncles, - size, - evmRoot, - utxoRoot, - uncles, - transactionsRoot, - extRollupRoot, - extTransactionsRoot, - } = this; + const { interlinkHashes, order, size, subManifest, totalEntropy, uncles, woBody, woHeader } = this; // Using getters to retrieve the transactions and extTransactions const transactions = this.transactions; @@ -737,34 +680,63 @@ export class Block implements BlockParams, Iterable { return { _type: 'Block', - baseFeePerGas: toJson(baseFeePerGas), - difficulty: toJson(difficulty), - extraData, - gasLimit: toJson(gasLimit), - gasUsed: toJson(gasUsed), - hash, - miner, - nonce, - number, - parentHash, - timestamp, - manifestHash, - location, - parentDeltaS, - parentEntropy, + interlinkHashes, order, + size: toJson(size), subManifest, - totalEntropy, - mixHash, - receiptsRoot, - sha3Uncles, - size, - evmRoot, - utxoRoot, + totalEntropy: toJson(totalEntropy), uncles, - transactionsRoot, - extRollupRoot, - extTransactionsRoot, + woBody: { + extTransactions: woBody.extTransactions, + header: { + baseFeePerGas: toJson(woBody.header.baseFeePerGas), + efficiencyScore: toJson(woBody.header.efficiencyScore), + etxEligibleSlices: woBody.header.etxEligibleSlices, + etxSetRoot: woBody.header.etxSetRoot, + evmRoot: woBody.header.evmRoot, + expansionNumber: woBody.header.expansionNumber, + extRollupRoot: woBody.header.extRollupRoot, + extTransactionsRoot: woBody.header.extTransactionsRoot, + extraData: woBody.header.extraData, + gasLimit: toJson(woBody.header.gasLimit), + gasUsed: toJson(woBody.header.gasUsed), + hash: woBody.header.hash, + interlinkRootHash: woBody.header.interlinkRootHash, + manifestHash: woBody.header.manifestHash, + miner: woBody.header.miner, + mixHash: woBody.header.mixHash, + nonce: woBody.header.nonce, + number: woBody.header.number, + parentDeltaS: woBody.header.parentDeltaS.map((val) => toJson(val)), + parentEntropy: woBody.header.parentEntropy.map((val) => toJson(val)), + parentHash: woBody.header.parentHash, + parentUncledS: woBody.header.parentUncledS.map((val) => toJson(val)), + parentUncledSubDeltaS: woBody.header.parentUncledSubDeltaS.map((val) => toJson(val)), + primeTerminus: woBody.header.primeTerminus, + receiptsRoot: woBody.header.receiptsRoot, + sha3Uncles: woBody.header.sha3Uncles, + size: toJson(woBody.header.size), + thresholdCount: toJson(woBody.header.thresholdCount), + transactionsRoot: woBody.header.transactionsRoot, + uncledS: toJson(woBody.header.uncledS), + utxoRoot: woBody.header.utxoRoot, + }, + interlinkHashes: woBody.interlinkHashes, + manifest: woBody.manifest, + transactions: woBody.transactions, + uncles: woBody.uncles, + }, + woHeader: { + difficulty: woHeader.difficulty, + headerHash: woHeader.headerHash, + location: woHeader.location, + mixHash: woHeader.mixHash, + nonce: woHeader.nonce, + number: woHeader.number, + parentHash: woHeader.parentHash, + time: woHeader.time, + txHash: woHeader.txHash, + }, transactions, // Includes the transaction hashes or full transactions based on the prefetched data extTransactions, // Includes the extended transaction hashes or full transactions based on the prefetched data }; @@ -798,10 +770,12 @@ export class Block implements BlockParams, Iterable { * included at. */ get date(): null | Date { - if (this.timestamp == null) { + const timestampHex = this.woHeader.time; + if (!timestampHex) { return null; } - return new Date(this.timestamp * 1000); + const timestamp = parseInt(timestampHex, 16); + return new Date(timestamp * 1000); } /** @@ -912,17 +886,20 @@ export class Block implements BlockParams, Iterable { * @returns {boolean} True if the block has been mined. */ isMined(): this is MinedBlock { - return !!this.hash; + return !!this.woBody.header.hash; } /** * @ignore */ orphanedEvent(): OrphanFilter { - if (!this.isMined()) { + if (!this.isMined() || !this.woHeader.number) { throw new Error(''); } - return createOrphanedBlockFilter(this); + return createOrphanedBlockFilter({ + hash: this.woBody.header.hash!, + number: parseInt(this.woHeader.number!, 16), + }); } } diff --git a/src/quais.ts b/src/quais.ts index 93ef638b..275d1094 100644 --- a/src/quais.ts +++ b/src/quais.ts @@ -49,6 +49,8 @@ export { ZeroHash, quaisymbol, MessagePrefix, + Zone, + Shard, } from './constants/index.js'; // CONTRACT diff --git a/src/signers/abstract-signer.ts b/src/signers/abstract-signer.ts index 5fc2d968..55aff0b2 100644 --- a/src/signers/abstract-signer.ts +++ b/src/signers/abstract-signer.ts @@ -39,7 +39,7 @@ async function populate(signer: AbstractSigner, tx: TransactionRequest): Promise if (pop.from != null) { const from = pop.from; - pop.from = Promise.all([signer.getAddress(), resolveAddress(from)]).then(([address, from]) => { + pop.from = await Promise.all([signer.getAddress(), resolveAddress(from)]).then(([address, from]) => { assertArgument(address.toLowerCase() === from.toLowerCase(), 'transaction from mismatch', 'tx.from', from); return address; }); diff --git a/src/wallet/hdwallet.ts b/src/wallet/hdwallet.ts index 887f2883..72a9fa30 100644 --- a/src/wallet/hdwallet.ts +++ b/src/wallet/hdwallet.ts @@ -1,197 +1,222 @@ -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 { 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 { 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: string; } // 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; - - // Map of account number to HDNodeWallet - protected _accounts: Map = new Map(); - - // Map of addresses to address info - protected _addresses: Map = new Map(); - - // Root node of the HD wallet - protected _root: HDNodeWallet; - - // Wallet parent path - protected static _parentPath: string = ""; - - protected provider?: Provider; - - /** - * @private - */ - protected constructor(root: HDNodeWallet, provider?: Provider) { - this._root = root; - this.provider = provider; - } - - protected parentPath(): string { - return (this.constructor as typeof HDWallet)._parentPath; - } - - protected coinType(): number { - return (this.constructor as typeof HDWallet)._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 { - const isValidAddressForZone = (address: string) => { + protected static _coinType?: number = 969 || 994; + + // Map of account number to HDNodeWallet + protected _accounts: Map = new Map(); + + // Map of addresses to address info + protected _addresses: Map = new Map(); + + // Root node of the HD wallet + protected _root: HDNodeWallet; + + // Wallet parent path + protected static _parentPath: string = ''; + + protected provider?: Provider; + + /** + * @private + */ + protected constructor(root: HDNodeWallet, provider?: Provider) { + this._root = root; + this.provider = provider; + } + + protected parentPath(): string { + return (this.constructor as typeof HDWallet)._parentPath; + } + + protected coinType(): number { + return (this.constructor as typeof HDWallet)._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 { + const isValidAddressForZone = (address: string) => { const zoneByte = getZoneForAddress(address); if (!zone) { 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)); - - return addressNode; - - } - - addAddress(account: number, zone: string, addressIndex: number): 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`); - } - }); - - const addressNode = this.deriveAddress(account, addressIndex, zone); - - // create the NeuteredAddressInfo object and update the maps - const neuteredAddressInfo = { - pubKey: addressNode.publicKey, - address: addressNode.address, - account: account, - index: addressNode.index, - change: false, - zone: zone - }; - - this._addresses.set(neuteredAddressInfo.address, neuteredAddressInfo); - - return neuteredAddressInfo; - - } - - getNextAddress(accountIndex: number, zone: string): NeuteredAddressInfo { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${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; - } - - getAddressInfo(address: string): NeuteredAddressInfo | null { - const addressInfo = this._addresses.get(address); - if (!addressInfo) { - return null; - } - return addressInfo; - } - - 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(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T { - const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any)._parentPath); - return new this(root); - } - - static fromMnemonic(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T { - return (this as any).createInstance(mnemonic); - } - - static createRandom(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(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 - - abstract sendTransaction(tx: TransactionRequest): Promise - - connect(provider: Provider): void { - this.provider = provider; - } + 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)); + + return addressNode; + } + + addAddress(account: number, zone: string, addressIndex: number): 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`); + } + }); + + const addressNode = this.deriveAddress(account, addressIndex, zone); + + // create the NeuteredAddressInfo object and update the maps + const neuteredAddressInfo = { + pubKey: addressNode.publicKey, + address: addressNode.address, + account: account, + index: addressNode.index, + change: false, + zone: zone, + }; + + this._addresses.set(neuteredAddressInfo.address, neuteredAddressInfo); + + return neuteredAddressInfo; + } + + getNextAddress(accountIndex: number, zone: string): NeuteredAddressInfo { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${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; + } + + getAddressInfo(address: string): NeuteredAddressInfo | null { + const addressInfo = this._addresses.get(address); + if (!addressInfo) { + return null; + } + return addressInfo; + } + + 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(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T { + const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any)._parentPath); + return new this(root); + } + + static fromMnemonic(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T { + return (this as any).createInstance(mnemonic); + } + + static createRandom( + 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( + 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; + + abstract sendTransaction(tx: TransactionRequest): Promise; + + connect(provider: Provider): void { + this.provider = provider; + } // helper function to validate the zone protected validateZone(zone: string): boolean { @@ -203,6 +228,5 @@ export abstract class HDWallet { shard.byte.toLowerCase() === zone, ); return shard !== undefined; - } - -} \ No newline at end of file + } +} diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 8e3529e1..ff436a5e 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -1,290 +1,289 @@ - - -import { HDWallet, NeuteredAddressInfo } from './hdwallet'; -import { HDNodeWallet } from "./hdnodewallet"; +import { HDWallet, NeuteredAddressInfo } from './hdwallet.js'; +import { HDNodeWallet } from './hdnodewallet.js'; import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; -import { computeAddress } from "../address/index.js"; +import { computeAddress } from '../address/index.js'; import { getBytes, hexlify } from '../utils/index.js'; import { TransactionLike, QiTransaction, TxInput } from '../transaction/index.js'; -import { MuSigFactory } from "@brandonblack/musig" -import { schnorr } from "@noble/curves/secp256k1"; -import { keccak_256 } from "@noble/hashes/sha3"; +import { MuSigFactory } from '@brandonblack/musig'; +import { schnorr } from '@noble/curves/secp256k1'; +import { keccak_256 } from '@noble/hashes/sha3'; import { musigCrypto } from '../crypto/index.js'; import { Outpoint } from '../transaction/utxo.js'; import { getZoneForAddress } from '../utils/index.js'; type OutpointInfo = { - outpoint: Outpoint; - address: string; - zone: string; - account?: number; + outpoint: Outpoint; + address: string; + zone: string; + account?: number; }; export class QiHDWallet extends HDWallet { + protected static _GAP_LIMIT: number = 20; + + protected static _cointype: number = 969; + + protected static _parentPath = `m/44'/${this._cointype}'`; + + // Map of change addresses to address info + protected _changeAddresses: Map = new Map(); + + // Array of naked addresses + protected _nakedChangeAddresses: NeuteredAddressInfo[] = []; + + // Array of naked change addresses + protected _nakedAddresses: NeuteredAddressInfo[] = []; + + protected _outpoints: OutpointInfo[] = []; + + private constructor(root: HDNodeWallet, provider?: Provider) { + super(root, provider); + } + + getNextChangeAddress(account: number, zone: string): NeuteredAddressInfo { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + if (!this._accounts.has(account)) { + this.addAccount(account); + } + const filteredAccountInfos = Array.from(this._changeAddresses.values()).filter( + (addressInfo) => addressInfo.account === account && addressInfo.zone === zone, + ); + const lastIndex = filteredAccountInfos.reduce( + (maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), + -1, + ); + // call derive address with change = true + const addressNode = this.deriveAddress(account, lastIndex + 1, zone, true); + + const neuteredAddressInfo = { + pubKey: addressNode.publicKey, + address: addressNode.address, + account: account, + index: addressNode.index, + change: true, + zone: zone, + }; + + this._changeAddresses.set(neuteredAddressInfo.address, neuteredAddressInfo); + + return neuteredAddressInfo; + } + + // getNextChangeAddress(account: number, zone: string): NeuteredAddressInfo { + // return this._getNextAddress(account, zone, this._changeAddresses, true); + // } + + importOutpoints(outpoints: OutpointInfo[]): void { + this._outpoints.push(...outpoints); + } + + getOutpoints(zone: string): OutpointInfo[] { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + return this._outpoints.filter((outpoint) => outpoint.zone === zone); + } + + /** + * Signs a Qi transaction and returns the serialized transaction + * + * @param {QiTransactionRequest} tx - The transaction to sign. + * + * @returns {Promise} The serialized transaction. + * @throws {Error} If the UTXO transaction is invalid. + */ + async signTransaction(tx: QiTransactionRequest): Promise { + const txobj = QiTransaction.from(tx); + if (!txobj.txInputs || txobj.txInputs.length == 0 || !txobj.txOutputs) + throw new Error('Invalid UTXO transaction, missing inputs or outputs'); + + const hash = keccak_256(txobj.unsignedSerialized); + + let signature: string; + + if (txobj.txInputs.length == 1) { + signature = this.createSchnorrSignature(txobj.txInputs[0], hash); + } else { + signature = this.createMuSigSignature(txobj, hash); + } + + txobj.signature = signature; + return txobj.serialized; + } + + async sendTransaction(tx: QiTransactionRequest): Promise { + if (!this.provider) { + throw new Error('Provider is not set'); + } + if (!tx.inputs || tx.inputs.length === 0) { + throw new Error('Transaction has no inputs'); + } + const input = tx.inputs[0]; + const address = computeAddress(hexlify(input.pub_key)); + const shard = getZoneForAddress(address); + if (!shard) { + throw new Error(`Address ${address} not found in any shard`); + } + + // verify all inputs are from the same shard + if (tx.inputs.some((input) => getZoneForAddress(computeAddress(hexlify(input.pub_key))) !== shard)) { + throw new Error('All inputs must be from the same shard'); + } + + const signedTx = await this.signTransaction(tx); + + return await this.provider.broadcastTransaction(shard, signedTx); + } + + // createSchnorrSignature returns a schnorr signature for the given message and private key + private createSchnorrSignature(input: TxInput, hash: Uint8Array): string { + const privKey = this.derivePrivateKeyForInput(input); + const signature = schnorr.sign(hash, getBytes(privKey)); + return hexlify(signature); + } + + // createMuSigSignature returns a MuSig signature for the given message + // and private keys corresponding to the input addresses + private createMuSigSignature(tx: QiTransaction, hash: Uint8Array): string { + const musig = MuSigFactory(musigCrypto); + + // Collect private keys corresponding to the pubkeys found on the inputs + const privKeysSet = new Set(); + tx.txInputs!.forEach((input) => { + const privKey = this.derivePrivateKeyForInput(input); + privKeysSet.add(privKey); + }); + const privKeys = Array.from(privKeysSet); + + // Create an array of public keys corresponding to the private keys for musig aggregation + const pubKeys: Uint8Array[] = privKeys + .map((privKey) => musigCrypto.getPublicKey(getBytes(privKey!), true)) + .filter((pubKey) => pubKey !== null) as Uint8Array[]; + + // Generate nonces for each public key + const nonces = pubKeys.map((pk) => musig.nonceGen({ publicKey: getBytes(pk!) })); + const aggNonce = musig.nonceAgg(nonces); + + const signingSession = musig.startSigningSession(aggNonce, hash, pubKeys); + + // Create partial signatures for each private key + const partialSignatures = privKeys.map((sk, index) => + musig.partialSign({ + secretKey: getBytes(sk || ''), + publicNonce: nonces[index], + sessionKey: signingSession, + verify: true, + }), + ); + + // Aggregate the partial signatures into a final aggregated signature + const finalSignature = musig.signAgg(partialSignatures, signingSession); + + return hexlify(finalSignature); + } + + // Helper method that returns the private key for the public key + private derivePrivateKeyForInput(input: TxInput): string { + if (!input.pub_key) throw new Error('Missing public key for input'); + const pubKey = hexlify(input.pub_key); + const address = computeAddress(pubKey); + // get address info + const addressInfo = this.getAddressInfo(address); + if (!addressInfo) throw new Error(`Address not found: ${address}`); + // derive an HDNode for the address and get the private key + const accountNode = this._accounts.get(addressInfo.account); + if (!accountNode) { + throw new Error(`Account ${addressInfo.account} not found for address ${address}`); + } + const changeNode = accountNode.deriveChild(0); + const addressNode = changeNode.deriveChild(addressInfo.index); + return addressNode.privateKey; + } + + // scan scans the specified zone for addresses with unspent outputs. + // Starting at index 0, tt will generate new addresses until + // the gap limit is reached for both naked and change addresses. + async scan(zone: string, account: number = 0): Promise { + // flush the existing addresses and outpoints + this._addresses = new Map(); + this._changeAddresses = new Map(); + this._nakedAddresses = []; + this._nakedChangeAddresses = []; + this._outpoints = []; + + await this._scan(zone, account); + } + + // sync scans the specified zone for addresses with unspent outputs. + // Starting at the last address index, it will generate new addresses until + // the gap limit is reached for both naked and change addresses. + // If no account is specified, it will scan all accounts known to the wallet + async sync(zone: string, account?: number): Promise { + if (account) { + await this._scan(zone, account); + } else { + for (const account of this._accounts.keys()) { + await this._scan(zone, account); + } + } + } + + private async _scan(zone: string, account: number = 0): Promise { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + if (!this.provider) throw new Error('Provider not set'); + + if (!this._accounts.has(account)) { + this.addAccount(account); + } + + let nakedAddressesCount = 0; + let changeNakedAddressesCount = 0; + + // helper function to handle the common logic for both naked and change addresses + const handleAddressScanning = async ( + getAddressInfo: () => NeuteredAddressInfo, + addressesCount: number, + nakedAddresArray: NeuteredAddressInfo[], + ): Promise => { + const addressInfo = getAddressInfo(); + const outpoints = await this.getOutpointsByAddress(addressInfo.address); + if (outpoints.length === 0) { + addressesCount++; + nakedAddresArray.push(addressInfo); + } else { + addressesCount = 0; + nakedAddresArray = []; + const newOutpointsInfo = outpoints.map((outpoint) => ({ + outpoint, + address: addressInfo.address, + zone: zone, + })); + this._outpoints.push(...newOutpointsInfo); + } + return addressesCount; + }; + + // main loop to scan addresses up to the gap limit + while (nakedAddressesCount < QiHDWallet._GAP_LIMIT || changeNakedAddressesCount < QiHDWallet._GAP_LIMIT) { + [nakedAddressesCount, changeNakedAddressesCount] = await Promise.all([ + nakedAddressesCount < QiHDWallet._GAP_LIMIT + ? handleAddressScanning( + () => this.getNextAddress(account, zone), + nakedAddressesCount, + this._nakedAddresses, + ) + : nakedAddressesCount, + + changeNakedAddressesCount < QiHDWallet._GAP_LIMIT + ? handleAddressScanning( + () => this.getNextChangeAddress(account, zone), + changeNakedAddressesCount, + this._nakedChangeAddresses, + ) + : changeNakedAddressesCount, + ]); + } + } - protected static _GAP_LIMIT: number = 20; - - protected static _cointype: number = 969; - - protected static _parentPath = `m/44'/${this._cointype}'`; - - // Map of change addresses to address info - protected _changeAddresses: Map = new Map(); - - // Array of naked addresses - protected _nakedChangeAddresses: NeuteredAddressInfo[] = []; - - // Array of naked change addresses - protected _nakedAddresses: NeuteredAddressInfo[] = []; - - protected _outpoints: OutpointInfo[] = []; - - private constructor(root: HDNodeWallet, provider?: Provider) { - super(root, provider); - } - - getNextChangeAddress(account: number, zone: string): NeuteredAddressInfo { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - if (!this._accounts.has(account)) { - this.addAccount(account); - } - const filteredAccountInfos = Array.from(this._changeAddresses.values()).filter((addressInfo) => - addressInfo.account === account && addressInfo.zone === zone - ); - const lastIndex = filteredAccountInfos.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1); - // call derive address with change = true - const addressNode = this.deriveAddress(account, lastIndex + 1, zone, true); - - const neuteredAddressInfo = { - pubKey: addressNode.publicKey, - address: addressNode.address, - account: account, - index: addressNode.index, - change: true, - zone: zone - }; - - this._changeAddresses.set(neuteredAddressInfo.address, neuteredAddressInfo); - - return neuteredAddressInfo; - } - - // getNextChangeAddress(account: number, zone: string): NeuteredAddressInfo { - // return this._getNextAddress(account, zone, this._changeAddresses, true); - // } - - importOutpoints(outpoints: OutpointInfo[]): void { - this._outpoints.push(...outpoints); - } - - getOutpoints(zone: string): OutpointInfo[] { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - return this._outpoints.filter((outpoint) => outpoint.zone === zone); - } - - /** - * Signs a Qi transaction and returns the serialized transaction - * - * @param {QiTransactionRequest} tx - The transaction to sign. - * - * @returns {Promise} The serialized transaction. - * @throws {Error} If the UTXO transaction is invalid. - */ - async signTransaction(tx: QiTransactionRequest): Promise { - const txobj = QiTransaction.from(tx); - if (!txobj.txInputs || txobj.txInputs.length == 0 || !txobj.txOutputs) - throw new Error('Invalid UTXO transaction, missing inputs or outputs'); - - const hash = keccak_256(txobj.unsignedSerialized); - - let signature: string; - - if (txobj.txInputs.length == 1) { - signature = this.createSchnorrSignature(txobj.txInputs[0], hash); - } else { - signature = this.createMuSigSignature(txobj, hash); - } - - txobj.signature = signature; - return txobj.serialized; - } - - async sendTransaction(tx: QiTransactionRequest): Promise { - if (!this.provider) { - throw new Error("Provider is not set"); - } - if (!tx.inputs || tx.inputs.length === 0) { - throw new Error('Transaction has no inputs'); - } - const input = tx.inputs[0]; - const address = computeAddress(hexlify(input.pub_key)); - const shard = getZoneForAddress(address); - if (!shard) { - throw new Error(`Address ${address} not found in any shard`); - } - - // verify all inputs are from the same shard - if (tx.inputs.some((input) => getZoneForAddress(computeAddress(hexlify(input.pub_key))) !== shard)) { - throw new Error('All inputs must be from the same shard'); - } - - const signedTx = await this.signTransaction(tx); - - return await this.provider.broadcastTransaction(shard, signedTx); - } - - // createSchnorrSignature returns a schnorr signature for the given message and private key - private createSchnorrSignature(input: TxInput, hash: Uint8Array): string { - const privKey = this.derivePrivateKeyForInput(input); - const signature = schnorr.sign(hash, getBytes(privKey)); - return hexlify(signature); - } - - // createMuSigSignature returns a MuSig signature for the given message - // and private keys corresponding to the input addresses - private createMuSigSignature(tx: QiTransaction, hash: Uint8Array): string { - const musig = MuSigFactory(musigCrypto); - - // Collect private keys corresponding to the pubkeys found on the inputs - const privKeysSet = new Set(); - tx.txInputs!.forEach((input) => { - const privKey = this.derivePrivateKeyForInput(input) - privKeysSet.add(privKey); - }); - const privKeys = Array.from(privKeysSet); - - // Create an array of public keys corresponding to the private keys for musig aggregation - const pubKeys: Uint8Array[] = privKeys - .map((privKey) => musigCrypto.getPublicKey(getBytes(privKey!), true)) - .filter((pubKey) => pubKey !== null) as Uint8Array[]; - - // Generate nonces for each public key - const nonces = pubKeys.map((pk) => musig.nonceGen({ publicKey: getBytes(pk!) })); - const aggNonce = musig.nonceAgg(nonces); - - const signingSession = musig.startSigningSession(aggNonce, hash, pubKeys); - - // Create partial signatures for each private key - const partialSignatures = privKeys.map((sk, index) => - musig.partialSign({ - secretKey: getBytes(sk || ''), - publicNonce: nonces[index], - sessionKey: signingSession, - verify: true, - }), - ); - - // Aggregate the partial signatures into a final aggregated signature - const finalSignature = musig.signAgg(partialSignatures, signingSession); - - return hexlify(finalSignature); - } - - // Helper method that returns the private key for the public key - private derivePrivateKeyForInput(input: TxInput): string { - if (!input.pub_key) throw new Error('Missing public key for input'); - const pubKey = hexlify(input.pub_key); - const address = computeAddress(pubKey); - // get address info - const addressInfo = this.getAddressInfo(address); - if (!addressInfo) throw new Error(`Address not found: ${address}`); - // derive an HDNode for the address and get the private key - const accountNode = this._accounts.get(addressInfo.account); - if (!accountNode) { - throw new Error(`Account ${addressInfo.account} not found for address ${address}`); - } - const changeNode = accountNode.deriveChild(0); - const addressNode = changeNode.deriveChild(addressInfo.index); - return addressNode.privateKey; - } - - // scan scans the specified zone for addresses with unspent outputs. - // Starting at index 0, tt will generate new addresses until - // the gap limit is reached for both naked and change addresses. - async scan(zone: string, account: number = 0): Promise { - // flush the existing addresses and outpoints - this._addresses = new Map(); - this._changeAddresses = new Map(); - this._nakedAddresses = []; - this._nakedChangeAddresses = []; - this._outpoints = []; - - await this._scan(zone, account); - } - - // sync scans the specified zone for addresses with unspent outputs. - // Starting at the last address index, it will generate new addresses until - // the gap limit is reached for both naked and change addresses. - // If no account is specified, it will scan all accounts known to the wallet - async sync(zone: string, account?: number): Promise { - if (account) { - await this._scan(zone, account); - } else { - for (const account of this._accounts.keys()) { - await this._scan(zone, account); - } - } - } - - private async _scan(zone: string, account: number = 0): Promise { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - if (!this.provider) throw new Error('Provider not set'); - - if (!this._accounts.has(account)) { - this.addAccount(account); - } - - let nakedAddressesCount = 0; - let changeNakedAddressesCount = 0; - - // helper function to handle the common logic for both naked and change addresses - const handleAddressScanning = async ( - getAddressInfo: () => NeuteredAddressInfo, - addressesCount: number, - nakedAddresArray: NeuteredAddressInfo[], - ): Promise => { - const addressInfo = getAddressInfo(); - const outpoints = await this.getOutpointsByAddress(addressInfo.address); - if (outpoints.length === 0) { - addressesCount++; - nakedAddresArray.push(addressInfo) - } else { - addressesCount = 0; - nakedAddresArray = []; - const newOutpointsInfo = outpoints.map((outpoint) => ({ - outpoint, - address: addressInfo.address, - zone: zone, - })); - this._outpoints.push(...newOutpointsInfo); - } - return addressesCount; - }; - - // main loop to scan addresses up to the gap limit - while (nakedAddressesCount < QiHDWallet._GAP_LIMIT || changeNakedAddressesCount < QiHDWallet._GAP_LIMIT) { - [nakedAddressesCount, changeNakedAddressesCount] = await Promise.all([ - nakedAddressesCount < QiHDWallet._GAP_LIMIT - ? handleAddressScanning( - () => this.getNextAddress(account, zone), - nakedAddressesCount, - this._nakedAddresses, - ) - : nakedAddressesCount, - - changeNakedAddressesCount < QiHDWallet._GAP_LIMIT - ? handleAddressScanning( - () => this.getNextChangeAddress(account, zone), - changeNakedAddressesCount, - this._nakedChangeAddresses, - ) - : changeNakedAddressesCount, - ]); - } - } - - - // getOutpointsByAddress queries the network node for the outpoints of the specified address + // getOutpointsByAddress queries the network node for the outpoints of the specified address private async getOutpointsByAddress(address: string): Promise { try { const outpointsMap = await this.provider!.getOutpointsByAddress(address); @@ -295,23 +294,23 @@ export class QiHDWallet extends HDWallet { } catch (error) { throw new Error(`Failed to get outpoints for address: ${address}`); } - } - - getChangeAddressesForZone(zone: string): NeuteredAddressInfo[] { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - const changeAddresses = this._changeAddresses.values(); - return Array.from(changeAddresses).filter((addressInfo) => addressInfo.zone === zone); - } - - getNakedAddressesForZone(zone: string): NeuteredAddressInfo[] { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - const nakedAddresses = this._nakedAddresses.filter((addressInfo) => addressInfo.zone === zone); - return nakedAddresses; - } - - getNakedChangeAddressesForZone(zone: string): NeuteredAddressInfo[] { - if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); - const nakedChangeAddresses = this._nakedChangeAddresses.filter((addressInfo) => addressInfo.zone === zone); - return nakedChangeAddresses; - } -} \ No newline at end of file + } + + getChangeAddressesForZone(zone: string): NeuteredAddressInfo[] { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + const changeAddresses = this._changeAddresses.values(); + return Array.from(changeAddresses).filter((addressInfo) => addressInfo.zone === zone); + } + + getNakedAddressesForZone(zone: string): NeuteredAddressInfo[] { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + const nakedAddresses = this._nakedAddresses.filter((addressInfo) => addressInfo.zone === zone); + return nakedAddresses; + } + + getNakedChangeAddressesForZone(zone: string): NeuteredAddressInfo[] { + if (!this.validateZone(zone)) throw new Error(`Invalid zone: ${zone}`); + const nakedChangeAddresses = this._nakedChangeAddresses.filter((addressInfo) => addressInfo.zone === zone); + return nakedChangeAddresses; + } +} diff --git a/src/wallet/quai-hdwallet.ts b/src/wallet/quai-hdwallet.ts index 48d704da..0f0b4d1e 100644 --- a/src/wallet/quai-hdwallet.ts +++ b/src/wallet/quai-hdwallet.ts @@ -1,8 +1,7 @@ - -import { HDWallet } from './hdwallet'; -import { HDNodeWallet } from "./hdnodewallet"; +import { HDWallet } from './hdwallet.js'; +import { HDNodeWallet } from './hdnodewallet.js'; import { QuaiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; -import { resolveAddress } from "../address/index.js"; +import { resolveAddress } from '../address/index.js'; export class QuaiHDWallet extends HDWallet { protected static _cointype: number = 994; @@ -32,16 +31,16 @@ export class QuaiHDWallet extends HDWallet { const from = await resolveAddress(tx.from); const fromNode = this._getHDNode(from); const signedTx = await fromNode.signTransaction(tx); - return signedTx; + return signedTx; } - async sendTransaction(tx: QuaiTransactionRequest): Promise { + async sendTransaction(tx: QuaiTransactionRequest): Promise { if (!this.provider) { - throw new Error("Provider is not set"); + throw new Error('Provider is not set'); } const from = await resolveAddress(tx.from); const fromNode = this._getHDNode(from); const fromNodeConnected = fromNode.connect(this.provider); return await fromNodeConnected.sendTransaction(tx); } -} \ No newline at end of file +}