]> git.codecow.com Git - libnemo.git/commitdiff
Refactor RPC responses implemented so far to produce full responses only and throw...
authorChris Duncan <chris@codecow.com>
Fri, 7 Aug 2026 16:05:24 +0000 (09:05 -0700)
committerChris Duncan <chris@codecow.com>
Fri, 7 Aug 2026 16:05:24 +0000 (09:05 -0700)
src/lib/ledger/index.ts
src/lib/rpc/account_balance.ts
src/lib/rpc/accounts_balances.ts
src/lib/rpc/block_info.ts
src/lib/rpc/blocks_info.ts
src/lib/rpc/index.ts
src/lib/rpc/process.ts
src/lib/rpc/schema.ts

index dfc1be3e9d3182ac7a811af0697456c6b6238843..8c1a76cdb358feb430d8e6c999b1be806f174a14 100644 (file)
@@ -8,7 +8,7 @@ import TransportUSB from '@ledgerhq/hw-transport-webusb'
 import { Block } from '../block'
 import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
 import { dec } from '../convert'
-import { Rpc, Schema } from '../rpc'
+import { Rpc } from '../rpc'
 import { _account } from './account'
 import { _cache } from './cache'
 import { _close } from './close'
@@ -233,27 +233,14 @@ export class Ledger {
                                'json_block': 'true',
                                'hash': input
                        }
