From 2ad933318c229010f902604039be568a1e0a0288 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Fri, 9 Jun 2023 14:02:44 +1000 Subject: [PATCH] hsd-ledger: Added node script for ledger wallets --- hsd-ledger | 473 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 hsd-ledger diff --git a/hsd-ledger b/hsd-ledger new file mode 100644 index 0000000..678f6f3 --- /dev/null +++ b/hsd-ledger @@ -0,0 +1,473 @@ +#!/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 + const batch = args[0]; // Get the batch file location + const nameslocation = args[1]; // Get the names file location + + await wclient.execute('selectwallet', [id]); // Select the wallet + + + const fs = require('fs'); // Import fs (used to read files) + try { + const data = fs.readFileSync(batch, 'utf8'); // Read the batch file + const json = JSON.parse(data); // Parse the batch file as JSON + const mtx = MTX.fromJSON(json.result); // Create a new MTX from the JSON + + const namefile = fs.readFileSync(nameslocation, 'utf8'); // Read the names file + const names = namefile.split(','); // Split the names file into an array + 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 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); +});