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
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
* 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.
*
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')
}
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)
*\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
* 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
* @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
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
//! 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>
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) {
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 })
}
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',
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> = {
//@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, '')
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())
})