FireWallet/hsd-ledger

468 lines
14 KiB
JavaScript

#!/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 <wallet-id>');
console.log(' $ hsd-ledger createaccount <account-name> <account-index>');
console.log(' $ hsd-ledger createaddress');
console.log(' $ hsd-ledger sendtoaddress <address> <amount>');
console.log(' $ hsd-ledger getwallets');
console.log(' $ hsd-ledger getaccounts');
console.log(' $ hsd-ledger getaccount <account-name>');
console.log(' $ hsd-ledger getbalance');
console.log('');
console.log('options:');
console.log(' --help');
console.log(' --version');
console.log(' -n, --network <id> (default "main")');
console.log(' -w, --wallet-id <id> (default "hsd-ledger")');
console.log(' -a, --account-name <name> (default "default")');
console.log(' -i, --account-index <index> (default 0)');
console.log('');
console.log('The following options configure the node and wallet clients:');
console.log(' --ssl');
console.log(' --url <url>');
console.log(' --api-key <api-key>');
console.log(' --host <host> (default "localhost")');
console.log('');
console.log('The following options configure the node client only:');
console.log(' --node-ssl');
console.log(' --node-url <url>');
console.log(' --node-api-key <api-key>');
console.log(' --node-host <host> (default "localhost")');
console.log(' --node-port <port> (default 14037)');
console.log('');
console.log('The following options configure the wallet client only:');
console.log(' --wallet-ssl');
console.log(' --wallet-url <url>');
console.log(' --wallet-api-key <api-key>');
console.log(' --wallet-host <host> (default "localhost")');
console.log(' --wallet-port <port> (default 14039)');
console.log('');
}
/*
* Execute
*/
main().catch((err) => {
usage(err);
process.exit(1);
});