├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── fixtures.json └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | before_install: 4 | - "npm install npm -g" 5 | node_js: 6 | - "0.12" 7 | - "iojs" 8 | - "4" 9 | - "5" 10 | - "6" 11 | env: 12 | - TEST_SUITE=standard 13 | - TEST_SUITE=unit 14 | script: "npm run-script $TEST_SUITE" 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.0 2 | * adds `buildTransaction`, WARNING: different API, maxFee is the maximum fee for transaction sanitization 3 | * removes `createTransaction` 4 | * bumps `async` to `2.0.0` 5 | * bumps `bitcoinjs` to `^2.3.0` 6 | 7 | # 0.6.1 8 | * bumps `async` to `1.5.0` 9 | * bumps `bitcoinjs` to `^2.2.0` 10 | 11 | # 0.6.0 12 | * adds optional `nLockTime` parameter to `createTransaction` 13 | 14 | # 0.5.0 15 | Warning, `createTransaction` may be removed in a future version, with this package being merged with `bip32-utils` to form purely BIP32 key management. 16 | 17 | The functionality of transaction formulation that is currently found in `createTransaction` (and the `coinSelect` package) will likely be moved to a new module. 18 | 19 | * removes `getUnspentOutputs`, `setUnspentOutputs` and `signWith` 20 | * changed `createTransaction` parameters to `inputs, outputs, wantedFee, external, internal` 21 | 22 | # 0.4.1 23 | * removes `.addresses` from chain JSON, was deterministic 24 | 25 | # 0.4.0 26 | * removes `Wallet.getConfirmedBalance`, `confirmations` no longer used in the API, you must now filter unspents yourself prior to use 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Cousens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bip32-wallet 2 | 3 | [![TRAVIS](https://secure.travis-ci.org/bitcoinjs/bip32-wallet.png)](http://travis-ci.org/bitcoinjs/bip32-wallet) 4 | [![NPM](https://img.shields.io/npm/v/bip32-wallet.svg)](https://www.npmjs.org/package/bip32-wallet) 5 | 6 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 7 | 8 | A BIP32 Wallet backed by bitcoinjs-lib, lite on features but heavily tested. 9 | 10 | # DEPRECATED - Use [BIP32-utils](https://github.com/bitcoinjs/bip32-utils) instead 11 | 12 | ## Derivation 13 | 14 | Derivation path of `m/0'/i/k`, where `i` is the account (external or internal) and `k` is the leaf node (for addresses). 15 | 16 | ![structure](https://github.com/bitcoin/bips/raw/master/bip-0032/derivation.png) 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var bip32utils = require('bip32-utils') 3 | var bip69 = require('bip69') 4 | var bitcoin = require('bitcoinjs-lib') 5 | 6 | var NETWORKS = bitcoin.networks 7 | 8 | function Wallet (external, internal) { 9 | var chains 10 | if (Array.isArray(external)) { 11 | chains = external 12 | this.external = chains[0].getParent() 13 | this.internal = chains[1].getParent() 14 | } else { 15 | chains = [new bip32utils.Chain(external), new bip32utils.Chain(internal)] 16 | 17 | this.external = external 18 | this.internal = internal 19 | } 20 | 21 | this.account = new bip32utils.Account(chains) 22 | } 23 | 24 | Wallet.fromJSON = function (json, network) { 25 | function toChain (cjson) { 26 | var node = bitcoin.HDNode.fromBase58(cjson.node, network) 27 | var chain = new bip32utils.Chain(node, cjson.k) 28 | chain.map = cjson.map 29 | chain.addresses = Object.keys(chain.map).sort(function (a, b) { 30 | return chain.map[a] - chain.map[b] 31 | }) 32 | 33 | return chain 34 | } 35 | 36 | var chains 37 | if (json.chains) { 38 | chains = json.chains.map(toChain) 39 | } else if (json.external) { 40 | chains = [toChain(json.external), toChain(json.internal)] 41 | } 42 | 43 | return new Wallet(chains) 44 | } 45 | 46 | Wallet.fromSeedBuffer = function (seed, network) { 47 | network = network || NETWORKS.bitcoin 48 | 49 | // HD first-level child derivation method should be hardened 50 | // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 51 | var m = bitcoin.HDNode.fromSeedBuffer(seed, network) 52 | var i = m.deriveHardened(0) 53 | var external = i.derive(0) 54 | var internal = i.derive(1) 55 | 56 | return new Wallet(external, internal) 57 | } 58 | 59 | Wallet.fromSeedHex = function (hex, network) { 60 | return Wallet.fromSeedBuffer(new Buffer(hex, 'hex'), network) 61 | } 62 | 63 | Wallet.prototype.buildTransaction = function (inputs, outputs, feeMax, external, internal, nLockTime) { 64 | if (!isFinite(feeMax)) throw new TypeError('Expected finite maximum fee') 65 | if (feeMax > 0.2 * 1e8) throw new Error('Maximum fee is absurd: ' + feeMax) 66 | 67 | external = external || this.external 68 | internal = internal || this.internal 69 | var network = this.getNetwork() 70 | 71 | // sanity checks 72 | var inputValue = inputs.reduce(function (a, x) { return a + x.value }, 0) 73 | var outputValue = outputs.reduce(function (a, x) { return a + x.value }, 0) 74 | if (outputValue > inputValue) throw new Error('Not enough funds: ' + inputValue + ' < ' + outputValue) 75 | 76 | // clone the internal chain to avoid inadvertently moving the wallet forward before usage 77 | var chain = this.account.getChain(1).clone() 78 | 79 | // map outputs to be BIP69 compatible 80 | // add missing change outputs 81 | outputs = outputs.map(function (output) { 82 | var script = output.script 83 | if (!script && output.address) { 84 | script = bitcoin.address.toOutputScript(output.address, network) 85 | } 86 | 87 | if (!script) { 88 | script = bitcoin.address.toOutputScript(chain.get(), network) 89 | chain.next() 90 | } 91 | 92 | return { 93 | script: script, 94 | value: output.value 95 | } 96 | }) 97 | 98 | var fee = inputValue - outputValue 99 | if (fee > feeMax) throw new Error('Fee is too high: ' + feeMax) 100 | 101 | // apply BIP69 for improved privacy 102 | inputs = bip69.sortInputs(inputs.concat()) 103 | outputs = bip69.sortOutputs(outputs) 104 | 105 | // get associated private keys 106 | var addresses = inputs.map(function (input) { return input.address }) 107 | var children = this.account.getChildren(addresses, [external, internal]) 108 | 109 | // build transaction 110 | var txb = new bitcoin.TransactionBuilder(network) 111 | 112 | if (nLockTime !== undefined) { 113 | txb.setLockTime(nLockTime) 114 | } 115 | 116 | inputs.forEach(function (input) { 117 | txb.addInput(input.txId, input.vout, input.sequence, input.prevOutScript) 118 | }) 119 | 120 | outputs.forEach(function (output) { 121 | txb.addOutput(output.script, output.value) 122 | }) 123 | 124 | // sign and return 125 | children.forEach(function (child, i) { 126 | txb.sign(i, child.keyPair) 127 | }) 128 | 129 | return { 130 | fee: fee, 131 | transaction: txb.build() 132 | } 133 | } 134 | 135 | Wallet.prototype.containsAddress = function (address) { return this.account.containsAddress(address) } 136 | Wallet.prototype.discover = function (gapLimit, queryCallback, done) { 137 | function discoverChain (chain, callback) { 138 | bip32utils.discovery(chain, gapLimit, queryCallback, function (err, used, checked) { 139 | if (err) return callback(err) 140 | 141 | // throw away ALL unused addresses AFTER the last unused address 142 | var unused = checked - used 143 | for (var i = 1; i < unused; ++i) chain.pop() 144 | 145 | callback() 146 | }) 147 | } 148 | 149 | async.each(this.account.getChains(), discoverChain, done) 150 | } 151 | Wallet.prototype.getAllAddresses = function () { return this.account.getAllAddresses() } 152 | Wallet.prototype.getNetwork = function () { return this.account.getNetwork() } 153 | Wallet.prototype.getReceiveAddress = function () { return this.account.getChainAddress(0) } 154 | Wallet.prototype.getChangeAddress = function () { return this.account.getChainAddress(1) } 155 | Wallet.prototype.isReceiveAddress = function (address) { return this.account.isChainAddress(0, address) } 156 | Wallet.prototype.isChangeAddress = function (address) { return this.account.isChainAddress(1, address) } 157 | Wallet.prototype.nextReceiveAddress = function () { return this.account.nextChainAddress(0) } 158 | Wallet.prototype.nextChangeAddress = function () { return this.account.nextChainAddress(1) } 159 | 160 | Wallet.prototype.toJSON = function () { 161 | var chains = this.account.chains.map(function (chain) { 162 | return { 163 | k: chain.k, 164 | map: chain.map, 165 | node: chain.getParent().toBase58() 166 | } 167 | }) 168 | 169 | return { 170 | external: chains[0], 171 | internal: chains[1] 172 | } 173 | } 174 | 175 | module.exports = Wallet 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bip32-wallet", 3 | "version": "0.7.1", 4 | "description": "A BIP32 Wallet backed by bitcoinjs-lib, lite on features but heavily tested.", 5 | "main": "./index.js", 6 | "scripts": { 7 | "prepublish": "npm run test", 8 | "standard": "standard", 9 | "test": "npm run-script unit", 10 | "unit": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/bitcoinjs/bip32-wallet.git" 15 | }, 16 | "author": "Daniel Cousens", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/bitcoinjs/bip32-wallet/issues" 20 | }, 21 | "homepage": "https://github.com/bitcoinjs/bip32-wallet", 22 | "dependencies": { 23 | "async": "^2.0.1", 24 | "bip32-utils": "^0.7.4", 25 | "bip69": "^1.0.3" 26 | }, 27 | "devDependencies": { 28 | "mocha": "*", 29 | "sinon": "^1.12.0", 30 | "standard": "*" 31 | }, 32 | "peerDependencies": { 33 | "bitcoinjs-lib": "^2.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "wallets": [ 3 | { 4 | "seed": "043c72ff615427ee4daf77b05206abd27de3b7655736539b88ea4e533bbd492d", 5 | "network": "testnet", 6 | "json": { 7 | "external": { 8 | "k": 0, 9 | "map": { 10 | "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa": 0, 11 | "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X": 1 12 | }, 13 | "node": "tprv8egjKjkFbDGo2jWKNuWRQn8krt7SXC6nCxey8kLEUK5tjEDEUbH5ugBm8fcqyuL8G1fp6aEUeTkmxmrrytPqAYbkXuG5E2yugr8f4jNHDS4" 14 | }, 15 | "internal": { 16 | "k": 0, 17 | "map": { 18 | "mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn": 0, 19 | "mtQrg4dcAVhDDzbyXawmRbpzRTRiUgfAE5": 1 20 | }, 21 | "node": "tprv8egjKjkFbDGo7k2fGBRge6BXkasGVw5Ag6Swh1p7KS96AXwFWWFJsg2MhesGtdn1pFXQmpurtJhxpFipEjt8yuS9L7AHnLHGSkNzAgC5RAZ" 22 | } 23 | } 24 | }, 25 | { 26 | "seed": "043c72ff615427ee4daf77b05206abd27de3b7655736539b88ea4e533bbd492d", 27 | "network": "litecoin", 28 | "json": { 29 | "external": { 30 | "k": 0, 31 | "map": { 32 | "LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os": 0, 33 | "LgNiV1WgUj59YxvnJKcRchx1vnBPGD7SgB": 0 34 | }, 35 | "node": "Ltpv76FuomkJfbn4GtvNAEhYPYjWk2A6Nfk7HPmQDpJCyzBVxSF2MrhtkmWyBFzCrFazogiKAzGEeBsdVRSCFo2rzbhVxpzwsdJta918gXQNeZQ" 36 | }, 37 | "internal": { 38 | "k": 0, 39 | "map": { 40 | "LSEiBaHCbW4m61Wn2JxyVAuEt9ZeKANb5s": 0, 41 | "LY7reDrTS8W1hgpWz9xgshgRmgCHiCb4Rq": 1 42 | }, 43 | "node": "Ltpv76FuomkJfbn4MuSi3WcocrnHdiuvMQiVkXZNn5n5q7EhPjy3Pmg7imMZkFEdkz2tMvZurEwct2ppLuJ9WeXAoxXtm2uARvcFL3FTnPAqi9f" 44 | } 45 | } 46 | } 47 | ], 48 | "transactions": [ 49 | { 50 | "description": "1:1 transaction", 51 | "wallet": 0, 52 | "inputs": [ 53 | { 54 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 55 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 56 | "vout": 0, 57 | "value": 120000 58 | } 59 | ], 60 | "outputs": [ 61 | { 62 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 63 | "value": 100000 64 | } 65 | ], 66 | "feeMax": 20000, 67 | "expected": { 68 | "fee": 20000, 69 | "txId": "b1773015d1ff341b34c7062b419c92f89bcfe11f0c91b5a7e43846b6f4be9188" 70 | } 71 | }, 72 | { 73 | "description": "1:1 transaction (with change)", 74 | "wallet": 0, 75 | "inputs": [ 76 | { 77 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 78 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 79 | "vout": 0, 80 | "value": 120000 81 | } 82 | ], 83 | "outputs": [ 84 | { 85 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 86 | "value": 100000 87 | }, 88 | { 89 | "value": 10000 90 | } 91 | ], 92 | "feeMax": 10000, 93 | "expected": { 94 | "fee": 10000, 95 | "txId": "437800fa251311d2df2bf44400e75b00d6f26e56bf236bdcb20d804eee44470e" 96 | } 97 | }, 98 | { 99 | "description": "1:1 transaction (change ignored)", 100 | "wallet": 0, 101 | "inputs": [ 102 | { 103 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 104 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 105 | "vout": 0, 106 | "value": 120000 107 | } 108 | ], 109 | "outputs": [ 110 | { 111 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 112 | "value": 50000 113 | }, 114 | { 115 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 116 | "value": 50000 117 | } 118 | ], 119 | "feeMax": 20000, 120 | "expected": { 121 | "fee": 20000, 122 | "txId": "939a0ee9ad6ec8f176a9b999b89adb7e7f25e78738f5f92974c372b3c1ff45a2" 123 | } 124 | }, 125 | { 126 | "description": "1:1 transaction (no fee)", 127 | "wallet": 0, 128 | "inputs": [ 129 | { 130 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 131 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 132 | "vout": 0, 133 | "value": 120000 134 | } 135 | ], 136 | "outputs": [ 137 | { 138 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 139 | "value": 50000 140 | }, 141 | { 142 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 143 | "value": 50000 144 | }, 145 | { 146 | "value": 20000 147 | } 148 | ], 149 | "feeMax": 0, 150 | "expected": { 151 | "fee": 0, 152 | "txId": "0c1558e3491c70869388f17a6988f4b66debdc527e6cb6957ac5a0b54fbc9f80" 153 | } 154 | }, 155 | { 156 | "description": "1:1 transaction (with locktime)", 157 | "wallet": 0, 158 | "locktime": 65535, 159 | "inputs": [ 160 | { 161 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 162 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 163 | "vout": 0, 164 | "value": 120000 165 | } 166 | ], 167 | "outputs": [ 168 | { 169 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 170 | "value": 100000 171 | } 172 | ], 173 | "feeMax": 20000, 174 | "expected": { 175 | "fee": 20000, 176 | "txId": "e52f646e27f3042626fc8f8863977ea0988c31656103f0c185691fbeb86184e9" 177 | } 178 | }, 179 | { 180 | "description": "1:2 transaction", 181 | "wallet": 0, 182 | "inputs": [ 183 | { 184 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 185 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 186 | "vout": 0, 187 | "value": 120000 188 | } 189 | ], 190 | "outputs": [ 191 | { 192 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 193 | "value": 50000 194 | }, 195 | { 196 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 197 | "value": 50000 198 | } 199 | ], 200 | "feeMax": 20000, 201 | "expected": { 202 | "fee": 20000, 203 | "txId": "939a0ee9ad6ec8f176a9b999b89adb7e7f25e78738f5f92974c372b3c1ff45a2" 204 | } 205 | }, 206 | { 207 | "description": "1:2 transaction (with change)", 208 | "wallet": 0, 209 | "inputs": [ 210 | { 211 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 212 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 213 | "vout": 0, 214 | "value": 120000 215 | } 216 | ], 217 | "outputs": [ 218 | { 219 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 220 | "value": 50000 221 | }, 222 | { 223 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 224 | "value": 50000 225 | }, 226 | { 227 | "value": 10000 228 | } 229 | ], 230 | "feeMax": 10000, 231 | "expected": { 232 | "fee": 10000, 233 | "txId": "eaf3551b9e6abb2224b39144fa287211863f69af18a3bb4dacea39d5438293a5" 234 | } 235 | }, 236 | { 237 | "description": "2:2 transaction", 238 | "wallet": 0, 239 | "inputs": [ 240 | { 241 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 242 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 243 | "vout": 0, 244 | "value": 100000 245 | }, 246 | { 247 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 248 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 249 | "vout": 1, 250 | "value": 100000 251 | } 252 | ], 253 | "outputs": [ 254 | { 255 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 256 | "value": 150000 257 | }, 258 | { 259 | "address": "n2PEoV6Abxnrpzoqqq1TJT5kw8G2dMPpBd", 260 | "value": 40000 261 | } 262 | ], 263 | "feeMax": 10000, 264 | "expected": { 265 | "fee": 10000, 266 | "txId": "1b929faea13dd73b9c416572dfed62e41dba0913bce59811ce45120b034b63ed" 267 | } 268 | }, 269 | { 270 | "description": "1:1 transaction (self-sweep)", 271 | "wallet": 0, 272 | "inputs": [ 273 | { 274 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 275 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 276 | "vout": 0, 277 | "value": 120000 278 | } 279 | ], 280 | "outputs": [ 281 | { 282 | "value": 110000 283 | } 284 | ], 285 | "feeMax": 10000, 286 | "expected": { 287 | "fee": 10000, 288 | "txId": "96d45aa51037d9fac674916b6ac02b47e31ccf7be011c3eefad6a52eecaf08f1" 289 | } 290 | }, 291 | { 292 | "description": "2:1 transaction (self-sweep, no fee)", 293 | "wallet": 0, 294 | "inputs": [ 295 | { 296 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 297 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 298 | "vout": 0, 299 | "value": 100000 300 | }, 301 | { 302 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 303 | "address": "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", 304 | "vout": 1, 305 | "value": 100000 306 | } 307 | ], 308 | "outputs": [ 309 | { 310 | "value": 200000 311 | } 312 | ], 313 | "feeMax": 0, 314 | "expected": { 315 | "fee": 0, 316 | "txId": "8491c5f6ab0812345f19f7044c762ea45cc7b5758bed3d5fa58c0b6e1fe84dc0" 317 | } 318 | }, 319 | { 320 | "description": "1:1 transaction (litecoin)", 321 | "wallet": 1, 322 | "inputs": [ 323 | { 324 | "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 325 | "address": "LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os", 326 | "vout": 0, 327 | "value": 120000 328 | } 329 | ], 330 | "outputs": [ 331 | { 332 | "address": "Lg6EmeK1sbbfJh2PJQ2NkYwCHM2buUDWjT", 333 | "value": 100000 334 | }, 335 | { 336 | "value": 10000 337 | } 338 | ], 339 | "feeMax": 10000, 340 | "expected": { 341 | "fee": 10000, 342 | "txId": "437800fa251311d2df2bf44400e75b00d6f26e56bf236bdcb20d804eee44470e" 343 | } 344 | }, 345 | { 346 | "exception": "Not enough funds: 0 < 10000", 347 | "wallet": 1, 348 | "inputs": [], 349 | "feeMax": 0, 350 | "outputs": [ 351 | { 352 | "address": "Lg6EmeK1sbbfJh2PJQ2NkYwCHM2buUDWjT", 353 | "value": 100000 354 | } 355 | ] 356 | }, 357 | { 358 | "exception": "Maximum fee is absurd: 50000000", 359 | "wallet": 1, 360 | "inputs": [], 361 | "feeMax": 50000000, 362 | "outputs": [] 363 | } 364 | ] 365 | } 366 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global beforeEach, describe, it */ 2 | 3 | var assert = require('assert') 4 | var bip32utils = require('bip32-utils') 5 | var bitcoin = require('bitcoinjs-lib') 6 | var sinon = require('sinon') 7 | 8 | var Wallet = require('../') 9 | var fixtures = require('./fixtures') 10 | 11 | var NETWORKS = bitcoin.networks 12 | 13 | describe('Wallet', function () { 14 | var seed, wallet 15 | 16 | beforeEach(function () { 17 | seed = new Buffer('the quick brown fox jumped over the lazy dog') 18 | wallet = Wallet.fromSeedBuffer(seed) 19 | }) 20 | 21 | describe('fromSeedBuffer', function () { 22 | it('defaults to Bitcoin network', function () { 23 | assert.equal(wallet.getNetwork(), NETWORKS.bitcoin) 24 | }) 25 | 26 | it('uses the network if specified', function () { 27 | wallet = Wallet.fromSeedBuffer(seed, NETWORKS.testnet) 28 | 29 | assert.equal(wallet.getNetwork(), NETWORKS.testnet) 30 | }) 31 | 32 | it("generates m/0'/0 as the external chain node", function () { 33 | assert.equal(wallet.external.index, 0) 34 | assert.equal(wallet.external.depth, 2) 35 | }) 36 | 37 | it("generates m/0'/1 as the internal chain node", function () { 38 | assert.equal(wallet.internal.index, 1) 39 | assert.equal(wallet.internal.depth, 2) 40 | }) 41 | }) 42 | 43 | describe('fromSeedHex', function () { 44 | var seedHex 45 | 46 | beforeEach(function () { 47 | seedHex = seed.toString('hex') 48 | }) 49 | 50 | it('wraps BIP32Wallet.fromSeedBuffer', sinon.test(function () { 51 | var mock = this.mock(Wallet) 52 | mock.expects('fromSeedBuffer').once().withArgs(seed, null) 53 | 54 | Wallet.fromSeedHex(seedHex, null) 55 | })) 56 | }) 57 | 58 | describe('discover', function () { 59 | it('wraps bip32utils.discovery', sinon.test(function () { 60 | var gapLimit = 2 61 | var query = function () {} 62 | var callback = function () {} 63 | 64 | var mock = this.mock(bip32utils).expects('discovery') 65 | mock.twice() 66 | 67 | wallet.discover(gapLimit, query, callback) 68 | mock.callArgWith(3, null, 0, 1) 69 | })) 70 | 71 | describe('discover', function () { 72 | it('each account retains all used addresses and ONE unused address', sinon.test(function () { 73 | var i = 0 74 | var results = [ 75 | // external 76 | true, true, true, false, false, false, 77 | 78 | // internal 79 | true, true, false, false 80 | ] 81 | 82 | var query = function (a, callback) { 83 | i += a.length 84 | return callback(null, results.slice(i - a.length, i)) 85 | } 86 | 87 | wallet.discover(2, query, function (err) { 88 | assert.ifError(err) 89 | assert.equal(wallet.account.chains[0].k, 3) 90 | assert.equal(wallet.account.chains[1].k, 2) 91 | }) 92 | })) 93 | }) 94 | }) 95 | 96 | function wrapsBIP32 (functionName, bip32FunctionName, fArgs, bfArgs) { 97 | bip32FunctionName = bip32FunctionName || functionName 98 | 99 | it('wraps account.' + bip32FunctionName + ' with ' + bfArgs, sinon.test(function () { 100 | var mock = this.mock(wallet.account).expects(bip32FunctionName) 101 | mock.withArgs.apply(mock, bfArgs) 102 | 103 | wallet[functionName].apply(wallet, fArgs) 104 | })) 105 | } 106 | 107 | describe('containsAddress', function () { 108 | wrapsBIP32('containsAddress', '', ['X'], ['X']) 109 | 110 | fixtures.wallets.forEach(function (f) { 111 | var network = NETWORKS[f.network] 112 | var wallet = Wallet.fromJSON(f.json, network) 113 | 114 | it('returns the expected results', function () { 115 | Object.keys(f.json.external.map).forEach(function (address) { 116 | assert(wallet.containsAddress(address)) 117 | }) 118 | 119 | Object.keys(f.json.internal.map).forEach(function (address) { 120 | assert(wallet.containsAddress(address)) 121 | }) 122 | 123 | assert(!wallet.containsAddress('1MsHWS1BnwMc3tLE8G35UXsS58fKipzB7a')) 124 | }) 125 | }) 126 | }) 127 | 128 | describe('getAllAddresses', function () { 129 | wrapsBIP32('getAllAddresses') 130 | 131 | fixtures.wallets.forEach(function (f) { 132 | var network = NETWORKS[f.network] 133 | var wallet = Wallet.fromJSON(f.json, network) 134 | 135 | it('returns all known addresses', function () { 136 | var fAllAddresses = Object.keys(f.json.external.map).concat(Object.keys(f.json.internal.map)) 137 | var allAddresses = wallet.getAllAddresses() 138 | 139 | fAllAddresses.forEach(function (address) { 140 | assert(allAddresses.indexOf(address) !== -1, address) 141 | }) 142 | }) 143 | }) 144 | }) 145 | 146 | describe('getNetwork', function () { 147 | wrapsBIP32('getNetwork') 148 | 149 | it('returns the accounts network', function () { 150 | assert.equal(wallet.getNetwork(), wallet.account.getNetwork()) 151 | }) 152 | }) 153 | 154 | describe('getReceiveAddress', function () { 155 | wrapsBIP32('getReceiveAddress', 'getChainAddress', [], [0]) 156 | 157 | it('returns the current external Address', function () { 158 | wallet.nextReceiveAddress() 159 | 160 | assert.equal(wallet.getReceiveAddress(), wallet.account.getChainAddress(0)) 161 | }) 162 | }) 163 | 164 | describe('getChangeAddress', function () { 165 | wrapsBIP32('getChangeAddress', 'getChainAddress', [], [1]) 166 | 167 | it('returns the current internal Address', function () { 168 | wallet.nextChangeAddress() 169 | 170 | assert.equal(wallet.getChangeAddress(), wallet.account.getChainAddress(1)) 171 | }) 172 | }) 173 | 174 | describe('isReceiveAddress', function () { 175 | wrapsBIP32('isReceiveAddress', 'isChainAddress', ['X'], [0, 'X']) 176 | 177 | it('returns true for a valid receive address', function () { 178 | assert(wallet.isReceiveAddress(wallet.getReceiveAddress())) 179 | }) 180 | }) 181 | 182 | describe('isChangeAddress', function () { 183 | wrapsBIP32('isChangeAddress', 'isChainAddress', ['X'], [1, 'X']) 184 | 185 | it('returns true for a valid change address', function () { 186 | assert(wallet.isChangeAddress(wallet.getChangeAddress())) 187 | }) 188 | }) 189 | 190 | describe('nextReceiveAddress', function () { 191 | wrapsBIP32('nextReceiveAddress', 'nextChainAddress', [], [0]) 192 | 193 | it('returns the new receive Address', function () { 194 | var result = wallet.nextReceiveAddress() 195 | 196 | assert.equal(result, wallet.getReceiveAddress()) 197 | }) 198 | }) 199 | 200 | describe('nextChangeAddress', function () { 201 | wrapsBIP32('nextChangeAddress', 'nextChainAddress', [], [1]) 202 | 203 | it('returns the new change Address', function () { 204 | var result = wallet.nextChangeAddress() 205 | 206 | assert.equal(result, wallet.getChangeAddress()) 207 | }) 208 | }) 209 | 210 | describe('fromJSON/toJSON', function () { 211 | fixtures.wallets.forEach(function (f) { 212 | var network = NETWORKS[f.network] 213 | var wallet = Wallet.fromJSON(f.json, network) 214 | 215 | it('imports ' + f.seed.slice(0, 20) + '... from JSON', function () { 216 | Object.keys(f.json.external.map).forEach(function (address) { 217 | assert(wallet.account.chains[0].addresses.indexOf(address) !== -1, address) 218 | }) 219 | 220 | Object.keys(f.json.internal.map).forEach(function (address) { 221 | assert(wallet.account.chains[1].addresses.indexOf(address) !== -1, address) 222 | }) 223 | 224 | assert.equal(wallet.account.chains[0].map, f.json.external.map) 225 | assert.equal(wallet.account.chains[1].map, f.json.internal.map) 226 | assert.equal(wallet.unspents, f.json.unspents) 227 | }) 228 | 229 | it('exports ' + f.seed.slice(0, 20) + '... to JSON', function () { 230 | assert.deepEqual(wallet.toJSON(), f.json) 231 | }) 232 | }) 233 | }) 234 | 235 | describe('buildTransaction', function () { 236 | fixtures.transactions.forEach(function (f) { 237 | var wallet 238 | 239 | beforeEach(function () { 240 | var fwallet = fixtures.wallets[f.wallet] 241 | var network = NETWORKS[fwallet.network] 242 | 243 | wallet = Wallet.fromJSON(fwallet.json, network) 244 | }) 245 | 246 | if (f.exception) { 247 | it('throws ' + f.exception, function () { 248 | assert.throws(function () { 249 | wallet.buildTransaction(f.inputs, f.outputs, f.feeMax) 250 | }, new RegExp(f.exception)) 251 | }) 252 | } else { 253 | it('builds ' + f.description + ' (' + f.expected.txId.slice(0, 20) + '... )', function () { 254 | var result = wallet.buildTransaction(f.inputs, f.outputs, f.feeMax, null, null, f.locktime) 255 | var txId = result.transaction.getId() 256 | 257 | // checks 258 | assert.equal(txId, f.expected.txId) 259 | assert.equal(result.fee, f.expected.fee) 260 | 261 | var transaction = result.transaction 262 | assert.equal(transaction.ins.length, f.inputs.length) 263 | f.inputs.forEach(function (input) { 264 | assert(transaction.ins.some(function (tinput) { 265 | return input.txId === bitcoin.bufferutils.reverse(tinput.hash).toString('hex') && input.vout === tinput.index 266 | })) 267 | }) 268 | 269 | assert.equal(transaction.outs.length, f.outputs.length) 270 | f.outputs.forEach(function (output) { 271 | // skip change addresses (see next loop) 272 | if (!output.address) return 273 | 274 | // otherwise, assert they are the expected scripts 275 | var outputScript = bitcoin.address.toOutputScript(output.address, wallet.getNetwork()) 276 | 277 | // assert address exists 278 | assert(transaction.outs.some(function (output2) { 279 | return outputScript.equals(output2.script) && output.value === output2.value 280 | })) 281 | }) 282 | 283 | // now lets do some sanity checking for the change outputs 284 | transaction.outs.forEach(function (output) { 285 | // enforce change addresses are ACTUAL change addresses 286 | var address = bitcoin.address.fromOutputScript(output.script, wallet.getNetwork()) 287 | 288 | if (wallet.containsAddress(address)) { 289 | // ensure the value exists as a change output 290 | assert(f.outputs.some(function (output2) { 291 | return output2.address === undefined && output.value === output2.value 292 | })) 293 | } 294 | }) 295 | }) 296 | } 297 | }) 298 | }) 299 | }) 300 | --------------------------------------------------------------------------------