]> git.codecow.com Git - libnemo.git/commitdiff
Fix refresh from account and from wallet.
authorChris Duncan <chris@zoso.dev>
Wed, 20 Aug 2025 15:58:03 +0000 (08:58 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 20 Aug 2025 15:58:03 +0000 (08:58 -0700)
src/lib/account.ts
src/lib/block.ts
src/lib/wallet/index.ts
src/lib/wallet/ledger.ts
src/lib/wallet/refresh.ts
src/lib/wallet/sign.ts
src/types.d.ts
test/test.refresh-accounts.mjs

index f7e187241ddceb35c0a64e404278790ad79a3339..cfb7d081f7404ede61fc4ad2caf49aa854428cc4 100644 (file)
@@ -232,7 +232,7 @@ export class Account {
                this.confirmed_receivable = confirmed_receivable\r
                this.balance = balance\r
                this.block_height = block_count\r
-               this.frontier_block = frontier\r
+               this.frontier = frontier\r
                this.open_block = open_block\r
                this.receivable = receivable\r
                this.representative = representative\r
@@ -240,25 +240,33 @@ export class Account {
                this.weight = weight\r
 \r
                const reqConfirmedFrontier = {\r
+                       json_block: true,\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
+               const { contents: confirmedFrontierContents, subtype: confirmedFrontierSubtype } = await rpc.call('block_info', reqConfirmedFrontier)\r
+               const confirmedFrontierBlock = new Block(confirmedFrontierContents.account, confirmedFrontierContents.balance, confirmedFrontierContents.previous, confirmedFrontierContents.representative)\r
+               if (typeof confirmedFrontierBlock[confirmedFrontierSubtype] !== 'function') {\r
+                       throw new TypeError('Unknown subtype of confirmed frontier block', { cause: confirmedFrontierSubtype })\r
+               }\r
+               confirmedFrontierBlock[confirmedFrontierSubtype](confirmedFrontierContents.link, 0).sign(confirmedFrontierContents.signature)\r
+               this.confirmed_frontier_block = confirmedFrontierBlock\r
 \r
                const reqFrontier = {\r
-                       hash: frontier\r
+                       json_block: true,\r
+                       hash: confirmed_frontier\r
+               }\r
+               const { contents: frontierContents, subtype: frontierSubtype } = await rpc.call('block_info', reqFrontier)\r
+               const frontierBlock = new Block(frontierContents.account, frontierContents.balance, frontierContents.previous, frontierContents.representative)\r
+               if (typeof frontierBlock[frontierSubtype] !== 'function') {\r
+                       throw new TypeError('Unknown subtype of frontier block', { cause: frontierSubtype })\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
+               frontierBlock[frontierSubtype](frontierContents.link, 0).sign(frontierContents.signature)\r
+               this.frontier_block = frontierBlock\r
        }\r
 \r
        /**\r
        * Validates a Nano address with 'nano' and 'xrb' prefixes\r
-       * Derived from https://github.com/alecrios/nano-address-validator\r
+       *       rived from https://github.com/alecrios/nano-address-validator\r
        *\r
        * @param {string} address - Nano address to validate\r
        * @throws Error if address is undefined, not a string, or an invalid format\r
index a9d04b3cfddd1544e684f672fc4623c1cdd3fe41..16bc88b56e9c4dcd32c5ec9c0f9b8759c90141fa 100644 (file)
@@ -14,6 +14,7 @@ import { convert } from './tools'
 * Represents a block as defined by the Nano cryptocurrency protocol.
 */
 export class Block {
+       [key: string]: bigint | string | Account | Function | Uint8Array<ArrayBuffer> | 'send' | 'receive' | 'change' | undefined
        /**
         * Validates block data.
         *
@@ -101,7 +102,7 @@ export class Block {
                                throw new TypeError('Invalid account')
                        }
                        balance ??= account.balance
-                       previous ??= account.frontier_block
+                       previous ??= account.frontier
                        representative ??= account.representative
                        if (typeof balance !== 'bigint' && typeof balance !== 'number' && typeof balance !== 'string') {
                                throw new TypeError('Account balance is unknown')
@@ -360,7 +361,7 @@ export class Block {
                        }
 
                        if (typeof account !== 'string' && !(account instanceof Account)) {
-                               throw new TypeError('Invalid account')
+                               throw new TypeError('Invalid account', { cause: account })
                        }
                        this.link = (typeof account === 'string')
                                ? hex.toBytes(Account.load(account).publicKey)
index 8af6bb09c7d9cd66c4c5e893d8d46e5a52294b53..d4c5ed64aa7184e52bad87330a7b5842675e5b5a 100644 (file)
@@ -262,8 +262,8 @@ export class Wallet {
        *\r
        * A successful response will set these properties on each account.\r
        *\r
-       * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
-       * @returns {Promise<Account[]>} Accounts with updated balances, frontiers, and representatives\r
+       * @param {(Rpc|string|URL)} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
+       * @returns {Promise<AccountList>} Promise for accounts with updated balances, frontiers, and representatives\r
        */\r
        async refresh (rpc: Rpc | string | URL, from: number = 0, to: number = from): Promise<AccountList> {\r
                return await _refresh(this, rpc, from, to)\r
@@ -275,10 +275,21 @@ export class Wallet {
        * before being returned. The wallet must be unlocked prior to signing.\r
        *\r
        * @param {number} index - Account to use for signing\r
-       * @param {(Block)} block - Block data to be hashed and signed\r
+       * @param {Block} block - Block data to be hashed and signed\r
+       */\r
+       async sign (index: number, block: Block): Promise<void>\r
+       /**\r
+       * Signs a block using a Ledger hardware wallet at the wallet index specified.\r
+       * The signature is appended to the signature field of the block before being\r
+       * returned. The wallet must be unlocked prior to signing.\r
+       *\r
+       * @param {number} index - Account to use for signing\r
+       * @param {Block} block - Block data to be hashed and signed\r
+       * @param {Block} frontier - Previous block data to be cached by Ledger wallet\r
        */\r
-       async sign (index: number, block: Block): Promise<void> {\r
-               await _sign(this, this.#vault, index, block)\r
+       async sign (index: number, block: Block, frontier: Block): Promise<void>\r
+       async sign (index: number, block: Block, frontier?: Block): Promise<void> {\r
+               await _sign(this, this.#vault, index, block, frontier)\r
        }\r
 \r
        /**\r
index 70a9d87f4e832ee0a70eca9ad21442b07d64a72f..09f2a4895f25e0027e33cd88402c6ea6ad18de9d 100644 (file)
@@ -156,8 +156,7 @@ export class Ledger {
        * @param {Block} block - Block data to sign\r
        * @param {Block} [frontier] - Previous block data to cache in the device\r
        */\r
-       static async sign (index: number, block: Block, frontier?: Block): Promise<void>\r
-       static async sign (index: number, block: Block, frontier?: Block): Promise<void> {\r
+       static async sign (index: number, block: Block, frontier?: Block): Promise<string> {\r
                try {\r
                        if (typeof index !== 'number') {\r
                                throw new TypeError('Index must be a number', { cause: index })\r
@@ -182,7 +181,7 @@ export class Ledger {
                        if (signature == null) {\r
                                throw new Error('Ledger silently failed to return signature')\r
                        }\r
-                       block.signature = signature\r
+                       return signature\r
                } catch (err) {\r
                        console.error(err)\r
                        throw new Error('Failed to sign block with Ledger', { cause: err })\r
index 148fc144415a8d90564c4be1fe2dcc2145f9706e..54b63936afae6115162dade149c1f9e641c0324a 100644 (file)
@@ -1,8 +1,10 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
+import { BlockInfo } from '#types'
 import { Wallet } from '#wallet'
 import { AccountList } from '../account'
+import { Block } from '../block'
 import { Rpc } from '../rpc'
 
 export async function _refresh (wallet: Wallet, rpc: Rpc | string | URL, from: number, to: number): Promise<AccountList>
@@ -24,14 +26,25 @@ export async function _refresh (wallet: Wallet, rpc: unknown, from: unknown, to:
                        addresses.push(account.address)
                }
                const data = {
+                       json_block: true,
                        accounts: addresses
                }
                const { balances } = await rpc.call('accounts_balances', data) as { balances: { [address: string]: { balance: string, receivable: string } } }
                const { frontiers } = await rpc.call('accounts_frontiers', data) as { frontiers: { [address: string]: string } }
+               const { blocks } = await rpc.call('blocks_info', { json_block: true, hashes: Object.values(frontiers) }) as BlockInfo
                for (const account of accounts) {
                        account.balance = balances[account.address]?.balance
                        account.receivable = balances[account.address]?.receivable
-                       account.frontier_block = frontiers[account.address]
+                       if (frontiers[account.address] != null) {
+                               account.frontier = frontiers[account.address]
+                               const { subtype, contents: { balance, link, previous, representative, signature } } = blocks[account.frontier]
+                               const frontierBlock = new Block(account, balance, previous, representative)
+                               if (typeof frontierBlock[subtype] !== 'function') {
+                                       throw new TypeError('Unknown frontier block subtype', { cause: subtype })
+                               }
+                               frontierBlock[subtype](link, 0).sign(signature)
+                               account.frontier_block = frontierBlock
+                       }
                }
                return accounts
        } catch (err) {
index b3bb3ff180e53342b6add8d601fb9a9625774c6c..c36326d6d8bd5c282070d5785c274bb2181131fd 100644 (file)
@@ -7,8 +7,8 @@ import { Block } from '../block'
 import { bytes, hex } from '../convert'
 import { Ledger } from './ledger'
 
-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> {
+export async function _sign (wallet: Wallet, vault: Vault, index: number, block: Block, frontier?: Block): Promise<void>
+export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block: unknown, frontier?: unknown): Promise<void> {
        try {
                if (typeof index !== 'number') {
                        throw new TypeError('Index must be a number', { cause: index })
@@ -18,15 +18,14 @@ export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block
                }
                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)
+                                       console.warn('Error updating Ledger cache of frontier block, attempting signature anyway', err)
                                }
                        }
-                       await Ledger.sign(index, block)
+                       block.signature = await Ledger.sign(index, block)
                } else {
                        const { signature } = await vault.request<ArrayBuffer>({
                                action: 'sign',
index f38c9ce1149d50d1ba7137cfdafd72c3b7c262d3..012487946c9d57b25dc2e689a523681494ea53b5 100644 (file)
@@ -374,6 +374,18 @@ export declare class Block {
        verify (key?: string): Promise<boolean>
 }
 
+export type BlockInfo = {
+       blocks: {
+               [hash: string]: {
+                       [p: string]: string
+               } & {
+                       contents: {
+                               [p: string]: string
+                       }
+               }
+       }
+}
+
 export type Data = boolean | number | number[] | string | string[] | ArrayBuffer | CryptoKey | { [key: string]: Data }
 
 export type NamedData<T extends Data = Data> = {
index 89b30a82d2f6b6a820ef16930d7d3c547fe09c2e..cbdb14ad64f8ed5505d79fd3e9a706b9bab0587c 100644 (file)
@@ -41,10 +41,10 @@ await Promise.all([
                        //@ts-expect-error
                        assert.ok(account.balance >= 0n)
 
-                       assert.exists(account.frontier_block)
-                       assert.equal(typeof account.frontier_block, 'string')
-                       assert.notEqual(account.frontier_block, '')
-                       assert.ok(/^[A-F0-9]{64}$/i.test(account.frontier_block ?? ''))
+                       assert.exists(account.frontier)
+                       assert.equal(typeof account.frontier, 'string')
+                       assert.notEqual(account.frontier, '')
+                       assert.ok(/^[A-F0-9]{64}$/i.test(account.frontier ?? ''))
 
                        assert.exists(account.representative)
                        assert.notEqual(account.representative, '')
@@ -173,8 +173,8 @@ await Promise.all([
 
                        assert.ok(account instanceof Account)
                        assert.equal(typeof account.balance, 'bigint')
-                       assert.exists(account.frontier_block)
-                       assert.equal(typeof account.frontier_block, 'string')
+                       assert.exists(account.frontier)
+                       assert.equal(typeof account.frontier, 'string')
 
                        await assert.resolves(wallet.destroy())
                })