From: Chris Duncan Date: Wed, 20 Aug 2025 04:53:32 +0000 (-0700) Subject: Get frontier block info into account object from refresh. Move some Ledger signing... X-Git-Tag: v0.10.5~41^2~44 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=b7fed6b399749c1232ae45e699b6b83f3d9b5103;p=libnemo.git Get frontier block info into account object from refresh. Move some Ledger signing functionality from block into wallet so that block can be wallet-agnostic. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index 21b0554..f7e1872 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -3,6 +3,7 @@ import { Blake2b, NanoNaCl } from '#crypto' import { Key, KeyPair } from '#types' +import { Block } from './block' import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants' import { base32, bytes, hex } from './convert' import { Rpc } from './rpc' @@ -22,13 +23,15 @@ export class Account { #confirmed_balance?: bigint #confirmed_block_height?: number - #confirmed_frontier_block?: string + #confirmed_frontier?: string + #confirmed_frontier_block?: Block #confirmed_receivable?: bigint #confirmed_representative?: Account #balance?: bigint #block_height?: number - #frontier_block?: string + #frontier?: string + #frontier_block?: Block #open_block?: string #receivable?: bigint #representative?: Account @@ -41,13 +44,15 @@ export class Account { get confirmed_balance (): bigint | undefined { return this.#confirmed_balance } get confirmed_block_height (): number | undefined { return this.#confirmed_block_height } - get confirmed_frontier_block (): string | undefined { return this.#confirmed_frontier_block } + get confirmed_frontier (): string | undefined { return this.#confirmed_frontier } + get confirmed_frontier_block (): Block | undefined { return this.#confirmed_frontier_block } get confirmed_receivable (): bigint | undefined { return this.#confirmed_receivable } get confirmed_representative (): Account | undefined { return this.#confirmed_representative } get balance (): bigint | undefined { return this.#balance } get block_height (): number | undefined { return this.#block_height } - get frontier_block (): string | undefined { return this.#frontier_block } + get frontier (): string | undefined { return this.#frontier } + get frontier_block (): Block | undefined { return this.#frontier_block } get open_block (): string | undefined { return this.#open_block } get receivable (): bigint | undefined { return this.#receivable } get representative (): Account | undefined { return this.#representative } @@ -56,7 +61,8 @@ export class Account { set confirmed_balance (v: bigint | number | string) { this.#confirmed_balance = BigInt(v) } set confirmed_block_height (v: number | undefined) { if (v !== undefined) this.#confirmed_block_height = v | 0 } - set confirmed_frontier_block (v: string | undefined) { this.#confirmed_frontier_block = v } + set confirmed_frontier (v: string | undefined) { this.#confirmed_frontier = v } + set confirmed_frontier_block (v: Block | undefined) { this.#confirmed_frontier_block = v } set confirmed_receivable (v: bigint | number | string) { this.#confirmed_receivable = BigInt(v) } set confirmed_representative (v: unknown) { if (v instanceof Account) { @@ -70,7 +76,8 @@ export class Account { set balance (v: bigint | number | string) { this.#balance = BigInt(v) } set block_height (v: number | undefined) { if (v !== undefined) this.#block_height = v | 0 } - set frontier_block (v: string | undefined) { this.#frontier_block = v } + set frontier (v: string | undefined) { this.#frontier = v } + set frontier_block (v: Block | undefined) { this.#frontier_block = v } set open_block (v: string | undefined) { this.#open_block = v } set receivable (v: bigint | number | string) { this.#receivable = BigInt(v) } set representative (v: unknown) { @@ -204,23 +211,23 @@ export class Account { if (!(rpc instanceof Rpc)) { throw new TypeError('RPC must be a valid node') } - const data = { + const reqAccountInfo = { account: this.address, include_confirmed: true, receivable: true, representative: true, weight: true } + const resAccountInfo = await rpc.call('account_info', reqAccountInfo) const { confirmed_balance, confirmed_frontier, confirmed_height, confirmed_receivable, confirmed_representative, balance, block_count, frontier, open_block, receivable, representative, representative_block, weight, - } = await rpc.call('account_info', data) - + } = resAccountInfo if (frontier == null) { throw new Error('Account not found') } this.confirmed_balance = confirmed_balance this.confirmed_block_height = confirmed_height - this.confirmed_frontier_block = confirmed_frontier + this.confirmed_frontier = confirmed_frontier this.confirmed_representative = confirmed_representative this.confirmed_receivable = confirmed_receivable this.balance = balance @@ -231,6 +238,22 @@ export class Account { this.representative = representative this.representative_block = representative_block this.weight = weight + + const reqConfirmedFrontier = { + hash: confirmed_frontier + } + const resConfirmedFrontier = await rpc.call('blocks_info', reqConfirmedFrontier) + let contents = resConfirmedFrontier.contents + this.confirmed_frontier_block = new Block(contents.account, contents.balance, contents.previous, contents.representative) + .sign(contents.signature) + + const reqFrontier = { + hash: frontier + } + const resFrontier = await rpc.call('blocks_info', reqFrontier) + contents = resFrontier.contents + this.frontier_block = new Block(contents.account, contents.balance, contents.previous, contents.representative) + .sign(contents.signature) } /** diff --git a/src/lib/block.ts b/src/lib/block.ts index ea3a512..a9d04b3 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -391,17 +391,6 @@ export class Block { */ async sign (key: string): Promise /** - * Signs the block using a Ledger hardware wallet. If that fails, an error is - * thrown with the status code from the device. If successful, the result is - * stored in the `signature` property of the block. The wallet must be unlocked - * prior to signing. - * - * @param {number} index - Account index between 0x0 and 0x7fffffff - * @param {object} [frontier] - JSON of frontier block for offline signing - * @returns Block with `signature` value set - */ - async sign (index: number, frontier?: Block): Promise - /** * Signs the block using a Wallet. If successful, the result is stored in * the `signature` property of the block. The wallet must be unlocked prior to * signing. @@ -411,8 +400,19 @@ export class Block { * @returns Block with `signature` value set */ async sign (wallet: Wallet, index: number): Promise - sign (input: unknown, param?: unknown): Block | Promise { - if (typeof input === 'string' && /^[A-F0-9]{128}$/.test(input)) { + /** + * Signs the block using a Ledger hardware wallet. If that fails, an error is + * thrown with the status code from the device. If successful, the result is + * stored in the `signature` property of the block. The wallet must be unlocked + * prior to signing. + * + * @param {number} index - Account index between 0x0 and 0x7fffffff + * @param {object} [frontier] - JSON of frontier block for offline signing + * @returns Block with `signature` value set + */ + async sign (wallet: Wallet, index: number, frontier?: Block): Promise + sign (input: unknown, index?: unknown, frontier?: unknown): Block | Promise { + if (typeof input === 'string' && /^[A-F0-9]{128}$/i.test(input)) { this.signature = input return this } @@ -421,26 +421,11 @@ export class Block { if (typeof input === 'string' && /^[A-F0-9]{64}$/i.test(input)) { const signature = await NanoNaCl.detached(this.#hash(), hex.toBytes(input)) this.signature = bytes.toHex(signature) - } else if (typeof input !== 'number' && typeof input !== 'string' && !(input instanceof Wallet)) { - throw new TypeError('Invalid signing input', { cause: input }) - } else if (input instanceof Wallet && typeof param === 'number') { + } else if (input instanceof Wallet && typeof index === 'number') { const wallet = input - await wallet.sign(param, this) - } else if (typeof input === 'number') { - const { Ledger } = await import('./wallet/ledger') - const index = input - const wallet = await Wallet.create('Ledger') - await wallet.unlock() - if (param && param instanceof Block) { - try { - await Ledger.updateCache(index, param) - } catch (err) { - console.warn('Error updating Ledger cache of previous block, attempting signature anyway', err) - } - } - await Ledger.sign(index, this) + await wallet.sign(index, this) } else { - throw new TypeError('Invalid key for block signature', { cause: input }) + throw new TypeError('Invalid input for block signature') } resolve(this) } catch (err) { diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts index 6af7806..8af6bb0 100644 --- a/src/lib/wallet/index.ts +++ b/src/lib/wallet/index.ts @@ -278,7 +278,7 @@ export class Wallet { * @param {(Block)} block - Block data to be hashed and signed */ async sign (index: number, block: Block): Promise { - await _sign(this.#vault, index, block) + await _sign(this, this.#vault, index, block) } /** diff --git a/src/lib/wallet/ledger.ts b/src/lib/wallet/ledger.ts index 2899d38..70a9d87 100644 --- a/src/lib/wallet/ledger.ts +++ b/src/lib/wallet/ledger.ts @@ -7,11 +7,10 @@ import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb' import { default as TransportHID } from '@ledgerhq/hw-transport-webhid' import { DeviceStatus, LedgerAccountResponse, LedgerResponse, LedgerSignResponse, LedgerVersionResponse } from '#types' import { Wallet } from '#wallet' -import { Account, AccountList } from '../account' +import { Account } from '../account' import { Block } from '../block' import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants' import { bytes, dec, hex } from '../convert' -import { Database } from '../database' import { Rpc } from '../rpc' /** @@ -19,11 +18,13 @@ import { Rpc } from '../rpc' * calls. This wallet does not feature any seed nor mnemonic phrase as all * private keys are held in the secure chip of the device. As such, the user * is responsible for using Ledger technology to back up these pieces of data. +* +* https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md */ export class Ledger { - static #isInternal: boolean = false + static #listenTimeout: 30000 = 30000 + static #openTimeout: 3000 = 3000 static #status: DeviceStatus = 'DISCONNECTED' - static #ADPU_CODES: { [key: string]: number } = Object.freeze({ class: 0xa1, bip32DerivationLevel: 0x03, @@ -57,10 +58,6 @@ export class Ledger { static UsbVendorId = ledgerUSBVendorId static SYMBOL: Symbol = Symbol('Ledger') - static get #listenTimeout (): 30000 { return 30000 } - static get #openTimeout (): 3000 { return 3000 } - static get status (): DeviceStatus { return this.#status } - /** * Check which transport protocols are supported by the browser and return the * transport type according to the following priorities: USB, Bluetooth, HID. @@ -82,6 +79,13 @@ export class Ledger { return true } + /** + * Status of the Ledger device connection. + * + * DISCONNECTED | BUSY | LOCKED | CONNECTED + */ + static get status (): DeviceStatus { return this.#status } + /** * Request an account at a specific BIP-44 index. * diff --git a/src/lib/wallet/sign.ts b/src/lib/wallet/sign.ts index b852aa7..b3bb3ff 100644 --- a/src/lib/wallet/sign.ts +++ b/src/lib/wallet/sign.ts @@ -2,11 +2,13 @@ //! SPDX-License-Identifier: GPL-3.0-or-later import { Vault } from '#vault' +import { Wallet } from '#wallet' import { Block } from '../block' import { bytes, hex } from '../convert' +import { Ledger } from './ledger' -export async function _sign (vault: Vault, index: number, block: Block): Promise -export async function _sign (vault: Vault, index: unknown, block: unknown): Promise { +export async function _sign (wallet: Wallet, vault: Vault, index: number, block: Block): Promise +export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block: unknown): Promise { try { if (typeof index !== 'number') { throw new TypeError('Index must be a number', { cause: index }) @@ -14,12 +16,25 @@ export async function _sign (vault: Vault, index: unknown, block: unknown): Prom if (!(block instanceof Block)) { throw new TypeError('Invalid Block', { cause: block }) } - const { signature } = await vault.request({ - action: 'sign', - index, - data: hex.toBuffer(block.hash) - }) - block.signature = bytes.toHex(new Uint8Array(signature)) + if (wallet.type === 'Ledger') { + const account = await wallet.account(index) + const frontier = account.confirmed_frontier_block + if (frontier && frontier instanceof Block) { + try { + await Ledger.updateCache(index, frontier) + } catch (err) { + console.warn('Error updating Ledger cache of previous block, attempting signature anyway', err) + } + } + await Ledger.sign(index, block) + } else { + const { signature } = await vault.request({ + action: 'sign', + index, + data: hex.toBuffer(block.hash) + }) + block.signature = bytes.toHex(new Uint8Array(signature)) + } } catch (err) { throw new Error(`Failed to sign block`, { cause: err }) }