diff --git a/bin/bwallet-cli b/bin/bwallet-cli index 14edc92e3..ff37a96b4 100755 --- a/bin/bwallet-cli +++ b/bin/bwallet-cli @@ -328,7 +328,9 @@ class CLI { smart: this.config.bool('smart'), rate: this.config.ufixed('rate', 8), subtractFee: this.config.bool('subtract-fee'), - sign: this.config.bool('sign') + sign: this.config.bool('sign'), + template: this.config.bool('template'), + paths: this.config.bool('paths') }; const tx = await this.wallet.createTX(options); diff --git a/lib/coins/coinview.js b/lib/coins/coinview.js index ff1aea1da..ca8215338 100644 --- a/lib/coins/coinview.js +++ b/lib/coins/coinview.js @@ -294,6 +294,17 @@ class CoinView { return coins.getOutput(index); } + /** + * Get an HD path by prevout. + * Implemented in {@link WalletCoinView}. + * @param {Outpoint} prevout + * @returns {null} + */ + + getPath(prevout) { + return null; + } + /** * Get coins height by prevout. * @param {Outpoint} prevout @@ -374,6 +385,17 @@ class CoinView { return this.getOutput(input.prevout); } + /** + * Get a single path by input. + * Implemented in {@link WalletCoinView}. + * @param {Input} input + * @returns {null} + */ + + getPathFor(input) { + return null; + } + /** * Get coins height by input. * @param {Input} input diff --git a/lib/primitives/input.js b/lib/primitives/input.js index 3c7783c95..9fc7ec315 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -309,10 +309,11 @@ class Input { * of little-endian uint256s. * @param {Network} network * @param {Coin} coin + * @param {Path} path * @returns {Object} */ - getJSON(network, coin) { + getJSON(network, coin, path) { network = Network.get(network); let addr; @@ -328,7 +329,8 @@ class Input { witness: this.witness.toJSON(), sequence: this.sequence, address: addr, - coin: coin ? coin.getJSON(network, true) : undefined + coin: coin ? coin.getJSON(network, true) : undefined, + path: path ? path.getJSON(network) : undefined }; } diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 3fc58da0c..0db4429ee 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -16,6 +16,8 @@ const Output = require('./output'); const Coin = require('./coin'); const Outpoint = require('./outpoint'); const CoinView = require('../coins/coinview'); +const Path = require('../wallet/path'); +const WalletCoinView = require('../wallet/walletcoinview'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const Stack = require('../script/stack'); @@ -1405,6 +1407,17 @@ class MTX extends TX { coin.index = prevout.index; this.view.addCoin(coin); + + if (!input.path) + continue; + + if (!(this.view instanceof WalletCoinView)) + this.view = WalletCoinView.fromCoinView(this.view); + + const outpoint = Outpoint.fromJSON(prevout); + const path = Path.fromJSON(input.path); + + this.view.addPath(outpoint, path); } return this; diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index fe4c0b069..0ad830e36 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -2188,7 +2188,8 @@ class TX { version: this.version, inputs: this.inputs.map((input) => { const coin = view ? view.getCoinFor(input) : null; - return input.getJSON(network, coin); + const path = view ? view.getPathFor(input) : null; + return input.getJSON(network, coin, path); }), outputs: this.outputs.map((output) => { return output.getJSON(network); diff --git a/lib/wallet/http.js b/lib/wallet/http.js index b3ec7d89a..0fc37c181 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -446,11 +446,13 @@ class HTTP extends Server { selection: valid.str('selection'), smart: valid.bool('smart'), account: valid.str('account'), + locktime: valid.u64('locktime'), sort: valid.bool('sort'), subtractFee: valid.bool('subtractFee'), subtractIndex: valid.i32('subtractIndex'), depth: valid.u32(['confirmations', 'depth']), useSelectEstimate: valid.bool('useSelectEstimate'), + paths: valid.bool('paths'), outputs: [] }; @@ -494,12 +496,14 @@ class HTTP extends Server { selection: valid.str('selection'), smart: valid.bool('smart'), account: valid.str('account'), + locktime: valid.u64('locktime'), sort: valid.bool('sort'), subtractFee: valid.bool('subtractFee'), subtractIndex: valid.i32('subtractIndex'), depth: valid.u32(['confirmations', 'depth']), template: valid.bool('template', sign), useSelectEstimate: valid.bool('useSelectEstimate'), + paths: valid.bool('paths'), outputs: [] }; @@ -527,7 +531,11 @@ class HTTP extends Server { if (sign) await req.wallet.sign(tx, passphrase); - res.json(200, tx.getJSON(this.network)); + const json = tx.getJSON(this.network); + if (options.paths) + await this.addOutputPaths(json, tx, req.wallet); + + res.json(200, json); }); // Sign TX @@ -883,6 +891,22 @@ class HTTP extends Server { }); } + /** + * Add wallet path information to JSON outputs + * @private + */ + + async addOutputPaths(json, tx, wallet) { + for (let i = 0; i < tx.outputs.length; i++) { + const address = tx.outputs[i].getAddress(); + const path = await wallet.getPath(address); + if (!path) + continue; + json.outputs[i].path = path.getJSON(this.network); + } + return json; + } + /** * Initialize websockets. * @private diff --git a/lib/wallet/path.js b/lib/wallet/path.js index b566bbacf..d17b2f141 100644 --- a/lib/wallet/path.js +++ b/lib/wallet/path.js @@ -9,6 +9,7 @@ const assert = require('bsert'); const bio = require('bufio'); const Address = require('../primitives/address'); +const Network = require('../protocol/network'); const {encoding} = bio; const {inspectSymbol} = require('../utils'); @@ -258,14 +259,23 @@ class Path { /** * Convert path object to string derivation path. + * @param {String|Network?} network - Network type. * @returns {String} */ - toPath() { + toPath(network) { if (this.keyType !== Path.types.HD) return null; - return `m/${this.account}'/${this.branch}/${this.index}`; + let prefix = 'm'; + + if (network) { + const purpose = 44; + network = Network.get(network); + prefix += `/${purpose}'/${network.keyPrefix.coinType}'`; + } + + return `${prefix}/${this.account}'/${this.branch}/${this.index}`; } /** @@ -279,18 +289,72 @@ class Path { /** * Convert path to a json-friendly object. + * @param {String|Network?} network - Network type. * @returns {Object} */ - toJSON() { + getJSON(network) { return { name: this.name, account: this.account, change: this.branch === 1, - derivation: this.toPath() + derivation: this.toPath(network) }; } + /** + * Convert the path to an object suitable + * for JSON serialization. + * @returns {Object} + */ + + toJSON() { + return this.getJSON(); + } + + /** + * Inject properties from a json object. + * @param {Object} json + * @returns {Path} + */ + + static fromJSON(json) { + return new this().fromJSON(json); + } + + /** + * Inject properties from a json object. + * @param {Object} json + * @returns {Path} + */ + + fromJSON(json) { + assert(json && typeof json === 'object'); + assert(json.derivation && typeof json.derivation === 'string'); + + // Note: this object is mutated below. + const path = json.derivation.split('/'); + + // Note: "m/X'/X'/X'/X/X" or "m/X'/X/X". + assert (path.length === 4 || path.length === 6); + + const index = parseInt(path.pop(), 10); + const branch = parseInt(path.pop(), 10); + const account = parseInt(path.pop(), 10); + + assert(account === json.account); + assert(branch === 0 || branch === 1); + assert(Boolean(branch) === json.change); + assert((index >>> 0) === index); + + this.name = json.name; + this.account = account; + this.branch = branch; + this.index = index; + + return this; + } + /** * Inspect the path. * @returns {String} diff --git a/lib/wallet/paths.js b/lib/wallet/paths.js new file mode 100644 index 000000000..af1035ea0 --- /dev/null +++ b/lib/wallet/paths.js @@ -0,0 +1,93 @@ +/*! + * paths.js - paths object for hsd + * Copyright (c) 2019, Boyma Fahnbulleh (MIT License). + * https://github.com/handshake-org/hsd + */ + +'use strict'; + +const assert = require('bsert'); + +/** + * Paths + * Represents the HD paths for coins in a single transaction. + * @alias module:wallet.Paths + * @property {Map[]} outputs - Paths. + */ + +class Paths { + /** + * Create paths + * @constructor + */ + + constructor() { + this.paths = new Map(); + } + + /** + * Add a single entry to the collection. + * @param {Number} index + * @param {Path} path + * @returns {Path} + */ + + add(index, path) { + assert((index >>> 0) === index); + assert(path); + this.paths.set(index, path); + return path; + } + + /** + * Test whether the collection has a path. + * @param {Number} index + * @returns {Boolean} + */ + + has(index) { + return this.paths.has(index); + } + + /** + * Get a path. + * @param {Number} index + * @returns {Path|null} + */ + + get(index) { + return this.paths.get(index) || null; + } + + /** + * Remove a path and return it. + * @param {Number} index + * @returns {Path|null} + */ + + remove(index) { + const path = this.get(index); + + if (!path) + return null; + + this.paths.delete(index); + + return path; + } + + /** + * Test whether there are paths. + * @returns {Boolean} + */ + + isEmpty() { + return this.paths.size === 0; + } +} + +/* + * Expose + */ + +module.exports = Paths; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 72e171377..642e75f26 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -161,6 +161,7 @@ class RPC extends RPCBase { this.add('sendfrom', this.sendFrom); this.add('sendmany', this.sendMany); this.add('sendtoaddress', this.sendToAddress); + this.add('createsendtoaddress', this.createSendToAddress); this.add('setaccount', this.setAccount); this.add('settxfee', this.setTXFee); this.add('signmessage', this.signMessage); @@ -1478,14 +1479,49 @@ class RPC extends RPCBase { } async sendToAddress(args, help) { - if (help || args.length < 2 || args.length > 6) { - throw new RPCError(errs.MISC_ERROR, - 'sendtoaddress "bitcoinaddress" amount' - + ' ( "comment" "comment-to" subtractfeefromamount' - + ' useSelectEstimate )'); - } + const opts = this._validateSendToAddress(args, help, 'createsendtoaddress'); + const wallet = this.wallet; + + const options = { + account: opts.account, + subtractFee: opts.subtract, + outputs: [{ + address: opts.addr, + value: opts.value + }] + }; + const tx = await wallet.send(options); + + return tx.txid(); + } + + async createSendToAddress(args, help) { + const opts = this._validateSendToAddress(args, help, 'createsendtoaddress'); const wallet = this.wallet; + + const options = { + paths: true, + account: opts.account, + subtractFee: opts.subtract, + outputs: [{ + address: opts.addr, + value: opts.value + }] + }; + + const mtx = await wallet.createTX(options); + + return mtx.getJSON(this.network); + } + + _validateSendToAddress(args, help, method) { + const msg = `${method} "address" amount ` + + '( "comment" "comment-to" subtractfeefromamount useSelectEstimate )'; + + if (help || args.length < 2 || args.length > 6) + throw new RPCError(errs.MISC_ERROR, msg); + const valid = new Validator(args); const str = valid.str(0); const value = valid.ufixed(1, 8); @@ -1497,18 +1533,12 @@ class RPC extends RPCBase { if (!addr || value == null) throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); - const options = { - subtractFee: subtract, - outputs: [{ - address: addr, - value: value - }], + return { + subtract, + addr, + value, useSelectEstimate }; - - const tx = await wallet.send(options); - - return tx.txid(); } async setAccount(args, help) { diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 3703deaf9..f16b7ba56 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -21,6 +21,8 @@ const common = require('./common'); const Address = require('../primitives/address'); const MTX = require('../primitives/mtx'); const Script = require('../script/script'); +const CoinView = require('../coins/coinview'); +const WalletCoinView = require('./walletcoinview'); const WalletKey = require('./walletkey'); const HD = require('../hd/hd'); const Output = require('../primitives/output'); @@ -1220,6 +1222,10 @@ class Wallet extends EventEmitter { assert(mtx.verifyInputs(this.wdb.state.height + 1), 'TX failed context check.'); + // Set the HD paths. + if (options.paths === true) + mtx.view = await this.getWalletCoinView(mtx, mtx.view); + if (options.template === false) return mtx; @@ -1738,6 +1744,58 @@ class Wallet extends EventEmitter { return this.txdb.getCoinView(tx); } + /** + * Get a wallet coin viewpoint with HD paths. + * @param {TX} tx + * @param {CoinView?} view - Coins to be used in wallet coin viewpoint. + * @returns {Promise} - Returns {@link WalletCoinView}. + */ + + async getWalletCoinView(tx, view) { + if (!(view instanceof CoinView)) + view = new CoinView(); + + if (!tx.hasCoins(view)) + view = await this.txdb.getCoinView(tx); + + view = WalletCoinView.fromCoinView(view); + + for (const input of tx.inputs) { + const prevout = input.prevout; + const coin = view.getCoin(prevout); + + if (!coin) + continue; + + const path = await this.getPath(coin.getAddress()); + + if (!path) + continue; + + const account = await this.getAccount(path.account); + + if (!account) + continue; + + // The account index in the db may be wrong. + // We must read it from the stored xpub to be + // sure of its correctness. + // + // For more details see: + // https://github.com/bcoin-org/bcoin/issues/698. + path.account = account.accountKey.childIndex; + + // Unharden the account index, if necessary. + if (path.account & HD.common.HARDENED) + path.account ^= HD.common.HARDENED; + + // Add path to the viewpoint. + view.addPath(prevout, path); + } + + return view; + } + /** * Get a historical coin viewpoint. * @param {TX} tx diff --git a/lib/wallet/walletcoinview.js b/lib/wallet/walletcoinview.js new file mode 100644 index 000000000..711b97caf --- /dev/null +++ b/lib/wallet/walletcoinview.js @@ -0,0 +1,199 @@ +/*! + * walletcoinview.js - wallet coin viewpoint object for hsd + * Copyright (c) 2019, Boyma Fahnbulleh (MIT License). + * https://github.com/handshake-org/hsd + */ + +'use strict'; + +const assert = require('bsert'); +const {BufferMap} = require('buffer-map'); +const Paths = require('./paths'); +const CoinView = require('../coins/coinview'); + +/** + * Wallet Coin View + * Represents a wallet, coin viewpoint: a snapshot of {@link Coins} objects + * and the HD paths for their associated keys. + * @alias module:wallet.WalletCoinView + * @property {Object} map + * @property {Object} paths + * @property {UndoCoins} undo + */ + +class WalletCoinView extends CoinView { + /** + * Create a wallet coin view. + * @constructor + */ + + constructor() { + super(); + this.paths = new BufferMap(); + } + + /** + * Inject properties from coin view object. + * @private + * @param {CoinView} view + */ + + fromCoinView(view) { + assert(view instanceof CoinView, 'View must be instance of CoinView'); + this.map = view.map; + this.undo = view.undo; + this.bits = view.bits; + return this; + } + + /** + * Instantiate wallet coin view from coin view. + * @param {CoinView} view + * @returns {WalletCoinView} + */ + + static fromCoinView(view) { + return new this().fromCoinView(view); + } + + /** + * Add paths to the collection. + * @param {Hash} hash + * @param {Paths} path + * @returns {Paths|null} + */ + + addPaths(hash, paths) { + this.paths.set(hash, paths); + return paths; + } + + /** + * Get paths. + * @param {Hash} hash + * @returns {Paths} paths + */ + + getPaths(hash) { + return this.paths.get(hash); + } + + /** + * Test whether the view has a paths entry. + * @param {Hash} hash + * @returns {Boolean} + */ + + hasPaths(hash) { + return this.paths.has(hash); + } + + /** + * Ensure existence of paths object in the collection. + * @param {Hash} hash + * @returns {Coins} + */ + + ensurePaths(hash) { + const paths = this.paths.get(hash); + + if (paths) + return paths; + + return this.addPaths(hash, new Paths()); + } + + /** + * Remove paths from the collection. + * @param {Paths} paths + * @returns {Paths|null} + */ + + removePaths(hash) { + const paths = this.paths.get(hash); + + if (!paths) + return null; + + this.paths.delete(hash); + + return paths; + } + + /** + * Add an HD path to the collection. + * @param {Outpoint} prevout + * @param {Path} path + * @returns {Path|null} + */ + + addPath(prevout, path) { + const {hash, index} = prevout; + const paths = this.ensurePaths(hash); + return paths.add(index, path); + } + + /** + * Get an HD path by prevout. + * @param {Outpoint} prevout + * @returns {Path|null} + */ + + getPath(prevout) { + const {hash, index} = prevout; + const paths = this.getPaths(hash); + + if (!paths) + return null; + + return paths.get(index); + } + + /** + * Remove an HD path. + * @param {Outpoint} prevout + * @returns {Path|null} + */ + + removePath(prevout) { + const {hash, index} = prevout; + const paths = this.getPaths(hash); + + if (!paths) + return null; + + return paths.remove(index); + } + + /** + * Test whether the view has a path by prevout. + * @param {Outpoint} prevout + * @returns {Boolean} + */ + + hasPath(prevout) { + const {hash, index} = prevout; + const paths = this.getPaths(hash); + + if (!paths) + return false; + + return paths.has(index); + } + + /** + * Get a single path by input. + * @param {Input} input + * @returns {Path|null} + */ + + getPathFor(input) { + return this.getPath(input.prevout); + } +} + +/* + * Expose + */ + +module.exports = WalletCoinView; diff --git a/test/data/mtx1.json b/test/data/mtx1.json new file mode 100644 index 000000000..364151202 --- /dev/null +++ b/test/data/mtx1.json @@ -0,0 +1,47 @@ +{ + "hash": "8396bd726e9834eeb1f448c55782ddbc6f707fb24da2b69b0ee1aba226fecde0", + "witnessHash": "3e16e5feeade314b77738abfd542dcbf6e411b3801c776b82dbb97cf2eba55cb", + "fee": 2860, + "rate": 20283, + "mtime": 1674747979, + "version": 1, + "inputs": [ + { + "prevout": { + "hash": "89a69a1055316c596fb420a5b2684b6f1263df14181479bd23d8c482a511eda3", + "index": 0 + }, + "script": "", + "witness": "024730440220523b1eae328e1ec4c13c782925dec548dfea48b157d30420e97fbad0201e63bf0220657222e89940116275ec6c284a761ebcb39c40cee7dca409eea79d452deb9cb00121029b38fd79f15cda4951135358c19068f8a1dce07fba0f8822b68944db93b38dad", + "sequence": 4294967295, + "coin": { + "version": 1, + "height": 1, + "value": 5000000000, + "script": "00144a0ba3c55883c763de299d1bc5db501544a89bdb", + "address": "bcrt1qfg96832cs0rk8h3fn5dutk6sz4z23x7m8jn2j9", + "coinbase": true + }, + "path": { + "name": "default", + "account": 0, + "change": false, + "derivation": "m/44'/1'/0'/0/0" + } + } + ], + "outputs": [ + { + "value": 100000000, + "script": "0014fe49ed2fc7e3bd0189e2df475c97c00f3adf1704", + "address": "bcrt1qley76t78uw7srz0zmar4e97qpuad79cywdem2k" + }, + { + "value": 4899997140, + "script": "0014969e2f20c15f94dce953bab06a0afc37bf8e9b3c", + "address": "bcrt1qj60z7gxpt72de62nh2cx5zhux7lcaxeujkegks" + } + ], + "locktime": 0, + "hex": "01000000000101a3ed11a582c4d823bd79141814df63126f4b68b2a520b46f596c3155109aa6890000000000ffffffff0200e1f50500000000160014fe49ed2fc7e3bd0189e2df475c97c00f3adf1704d405102401000000160014969e2f20c15f94dce953bab06a0afc37bf8e9b3c024730440220523b1eae328e1ec4c13c782925dec548dfea48b157d30420e97fbad0201e63bf0220657222e89940116275ec6c284a761ebcb39c40cee7dca409eea79d452deb9cb00121029b38fd79f15cda4951135358c19068f8a1dce07fba0f8822b68944db93b38dad00000000" +} \ No newline at end of file diff --git a/test/data/mtx2.json b/test/data/mtx2.json new file mode 100644 index 000000000..315ae6aa8 --- /dev/null +++ b/test/data/mtx2.json @@ -0,0 +1,41 @@ +{ + "hash": "8396bd726e9834eeb1f448c55782ddbc6f707fb24da2b69b0ee1aba226fecde0", + "witnessHash": "3e16e5feeade314b77738abfd542dcbf6e411b3801c776b82dbb97cf2eba55cb", + "fee": 2860, + "rate": 20283, + "mtime": 1674747979, + "version": 1, + "inputs": [ + { + "prevout": { + "hash": "89a69a1055316c596fb420a5b2684b6f1263df14181479bd23d8c482a511eda3", + "index": 0 + }, + "script": "", + "witness": "024730440220523b1eae328e1ec4c13c782925dec548dfea48b157d30420e97fbad0201e63bf0220657222e89940116275ec6c284a761ebcb39c40cee7dca409eea79d452deb9cb00121029b38fd79f15cda4951135358c19068f8a1dce07fba0f8822b68944db93b38dad", + "sequence": 4294967295, + "coin": { + "version": 1, + "height": 1, + "value": 5000000000, + "script": "00144a0ba3c55883c763de299d1bc5db501544a89bdb", + "address": "bcrt1qfg96832cs0rk8h3fn5dutk6sz4z23x7m8jn2j9", + "coinbase": true + } + } + ], + "outputs": [ + { + "value": 100000000, + "script": "0014fe49ed2fc7e3bd0189e2df475c97c00f3adf1704", + "address": "bcrt1qley76t78uw7srz0zmar4e97qpuad79cywdem2k" + }, + { + "value": 4899997140, + "script": "0014969e2f20c15f94dce953bab06a0afc37bf8e9b3c", + "address": "bcrt1qj60z7gxpt72de62nh2cx5zhux7lcaxeujkegks" + } + ], + "locktime": 0, + "hex": "01000000000101a3ed11a582c4d823bd79141814df63126f4b68b2a520b46f596c3155109aa6890000000000ffffffff0200e1f50500000000160014fe49ed2fc7e3bd0189e2df475c97c00f3adf1704d405102401000000160014969e2f20c15f94dce953bab06a0afc37bf8e9b3c024730440220523b1eae328e1ec4c13c782925dec548dfea48b157d30420e97fbad0201e63bf0220657222e89940116275ec6c284a761ebcb39c40cee7dca409eea79d452deb9cb00121029b38fd79f15cda4951135358c19068f8a1dce07fba0f8822b68944db93b38dad00000000" +} \ No newline at end of file diff --git a/test/input-test.js b/test/input-test.js index 7be7d9b04..18f1a88cd 100644 --- a/test/input-test.js +++ b/test/input-test.js @@ -8,6 +8,12 @@ const util = require('../lib/utils/util'); const Input = require('../lib/primitives/input'); const assert = require('bsert'); const common = require('./util/common'); +const MTX = require('../lib/primitives/mtx'); + +const mtx1json = require('./data/mtx1.json'); +const mtx2json = require('./data/mtx2.json'); +const mtx1 = MTX.fromJSON(mtx1json); +const mtx2 = MTX.fromJSON(mtx2json); // Take input rawbytes from the raw data format // p2pkh @@ -25,6 +31,37 @@ const input3 = tx3.getRaw().slice(5, 266); // test files: https://github.com/bitcoinjs/bip69/blob/master/test/fixtures.json const bip69tests = require('./data/bip69/bip69.json'); +describe('MTX', function() { + it('should serialize path', () => { + const input = mtx1.inputs[0]; + const view = mtx1.view; + const coin = view.getCoinFor(input); + const path = view.getPathFor(input); + const json = input.getJSON('regtest', coin, path); + const got = json.path; + const want = { + name: 'default', + account: 0, + change: false, + derivation: 'm/44\'/1\'/0\'/0/0' + }; + + assert.deepStrictEqual(got, want); + }); + + it('should not serialize path', () => { + const input = mtx2.inputs[0]; + const view = mtx2.view; + const coin = view.getCoinFor(input); + const path = view.getPathFor(input); + const json = input.getJSON('regtest', coin, path); + const got = json.path; + const want = undefined; + + assert.deepStrictEqual(got, want); + }); +}); + describe('Input', function() { it('should return same raw', () => { [input1, input2, input3].forEach((rawinput) => { diff --git a/test/mtx-test.js b/test/mtx-test.js index 7b885012c..352ff3cb2 100644 --- a/test/mtx-test.js +++ b/test/mtx-test.js @@ -4,12 +4,65 @@ 'use strict'; const assert = require('bsert'); +const CoinView = require('../lib/coins/coinview'); +const WalletCoinView = require('../lib/wallet/walletcoinview'); const MTX = require('../lib/primitives/mtx'); +const Path = require('../lib/wallet/path'); const Address = require('../lib/primitives/address'); const Input = require('../lib/primitives/input'); const Output = require('../lib/primitives/output'); +const mtx1json = require('./data/mtx1.json'); +const mtx2json = require('./data/mtx2.json'); +const mtx1 = MTX.fromJSON(mtx1json); +const mtx2 = MTX.fromJSON(mtx2json); + describe('MTX', function () { + it('should serialize wallet coin view', () => { + const json = mtx1.getJSON('regtest'); + const got = json.inputs[0].path; + const want = { + name: 'default', + account: 0, + change: false, + derivation: 'm/44\'/1\'/0\'/0/0' + }; + + assert.deepStrictEqual(got, want); + }); + + it('should deserialize wallet coin view', () => { + const view = mtx1.view; + const input = mtx1.inputs[0]; + const got = view.getPathFor(input); + const want = new Path(); + want.name = 'default'; + want.account = 0; + want.branch = 0; + want.index = 0; + + assert.ok(view instanceof WalletCoinView); + assert.deepStrictEqual(got, want); + }); + + it('should serialize coin view', () => { + const json = mtx2.getJSON('regtest'); + const got = json.inputs[0].path; + const want = undefined; + + assert.deepStrictEqual(got, want); + }); + + it('should deserialize coin view', () => { + const view = mtx2.view; + const input = mtx2.inputs[0]; + const got = view.getPathFor(input); + const want = null; + + assert.ok(view instanceof CoinView); + assert.deepStrictEqual(got, want); + }); + it('should en/decode mtx with 1 in, 1 out', () => { const input = new Input({ prevout: { diff --git a/test/path-test.js b/test/path-test.js new file mode 100644 index 000000000..3eedf8e7c --- /dev/null +++ b/test/path-test.js @@ -0,0 +1,67 @@ +'use strict'; + +const assert = require('bsert'); +const MTX = require('../lib/primitives/mtx'); +const Path = require('../lib/wallet/path'); + +const mtx1json = require('./data/mtx1.json'); +const mtx1 = MTX.fromJSON(mtx1json); + +describe('MTX', function() { + it('should serialize path', () => { + const input = mtx1.inputs[0]; + const view = mtx1.view; + const path = view.getPathFor(input); + + { + const got = path.getJSON(); + const want = { + name: 'default', + account: 0, + change: false, + derivation: 'm/0\'/0/0' + }; + + assert.deepStrictEqual(got, want); + } + + { + const got = path.getJSON('regtest'); + const want = { + name: 'default', + account: 0, + change: false, + derivation: 'm/44\'/1\'/0\'/0/0' + }; + + assert.deepStrictEqual(got, want); + } + }); + + it('should deserialize path', () => { + const path1 = Path.fromJSON({ + name: 'default', + account: 0, + change: true, + derivation: 'm/0\'/1/1' + }); + + const path2 = new Path().fromJSON({ + name: 'default', + account: 0, + change: true, + derivation: 'm/44\'/1\'/0\'/1/1' + }); + + assert.deepStrictEqual(path1, path2); + + const got = path1; + const want = new Path(); + want.name = 'default'; + want.account = 0; + want.branch = 1; + want.index = 1; + + assert.deepStrictEqual(got, want); + }); +}); diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index b71742c4c..a51fd4365 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -268,6 +268,88 @@ for (const witnessOpt of witnessOptions) { assert.strictEqual(info.n, 2); }); + it('should create a transaction', async () => { + const tx = await wallet.createTX({ + outputs: [{ + address: 'bcrt1qfg96832cs0rk8h3fn5dutk6sz4z23x7m8jn2j9', + value: 1e4 + }] + }); + + assert.ok(tx); + assert.equal(tx.outputs.length, 1 + 1); // send + change + assert.equal(tx.locktime, 0); + }); + + it('should create self-send transaction with HD paths', async () => { + const {address} = await wallet.createAddress('default'); + const tx = await wallet.createTX({ + paths: true, + outputs: [{ + address, + value: 1e4 + }] + }); + + assert.ok(tx); + assert.ok(tx.inputs); + + for (let i = 0; i < tx.inputs.length; i++) { + const path = tx.inputs[i].path; + + assert.ok(typeof path.name === 'string'); + assert.ok(typeof path.account === 'number'); + assert.ok(typeof path.change === 'boolean'); + assert.ok(typeof path.derivation === 'string'); + } + + // Self send, so all output paths including change should be known + for (let i = 0; i < tx.outputs.length; i++) { + const path = tx.outputs[i].path; + + assert.ok(typeof path.name === 'string'); + assert.ok(typeof path.account === 'number'); + assert.ok(typeof path.change === 'boolean'); + assert.ok(typeof path.derivation === 'string'); + } + }); + + it('should create a transaction with HD paths', async () => { + const tx = await wallet.createTX({ + paths: true, + outputs: [{ + address: 'bcrt1qfg96832cs0rk8h3fn5dutk6sz4z23x7m8jn2j9', + value: 1e4 + }] + }); + + assert.ok(tx); + assert.ok(tx.inputs); + + for (let i = 0; i < tx.inputs.length; i++) { + const path = tx.inputs[i].path; + + assert.ok(typeof path.name === 'string'); + assert.ok(typeof path.account === 'number'); + assert.ok(typeof path.change === 'boolean'); + assert.ok(typeof path.derivation === 'string'); + } + }); + + it('should create a transaction with a locktime', async () => { + const locktime = 100000; + + const tx = await wallet.createTX({ + locktime: locktime, + outputs: [{ + address: 'bcrt1qfg96832cs0rk8h3fn5dutk6sz4z23x7m8jn2j9', + value: 1e4 + }] + }); + + assert.equal(tx.locktime, locktime); + }); + for (const template of [true, false]) { const suffix = template ? 'with template' : 'without template'; it(`should create and sign transaction ${suffix}`, async () => {