#!/usr/bin/env node 'use strict'; /** * Module imports */ const Config = require('bcfg'); const { NodeClient, WalletClient } = require('hs-client'); const { hd, MTX, Network } = require('hsd'); const { Rules } = require('hsd/lib/covenants'); const { hashName, types } = Rules; const { util, HID, LedgerHSD, LedgerChange, LedgerCovenant } = require('..'); const { Device } = HID; /** * Global constants */ const VALID_CMDS = [ 'createwallet', 'createaccount', 'createaddress', 'sendtoaddress', 'getwallets', 'getaccounts', 'getaccount', 'getbalance', 'sendraw' ]; const VERSION = require('../package').version; async function createWallet(client, config, ledger, args) { if (args.length !== 1) throw new Error('Invalid arguments'); const id = args[0]; const index = config.uint('account-index'); const xpub = await ledger.getAccountXPUB(index); const wallet = await client.createWallet(id, { watchOnly: true, accountKey: xpub.xpubkey(config.str('network')) }); console.log(`Created wallet (id=hsd-ledger, wid=${wallet.wid}).`); console.log(`Created account (name=default, account-index=${index}').`); } async function createAccount(client, config, ledger, args) { if (args.length !== 2) throw new Error('Invalid arguments'); const id = config.str('wallet-id'); const name = args[0]; const index = parseInt(args[1], 10); const xpub = await ledger.getAccountXPUB(index); await client.createAccount(id, name, { watchOnly: true, accountKey: xpub.xpubkey(config.str('network')) }); console.log(`Created account (name=${name}, account-index=${index}').`); } async function createAddress(client, config, ledger, args) { if (args.length !== 0) throw new Error('Invalid arguments'); const id = config.str('wallet-id'); const name = config.str('account-name'); const addr = await client.createAddress(id, name); const acct = await client.getAccount(id, name); const xpub = hd.PublicKey.fromBase58(acct.accountKey, config.str('network')); const account = (xpub.childIndex ^ hd.common.HARDENED) >>> 0; console.log(`Verify address on Ledger device: ${addr.address}`); await ledger.getAddress(account, addr.branch, addr.index, { confirm: true }); } async function sendToAddress(wclient, nclient, config, ledger, args) { if (args.length !== 2) throw new Error('Invalid arguments'); const network = Network.get(config.str('network')); const id = config.str('wallet-id'); const acct = config.str('account-name'); const address = args[0]; const amount = parseFloat(args[1]); await wclient.execute('selectwallet', [id]); const params = [address, amount, '', '', false, acct]; const json = await wclient.execute('createsendtoaddress', params); const mtx = MTX.fromJSON(json); let i, key; for (i = mtx.outputs.length - 1; i >= 0; i--) { const output = mtx.outputs[i]; const addr = output.address.toString(network.type); key = await wclient.getKey(id, addr); if (key && key.branch) break; } if (!key || !key.branch) throw new Error('Expected change address.'); const { account, branch, index } = key; const coinType = network.keyPrefix.coinType; const options = { change: new LedgerChange({ path: `m/44'/${coinType}'/${account}'/${branch}/${index}`, index: i, version: 0 }) }; util.displayDetails(console, network, mtx, options); const signed = await ledger.signTransaction(mtx, options); const rawtx = signed.encode().toString('hex'); const txid = await nclient.execute('sendrawtransaction', [rawtx]); console.log(`Submitted TXID: ${txid}`); } async function sendRaw(wclient, nclient, config, ledger, args) { // Create a function to sign raw transactions if (args.length !== 2) // Make sure there are two arguments (batch, names) throw new Error('Invalid arguments'); // Throw an error if there are not two arguments const network = Network.get(config.str('network')); // Get the network const id = config.str('wallet-id'); // Get the wallet id const acct = config.str('account-name'); // Get the account name // Log the arguments to the console (for debugging) const batch = JSON.parse(args[0]); // Get the batch const names = JSON.parse(args[1]); // Get the names await wclient.execute('selectwallet', [id]); // Select the wallet try { const mtx = MTX.fromJSON(batch.result); // Create a new MTX from the JSON const hashes = {}; // Create an empty object to store the hashes for (const name of names) { // Loop through the names const hash = hashName(name); // Hash the name hashes[hash] = name; // Add the hash to the hashes object to use later } let i, key; // Create variables to use later const options = []; // Create an empty array to store the options for (i = mtx.outputs.length - 1; i >= 0; i--) { // Loop through the outputs const output = mtx.outputs[i]; // Get the output const addr = output.address.toString(network.type); // Get the address key = await wclient.getKey(id, addr); // Get the key if (!key) // If there is no key continue; // Continue to the next output if (key.branch === 1) { // If the key is a change address if (options.change) // If there is already a change address throw new Error('Transaction should only have one change output.'); // Throw an error const path = `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`; // Create the derivation path options.change = new LedgerChange({ path: `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`, index: i, version: 0 }); // Add the change address to the options } const { account, branch, index } = key; // Get the account, branch, and index from the key const coinType = network.keyPrefix.coinType; // Get the coin type from the network switch (output.covenant.type) { case types.NONE: case types.OPEN: case types.BID: case types.FINALIZE: break; case types.REVEAL: case types.REDEEM: case types.REGISTER: case types.UPDATE: case types.RENEW: case types.TRANSFER: case types.REVOKE: { // If the covenant type is any of REVEAL, REDEEM, REGISTER, UPDATE, RENEW, TRANSFER, or REVOKE if (options.covenants == null) // If there are no covenants options.covenants = []; // Create an empty array for the covenants const hash = output.covenant.items[0]; // Get the hash from the covenant const name = hashes[hash]; // Get the name from the hashes object (is needed for SPV nodes) if (name == undefined) { // If the name is not found console.log("Name not found in file"); // Log that the name was not found console.log(hash); // Log the hash (for debugging) } options.covenants.push(new LedgerCovenant({ index: i, name })); // Add the covenant to the options break; } default: throw new Error('Unrecognized covenant type.'); } } // end for loop util.displayDetails(console, network, mtx, options); // Display the details to the log for user verification const signed = await ledger.signTransaction(mtx, options); // Sign the transaction with the ledger const rawtx = signed.encode().toString('hex'); // Encode the transaction as hex const txid = await nclient.execute('sendrawtransaction', [rawtx]); // Send the transaction to the network console.log(`Submitted TXID: ${txid}`); // Log the TXID to the console to view the transaction on a block explorer } catch (err) { // Catch any errors console.error(err); // Log the error to the console } } async function getWallets(client, args) { if (args.length) throw new Error('Too many arguments'); const wallets = await client.getWallets(); console.log(wallets); } async function getAccounts(client, config, args) { if (args.length) throw new Error('Too many arguments'); const id = config.str('wallet-id'); const accounts = await client.getAccounts(id); console.log(accounts); } async function getAccount(client, config, args) { if (args.length !== 1) throw new Error('Invalid arguments'); const id = config.str('wallet-id'); const name = args[0]; const account = await client.getAccount(id, name); console.log(account); } async function getBalance(client, config, args) { if (args.length !== 0) throw new Error('Invalid arguments'); const id = config.str('wallet-id'); const name = config.str('account-name'); const balance = await client.getBalance(id, name); console.log(balance); } async function main() { const device = await Device.requestDevice(); device.set({ timeout: 300000 // 5 minutes. }); await device.open(); const config = new Config('hsd', { suffix: 'network', fallback: 'main', alias: { 'h': 'help', 'i': 'accountindex', 'a': 'accountname', 'n': 'network', 'p': 'projectsponsor', 'q': 'qrcode', 'k': 'showkey', 'w': 'walletid' } }); config.load({ argv: true, env: true }); config.inject({ accountIndex: 0, accountName: 'default', network: 'main', walletId: 'hsd-ledger', token: '' }); const argv = config.argv; const cmd = argv.shift(); const args = argv; const type = config.str('network'); const network = Network.get(type); const id = config.str('wallet-id'); const token = config.str('token'); if (config.str('help') && argv.length === 0) { usage(); process.exit(0); } if (config.str('version') && argv.length === 0) { version(); process.exit(0); } const ledger = new LedgerHSD({ device: device, network: type }); const nclient = new NodeClient({ url: config.str('url') || config.str('node-url'), apiKey: config.str('api-key') || config.str('node-api-key'), ssl: config.bool('ssl') || config.str('node-ssl'), host: config.str('http-host') || config.str('node-http-host'), port: config.uint('node-http-port') || network.rpcPort }); const wclient = new WalletClient({ url: config.str('url') || config.str('wallet-url'), apiKey: config.str('api-key') || config.str('wallet-api-key'), ssl: config.bool('ssl') || config.bool('wallet-ssl'), host: config.str('http-host') || config.str('wallet-http-host'), port: config.uint('wallet-http-port') || network.walletPort, token }); await nclient.open(); await wclient.open(); try { const wallets = await wclient.getWallets(); if (!wallets.includes(id)) { if (id !== 'hsd-ledger') throw new Error(`Wallet "${id}" does not exist.`); console.log('Default hsd-ledger wallet not detected.'); await createWallet(wclient, config, ledger, [id]); } switch (cmd) { case VALID_CMDS[0]: await createWallet(wclient, config, ledger, args); break; case VALID_CMDS[1]: await createAccount(wclient, config, ledger, args); break; case VALID_CMDS[2]: await createAddress(wclient, config, ledger, args); break; case VALID_CMDS[3]: await sendToAddress(wclient, nclient, config, ledger, args); break; case VALID_CMDS[4]: await getWallets(wclient, args); break; case VALID_CMDS[5]: await getAccounts(wclient, config, args); break; case VALID_CMDS[6]: await getAccount(wclient, config, args); break; case VALID_CMDS[7]: await getBalance(wclient, config, args); break; case VALID_CMDS[8]: await sendRaw(wclient, nclient, config, ledger, args); break; default: usage(new Error('Must provide valid command.')); process.exit(1); break; } } catch (e) { throw (e); } finally { await wclient.close(); await nclient.close(); await device.close(); } process.exit(0); } /** * Displays application version. */ function version() { console.log(`hsd-ledger v${VERSION}`); console.log(''); } /** * Displays usage or error message. * @param {String|Error} err - the error message or object */ function usage(err) { if (err) { console.error(`${err.stack}`); console.error(''); return; } console.log('usage:'); console.log(' $ hsd-ledger createwallet '); console.log(' $ hsd-ledger createaccount '); console.log(' $ hsd-ledger createaddress'); console.log(' $ hsd-ledger sendtoaddress
'); console.log(' $ hsd-ledger sendraw '); console.log(' $ hsd-ledger getwallets'); console.log(' $ hsd-ledger getaccounts'); console.log(' $ hsd-ledger getaccount '); console.log(' $ hsd-ledger getbalance'); console.log(''); console.log('options:'); console.log(' --help'); console.log(' --version'); console.log(' -n, --network (default "main")'); console.log(' -w, --wallet-id (default "hsd-ledger")'); console.log(' -a, --account-name (default "default")'); console.log(' -i, --account-index (default 0)'); console.log(''); console.log('The following options configure the node and wallet clients:'); console.log(' --ssl'); console.log(' --url '); console.log(' --api-key '); console.log(' --host (default "localhost")'); console.log(''); console.log('The following options configure the node client only:'); console.log(' --node-ssl'); console.log(' --node-url '); console.log(' --node-api-key '); console.log(' --node-host (default "localhost")'); console.log(' --node-port (default 14037)'); console.log(''); console.log('The following options configure the wallet client only:'); console.log(' --wallet-ssl'); console.log(' --wallet-url '); console.log(' --wallet-api-key '); console.log(' --wallet-host (default "localhost")'); console.log(' --wallet-port (default 14039)'); console.log(''); } /* * Execute */ main().catch((err) => { usage(err); process.exit(1); });