From 76016f0d7f8c5efda6029ae2bd12d1d73033c7b8 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 6 Aug 2026 23:37:24 -0700 Subject: [PATCH] Implement more RPC response validators. --- src/lib/rpc/accounts_frontiers.ts | 73 ++++++++++++++++++----- src/lib/rpc/block_info.ts | 97 ++++++++++++++++++++++--------- src/lib/rpc/blocks_info.ts | 90 +++++++++++++++++----------- src/lib/rpc/schema.ts | 8 ++- src/lib/wallet/refresh.ts | 16 +---- 5 files changed, 192 insertions(+), 92 deletions(-) diff --git a/src/lib/rpc/accounts_frontiers.ts b/src/lib/rpc/accounts_frontiers.ts index 90ac81f..dce1ee0 100644 --- a/src/lib/rpc/accounts_frontiers.ts +++ b/src/lib/rpc/accounts_frontiers.ts @@ -1,19 +1,60 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Account } from '../account' +import { hex } from '../convert' + +type AccountsFrontiers = { + frontiers?: { + [address: string]: string + } + errors?: { + [address: string]: string + } +} + +export function accounts_frontiers (body: unknown): AccountsFrontiers { + const response: AccountsFrontiers = {} + if (body == null || typeof body !== 'object') { + return response + } + const frontiers = 'frontiers' in body ? body.frontiers : null + const errors = 'errors' in body ? body.errors : null + + if (frontiers != null) { + if (typeof frontiers === 'object') { + for (const [address, frontier] of Object.entries(frontiers) as [string, unknown][]) { + try { + Account.validate(address) + if (hex.is(frontier)) { + response.frontiers ??= {} + response.frontiers[address] = frontier + } + } 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][]) { + try { + Account.validate(address) + if (typeof error === 'string') { + throw new Error(error) + } + } catch (err: any) { + response.errors ??= {} + response.errors[address] = err?.message + } + } + } + } -export const accounts_frontiers = Object.freeze({ - fields: [ - 'errors', - 'frontiers', - ], - errors: { - address: { - error: {}, - }, - }, - frontiers: { - address: { - hash: {}, - }, - }, -}) + return response +} diff --git a/src/lib/rpc/block_info.ts b/src/lib/rpc/block_info.ts index fc2ec56..da3bf7f 100644 --- a/src/lib/rpc/block_info.ts +++ b/src/lib/rpc/block_info.ts @@ -1,30 +1,75 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -export const block_info = Object.freeze({ - fields: [ - 'amount', - 'balance', - 'block_account', - 'confirmed', - 'contents', - 'height', - 'linked_account', - 'local_timestamp', - 'subtype', - 'successor', - ], + +export type BlockInfo = { contents: { - fields: [ - 'account', - 'balance', - 'link', - 'link_as_account', - 'previous', - 'representative', - 'signature', - 'type', - 'work', - ], - }, -}) + account: string + balance: string + link: string + link_as_account: string + previous: string + representative: string + signature: string + type: string + work: string + } + amount: string + balance: string + block_account: string + confirmed: string + height: string + linked_account: string + local_timestamp: string + subtype: string + 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 && typeof body.subtype === 'string' + && '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, + } + } else { + throw new Error('Invalid block_info body') + } +} diff --git a/src/lib/rpc/blocks_info.ts b/src/lib/rpc/blocks_info.ts index 532dc27..1615996 100644 --- a/src/lib/rpc/blocks_info.ts +++ b/src/lib/rpc/blocks_info.ts @@ -1,37 +1,61 @@ + //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -export const blocks_info = Object.freeze({ - fields: [ - 'blocks', - ], - blocks: { - hash: { - fields: [ - 'amount', - 'balance', - 'block_account', - 'confirmed', - 'contents', - 'height', - 'linked_account', - 'local_timestamp', - 'subtype', - 'successor', - ], - contents: { - fields: [ - 'account', - 'balance', - 'link', - 'link_as_account', - 'previous', - 'representative', - 'signature', - 'type', - 'work', - ], - }, - }, +import { hex } from '../convert' +import { BlockInfo, block_info } from './block_info' + +type BlocksInfo = { + blocks?: { + [hash: string]: BlockInfo }, -}) + errors?: { + [address: 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 + + if (blocks != null) { + if (typeof blocks === 'object') { + for (const [hash, data] of Object.entries(blocks) as [string, unknown][]) { + try { + if (hex.is(hash)) { + response.blocks ??= {} + response.blocks[hash] = block_info(data) + } else { + throw new Error('Invalid blocks_info hash') + } + } catch (err: any) { + response.errors ??= {} + response.errors[hash] = err?.message + } + } + } + } + + if (errors != null) { + if (typeof errors === 'object') { + for (const [hash, error] of Object.entries(errors) as [string, unknown][]) { + try { + if (hex.is(hash) && typeof error === 'string') { + throw new Error(error) + } else { + throw new Error('Invalid blocks_info error') + } + } catch (err: any) { + response.errors ??= {} + response.errors[hash] = err?.message + } + } + } + } + + return response +} diff --git a/src/lib/rpc/schema.ts b/src/lib/rpc/schema.ts index 6d4c444..bb3a7ce 100644 --- a/src/lib/rpc/schema.ts +++ b/src/lib/rpc/schema.ts @@ -1,5 +1,6 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { account_balance } from './account_balance' import { account_info } from './account_info' import { account_representative } from './account_representative' import { accounts_balances } from './accounts_balances' @@ -10,12 +11,13 @@ import { blocks_info } from './blocks_info' import { process } from './process' export const Schema = Object.freeze({ + account_balance, // account_info, // account_representative, accounts_balances, - // accounts_frontiers, + accounts_frontiers, // accounts_representatives, - // block_info, - // blocks_info, + block_info, + blocks_info, // process, }) diff --git a/src/lib/wallet/refresh.ts b/src/lib/wallet/refresh.ts index 4ef3d43..eb699ee 100644 --- a/src/lib/wallet/refresh.ts +++ b/src/lib/wallet/refresh.ts @@ -6,18 +6,6 @@ import { Block } from '../block' import { Rpc, Schema } from '../rpc' import { Wallet } from '../wallet' -export type BlockInfo = { - blocks: { - [hash: string]: { - [p: string]: string - } & { - contents: { - [p: string]: string - } - } - } -} - export async function _refresh (wallet: Wallet, rpc: Rpc | string | URL, from: number, to: number): Promise> export async function _refresh (wallet: Wallet, rpc: unknown, from: unknown, to: unknown): Promise> { try { @@ -41,8 +29,8 @@ export async function _refresh (wallet: Wallet, rpc: unknown, from: unknown, to: accounts: addresses } const { balances } = await rpc.post('accounts_balances', data) - const { frontiers } = await rpc.post('accounts_frontiers', data) as { frontiers: { [address: string]: string } } - const { blocks } = await rpc.post('blocks_info', { json_block: true, hashes: Object.values(frontiers) }) as BlockInfo + const { frontiers } = await rpc.post('accounts_frontiers', data) + const { blocks } = await rpc.post('blocks_info', { json_block: true, include_not_found: true, hashes: Object.values(frontiers) }) as BlockInfo for (const account of accounts.values()) { account.balance = balances?.[account.address]?.balance ?? 0 account.receivable = balances?.[account.address]?.receivable ?? 0 -- 2.52.0