]> git.codecow.com Git - libnemo.git/commitdiff
Get frontier block info into account object from refresh. Move some Ledger signing...
authorChris Duncan <chris@zoso.dev>
Wed, 20 Aug 2025 04:53:32 +0000 (21:53 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 20 Aug 2025 04:53:32 +0000 (21:53 -0700)
src/lib/account.ts
src/lib/block.ts
src/lib/wallet/index.ts
src/lib/wallet/ledger.ts
src/lib/wallet/sign.ts

index 21b055492483657f27225410249a6b91e96830d2..f7e187241ddceb35c0a64e404278790ad79a3339 100644 (file)
@@ -3,6 +3,7 @@
 \r
 import { Blake2b, NanoNaCl } from '#crypto'\r
 import { Key, KeyPair } from '#types'\r
+import { Block } from './block'\r
 import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
 import { base32, bytes, hex } from './convert'\r
 import { Rpc } from './rpc'\r
@@ -22,13 +23,15 @@ export class Account {
 \r
        #confirmed_balance?: bigint\r
        #confirmed_block_height?: number\r
-       #confirmed_frontier_block?: string\r
+       #confirmed_frontier?: string\r
+       #confirmed_frontier_block?: Block\r
        #confirmed_receivable?: bigint\r
        #confirmed_representative?: Account\r
 \r
        #balance?: bigint\r
        #block_height?: number\r
-       #frontier_block?: string\r
+       #frontier?: string\r
+       #frontier_block?: Block\r
        #open_block?: string\r
        #receivable?: bigint\r
        #representative?: Account\r
@@ -41,13 +44,15 @@ export class Account {
 \r
        get confirmed_balance (): bigint | undefined { return this.#confirmed_balance }\r
        get confirmed_block_height (): number | undefined { return this.#confirmed_block_height }\r
-       get confirmed_frontier_block (): string | undefined { return this.#confirmed_frontier_block }\r
+       get confirmed_frontier (): string | undefined { return this.#confirmed_frontier }\r
+       get confirmed_frontier_block (): Block | undefined { return this.#confirmed_frontier_block }\r
        get confirmed_receivable (): bigint | undefined { return this.#confirmed_receivable }\r
        get confirmed_representative (): Account | undefined { return this.#confirmed_representative }\r
 \r
        get balance (): bigint | undefined { return this.#balance }\r
        get block_height (): number | undefined { return this.#block_height }\r
-       get frontier_block (): string | undefined { return this.#frontier_block }\r
+       get frontier (): string | undefined { return this.#frontier }\r
+       get frontier_block (): Block | undefined { return this.#frontier_block }\r
        get open_block (): string | undefined { return this.#open_block }\r
        get receivable (): bigint | undefined { return this.#receivable }\r
        get representative (): Account | undefined { return this.#representative }\r
@@ -56,7 +61,8 @@ export class Account {
 \r
        set confirmed_balance (v: bigint | number | string) { this.#confirmed_balance = BigInt(v) }\r
        set confirmed_block_height (v: number | undefined) { if (v !== undefined) this.#confirmed_block_height = v | 0 }\r
-       set confirmed_frontier_block (v: string | undefined) { this.#confirmed_frontier_block = v }\r
+       set confirmed_frontier (v: string | undefined) { this.#confirmed_frontier = v }\r
+       set confirmed_frontier_block (v: Block | undefined) { this.#confirmed_frontier_block = v }\r
        set confirmed_receivable (v: bigint | number | string) { this.#confirmed_receivable = BigInt(v) }\r
        set confirmed_representative (v: unknown) {\r
                if (v instanceof Account) {\r
@@ -70,7 +76,8 @@ export class Account {
 \r
        set balance (v: bigint | number | string) { this.#balance = BigInt(v) }\r
        set block_height (v: number | undefined) { if (v !== undefined) this.#block_height = v | 0 }\r
-       set frontier_block (v: string | undefined) { this.#frontier_block = v }\r
+       set frontier (v: string | undefined) { this.#frontier = v }\r
+       set frontier_block (v: Block | undefined) { this.#frontier_block = v }\r
        set open_block (v: string | undefined) { this.#open_block = v }\r
        set receivable (v: bigint | number | string) { this.#receivable = BigInt(v) }\r
        set representative (v: unknown) {\r
@@ -204,23 +211,23 @@ export class Account {
                if (!(rpc instanceof Rpc)) {\r
                        throw new TypeError('RPC must be a valid node')\r
                }\r
-               const data = {\r
+               const reqAccountInfo = {\r
                        account: this.address,\r
                        include_confirmed: true,\r
                        receivable: true,\r
                        representative: true,\r
                        weight: true\r
                }\r
+               const resAccountInfo = await rpc.call('account_info', reqAccountInfo)\r
                const { confirmed_balance, confirmed_frontier, confirmed_height, confirmed_receivable, confirmed_representative,\r
                        balance, block_count, frontier, open_block, receivable, representative, representative_block, weight,\r
-               } = await rpc.call('account_info', data)\r
-\r
+               } = resAccountInfo\r
                if (frontier == null) {\r
                        throw new Error('Account not found')\r
                }\r
                this.confirmed_balance = confirmed_balance\r
                this.confirmed_block_height = confirmed_height\r
-               this.confirmed_frontier_block = confirmed_frontier\r
+               this.confirmed_frontier = confirmed_frontier\r
                this.confirmed_representative = confirmed_representative\r
                this.confirmed_receivable = confirmed_receivable\r
                this.balance = balance\r
@@ -231,6 +238,22 @@ export class Account {
                this.representative = representative\r
                this.representative_block = representative_block\r
                this.weight = weight\r
+\r
+               const reqConfirmedFrontier = {\r
+                       hash: confirmed_frontier\r
+               }\r
+               const resConfirmedFrontier = await rpc.call('blocks_info', reqConfirmedFrontier)\r
+               let contents = resConfirmedFrontier.contents\r
+               this.confirmed_frontier_block = new Block(contents.account, contents.balance, contents.previous, contents.representative)\r
+                       .sign(contents.signature)\r
+\r
+               const reqFrontier = {\r
+                       hash: frontier\r
+               }\r
+               const resFrontier = await rpc.call('blocks_info', reqFrontier)\r
+               contents = resFrontier.contents\r
+               this.frontier_block = new Block(contents.account, contents.balance, contents.previous, contents.representative)\r
+                       .sign(contents.signature)\r
        }\r
 \r
        /**\r
index ea3a5122c5ae7868f6fa79e98adcfba3ed321435..a9d04b3cfddd1544e684f672fc4623c1cdd3fe41 100644 (file)
@@ -391,17 +391,6 @@ export class Block {
        */
        async sign (key: string): Promise<Block>
        /**
-       * 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<Block>
-       /**
        * 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<Block>
-       sign (input: unknown, param?: unknown): Block | Promise<Block> {
-               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<Block>
+       sign (input: unknown, index?: unknown, frontier?: unknown): Block | Promise<Block> {
+               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) {
index 6af7806e51cde27bd635948fe78e754db7ac9ebc..8af6bb09c7d9cd66c4c5e893d8d46e5a52294b53 100644 (file)
@@ -278,7 +278,7 @@ export class Wallet {
        * @param {(Block)} block - Block data to be hashed and signed\r
        */\r
        async sign (index: number, block: Block): Promise<void> {\r
-               await _sign(this.#vault, index, block)\r
+               await _sign(this, this.#vault, index, block)\r
        }\r
 \r
        /**\r
index 2899d3857692091d2811d168a411ea12d811e022..70a9d87f4e832ee0a70eca9ad21442b07d64a72f 100644 (file)
@@ -7,11 +7,10 @@ import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'
 import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'\r
 import { DeviceStatus, LedgerAccountResponse, LedgerResponse, LedgerSignResponse, LedgerVersionResponse } from '#types'\r
 import { Wallet } from '#wallet'\r
-import { Account, AccountList } from '../account'\r
+import { Account } from '../account'\r
 import { Block } from '../block'\r
 import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'\r
 import { bytes, dec, hex } from '../convert'\r
-import { Database } from '../database'\r
 import { Rpc } from '../rpc'\r
 \r
 /**\r
@@ -19,11 +18,13 @@ import { Rpc } from '../rpc'
 * calls. This wallet does not feature any seed nor mnemonic phrase as all\r
 * private keys are held in the secure chip of the device. As such, the user\r
 * is responsible for using Ledger technology to back up these pieces of data.\r
+*\r
+* https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md\r
 */\r
 export class Ledger {\r
-       static #isInternal: boolean = false\r
+       static #listenTimeout: 30000 = 30000\r
+       static #openTimeout: 3000 = 3000\r
        static #status: DeviceStatus = 'DISCONNECTED'\r
-\r
        static #ADPU_CODES: { [key: string]: number } = Object.freeze({\r
                class: 0xa1,\r
                bip32DerivationLevel: 0x03,\r
@@ -57,10 +58,6 @@ export class Ledger {
        static UsbVendorId = ledgerUSBVendorId\r
        static SYMBOL: Symbol = Symbol('Ledger')\r
 \r
-       static get #listenTimeout (): 30000 { return 30000 }\r
-       static get #openTimeout (): 3000 { return 3000 }\r
-       static get status (): DeviceStatus { return this.#status }\r
-\r
        /**\r
        * Check which transport protocols are supported by the browser and return the\r
        * transport type according to the following priorities: USB, Bluetooth, HID.\r
@@ -82,6 +79,13 @@ export class Ledger {
                return true\r
        }\r
 \r
+       /**\r
+       * Status of the Ledger device connection.\r
+       *\r
+       * DISCONNECTED | BUSY | LOCKED | CONNECTED\r
+       */\r
+       static get status (): DeviceStatus { return this.#status }\r
+\r
        /**\r
        * Request an account at a specific BIP-44 index.\r
        *\r
index b852aa7ef988ae715066e5c0edfdc0a3aac64e9b..b3bb3ff180e53342b6add8d601fb9a9625774c6c 100644 (file)
@@ -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<void>
-export async function _sign (vault: Vault, index: unknown, block: unknown): Promise<void> {
+export async function _sign (wallet: Wallet, vault: Vault, index: number, block: Block): Promise<void>
+export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block: unknown): Promise<void> {
        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<ArrayBuffer>({
-                       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<ArrayBuffer>({
+                               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 })
        }