* property.
*
* @param {number} index - Account index between 0x0 and 0x7fffffff
- * @param {object} [block] - JSON of previous block for offline signing
+ * @param {object} [frontier] - JSON of frontier block for offline signing
*/
- async sign (index?: number, block?: { [key: string]: string }): Promise<void>
- async sign (input?: number | string, block?: { [key: string]: string }): Promise<void> {
+ async sign (index?: number, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<void>
+ async sign (input?: number | string, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<void> {
if (typeof input === 'number') {
const index = input
const { LedgerWallet } = await import('./wallets')
const ledger = await LedgerWallet.create()
await ledger.connect()
- if (block) {
+ if (frontier) {
try {
- await ledger.updateCache(index, block)
+ await ledger.updateCache(index, frontier)
} catch (err) {
console.warn('Error updating Ledger cache of previous block, attempting signature anyway', err)
}
}
- const result = await ledger.sign(index, this as SendBlock | ReceiveBlock | ChangeBlock)
- if (result.status !== 'OK' || result.signature == null) {
- throw new Error(result.status)
+ try {
+ this.signature = await ledger.sign(index, this as SendBlock | ReceiveBlock | ChangeBlock)
+ } catch (err) {
+ throw new Error('failed to sign with ledger', { cause: err })
}
- this.signature = result.signature
} else if (typeof input === 'string') {
try {
const account = await Account.import({ index: 0, privateKey: input }, '')
import { bytes, dec, hex } from '#src/lib/convert.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
import { Rpc } from '#src/lib/rpc.js'\r
-import { KeyPair } from '#types'\r
-\r
-type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'\r
-\r
-interface LedgerResponse {\r
- status: string\r
-}\r
-\r
-interface LedgerVersionResponse extends LedgerResponse {\r
- name: string | null,\r
- version: string | null\r
-}\r
-\r
-interface LedgerAccountResponse extends LedgerResponse {\r
- publicKey: string | null,\r
- address: string | null\r
-}\r
-\r
-interface LedgerSignResponse extends LedgerResponse {\r
- signature: string | null,\r
- hash?: string\r
-}\r
+import { DeviceStatus, KeyPair, LedgerAccountResponse, LedgerResponse, LedgerSignResponse, LedgerVersionResponse } from '#types'\r
\r
/**\r
* Ledger hardware wallet created by communicating with a Ledger device via ADPU\r
*\r
* @param {number} index - Account number\r
* @param {object} block - Block data to sign\r
- * @returns {Promise} Status, signature, and block hash\r
+ * @returns {Promise<string>} Signature\r
*/\r
- async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse>\r
- /**\r
- * Sign a nonce with the Ledger device.\r
- *\r
- * @param {number} index - Account number\r
- * @param {Uint8Array} nonce - 128-bit value to sign\r
- * @returns {Promise} Status and signature\r
- */\r
- async sign (index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse>\r
- async sign (index: number = 0, input: Uint8Array<ArrayBuffer> | SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse> {\r
+ async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock, frontier?: SendBlock | ReceiveBlock | ChangeBlock): Promise<string> {\r
if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
throw new TypeError('Invalid account index')\r
}\r
+ if (frontier != null) {\r
+ const { status } = await this.#cacheBlock(index, frontier)\r
+ if (status !== 'OK') {\r
+ throw new Error('failed to cache frontier block in ledger', { cause: status })\r
+ }\r
+ }\r
console.log('Waiting for signature confirmation on Ledger device...')\r
- if (input instanceof Uint8Array) {\r
- // input is a nonce\r
- return await this.#signNonce(index, input)\r
- } else {\r
- // input is a block\r
- return await this.#signBlock(index, input)\r
+ const { status, signature, hash } = await this.#signBlock(index, block)\r
+ if (status !== 'OK') {\r
+ throw new Error('signing with ledger failed', { cause: status })\r
+ }\r
+ if (hash !== block.hash) {\r
+ throw new Error('hash from ledger does not match hash from block', { cause: `${hash} | ${block.hash}` })\r
+ }\r
+ if (signature == null) {\r
+ throw new Error('ledger failed to return signature')\r
}\r
+ block.signature = signature\r
+ return signature\r
}\r
\r
/**\r
* @param {number} index - Account number\r
* @param {object} block - JSON-formatted block data\r
*/\r
- async updateCache (index: number, block: { [key: string]: string }): Promise<LedgerResponse>\r
+ async updateCache (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<LedgerResponse>\r
/**\r
* Update cache from a block hash by calling out to a node. Suitable for online\r
* use only.\r
}\r
const { status } = await this.#cacheBlock(index, input)\r
if (status !== 'OK') {\r
- throw new Error(status)\r
+ throw new Error('failed to cache frontier block in ledger', { cause: status })\r
}\r
return { status }\r
}\r
* @param {any} block - Block data to cache\r
* @returns Status of command\r
*/\r
- async #cacheBlock (index: number = 0, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerResponse> {\r
+ async #cacheBlock (index: number = 0, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<LedgerResponse> {\r
if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
throw new TypeError('Invalid account index')\r
}\r
- if (!(block instanceof SendBlock) && !(block instanceof ReceiveBlock) && !(block instanceof ChangeBlock)) {\r
+ if (!(block instanceof ChangeBlock) && !(block instanceof ReceiveBlock) && !(block instanceof SendBlock)) {\r
throw new TypeError('Invalid block format')\r
}\r
if (!block.signature) {\r
//! SPDX-License-Identifier: GPL-3.0-or-later\r
\r
import { Account, AccountList } from '#src/lib/account.js'\r
+import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { ADDRESS_GAP } from '#src/lib/constants.js'\r
import { bytes, hex, utf8 } from '#src/lib/convert.js'\r
return accounts\r
}\r
\r
+ /**\r
+ * Signs a block using the private key of the account at the wallet index\r
+ * specified. The signature is appended to the signature field of the block\r
+ * before being returned. The wallet must be unlocked prior to signing.\r
+ *\r
+ * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed\r
+ * @param {number} index - Account to use for signing\r
+ * @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
+ */\r
+ async sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {\r
+ if (this.#locked) throw new Error('wallet must be unlocked to sign')\r
+ if (this.#s == null) throw new Error('wallet seed not found')\r
+ try {\r
+ const account = this.account(index)\r
+ return (await account).sign(block, new Uint8Array(this.#s))\r
+ } catch (err) {\r
+ throw new Error(`failed to sign block`, { cause: err })\r
+ }\r
+ }\r
+\r
/**\r
* Unlocks the wallet using the same password as used prior to lock it.\r
*\r
* property.
*
* @param {number} index - Account index between 0x0 and 0x7fffffff
- * @param {object} [block] - JSON of previous block for offline signing
+ * @param {object} [frontier] - JSON of frontier block for offline signing
*/
- sign (index?: number, block?: {
- [key: string]: string
- }): Promise<void>
+ sign (index?: number, frontier?: ChangeBlock | ReceiveBlock | SendBlock): Promise<void>
/**
* Verifies the signature of the block. If a key is not provided, the public
* key of the block's account will be used if it exists.
*/
refresh (rpc: Rpc | string | URL, from?: number, to?: number): Promise<AccountList>
/**
+ * Signs a block using the private key of the account at the wallet index
+ * specified. The signature is appended to the signature field of the block
+ * before being returned. The wallet must be unlocked prior to signing.
+ *
+ * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - Block data to be hashed and signed
+ * @param {number} index - Account to use for signing
+ * @returns {Promise<string>} Hexadecimal-formatted 64-byte signature
+ */
+ sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string>
+ /**
* Unlocks the wallet using the same password as used prior to lock it.
*
* @param {Key} password Used previously to lock the wallet
}
type DeviceStatus = 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
+
interface LedgerResponse {
status: string
}
+
interface LedgerVersionResponse extends LedgerResponse {
- name: string | null
+ name: string | null,
version: string | null
}
+
+interface LedgerAccountResponse extends LedgerResponse {
+ publicKey: string | null,
+ address: string | null
+}
+
interface LedgerSignResponse extends LedgerResponse {
- signature: string | null
+ signature: string | null,
hash?: string
}
+
/**
* Ledger hardware wallet created by communicating with a Ledger device via ADPU
* calls. This wallet does not feature any seed nor mnemonic phrase as all
*
* @param {number} index - Account number
* @param {object} block - Block data to sign
- * @returns {Promise} Status, signature, and block hash
- */
- sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse>
- /**
- * Sign a nonce with the Ledger device.
- *
- * @param {number} index - Account number
- * @param {Uint8Array} nonce - 128-bit value to sign
- * @returns {Promise} Status and signature
+ * @returns {Promise<string>} Signature
*/
- sign (index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse>
+ sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock, frontier?: SendBlock | ReceiveBlock | ChangeBlock): Promise<string>
/**
* Attempts to connect to the Ledger device.
*
* @param {number} index - Account number
* @param {object} block - JSON-formatted block data
*/
- updateCache (index: number, block: {
- [key: string]: string
- }): Promise<LedgerResponse>
+ updateCache (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<LedgerResponse>
/**
* Update cache from a block hash by calling out to a node. Suitable for online
* use only.
import { assert, isNode, suite, test } from './GLOBALS.mjs'\r
import { NANO_TEST_VECTORS } from './VECTORS.mjs'\r
\r
+/**\r
+* @type {typeof import('../dist/types.d.ts').Bip44Wallet}\r
+*/\r
+let Bip44Wallet\r
/**\r
* @type {typeof import('../dist/types.d.ts').ChangeBlock}\r
*/\r
*/\r
let SendBlock\r
if (isNode) {\r
- ({ ChangeBlock, ReceiveBlock, SendBlock } = await import('../dist/nodejs.min.js'))\r
+ ({ Bip44Wallet, ChangeBlock, ReceiveBlock, SendBlock } = await import('../dist/nodejs.min.js'))\r
} else {\r
- ({ ChangeBlock, ReceiveBlock, SendBlock } = await import('../dist/browser.min.js'))\r
+ ({ Bip44Wallet, ChangeBlock, ReceiveBlock, SendBlock } = await import('../dist/browser.min.js'))\r
}\r
\r
await Promise.all([\r
\r
suite('Block signing using official test vectors', async () => {\r
\r
- await test('sign open block', async () => {\r
+ await test('sign open block with wallet', async () => {\r
+ const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+ await assert.resolves(wallet.unlock(NANO_TEST_VECTORS.PASSWORD))\r
+\r
+ const block = new ReceiveBlock(\r
+ NANO_TEST_VECTORS.ADDRESS_0,\r
+ '0',\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.link,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.balance,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.representative,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.previous,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.work\r
+ )\r
+ const signature = await wallet.sign(0, block)\r
+ assert.equal(block.signature, signature)\r
+ })\r
+\r
+ await test('sign open block with account', async () => {\r
+ const block = new ReceiveBlock(\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.account,\r
+ '0',\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.link,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.balance,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.representative,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.previous,\r
+ NANO_TEST_VECTORS.OPEN_BLOCK.work\r
+ )\r
+ await block.sign(NANO_TEST_VECTORS.OPEN_BLOCK.key)\r
+ assert.equal(block.hash, NANO_TEST_VECTORS.OPEN_BLOCK.hash)\r
+ assert.equal(block.signature, NANO_TEST_VECTORS.OPEN_BLOCK.signature)\r
+ })\r
+\r
+ await test('sign open block with private key', async () => {\r
const block = new ReceiveBlock(\r
NANO_TEST_VECTORS.OPEN_BLOCK.account,\r
'0',\r
*/
await Promise.all([
/* node:coverage disable */
- suite('Ledger hardware wallet', { skip: true || isNode }, async () => {
+ suite('Ledger hardware wallet', { skip: false || isNode }, async () => {
let wallet, account, openBlock, sendBlock, receiveBlock
assert.nullish(openBlock.signature)
assert.equal(openBlock.account.publicKey, account.publicKey)
- const { status, hash, signature } = await wallet.sign(0, openBlock)
+ const signature = await wallet.sign(0, openBlock)
- assert.equal(status, 'OK')
- assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash))
assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature))
await openBlock.sign(0)
assert.nullish(sendBlock.signature)
assert.equal(sendBlock.account.publicKey, account.publicKey)
- const { status, hash, signature } = await wallet.sign(0, sendBlock)
+ const signature = await wallet.sign(0, sendBlock)
- assert.equal(status, 'OK')
- assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash))
assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature))
- sendBlock.signature = signature
+ assert.equal(signature, sendBlock.signature)
})
await test('sign a receive block from block object which can accept previous block for cache', async () => {
assert.ok(/^[A-Fa-f0-9]{128}$/.test(receiveBlock.signature ?? ''))
})
- // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14
- await test('sign a nonce', { skip: true }, async () => {
- const nonce = new TextEncoder().encode('0123456789abcdef')
- const { status, signature } = await click('Click to sign nonce', wallet.sign(0, nonce))
+ await test('sign a receive block from wallet including frontier block for cache', async () => {
+ sendBlock = new ReceiveBlock(
+ account,
+ receiveBlock.balance,
+ receiveBlock.hash,
+ '0',
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,
+ receiveBlock.hash
+ )
+
+ assert.ok(/^[A-Fa-f0-9]{64}$/.test(receiveBlock.hash))
+ assert.nullish(sendBlock.signature)
+ assert.equal(sendBlock.account.publicKey, account.publicKey)
+
+ const signature = await wallet.sign(0, sendBlock, receiveBlock)
- assert.equal(status, 'OK')
- assert.OK(/^[A-Fa-f0-9]{128}$/.test(signature))
+ assert.exists(sendBlock.signature)
+ assert.ok(/^[A-Fa-f0-9]{128}$/.test(sendBlock.signature ?? ''))
+ assert.equal(signature, sendBlock.signature)
})
await test('destroy wallet', async () => {