-                       const res = await node.post('block_info', data)
-                       if (!res || typeof res !== 'object' || !('ok' in res) || 'error' in res) {
-                               throw new Error('Unable to fetch block info', { cause: res })
-                       }
-                       const blockInfo = res as Record<string, unknown> & Record<'ok', unknown>
-                       if (!(blockInfo.contents != null && typeof blockInfo.contents === 'object')) {
-                               throw new Error('Block info missing contents', { cause: blockInfo })
-                       }
-                       const contents = {} as Record<string, string>
-                       for (const [k, v] of Object.entries(blockInfo.contents)) {
-                               if (Schema.block_info.contents.fields.includes(k) && typeof v === 'string') {
-                                       contents[k] = v
-                               }
-                       }
-                       const { account, balance, link, previous, representative, signature } = contents
+                       const blockInfo = await node.post('block_info', data)
+                       const { account, balance, link, previous, representative, signature } = blockInfo.contents
                        const block = new Block(account, balance, previous, representative).receive(link, 0)
                        block.signature = signature
                        input = block
                }
                if (!(input instanceof Block)) {
-                       throw new TypeError('Invalid block format')
+                       throw new TypeError('Invalid block format for Ledger cache')
                }
                return queue<LedgerResponse>(async () => _cache(this.#transport, index, input))
        }
index 988ce06eab1ccfb9fb9b44f45072d79d0798c30b..90121a4bcc0b57414c888c2afbb0591e27bafc0d 100644 (file)
@@ -1,29 +1,31 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-export type AccountBalance = {
+import { RpcError } from '../errors'
+
+export type AccountBalanceResponse = {
        balance: string
-       pending: string
+       pending?: string
        receivable: string
 }
 
-export function account_balance (body: unknown): AccountBalance {
-       const response: AccountBalance = {
-               balance: '0',
-               pending: '0',
-               receivable: '0',
-       }
-       if (body == null || typeof body !== 'object') {
-               return response
-       }
-       if ('balance' in body && typeof body.balance === 'string') {
-               response.balance = body.balance
-       }
-       if ('pending' in body && typeof body.pending === 'string') {
-               response.pending = body.pending
-       }
-       if ('receivable' in body && typeof body.receivable === 'string') {
-               response.receivable = body.receivable
+export function account_balance (body: unknown): AccountBalanceResponse {
+       if (body != null && typeof body === 'object') {
+               if ('balance' in body && typeof body.balance === 'string'
+                       && 'receivable' in body && typeof body.receivable === 'string'
+               ) {
+                       const response: AccountBalanceResponse = {
+                               balance: body.balance,
+                               receivable: body.receivable,
+                       }
+                       if ('pending' in body && typeof body.pending === 'string') {
+                               response.pending = body.pending
+                       }
+                       return response
+               }
+               if ('error' in body && typeof body.error === 'string') {
+                       throw new RpcError(body.error)
+               }
        }
-       return response
+       throw new RpcError('Invalid account_balance response')
 }
index 30bacf1ec8c917897bd6943c68bbffa0842c2ca3..464f04ab07ff71571e1e800099b2686b6b404005 100644 (file)
@@ -2,58 +2,53 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { Account } from '../account'
-import { AccountBalance, account_balance } from './account_balance'
+import { RpcError } from '../errors'
+import { AccountBalanceResponse, account_balance } from './account_balance'
 
-type AccountsBalances = {
+type AccountsBalancesResponse = {
        balances?: {
-               [address: string]: AccountBalance
+               [address: string]: AccountBalanceResponse
        }
        errors?: {
                [address: string]: string
        }
 }
 
-export function accounts_balances (body: unknown): AccountsBalances {
-       const response: AccountsBalances = {}
-       if (body == null || typeof body !== 'object') {
-               return response
-       }
-       const balances = 'balances' in body ? body.balances : null
-       const errors = 'errors' in body ? body.errors : null
+export function accounts_balances (body: unknown): AccountsBalancesResponse {
+       if (body != null && typeof body === 'object') {
+               const response: AccountsBalancesResponse = {}
 
-       if (balances != null) {
-               if (typeof balances === 'object') {
-                       for (const [address, data] of Object.entries(balances) as [string, unknown][]) {
+               if ('errors' in body && body.errors != null && typeof body.errors === 'object') {
+                       for (const [address, error] of Object.entries(body.errors) as [string, unknown][]) {
                                try {
                                        Account.validate(address)
-                                       if (data == null || typeof data !== 'object') {
-                                               throw new Error('Invalid account balance data')
+                                       if (typeof error === 'string') {
+                                               throw new Error(error)
                                        }
-                                       response.balances ??= {}
-                                       response.balances[address] = account_balance(data)
                                } catch (err: any) {
                                        response.errors ??= {}
                                        response.errors[address] = err?.message
                                }
                        }
                }
-       }
 
-       if (errors != null) {
-               if (typeof errors === 'object') {
-                       for (const [address, error] of Object.entries(errors) as [string, unknown][]) {
+               if ('balances' in body && body.balances != null && typeof body.balances === 'object') {
+                       for (const [address, data] of Object.entries(body.balances) as [string, unknown][]) {
                                try {
                                        Account.validate(address)
-                                       if (typeof error === 'string') {
-                                               throw new Error(error)
+                                       if (data == null || typeof data !== 'object') {
+                                               throw new Error('Invalid account balance data')
                                        }
+                                       response.balances ??= {}
+                                       response.balances[address] = account_balance(data)
                                } catch (err: any) {
                                        response.errors ??= {}
-                                       response.errors[address] = err?.message
+                                       response.errors[address] ??= err?.message
                                }
                        }
                }
-       }
 
-       return response
+               return response
+       }
+       throw new RpcError('Invalid accounts_balance response')
 }
index b9dbb36d987565d8610215677cddadb440af73f3..e4b98f05d54902e9c60e456bc71020c7da29dd9f 100644 (file)
@@ -3,18 +3,20 @@
 
 import { RpcError } from '../errors'
 
-export type BlockInfo = {
-       contents: {
-               account: string
-               balance: string
-               link: string
-               link_as_account: string
-               previous: string
-               representative: string
-               signature: string
-               type: string
-               work: string
-       }
+export type BlockInfoContents = {
+       account: string
+       balance: string
+       link: string
+       link_as_account: string
+       previous: string
+       representative: string
+       signature: string
+       type: 'state'
+       work: string
+}
+
+export type BlockInfoResponse = {
+       contents: BlockInfoContents
        amount: string
        balance: string
        block_account: string
@@ -26,51 +28,54 @@ export type BlockInfo = {
        successor: string
 }
 
-export function block_info (body: unknown): BlockInfo {
-       if (body != null && typeof body === 'object'
-               && 'amount' in body && typeof body.amount === 'string'
-               && 'balance' in body && typeof body.balance === 'string'
-               && 'block_account' in body && typeof body.block_account === 'string'
-               && 'confirmed' in body && typeof body.confirmed === 'string'
-               && 'height' in body && typeof body.height === 'string'
-               && 'linked_account' in body && typeof body.linked_account === 'string'
-               && 'local_timestamp' in body && typeof body.local_timestamp === 'string'
-               && 'subtype' in body && (body.subtype === 'change' || body.subtype === 'epoch' || body.subtype === 'open' || body.subtype === 'receive' || body.subtype === 'send')
-               && 'successor' in body && typeof body.successor === 'string'
-               && 'contents' in body && body.contents != null && typeof body.contents === 'object'
-               && 'account' in body.contents && typeof body.contents.account === 'string'
-               && 'balance' in body.contents && typeof body.contents.balance === 'string'
-               && 'link' in body.contents && typeof body.contents.link === 'string'
-               && 'link_as_account' in body.contents && typeof body.contents.link_as_account === 'string'
-               && 'previous' in body.contents && typeof body.contents.previous === 'string'
-               && 'representative' in body.contents && typeof body.contents.representative === 'string'
-               && 'signature' in body.contents && typeof body.contents.signature === 'string'
-               && 'type' in body.contents && typeof body.contents.type === 'string'
-               && 'work' in body.contents && typeof body.contents.work === 'string'
-       ) {
-               return {
-                       contents: {
-                               account: body.contents.account,
-                               balance: body.contents.balance,
-                               link: body.contents.link,
-                               link_as_account: body.contents.link_as_account,
-                               previous: body.contents.previous,
-                               representative: body.contents.representative,
-                               signature: body.contents.signature,
-                               type: body.contents.type,
-                               work: body.contents.work,
-                       },
-                       amount: body.amount,
-                       balance: body.balance,
-                       block_account: body.block_account,
-                       confirmed: body.confirmed,
-                       height: body.height,
-                       linked_account: body.linked_account,
-                       local_timestamp: body.local_timestamp,
-                       subtype: body.subtype,
-                       successor: body.successor,
+export function block_info (body: unknown): BlockInfoResponse {
+       if (body != null && typeof body === 'object') {
+               if ('amount' in body && typeof body.amount === 'string'
+                       && 'balance' in body && typeof body.balance === 'string'
+                       && 'block_account' in body && typeof body.block_account === 'string'
+                       && 'confirmed' in body && typeof body.confirmed === 'string'
+                       && 'height' in body && typeof body.height === 'string'
+                       && 'linked_account' in body && typeof body.linked_account === 'string'
+                       && 'local_timestamp' in body && typeof body.local_timestamp === 'string'
+                       && 'subtype' in body && (body.subtype === 'change' || body.subtype === 'epoch' || body.subtype === 'open' || body.subtype === 'receive' || body.subtype === 'send')
+                       && 'successor' in body && typeof body.successor === 'string'
+                       && 'contents' in body && body.contents != null && typeof body.contents === 'object'
+                       && 'account' in body.contents && typeof body.contents.account === 'string'
+                       && 'balance' in body.contents && typeof body.contents.balance === 'string'
+                       && 'link' in body.contents && typeof body.contents.link === 'string'
+                       && 'link_as_account' in body.contents && typeof body.contents.link_as_account === 'string'
+                       && 'previous' in body.contents && typeof body.contents.previous === 'string'
+                       && 'representative' in body.contents && typeof body.contents.representative === 'string'
+                       && 'signature' in body.contents && typeof body.contents.signature === 'string'
+                       && 'type' in body.contents && body.contents.type === 'state'
+                       && 'work' in body.contents && typeof body.contents.work === 'string'
+               ) {
+                       return {
+                               contents: {
+                                       account: body.contents.account,
+                                       balance: body.contents.balance,
+                                       link: body.contents.link,
+                                       link_as_account: body.contents.link_as_account,
+                                       previous: body.contents.previous,
+                                       representative: body.contents.representative,
+                                       signature: body.contents.signature,
+                                       type: body.contents.type,
+                                       work: body.contents.work,
+                               },
+                               amount: body.amount,
+                               balance: body.balance,
+                               block_account: body.block_account,
+                               confirmed: body.confirmed,
+                               height: body.height,
+                               linked_account: body.linked_account,
+                               local_timestamp: body.local_timestamp,
+                               subtype: body.subtype,
+                               successor: body.successor,
+                       }
+               }
+               if ('error' in body && typeof body.error === 'string') {
+                       throw new RpcError(body.error)
                }
-       } else {
-               throw new RpcError('Invalid block_info response')
        }
+       throw new RpcError('Invalid block_info response')
 }
index 16159960e4c9eb2ebefe8db4bb10508f4fd6ad21..cc8bc357686b384c41156cf20906c69ab45ea2d2 100644 (file)
@@ -3,34 +3,29 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { hex } from '../convert'
-import { BlockInfo, block_info } from './block_info'
+import { RpcError } from '../errors'
+import { BlockInfoResponse, block_info } from './block_info'
 
-type BlocksInfo = {
+type BlocksInfoResponse = {
        blocks?: {
-               [hash: string]: BlockInfo
-       },
+               [hash: Hex]: BlockInfoResponse
+       }
        errors?: {
-               [address: string]: string
-       },
+               [hash: string]: string
+       }
 }
 
-export function blocks_info (body: unknown): BlocksInfo {
-       const response: BlocksInfo = {}
-       if (body == null || typeof body !== 'object') {
-               return response
-       }
-       const blocks = 'blocks' in body ? body.blocks : null
-       const errors = 'errors' in body ? body.errors : null
+export function blocks_info (body: unknown): BlocksInfoResponse {
+       if (body != null && typeof body === 'object') {
+               const response: BlocksInfoResponse = {}
 
-       if (blocks != null) {
-               if (typeof blocks === 'object') {
-                       for (const [hash, data] of Object.entries(blocks) as [string, unknown][]) {
+               if ('errors' in body && body.errors != null && typeof body.errors === 'object') {
+                       for (const [hash, error] of Object.entries(body.errors) as [string, unknown][]) {
                                try {
-                                       if (hex.is(hash)) {
-                                               response.blocks ??= {}
-                                               response.blocks[hash] = block_info(data)
+                                       if (hex.is(hash) && typeof error === 'string') {
+                                               throw new Error(error)
                                        } else {
-                                               throw new Error('Invalid blocks_info hash')
+                                               throw new Error('Invalid blocks_info error')
                                        }
                                } catch (err: any) {
                                        response.errors ??= {}
@@ -38,24 +33,24 @@ export function blocks_info (body: unknown): BlocksInfo {
                                }
                        }
                }
-       }
 
-       if (errors != null) {
-               if (typeof errors === 'object') {
-                       for (const [hash, error] of Object.entries(errors) as [string, unknown][]) {
+               if ('blocks' in body && body.blocks != null && typeof body.blocks === 'object') {
+                       for (const [hash, data] of Object.entries(body.blocks) as [string, unknown][]) {
                                try {
-                                       if (hex.is(hash) && typeof error === 'string') {
-                                               throw new Error(error)
+                                       if (hex.is(hash)) {
+                                               response.blocks ??= {}
+                                               response.blocks[hash] = block_info(data)
                                        } else {
-                                               throw new Error('Invalid blocks_info error')
+                                               throw new Error('Invalid blocks_info hash')
                                        }
                                } catch (err: any) {
                                        response.errors ??= {}
-                                       response.errors[hash] = err?.message
+                                       response.errors[hash] ??= err?.message
                                }
                        }
                }
-       }
 
-       return response
+               return response
+       }
+       throw new RpcError('Invalid blocks_info response')
 }
index aed417b778a1c32b5d6f542dd6bd217fd2710906..1b5cb46aa2e8d7c08b60d261b37e3ce45791941c 100644 (file)
@@ -3,8 +3,6 @@
 import { RpcError } from '../errors'
 import { Schema } from './schema'
 
-export { Schema }
-
 /**
  * Represents a Nano network node. It primarily consists of a URL which will
  * accept RPC calls; an optional authentication header name can be passed if an
index 776bc9b8ec43494156ad41ac3c3bde1d1c8be7a9..99b62ba89f1ef2cb2f4214680a5ca1b34e6f1d2f 100644 (file)
@@ -1,25 +1,31 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-export const process = Object.freeze({
-       fields: [
-               'action',
-               'async',
-               'block',
-               'json_block',
-               'subtype',
-       ],
-       block: {
-               fields: [
-                       'account',
-                       'balance',
-                       'link',
-                       'link_as_account',
-                       'previous',
-                       'representative',
-                       'signature',
-                       'type',
-                       'work',
-               ],
-       },
-})
+import { hex } from '../convert'
+import { RpcError } from '../errors'
+import { BlockInfoContents } from './block_info'
+
+export type ProcessRequest = {
+       action: 'process'
+       block: BlockInfoContents
+       async: boolean
+       force: boolean
+       json_block: boolean
+       subtype: 'change' | 'epoch' | 'open' | 'receive' | 'send'
+}
+
+export type ProcessResponse = {
+       hash: Hex
+}
+
+export function process (body: unknown): ProcessResponse {
+       if (body != null && typeof body === 'object') {
+               if ('hash' in body && hex.is(body.hash)) {
+                       return { hash: body.hash }
+               }
+               if ('error' in body && typeof body.error === 'string') {
+                       throw new RpcError(body.error)
+               }
+       }
+       throw new RpcError('Invalid process response')
+}
index bb3a7ce7eef6b94ced117ddc8178e0cecc49fc34..295a92b75bafab65e0758382e5f049aaa86e0e2b 100644 (file)
@@ -19,5 +19,5 @@ export const Schema = Object.freeze({
        // accounts_representatives,
        block_info,
        blocks_info,
-       // process,
+       process,
 })