import { _lock } from './lock'\r
import { _refresh } from './refresh'\r
import { _restore } from './restore'\r
-import { _sign } from './sign'\r
+import { _signBlock, _signData } from './sign'\r
import { _unlock } from './unlock'\r
import { _unopened } from './unopened'\r
import { _update } from './update'\r
return await _refresh(this, rpc, from, to)\r
}\r
\r
+ /**\r
+ * Signs arbitrary strings using the private key of the account at the wallet\r
+ * index specified and returns a detached signature as a hexadecimal string.\r
+ * For compatibility with other Nano tools, the data is first hashed to a\r
+ * 32-byte value using BLAKE2b. The wallet must be unlocked prior to signing.\r
+ *\r
+ * @param {number} index - Account to use for signing\r
+ * @param {(string|string[])} data - Arbitrary data to be hashed and signed\r
+ */\r
+ async sign (index: number, data: string | string[]): Promise<string>\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
* @param {Block} [frontier] - Previous block data to be cached by Ledger wallet\r
*/\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
+ async sign (index: number, data: string | string[] | Block, frontier?: Block): Promise<void | string> {\r
+ return data instanceof Block\r
+ ? await _signBlock(this, this.#vault, index, data, frontier)\r
+ : await _signData(this, this.#vault, index, data)\r
}\r
\r
/**\r
//! SPDX-License-Identifier: GPL-3.0-or-later
import { Block } from '../block'
-import { bytes, hex } from '../convert'
+import { bytes, hex, utf8 } from '../convert'
+import { Blake2b } from '../crypto'
import { Vault } from '../vault'
import { Wallet } from '../wallet'
-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> {
+export async function _signData (wallet: Wallet, vault: Vault, index: number, data: string | string[]): Promise<string>
+export async function _signData (wallet: Wallet, vault: Vault, index: unknown, data: unknown): Promise<string> {
+ try {
+ if (wallet.type === 'Ledger') {
+ throw new TypeError('Ledger wallet cannot sign arbitrary data')
+ }
+ if (typeof index !== 'number') {
+ throw new TypeError('Index must be a number', { cause: index })
+ }
+ const message = Array.isArray(data) ? data : [data]
+ if (message.some(s => typeof s !== 'string')) {
+ throw new TypeError('Data to sign must be strings', { cause: data })
+ }
+ const hash = new Blake2b(32)
+ message.forEach(s => hash.update(utf8.toBytes(s)))
+ const { signature } = await vault.request<ArrayBuffer>({
+ action: 'sign',
+ index,
+ message: hash.digest().buffer
+ })
+ return bytes.toHex(new Uint8Array(signature))
+ } catch (err) {
+ throw new Error('Failed to sign data', { cause: err })
+ }
+}
+
+
+export async function _signBlock (wallet: Wallet, vault: Vault, index: number, block: Block, frontier?: Block): Promise<void>
+export async function _signBlock (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 })
*/
refresh (rpc: Rpc | string | URL, from?: number, to?: number): Promise<AccountList>
/**
+ * Signs arbitrary strings using the private key of the account at the wallet
+ * index specified and returns a detached signature as a hexadecimal string.
+ * For compatibility with other Nano tools, the data is first hashed to a
+ * 32-byte value using BLAKE2b. The wallet must be unlocked prior to signing.
+ *
+ * @param {number} index - Account to use for signing
+ * @param {(string|string[])} data - Arbitrary data to be hashed and signed
+ */
+ sign (index: number, data: string | string[]): Promise<string>
+ /**
* 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.
import './test.manage-rolodex.mjs'
import './test.refresh-accounts.mjs'
import './test.tools.mjs'
+import './test.wallet-sign.mjs'
console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold')
console.log('%cPASS: ', 'color:green;font-weight:bold', passes.length)
assert.equal(result3, false)\r
})\r
\r
- await test('should verify a block using the public key', async () => {\r
- const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
- await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const account = await wallet.account()\r
- if (account.index == null) {\r
- throw new Error('Account index missing')\r
- }\r
- const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
- .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
- .sign(wallet, account.index)\r
-\r
- assert.ok(await sendBlock.verify(account.publicKey))\r
- await assert.resolves(wallet.destroy())\r
- })\r
-\r
- await test('should reject a block using the wrong public key', async () => {\r
- const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
- await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
- const account = await wallet.account()\r
- if (account.index == null) {\r
- throw new Error('Account index missing')\r
- }\r
- assert.equal(account.index, 0)\r
-\r
- const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
- .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
- .sign(wallet, account.index)\r
- assert.ok(await sendBlock.verify(account.publicKey))\r
-\r
- const wrongAccount = Account.load('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')\r
- assert.equal(await sendBlock.verify(wrongAccount.publicKey), false)\r
-\r
- await assert.resolves(wallet.destroy())\r
- })\r
-\r
await test('sweeper throws without required parameters', async () => {\r
//@ts-expect-error\r
await assert.rejects(Tools.sweep(),\r
--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
+//! SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+'use strict'\r
+\r
+import { assert, env, isNode, suite, test } from './GLOBALS.mjs'\r
+import { MAX_RAW, MAX_SUPPLY, NANO_TEST_VECTORS } from './VECTORS.mjs'\r
+\r
+/**\r
+* @type {typeof import('../dist/types').Account}\r
+*/\r
+let Account\r
+/**\r
+* @type {typeof import('../dist/types').Block}\r
+*/\r
+let Block\r
+/**\r
+* @type {typeof import('../dist/types').Tools}\r
+*/\r
+let Tools\r
+/**\r
+* @type {typeof import('../dist/types').Wallet}\r
+*/\r
+let Wallet\r
+if (isNode) {\r
+ ({ Account, Block, Tools, Wallet } = await import('../dist/nodejs.min.js'))\r
+} else {\r
+ ({ Account, Block, Tools, Wallet } = await import('../dist/browser.min.js'))\r
+}\r
+\r
+await Promise.all([\r
+\r
+ suite('sign with Wallet', async () => {\r
+\r
+ await test('sign an arbitrary string', async () => {\r
+ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const account = await wallet.account()\r
+ if (account.index == null) {\r
+ throw new Error('Account index missing')\r
+ }\r
+ const data = crypto.randomUUID()\r
+ const signature = await wallet.sign(account.index, data)\r
+\r
+ assert.ok(await Tools.verify(account.publicKey, signature, data))\r
+ await assert.resolves(wallet.destroy())\r
+ })\r
+\r
+ await test('verify Block signature using the correct public key', async () => {\r
+ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const account = await wallet.account()\r
+ if (account.index == null) {\r
+ throw new Error('Account index missing')\r
+ }\r
+ const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
+ .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
+ .sign(wallet, account.index)\r
+\r
+ assert.ok(await sendBlock.verify(account.publicKey))\r
+ await assert.resolves(wallet.destroy())\r
+ })\r
+\r
+ await test('reject Block signature using the wrong public key', async () => {\r
+ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+ const account = await wallet.account()\r
+ if (account.index == null) {\r
+ throw new Error('Account index missing')\r
+ }\r
+ assert.equal(account.index, 0)\r
+\r
+ const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
+ .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
+ .sign(wallet, account.index)\r
+ assert.ok(await sendBlock.verify(account.publicKey))\r
+\r
+ const wrongAccount = Account.load('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')\r
+ assert.equal(await sendBlock.verify(wrongAccount.publicKey), false)\r
+\r
+ await assert.resolves(wallet.destroy())\r
+ })\r
+ })\r
+])\r