From: Chris Duncan Date: Mon, 13 Apr 2026 05:32:57 +0000 (-0700) Subject: Refactor accounts to instantiate synchronously. Remove index from accounts since... X-Git-Tag: v0.12.0~5^2~1 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=d6af04092341584c7488b064d0675fda41615348;p=libnemo.git Refactor accounts to instantiate synchronously. Remove index from accounts since that is a wallet data point. Update relevant references and tests. --- diff --git a/src/lib/account/index.ts b/src/lib/account/index.ts index 5c5e2ff..8172397 100644 --- a/src/lib/account/index.ts +++ b/src/lib/account/index.ts @@ -10,12 +10,6 @@ import { Address } from './address' import { _refresh } from './refresh' import { _validate } from './validate' -type KeyPair = { - index?: number - privateKey?: string | Uint8Array - publicKey?: string | Uint8Array -} - /** * Represents a single Nano address and the associated public key. To include * the matching private key, it must be known at the time of object @@ -25,16 +19,11 @@ type KeyPair = { export class Account { [key: string]: any - static #isInternal: boolean = false - static get isInternal (): boolean { return this.#isInternal } - /** - * @returns {'Account'} - */ + /** @returns {'Account'} */ static get DB_NAME (): 'Account' { return 'Account' } + #publicKey: Uint8Array = new Uint8Array(32) #address?: Address - #index?: number - #publicKey: Uint8Array #confirmed_balance?: bigint #confirmed_height?: number @@ -53,14 +42,13 @@ export class Account { #representative_block?: string #weight?: bigint + get publicKey (): string { return bytes.toHex(this.#publicKey) } get address (): string { - if (this.#address == null) { + if (this.#address === undefined) { throw new Error('Account not found') } return this.#address.toString() } - get index (): number | undefined { return this.#index } - get publicKey (): string { return bytes.toHex(this.#publicKey) } get confirmed_balance (): bigint | undefined { return this.#confirmed_balance } get confirmed_height (): number | undefined { return this.#confirmed_height } @@ -88,7 +76,7 @@ export class Account { if (v instanceof (this.constructor as typeof Account)) { this.#confirmed_representative = v } else if (typeof v === 'string') { - this.#confirmed_representative = (this.constructor as typeof Account).load(v) + this.#confirmed_representative = new Account(v) } else { throw new TypeError(`Invalid argument for account confirmed representative: ${v}`) } @@ -104,7 +92,7 @@ export class Account { if (v instanceof (this.constructor as typeof Account) || v === undefined) { this.#representative = v } else if (typeof v === 'string') { - this.#representative = (this.constructor as typeof Account).load(v) + this.#representative = new Account(v) } else { throw new TypeError(`Invalid argument for account representative: ${v}`) } @@ -112,22 +100,12 @@ export class Account { set representative_block (v: string | undefined) { this.#representative_block = v } set weight (v: bigint | number | string) { this.#weight = BigInt(v) } - private constructor (address: Address, publicKey: Uint8Array, index?: number) { - if (!(this.constructor as typeof Account).isInternal) { - throw new Error('Account cannot be instantiated directly. Use `load()` instead.') - } - this.#address = address - this.#publicKey = publicKey - this.#index = index - } - /** * Releases variable references to allow garbage collection. */ destroy (): void { - this.#address = undefined - this.#index = undefined this.#publicKey.fill(0) + this.#address = undefined this.#confirmed_balance = undefined this.#confirmed_height = undefined @@ -153,9 +131,8 @@ export class Account { */ toJSON () { return { - address: this.address, - index: this.index, publicKey: this.publicKey, + address: this.address, confirmed_balance: this.confirmed_balance?.toString(), confirmed_height: this.confirmed_height?.toString(), confirmed_frontier: this.confirmed_frontier, @@ -177,55 +154,60 @@ export class Account { * @param {string} address - Address of the account * @returns {Account} A new Account object */ - static load (address: string): Account + constructor (address: string) /** - * Instantiates Account objects from their Nano addresses. - * @param {string[]} addresses - Addresses of the accounts - * @returns {Account[]} Array of new Account objects - */ - static load (addresses: string[]): Account[] - /** - * Instantiates an Account object from its public key. It is unable to sign - * blocks or messages since it has no private key. - * @param {string | Uint8Array} publicKey - Public key of the account + * Instantiates an Account object from its public key. + * @param {(string | Uint8Array)} publicKey - Public key of the account * @returns {Account} A new Account object */ - static load (publicKey: string | Uint8Array): Account - /** - * Instantiates Account objects from their public keys. They are unable to sign - * blocks or messages since they have no private key. - * @param {string | Uint8Array[]} publicKeys - Public keys of the accounts - * @returns {Account[]} Array of new Account objects - */ - static load (publicKeys: string | Uint8Array[]): Account[] + constructor (publicKey: string | ArrayBuffer | Uint8Array) /** - * Instantiates an Account object from its public key. It is unable to sign - * blocks or messages since it has no private key. - * @param {KeyPair} keypair - Index and keys of the account + * Instantiates an Account object from its public or private key. + * + * If the key is indicated as private, then it is copied locally, used to + * derive the corresponding public key, and finally zeroed out; the user is + * responsible for securely handling the original input. + * @param {(string | Uint8Array)} key - Public or private key of the account + * @param {string} type - Indicates which type the key is * @returns {Account} A new Account object */ - static load (keypair: KeyPair): Account - /** - * Instantiates Account objects from their public keys. They are unable to sign - * blocks or messages since they have no private key. - * @param {KeyPair[]} keypairs - Indexes and keys of the accounts - * @returns {Account[]} Array of new Account objects - */ - static load (keypairs: KeyPair[]): Account[] - /** - * Instantiates Account objects from their private keys which are used to - * derive public keys and then discarded. - * @param {(string | Uint8Array | KeyPair | (string | Uint8Array | KeyPair)[])} input - Indexes and keys of the accounts - * @returns {(Account | Account[])} Array of new Account objects - */ - static load (input: string | Uint8Array | KeyPair | (string | Uint8Array | KeyPair)[], type?: 'private'): Account | Account[] { - const isInputArray = Array.isArray(input) - const inputs = isInputArray ? input : [input] - if (this.#isKeyPairs(inputs) && type === 'private') { - const r = this.#fromPrivate(inputs) - return isInputArray ? r : r[0] + constructor (key: string | ArrayBuffer | Uint8Array, type: 'public' | 'private') + constructor (input: unknown, type: unknown = 'public') { + if (type === 'private') { + try { + if (typeof input === 'string' && RegExp(`^[A-F0-9]{${ACCOUNT_KEY_HEX_LENGTH}}$`, 'i').test(input)) { + input = hex.toBytes(input) + } + if (input instanceof Uint8Array) { + input = input.buffer + } + if (!(input instanceof ArrayBuffer) || input.byteLength !== ACCOUNT_KEY_BYTE_LENGTH) { + throw new TypeError('Invalid private key') + } + const prv = new Uint8Array(input.slice()) + try { + this.#publicKey = nano25519_derive(prv) + this.#address = new Address(this.#publicKey) + } finally { + prv.fill(0) + } + } catch (err) { + throw new Error('Failed to import Account from private key', { cause: err }) + } } else { - return isInputArray ? this.#fromPublic(inputs) : this.#fromPublic(inputs)[0] + try { + if (input instanceof Uint8Array) { + input = input.buffer + } + if (typeof input !== 'string' && !(input instanceof ArrayBuffer)) { + throw new TypeError('Invalid argument') + } + this.#address = new Address(input) + this.#publicKey = this.#address.toPublicKey() + } catch (err) { + console.error(err) + throw new TypeError('Failed to import Account from public data', { cause: { err } }) + } } } @@ -248,121 +230,4 @@ export class Account { static validate (address: string): asserts address is string { return _validate(address) } - - /** - * Instantiates an Account object from its private key which is used to derive - * the corresponding public key and then discarded. - * - * @param {KeyPair} keypairs - Indexes and keys of the accounts - * @returns {Promise} Promise for new Account objects - */ - static #fromPrivate (keypairs: KeyPair[]): Account[] { - try { - const accounts: Account[] = [] - for (let keypair of keypairs) { - let { index, privateKey } = keypair - if (index == null) { - throw new RangeError('Index missing for Account') - } - if (typeof privateKey === 'string' && RegExp(`^[A-F0-9]{${ACCOUNT_KEY_HEX_LENGTH}}$`, 'i').test(privateKey)) { - privateKey = hex.toBytes(privateKey) - } - if (!(privateKey instanceof Uint8Array) || privateKey.every(v => v === 0)) { - throw new TypeError('Invalid private key') - } - if (privateKey.byteLength !== ACCOUNT_KEY_BYTE_LENGTH) { - throw new TypeError(`Private key must be ${ACCOUNT_KEY_BYTE_LENGTH} bytes`) - } - const publicKey = nano25519_derive(privateKey) - const address = new Address(publicKey) - this.#isInternal = true - const account = new this(address, publicKey, index) - this.#isInternal = false - accounts.push(account) - } - return accounts - } catch (err) { - throw new Error('Failed to import Accounts from private keys', { cause: err }) - } - } - - /** - * Instantiates Account objects from public data, each specifying either its - * public key or its Nano address. - * - * @param {(string | Uint8Array|KeyPair)[]} input - Public keys or addresses of the accounts - * @returns {Account[]} The instantiated Account objects - */ - static #fromPublic (input: (string | Uint8Array | KeyPair)[] | unknown): Account[] { - try { - const keypairs = this.#isKeyPairs(input) - ? input - : this.#isKeys(input) - ? input.map(i => { return { publicKey: i } as KeyPair }) - : [] - if (keypairs.length === 0) { - throw new TypeError('Invalid public input for Account') - } - - const accounts: Account[] = [] - for (let keypair of keypairs) { - if (keypair.publicKey == null) { - throw new TypeError('Account address or public key is required', { cause: keypair.publicKey }) - } - const { index } = keypair - const address = new Address(keypair.publicKey) - const publicKey = address.toPublicKey() - this.#isInternal = true - const account = new this(address, publicKey, index) - this.#isInternal = false - accounts.push(account) - } - return accounts - } catch (err) { - console.error(err) - throw new TypeError('Failed to import Account from public data', { cause: { err } }) - } - } - - static #isKey (input: unknown): input is string | Uint8Array { - return typeof input === 'string' || (input instanceof Uint8Array && 'buffer' in input) - } - - static #isKeys (input: unknown): input is (string | Uint8Array)[] { - if (Array.isArray(input)) { - for (const i of input) { - if (!this.#isKey(i)) { - return false - } - } - } - return true - } - - static #isKeyPair (input: unknown): input is KeyPair { - if (typeof input === 'object') { - const obj = input as Record - if ('index' in obj && typeof obj.index === 'number') { - return true - } - if ('publicKey' in obj && this.#isKey(obj.publicKey)) { - return true - } - if ('privateKey' in obj && this.#isKey(obj.privateKey)) { - return true - } - } - return false - } - - static #isKeyPairs (input: unknown): input is KeyPair[] { - if (Array.isArray(input)) { - for (const i of input) { - if (!this.#isKeyPair(i)) { - return false - } - } - } - return true - } } diff --git a/src/lib/block.ts b/src/lib/block.ts index 17f5bc3..4f9d057 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -94,10 +94,10 @@ export class Block { constructor (account: unknown, balance: unknown, previous: unknown, representative: unknown) { try { if (typeof account === 'string') { - account = Account.load(account) + account = new Account(account) } if (typeof representative === 'string') { - representative = Account.load(representative) + representative = new Account(representative) } if (!(account instanceof Account)) { throw new TypeError('Invalid account') @@ -201,7 +201,7 @@ export class Block { throw new TypeError('Invalid account') } this.representative = (typeof representative === 'string') - ? Account.load(representative) + ? new Account(representative) : representative this.link = hex.toBytes(BURN_PUBLIC_KEY) @@ -356,7 +356,7 @@ export class Block { throw new TypeError('Invalid account', { cause: account }) } this.link = (typeof account === 'string') - ? hex.toBytes(Account.load(account).publicKey) + ? hex.toBytes(new Account(account).publicKey) : hex.toBytes(account.publicKey) return this diff --git a/src/lib/rolodex.ts b/src/lib/rolodex.ts index dcdb705..1b8d00a 100644 --- a/src/lib/rolodex.ts +++ b/src/lib/rolodex.ts @@ -47,7 +47,7 @@ export class Rolodex { .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('\\', '\\u005c') - const account = Account.load(address) + const account = new Account(address) try { const existingName = await this.getName(account.address) @@ -185,7 +185,7 @@ export class Rolodex { static async verify (name: string, signature: string, ...data: string[]): Promise { const addresses = await this.getAddresses(name) for (const address of addresses) { - const { publicKey } = Account.load(address) + const { publicKey } = new Account(address) const verified = await Tools.verify(publicKey, signature, ...data) if (verified) { return true diff --git a/src/lib/tools.ts b/src/lib/tools.ts index ce548e5..e6ae8fa 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -180,7 +180,7 @@ export class Tools { const blockQueue: Promise[] = [] const results: SweepResult[] = [] - const recipientAccount = Account.load(recipient) + const recipientAccount = new Account(recipient) const accounts = await wallet.refresh(rpc, from, to) for (const [index, account] of accounts) { diff --git a/src/lib/wallet/accounts.ts b/src/lib/wallet/accounts.ts index 7e8f7f5..771e8cd 100644 --- a/src/lib/wallet/accounts.ts +++ b/src/lib/wallet/accounts.ts @@ -33,27 +33,26 @@ export async function _accounts (type: WalletType, accounts: Map({ + promises.push(vault.request({ action: 'derive', index })) } const publicKeys = await Promise.all(promises) - if (publicKeys.length > 0) { - publicAccounts.push(...Account.load(publicKeys)) - } - } - for (const a of publicAccounts) { - if (a.index == null) { - throw new RangeError('Index missing for Account') + for (const { index, publicKey } of publicKeys) { + if (typeof index === 'number' && publicKey instanceof ArrayBuffer) { + const account = new Account(publicKey) + output.set(index, account) + accounts.set(index, account) + } } - output.set(a.index, a) - accounts.set(a.index, a) } } if (isSingle) { diff --git a/test/test.derive-accounts.mjs b/test/test.derive-accounts.mjs index 8d20826..7b7a062 100644 --- a/test/test.derive-accounts.mjs +++ b/test/test.derive-accounts.mjs @@ -17,7 +17,6 @@ await Promise.all([ assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) - assert.equal(account.index, 0) const accounts = await wallet.accounts() assert.exists(accounts.get(0)) @@ -37,13 +36,11 @@ await Promise.all([ assert.exists(account1) assert.equal(account1.publicKey, NANO_TEST_VECTORS.PUBLIC_1) assert.equal(account1.address, NANO_TEST_VECTORS.ADDRESS_1) - assert.equal(account1.index, 1) const account2 = accounts.get(2) assert.exists(account2) assert.equal(account2.publicKey, NANO_TEST_VECTORS.PUBLIC_2) assert.equal(account2.address, NANO_TEST_VECTORS.ADDRESS_2) - assert.equal(account2.index, 2) await assert.resolves(wallet.destroy()) }) @@ -57,7 +54,6 @@ await Promise.all([ for (let i = 0x70000000; i < 0x7000000f; i++) { const a = accounts.get(i) assert.exists(a) - assert.equal(a.index, i) assert.exists(a.address) assert.exists(a.publicKey) } @@ -73,7 +69,6 @@ await Promise.all([ assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) - assert.equal(account.index, 0) const accounts = await restored.accounts() assert.exists(accounts.get(0)) @@ -93,7 +88,6 @@ await Promise.all([ assert.equal(account.publicKey, NANO_TEST_VECTORS.BLAKE2B_PUBLIC_1) assert.equal(account.address, NANO_TEST_VECTORS.BLAKE2B_ADDRESS_1) - assert.equal(account.index, 1) const accounts = await wallet.accounts(1) const account1 = accounts.get(1) @@ -116,13 +110,11 @@ await Promise.all([ assert.exists(account2) assert.exists(account2.publicKey) assert.exists(account2.address) - assert.equal(account2.index, 2) const account3 = accounts.get(3) assert.exists(account3) assert.exists(account3.publicKey) assert.exists(account3.address) - assert.equal(account3.index, 3) await assert.resolves(wallet.destroy()) }) @@ -136,7 +128,6 @@ await Promise.all([ for (let i = 0x70000000; i < 0x7000000f; i++) { const a = accounts.get(i) assert.exists(a) - assert.equal(a.index, i) assert.exists(a.address) assert.exists(a.publicKey) } @@ -152,7 +143,6 @@ await Promise.all([ assert.equal(account.publicKey, NANO_TEST_VECTORS.BLAKE2B_PUBLIC_1) assert.equal(account.address, NANO_TEST_VECTORS.BLAKE2B_ADDRESS_1) - assert.equal(account.index, 1) const accounts = await restored.accounts(1) assert.exists(accounts.get(1)) diff --git a/test/test.import-wallet.mjs b/test/test.import-wallet.mjs index 461d6f6..669856a 100644 --- a/test/test.import-wallet.mjs +++ b/test/test.import-wallet.mjs @@ -305,7 +305,7 @@ await Promise.all([ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - const legacy = Account.load(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_')) + const legacy = new Account(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_')) assert.equal(account.address, legacy.address) assert.equal(NANO_TEST_VECTORS.ADDRESS_0, legacy.address) diff --git a/test/test.refresh-accounts.mjs b/test/test.refresh-accounts.mjs index 914052a..2fe0898 100644 --- a/test/test.refresh-accounts.mjs +++ b/test/test.refresh-accounts.mjs @@ -171,7 +171,6 @@ await Promise.all([ const account = accounts.get(i) assert.exists(account) assert.ok(account instanceof Account) - assert.equal(account.index, i) assert.equal(account.balance, 0n) assert.equal(account.receivable, 0n) } diff --git a/test/test.wallet-sign.mjs b/test/test.wallet-sign.mjs index a6e7296..5763d94 100644 --- a/test/test.wallet-sign.mjs +++ b/test/test.wallet-sign.mjs @@ -15,11 +15,8 @@ await Promise.all([ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - if (account.index == null) { - throw new Error('Account index missing') - } const data = crypto.randomUUID() - const signature = await wallet.sign(account.index, data) + const signature = await wallet.sign(0, data) assert.ok(await Tools.verify(account.publicKey, signature, data)) await assert.resolves(wallet.destroy()) @@ -29,12 +26,9 @@ await Promise.all([ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - if (account.index == null) { - throw new Error('Account index missing') - } const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou') .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000') - .sign(wallet, account.index) + .sign(wallet, 0) assert.ok(await sendBlock.verify(account.publicKey)) await assert.resolves(wallet.destroy()) @@ -44,17 +38,13 @@ await Promise.all([ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - if (account.index == null) { - throw new Error('Account index missing') - } - assert.equal(account.index, 0) const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou') .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000') - .sign(wallet, account.index) + .sign(wallet, 0) assert.ok(await sendBlock.verify(account.publicKey)) - const wrongAccount = Account.load('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p') + const wrongAccount = new Account('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p') assert.equal(await sendBlock.verify(wrongAccount.publicKey), false) await assert.resolves(wallet.destroy())