a = a.replace('%c', '<span style="font-weight:bold">')
output.innerHTML += `<pre>${a}</pre>`
consoleGroup(...args)
- window?.scrollTo(0, document.body.scrollHeight)
}
const consoleError = console.error
console.error = (...args) => {
a = a.replace('%cFAIL ,color:red,', '<span style="color:red">FAIL </span>')
output.innerHTML += `<pre>${a}</pre>`
consoleError(...args)
- window?.scrollTo(0, document.body.scrollHeight)
}
const consoleLog = console.log
console.log = (...args) => {
a = a.replace('%cTESTING COMPLETE,color:orange;font-weight:bold', '<span style="color:orange;font-weight:bold">TESTING COMPLETE</span>')
output.innerHTML += `<pre>${a}</pre>`
consoleLog(...args)
- window?.scrollTo(0, document.body.scrollHeight)
}
})()
</script>
*/\r
async destroy (): Promise<void> {\r
await super.destroy()\r
- await this.close()\r
+ const { status } = await this.close()\r
+ if (status !== 'OK') {\r
+ throw new Error('Failed to close wallet', { cause: status })\r
+ }\r
}\r
\r
/**\r
* @returns True if successfully locked\r
*/\r
async lock (): Promise<boolean> {\r
- const result = await this.close()\r
- return result.status === 'OK'\r
+ const { status } = await this.close()\r
+ return status === 'OK'\r
}\r
\r
/**\r
* @returns True if successfully unlocked\r
*/\r
async unlock (): Promise<boolean> {\r
- const result = await this.connect()\r
- return result === 'OK'\r
+ const { status } = await this.connect()\r
+ return status === 'OK'\r
}\r
\r
async init (): Promise<void> {\r
}\r
}\r
\r
- async connect (): Promise<string> {\r
+ async connect (): Promise<LedgerResponse> {\r
const { usb } = globalThis.navigator\r
if (usb) {\r
usb.removeEventListener('disconnect', this.onDisconnectUsb.bind(this))\r
} else {\r
this.#status = 'DISCONNECTED'\r
}\r
- return this.status\r
+ return { status: this.status }\r
}\r
\r
async onDisconnectUsb (e: USBConnectionEvent): Promise<void> {\r
}\r
\r
/**\r
- * Open Nano app by launching user flow.\r
+ * Open the Nano app by launching a user flow.\r
*\r
* https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application\r
*\r
}\r
\r
/**\r
- * Close the currently running app.\r
+ * Close the currently running app and return to the device dashboard.\r
*\r
* https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application\r
*\r
const statusCode = bytes.toDec(response.slice(-2)) as number\r
const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
\r
- if (status !== 'OK') {\r
+ if (response.byteLength === 2) {\r
return { status, signature: null }\r
}\r
- if (response.byteLength > 66) {\r
+ if (response.byteLength === 66) {\r
const signature = bytes.toHex(response.slice(0, 64))\r
return { status, signature }\r
}\r
+ if (response.byteLength === 98) {\r
+ const hash = bytes.toHex(response.slice(0, 32))\r
+ const signature = bytes.toHex(response.slice(32, 96))\r
+ return { status, signature, hash }\r
+ }\r
\r
- const hash = bytes.toHex(response.slice(0, 32))\r
- const signature = bytes.toHex(response.slice(32, 96))\r
- return { status, signature, hash }\r
-\r
+ throw new Error('Unexpected byte length from device signature', { cause: response })\r
}\r
\r
/**\r
export const isNode = process.versions?.node != null
+export async function click (text, fn) {
+ return new Promise((resolve, rejects) => {
+ const button = document.createElement('button')
+ const hourglass = document.createTextNode('⏳')
+ button.innerText = text
+ button.addEventListener('click', async () => {
+ button.innerText = 'Waiting for device...'
+ button.after(hourglass)
+ try {
+ const result = await fn()
+ resolve(result)
+ } catch (err) {
+ rejects(err)
+ } finally {
+ hourglass.remove()
+ button.remove()
+ }
+ })
+ document.body.appendChild(button)
+ window?.scrollTo(0, document.body.scrollHeight)
+ })
+}
+
export function stats (times) {
if (times == null || times.length === 0) return null
console.groupEnd()
return
}
- if (fn.constructor.name === 'AsyncFunction') fn = fn()
+ if (fn?.constructor?.name === 'AsyncFunction') fn = fn()
if (typeof fn === 'function') fn = new Promise(resolve => resolve(fn()))
console.group(`%c${name}`, 'font-weight:bold')
await fn
} else {
fail(`${name}: test cannot execute on ${typeof fn} ${fn}`)
}
+ window?.scrollTo(0, document.body.scrollHeight)
}
export const assert = {
'use strict'
-import { assert, isNode, suite, test } from './GLOBALS.mjs'
+import { assert, click, isNode, suite, test } from './GLOBALS.mjs'
import { NANO_TEST_VECTORS } from './VECTORS.js'
-import { LedgerWallet } from '../dist/main.min.js'
+import { Account, LedgerWallet, ReceiveBlock, SendBlock } from '../dist/main.min.js'
-async function click (text, fn) {
- return new Promise(resolve => {
- const button = document.createElement('button')
- button.innerText = text
- button.addEventListener('click', async (event) => {
- const result = await fn
- document.body.removeChild(button)
- resolve(result)
- })
- document.body.appendChild(button)
+/**
+* HID interactions require user gestures, so to reduce clicks, the variables
+* shared among tests like wallet and account are declared at the top-level.
+*/
+await suite('Ledger hardware wallet', { skip: false || isNode }, async () => {
+
+ let wallet, account, openBlock, sendBlock, receiveBlock
+
+ await test('connect to the device and then disconnect', async () => {
+ wallet = await LedgerWallet.create()
+ const { status } = await click(
+ 'Unlock, then click to connect',
+ async () => wallet.connect()
+ )
+ assert.equals(status, 'CONNECTED')
})
-}
-await suite('Ledger hardware wallet', async () => {
+ // 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('fail when using new', async () => {
- assert.throws(() => new LedgerWallet())
+ // assert.equals(resultSignNonce.status, 'OK')
+ // assert.OK(/[A-Fa-f0-9]{128}/.test(resultSignNonce.signature))
+ })
+
+ await test('get first account', async () => {
+ account = await click(
+ 'Click to get account',
+ async () => wallet.account()
+ )
+
+ assert.exists(account)
+ assert.ok(account instanceof Account)
+ assert.exists(account.publicKey)
+ assert.exists(account.address)
+ })
+
+ await test('sign open block from block', async () => {
+ openBlock = new ReceiveBlock(
+ account,
+ '0',
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.link,
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.balance,
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,
+ '0'
+ )
+
+ assert.ok(/[A-Fa-f0-9]{64}/.test(openBlock.hash))
+ assert.nullish(openBlock.signature)
+ assert.equals(openBlock.account.publicKey, account.publicKey)
+
+ const { status, hash, signature } = await click(
+ 'Click to sign opening ReceiveBlock from wallet',
+ async () => wallet.sign(0, openBlock)
+ )
+
+ assert.equals(status, 'OK')
+ assert.ok(/[A-Fa-f0-9]{64}/.test(hash))
+ assert.ok(/[A-Fa-f0-9]{128}/.test(signature))
+
+ await click(
+ 'Click to sign open ReceiveBlock from block and compare signatures',
+ async () => openBlock.sign(0)
+ )
+
+ assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))
+ assert.equals(signature, openBlock.signature)
})
- await test('connect to a Ledger device and then disconnect', { skip: false || isNode }, async () => {
- const wallet = await LedgerWallet.create()
- const openStatus = await click('Unlock, then click to connect', wallet.connect())
+ await test('cache open block', async () => {
+ const { status } = await click(
+ 'Click to cache open block',
+ async () => wallet.updateCache(0, openBlock)
+ )
+
+ assert.equals(status, 'OK')
+ })
- assert.equals(openStatus, 'CONNECTED')
+ await test('sign send block from wallet which requires cache to be up-to-date', async () => {
+ sendBlock = new SendBlock(
+ account,
+ openBlock.balance,
+ account.address,
+ '0',
+ NANO_TEST_VECTORS.SEND_BLOCK.representative,
+ openBlock.hash
+ )
+
+ assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash))
+ assert.nullish(sendBlock.signature)
+ assert.equals(sendBlock.account.publicKey, account.publicKey)
+
+ const { status, hash, signature } = await click(
+ 'Click to sign SendBlock from wallet',
+ async () => wallet.sign(0, sendBlock)
+ )
+
+ assert.equals(status, 'OK')
+ assert.ok(/[A-Fa-f0-9]{64}/.test(hash))
+ assert.ok(/[A-Fa-f0-9]{128}/.test(signature))
+ sendBlock.signature = signature
+ })
+
+ await test('sign a receive block from block object which can accept previous block for cache', async () => {
+ receiveBlock = new ReceiveBlock(
+ account,
+ sendBlock.balance,
+ sendBlock.hash,
+ '0',
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,
+ sendBlock.hash
+ )
+
+ assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash))
+ assert.nullish(receiveBlock.signature)
+ assert.equals(receiveBlock.account.publicKey, account.publicKey)
+
+ await click(
+ 'Click to sign SendBlock from block',
+ async () => receiveBlock.sign(0, sendBlock)
+ )
+
+ assert.ok(/[A-Fa-f0-9]{128}/.test(receiveBlock.signature))
+ })
+
+ await test('destroy wallet', async () => {
+ const { status } = await click(
+ 'Click to close',
+ async () => wallet.close()
+ )
+ assert.equals(status, 'OK')
+ await wallet.destroy()
+ })
+
+ await test('fail when using new', async () => {
+ assert.throws(() => new LedgerWallet())
+ })
- const closeStatus = await click('Click to close', wallet.close())
- assert.equals(closeStatus.status, 'OK')
+ await test('fail to sign a block without caching frontier', async () => {
+ sendBlock = new SendBlock(
+ account,
+ receiveBlock.balance,
+ account.address,
+ '0',
+ NANO_TEST_VECTORS.SEND_BLOCK.representative,
+ receiveBlock.previous
+ )
+ await assert.rejects(click(
+ 'Fail to sign',
+ async () => sendBlock.sign(0)
+ ))
})
})
\r
import { assert, suite, test } from './GLOBALS.mjs'\r
import { NANO_TEST_VECTORS } from './VECTORS.js'\r
-import { SendBlock, ReceiveBlock, ChangeBlock, LedgerWallet } from '../dist/main.min.js'\r
+import { SendBlock, ReceiveBlock, ChangeBlock } from '../dist/main.min.js'\r
\r
await suite('Valid blocks', async () => {\r
\r
assert.equals(block.work, '')\r
})\r
})\r
-\r
-await suite('Ledger device signing tests', { skip: false }, async () => {\r
- const wallet = await LedgerWallet.create()\r
- const account = await new Promise(resolve => {\r
- const button = document.createElement('button')\r
- button.innerText = 'Unlock Ledger, then click to continue'\r
- button.addEventListener('click', async (event) => {\r
- await wallet.connect()\r
- resolve(await wallet.account())\r
- document.body.removeChild(button)\r
- })\r
- document.body.appendChild(button)\r
- })\r
-\r
- // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14\r
- await test('should sign a nonce with a Ledger device', { skip: true }, async () => {\r
- let status = await new Promise(resolve => {\r
- const button = document.createElement('button')\r
- button.innerText = 'Unlock Ledger, then click to continue'\r
- button.addEventListener('click', async (event) => {\r
- await wallet.connect()\r
- const result = await wallet.sign(0, new TextEncoder().encode('0123456789abcdef'))\r
-\r
- assert.equals(result.status, 'OK')\r
- assert.equals(result.signature.toUpperCase(), '2BD2F905E74B5BEE3E2277CED1D1E3F7535E5286B6E22F7B08A814AA9E5C4E1FEA69B61D60B435ADC2CE756E6EE5F5BE7EC691FE87E024A0B22A3D980CA5B305')\r
-\r
- document.body.removeChild(button)\r
- resolve(result)\r
- })\r
- document.body.appendChild(button)\r
- })\r
- })\r
-\r
- await test('should sign an open block and send block with a Ledger device', async () => {\r
- const openBlock = new ReceiveBlock(\r
- account,\r
- '0',\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.link,\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.balance,\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,\r
- '0'\r
- )\r
- await new Promise(resolve => {\r
- const button = document.createElement('button')\r
- button.innerText = 'Unlock Ledger, then click to test signing open ReceiveBlock'\r
- button.addEventListener('click', async (event) => {\r
- assert.nullish(openBlock.signature)\r
- await openBlock.sign(0)\r
- assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))\r
- resolve(document.body.removeChild(button))\r
- })\r
- document.body.appendChild(button)\r
- })\r
- assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))\r
- assert.ok(/[A-Fa-f0-9]{64}/.test(openBlock.hash))\r
-\r
- const sendBlock = new SendBlock(\r
- account,\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.balance,\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.account,\r
- '0',\r
- NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,\r
- openBlock.hash\r
- )\r
- await new Promise(resolve => {\r
- const button = document.createElement('button')\r
- button.innerText = 'Unlock Ledger, then click to test signing SendBlock'\r
- button.addEventListener('click', async (event) => {\r
- assert.nullish(sendBlock.signature)\r
- await sendBlock.sign(0, openBlock)\r
- assert.ok(/[A-Fa-f0-9]{128}/.test(sendBlock.signature))\r
- resolve(document.body.removeChild(button))\r
- })\r
- document.body.appendChild(button)\r
- })\r
- })\r
-\r
- await test('should fail sign a block with a Ledger device without caching frontier', async () => {\r
- const block = new SendBlock(\r
- NANO_TEST_VECTORS.SEND_BLOCK.account,\r
- NANO_TEST_VECTORS.SEND_BLOCK.balance,\r
- NANO_TEST_VECTORS.SEND_BLOCK.link,\r
- '0',\r
- NANO_TEST_VECTORS.SEND_BLOCK.representative,\r
- NANO_TEST_VECTORS.SEND_BLOCK.previous,\r
- NANO_TEST_VECTORS.SEND_BLOCK.work\r
- )\r
- assert.rejects(block.sign(0))\r
- })\r
-\r
- await test('destroy Ledger wallet', async () => {\r
- await wallet.destroy()\r
- })\r
-})\r