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