├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENCE ├── Makefile ├── README.md ├── assets └── banners │ ├── banner-egifter.png │ ├── banner-purseio.jpg │ ├── banners.example.json │ ├── banners.json │ └── example-image.png ├── bcmonitor └── bcmonitor.js ├── bitcorenode └── index.js ├── bws.js ├── config.js ├── db └── .empty ├── emailservice └── emailservice.js ├── fiatrateservice └── fiatrateservice.js ├── index.js ├── installation.md ├── lib ├── bannerservice.js ├── blockchainexplorer.js ├── blockchainexplorers │ ├── insight.js │ └── request-list.js ├── blockchainmonitor.js ├── common │ ├── constants.js │ ├── defaults.js │ ├── index.js │ └── utils.js ├── emailservice.js ├── errors │ ├── clienterror.js │ └── errordefinitions.js ├── expressapp.js ├── fiatrateproviders │ ├── bitpay.js │ ├── bitstamp.js │ └── index.js ├── fiatrateservice.js ├── index.js ├── locallock.js ├── lock.js ├── messagebroker.js ├── model │ ├── address.js │ ├── addressmanager.js │ ├── copayer.js │ ├── email.js │ ├── index.js │ ├── notification.js │ ├── preferences.js │ ├── pushnotificationsub.js │ ├── session.js │ ├── txconfirmationsub.js │ ├── txnote.js │ ├── txproposal.js │ ├── txproposal_legacy.js │ ├── txproposalaction.js │ └── wallet.js ├── notificationbroadcaster.js ├── pushnotificationsservice.js ├── server.js ├── stats.js ├── storage.js └── templates │ ├── en │ ├── new_copayer.plain │ ├── new_incoming_tx.plain │ ├── new_outgoing_tx.plain │ ├── new_tx_proposal.plain │ ├── tx_confirmation.plain │ ├── txp_finally_rejected.plain │ └── wallet_complete.plain │ ├── es │ ├── new_copayer.plain │ ├── new_incoming_tx.plain │ ├── new_outgoing_tx.plain │ ├── new_tx_proposal.plain │ ├── tx_confirmation.plain │ ├── txp_finally_rejected.plain │ └── wallet_complete.plain │ ├── fr │ ├── new_copayer.plain │ ├── new_incoming_tx.plain │ ├── new_outgoing_tx.plain │ ├── new_tx_proposal.plain │ ├── txp_finally_rejected.plain │ └── wallet_complete.plain │ └── ja │ ├── new_copayer.plain │ ├── new_incoming_tx.plain │ ├── new_outgoing_tx.plain │ ├── new_tx_proposal.plain │ ├── txp_finally_rejected.plain │ └── wallet_complete.plain ├── locker └── locker.js ├── messagebroker └── messagebroker.js ├── package.json ├── pushnotificationsservice └── pushnotificationsservice.js ├── scripts ├── clean_db.mongodb └── level2mongo.js ├── start.sh ├── stop.sh └── test ├── bitcorenode.js ├── blockchainexplorer.js ├── expressapp.js ├── integration ├── bcmonitor.js ├── emailnotifications.js ├── fiatrateservice.js ├── helpers.js ├── pushNotifications.js └── server.js ├── locallock.js ├── mocha.opts ├── model ├── address.js ├── addressmanager.js ├── copayer.js ├── txproposal.js └── wallet.js ├── request-list.js ├── storage.js ├── testdata.js └── utils.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: sPSI9ALVcN1NzwkXUxIMHVCfbWO1XNH9h 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.sw* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | *.swp 32 | out/ 33 | db/* 34 | multilevel/db/* 35 | 36 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | compiler: 4 | - gcc 5 | - clang 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | packages: 11 | - gcc-4.8 12 | - g++-4.8 13 | - clang 14 | node_js: 15 | - '8' 16 | before_install: 17 | - export CXX="g++-4.8" CC="gcc-4.8" 18 | install: 19 | - npm install 20 | after_success: 21 | - npm run coveralls 22 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 BitPay 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover 2 | test: 3 | ./node_modules/.bin/mocha 4 | cover: 5 | ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --reporter spec test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # bitcore-wallet-service 3 | 4 | [![NPM Package](https://img.shields.io/npm/v/bitcore-wallet-service.svg?style=flat-square)](https://www.npmjs.org/package/bitcore-wallet-service) 5 | [![Build Status](https://img.shields.io/travis/bitpay/bitcore-wallet-service.svg?branch=master&style=flat-square)](https://travis-ci.org/bitpay/bitcore-wallet-service) 6 | [![Coverage Status](https://coveralls.io/repos/bitpay/bitcore-wallet-service/badge.svg?branch=master)](https://coveralls.io/r/bitpay/bitcore-wallet-service?branch=master) 7 | 8 | A Multisig HD Bitcore Wallet Service. 9 | 10 | # Description 11 | 12 | Bitcore Wallet Service facilitates multisig HD wallets creation and operation through a (hopefully) simple and intuitive REST API. 13 | 14 | BWS can usually be installed within minutes and accommodates all the needed infrastructure for peers in a multisig wallet to communicate and operate – with minimum server trust. 15 | 16 | See [Bitcore-wallet-client](https://github.com/bitpay/bitcore-wallet-client) for the *official* client library that communicates to BWS and verifies its response. Also check [Bitcore-wallet](https://github.com/bitpay/bitcore-wallet) for a simple CLI wallet implementation that relies on BWS. 17 | 18 | BWS is been used in production enviroments for [Copay Wallet](https://copay.io), [Bitpay App wallet](https://bitpay.com/wallet) and others. 19 | 20 | More about BWS at https://blog.bitpay.com/announcing-the-bitcore-wallet-suite/ 21 | 22 | # Getting Started 23 | ``` 24 | git clone https://github.com/bitpay/bitcore-wallet-service.git 25 | cd bitcore-wallet-service && npm start 26 | ``` 27 | 28 | This will launch the BWS service (with default settings) at `http://localhost:3232/bws/api`. 29 | 30 | BWS needs mongoDB. You can configure the connection at `config.js` 31 | 32 | BWS supports SSL and Clustering. For a detailed guide on installing BWS with extra features see [Installing BWS](https://github.com/bitpay/bitcore-wallet-service/blob/master/installation.md). 33 | 34 | BWS uses by default a Request Rate Limitation to CreateWallet endpoint. If you need to modify it, check defaults.js' `Defaults.RateLimit` 35 | 36 | # Security Considerations 37 | * Private keys are never sent to BWS. Copayers store them locally. 38 | * Extended public keys are stored on BWS. This allows BWS to easily check wallet balance, send offline notifications to copayers, etc. 39 | * During wallet creation, the initial copayer creates a wallet secret that contains a private key. All copayers need to prove they have the secret by signing their information with this private key when joining the wallet. The secret should be shared using secured channels. 40 | * A copayer could join the wallet more than once, and there is no mechanism to prevent this. See [wallet](https://github.com/bitpay/bitcore-wallet)'s confirm command, for a method for confirming copayers. 41 | * All BWS responses are verified: 42 | * Addresses and change addresses are derived independently and locally by the copayers from their local data. 43 | * TX Proposals templates are signed by copayers and verified by others, so the BWS cannot create or tamper with them. 44 | 45 | # REST API 46 | ## Authentication 47 | 48 | In order to access a wallet, clients are required to send the headers: 49 | ``` 50 | x-identity 51 | x-signature 52 | ``` 53 | Identity is the Peer-ID, this will identify the peer and its wallet. Signature is the current request signature, using `requestSigningKey`, the `m/1/1` derivative of the Extended Private Key. 54 | 55 | See [Bitcore Wallet Client](https://github.com/bitpay/bitcore-wallet-client/blob/master/lib/api.js#L73) for implementation details. 56 | 57 | 58 | ## GET Endpoints 59 | `/v1/wallets/`: Get wallet information 60 | 61 | Returns: 62 | * Wallet object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/wallet.js)). 63 | 64 | `/v1/txhistory/`: Get Wallet's transaction history 65 | 66 | Optional Arguments: 67 | * skip: Records to skip from the result (defaults to 0) 68 | * limit: Total number of records to return (return all available records if not specified). 69 | 70 | Returns: 71 | * History of incoming and outgoing transactions of the wallet. The list is paginated using the `skip` & `limit` params. Each item has the following fields: 72 | * action ('sent', 'received', 'moved') 73 | * amount 74 | * fees 75 | * time 76 | * addressTo 77 | * confirmations 78 | * proposalId 79 | * creatorName 80 | * message 81 | * actions array ['createdOn', 'type', 'copayerId', 'copayerName', 'comment'] 82 | 83 | 84 | `/v1/txproposals/`: Get Wallet's pending transaction proposals and their status 85 | Returns: 86 | * List of pending TX Proposals. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)) 87 | 88 | `/v1/addresses/`: Get Wallet's main addresses (does not include change addresses) 89 | 90 | Returns: 91 | * List of Addresses object: (https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/address.js)). This call is mainly provided so the client check this addresses for incoming transactions (using a service like [Insight](https://insight.is) 92 | 93 | `/v1/balance/`: Get Wallet's balance 94 | 95 | Returns: 96 | * totalAmount: Wallet's total balance 97 | * lockedAmount: Current balance of outstanding transaction proposals, that cannot be used on new transactions. 98 | * availableAmount: Funds available for new proposals. 99 | * totalConfirmedAmount: Same as totalAmount for confirmed UTXOs only. 100 | * lockedConfirmedAmount: Same as lockedAmount for confirmed UTXOs only. 101 | * availableConfirmedAmount: Same as availableAmount for confirmed UTXOs only. 102 | * byAddress array ['address', 'path', 'amount']: A list of addresses holding funds. 103 | * totalKbToSendMax: An estimation of the number of KiB required to include all available UTXOs in a tx (including unconfirmed). 104 | 105 | `/v1/txnotes/:txid`: Get user notes associated to the specified transaction. 106 | Returns: 107 | * The note associated to the `txid` as a string. 108 | 109 | `/v1/fiatrates/:code`: Get the fiat rate for the specified ISO 4217 code. 110 | Optional Arguments: 111 | * provider: An identifier representing the source of the rates. 112 | * ts: The timestamp for the fiat rate (defaults to now). 113 | 114 | Returns: 115 | * The fiat exchange rate. 116 | 117 | ## POST Endpoints 118 | `/v1/wallets/`: Create a new Wallet 119 | 120 | Required Arguments: 121 | * name: Name of the wallet 122 | * m: Number of required peers to sign transactions 123 | * n: Number of total peers on the wallet 124 | * pubKey: Wallet Creation Public key to check joining copayer's signatures (the private key is unknown by BWS and must be communicated 125 | by the creator peer to other peers). 126 | 127 | Returns: 128 | * walletId: Id of the new created wallet 129 | 130 | 131 | `/v1/wallets/:id/copayers/`: Join a Wallet in creation 132 | 133 | Required Arguments: 134 | * walletId: Id of the wallet to join 135 | * name: Copayer Name 136 | * xPubKey - Extended Public Key for this copayer. 137 | * requestPubKey - Public Key used to check requests from this copayer. 138 | * copayerSignature - Signature used by other copayers to verify that the copayer joining knows the wallet secret. 139 | 140 | Returns: 141 | * copayerId: Assigned ID of the copayer (to be used on x-identity header) 142 | * wallet: Object with wallet's information 143 | 144 | `/v1/txproposals/`: Add a new transaction proposal 145 | 146 | Required Arguments: 147 | * toAddress: RCPT Bitcoin address. 148 | * amount: amount (in satoshis) of the mount proposed to be transfered 149 | * proposalsSignature: Signature of the proposal by the creator peer, using proposalSigningKey. 150 | * (opt) message: Encrypted private message to peers. 151 | * (opt) payProUrl: Paypro URL for peers to verify TX 152 | * (opt) feePerKb: Use an alternative fee per KB for this TX. 153 | * (opt) excludeUnconfirmedUtxos: Do not use UTXOs of unconfirmed transactions as inputs for this TX. 154 | 155 | Returns: 156 | * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.id` is probably needed in this case. 157 | 158 | 159 | `/v1/addresses/`: Request a new main address from wallet 160 | 161 | Returns: 162 | * Address object: (https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/address.js)). Note that `path` is returned so client can derive the address independently and check server's response. 163 | 164 | `/v1/txproposals/:id/signatures/`: Sign a transaction proposal 165 | 166 | Required Arguments: 167 | * signatures: All Transaction's input signatures, in order of appearance. 168 | 169 | Returns: 170 | * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.status` is probably needed in this case. 171 | 172 | `/v1/txproposals/:id/broadcast/`: Broadcast a transaction proposal 173 | 174 | Returns: 175 | * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.status` is probably needed in this case. 176 | 177 | `/v1/txproposals/:id/rejections`: Reject a transaction proposal 178 | 179 | Returns: 180 | * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.status` is probably needed in this case. 181 | 182 | `/v1/addresses/scan`: Start an address scan process looking for activity. 183 | 184 | Optional Arguments: 185 | * includeCopayerBranches: Scan all copayer branches following BIP45 recommendation (defaults to false). 186 | 187 | `/v1/txconfirmations/`: Subscribe to receive push notifications when the specified transaction gets confirmed. 188 | Required Arguments: 189 | * txid: The transaction to subscribe to. 190 | 191 | ## PUT Endpoints 192 | `/v1/txnotes/:txid/`: Modify a note for a tx. 193 | 194 | 195 | ## DELETE Endpoints 196 | `/v1/txproposals/:id/`: Deletes a transaction proposal. Only the creator can delete a TX Proposal, and only if it has no other signatures or rejections 197 | 198 | Returns: 199 | * TX Proposal object. (see [fields on the source code](https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js)). `.id` is probably needed in this case. 200 | 201 | `/v1/txconfirmations/:txid`: Unsubscribe from transaction `txid` and no longer listen to its confirmation. 202 | 203 | 204 | # Push Notifications 205 | Recomended to complete config.js file: 206 | 207 | * [GCM documentation to get your API key](https://developers.google.com/cloud-messaging/gcm) 208 | * [Apple's Notification guide to know how to get your certificates for APN](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/Introduction.html) 209 | 210 | 211 | ## POST Endpoints 212 | `/v1/pushnotifications/subscriptions/`: Adds subscriptions for push notifications service at database. 213 | 214 | 215 | ## DELETE Endopints 216 | `/v2/pushnotifications/subscriptions/`: Remove subscriptions for push notifications service from database. 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /assets/banners/banner-egifter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/bitcore-wallet-service/8825de4b1ff370893e952bae0d6fecb2bcc6fa0d/assets/banners/banner-egifter.png -------------------------------------------------------------------------------- /assets/banners/banner-purseio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/bitcore-wallet-service/8825de4b1ff370893e952bae0d6fecb2bcc6fa0d/assets/banners/banner-purseio.jpg -------------------------------------------------------------------------------- /assets/banners/banners.example.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "example-banner", 3 | "image": "example-image.png", 4 | "url": "https://example.com/", 5 | "enabled": true 6 | },{ 7 | "id": "example-disabled-banner", 8 | "image": "example-image.png", 9 | "url": "https://example.com/", 10 | "enabled": false 11 | }] -------------------------------------------------------------------------------- /assets/banners/banners.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "egifter-banner", 4 | "image": "banner-egifter.png", 5 | "url": "https://giftcards.bitcoin.com/?utm_source=wallet", 6 | "enabled": true 7 | } 8 | , { 9 | "id": "purse-io-banner", 10 | "image": "banner-purseio.jpg", 11 | "url": "https://app.purse.io?_r=bitcoinwallet", 12 | "enabled": false 13 | } 14 | ] -------------------------------------------------------------------------------- /assets/banners/example-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/bitcore-wallet-service/8825de4b1ff370893e952bae0d6fecb2bcc6fa0d/assets/banners/example-image.png -------------------------------------------------------------------------------- /bcmonitor/bcmonitor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var log = require('npmlog'); 7 | log.debug = log.verbose; 8 | 9 | var config = require('../config'); 10 | var BlockchainMonitor = require('../lib/blockchainmonitor'); 11 | 12 | var bcm = new BlockchainMonitor(); 13 | bcm.start(config, function(err) { 14 | if (err) throw err; 15 | 16 | console.log('Blockchain monitor started'); 17 | }); 18 | -------------------------------------------------------------------------------- /bitcorenode/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var fs = require('fs'); 5 | var io = require('socket.io'); 6 | var https = require('https'); 7 | var http = require('http'); 8 | var async = require('async'); 9 | var path = require('path'); 10 | var bitcore = require('bitcore-lib'); 11 | var Networks = bitcore.Networks; 12 | var Locker = require('locker-server'); 13 | var BlockchainMonitor = require('../lib/blockchainmonitor'); 14 | var EmailService = require('../lib/emailservice'); 15 | var ExpressApp = require('../lib/expressapp'); 16 | var child_process = require('child_process'); 17 | var spawn = child_process.spawn; 18 | var EventEmitter = require('events').EventEmitter; 19 | var baseConfig = require('../config'); 20 | 21 | /** 22 | * A Bitcore Node Service module 23 | * @param {Object} options 24 | * @param {Node} options.node - A reference to the Bitcore Node instance 25 | -* @param {Boolean} options.https - Enable https for this module, defaults to node settings. 26 | * @param {Number} options.bwsPort - Port for Bitcore Wallet Service API 27 | * @param {Number} options.messageBrokerPort - Port for BWS message broker 28 | * @param {Number} options.lockerPort - Port for BWS locker port 29 | */ 30 | var Service = function(options) { 31 | EventEmitter.call(this); 32 | 33 | this.node = options.node; 34 | this.https = options.https || this.node.https; 35 | this.httpsOptions = options.httpsOptions || this.node.httpsOptions; 36 | this.bwsPort = options.bwsPort || baseConfig.port; 37 | this.messageBrokerPort = options.messageBrokerPort || 3380; 38 | if (baseConfig.lockOpts) { 39 | this.lockerPort = baseConfig.lockOpts.lockerServer.port; 40 | } 41 | this.lockerPort = options.lockerPort || this.lockerPort; 42 | }; 43 | 44 | util.inherits(Service, EventEmitter); 45 | 46 | Service.dependencies = ['insight-api']; 47 | 48 | /** 49 | * This method will read `key` and `cert` files from disk based on `httpsOptions` and 50 | * return `serverOpts` with the read files. 51 | * @returns {Object} 52 | */ 53 | Service.prototype._readHttpsOptions = function() { 54 | if (!this.httpsOptions || !this.httpsOptions.key || !this.httpsOptions.cert) { 55 | throw new Error('Missing https options'); 56 | } 57 | 58 | var serverOpts = {}; 59 | serverOpts.key = fs.readFileSync(this.httpsOptions.key); 60 | serverOpts.cert = fs.readFileSync(this.httpsOptions.cert); 61 | 62 | // This sets the intermediate CA certs only if they have all been designated in the config.js 63 | if (this.httpsOptions.CAinter1 && this.httpsOptions.CAinter2 && this.httpsOptions.CAroot) { 64 | serverOpts.ca = [ 65 | fs.readFileSync(this.httpsOptions.CAinter1), 66 | fs.readFileSync(this.httpsOptions.CAinter2), 67 | fs.readFileSync(this.httpsOptions.CAroot) 68 | ]; 69 | } 70 | return serverOpts; 71 | }; 72 | 73 | /** 74 | * Will get the configuration with settings for the locally 75 | * running Insight API. 76 | * @returns {Object} 77 | */ 78 | Service.prototype._getConfiguration = function() { 79 | var self = this; 80 | 81 | var providerOptions = { 82 | provider: 'insight', 83 | url: (self.node.https ? 'https://' : 'http://') + 'localhost:' + self.node.port, 84 | apiPrefix: '/insight-api' 85 | }; 86 | 87 | // A bitcore-node is either livenet or testnet, so we'll pass 88 | // the configuration options to communicate via the local running 89 | // instance of the insight-api service. 90 | if (self.node.network === Networks.livenet) { 91 | baseConfig.blockchainExplorerOpts = { 92 | livenet: providerOptions 93 | }; 94 | } else if (self.node.network === Networks.testnet) { 95 | baseConfig.blockchainExplorerOpts = { 96 | testnet: providerOptions 97 | }; 98 | } else { 99 | throw new Error('Unknown network'); 100 | } 101 | 102 | return baseConfig; 103 | 104 | }; 105 | 106 | /** 107 | * Will start the HTTP web server and socket.io for the wallet service. 108 | */ 109 | Service.prototype._startWalletService = function(config, next) { 110 | var self = this; 111 | var expressApp = new ExpressApp(); 112 | 113 | if (self.https) { 114 | var serverOpts = self._readHttpsOptions(); 115 | self.server = https.createServer(serverOpts, expressApp.app); 116 | } else { 117 | self.server = http.Server(expressApp.app); 118 | } 119 | 120 | expressApp.start(config, function(err){ 121 | if (err) { 122 | return next(err); 123 | } 124 | self.server.listen(self.bwsPort, next); 125 | }); 126 | }; 127 | 128 | /** 129 | * Called by the node to start the service 130 | */ 131 | Service.prototype.start = function(done) { 132 | 133 | var self = this; 134 | var config; 135 | try { 136 | config = self._getConfiguration(); 137 | } catch (err) { 138 | return done(err); 139 | } 140 | 141 | // Locker Server 142 | var locker = new Locker(); 143 | locker.listen(self.lockerPort); 144 | 145 | // Message Broker 146 | var messageServer = io(self.messageBrokerPort); 147 | messageServer.on('connection', function(s) { 148 | s.on('msg', function(d) { 149 | messageServer.emit('msg', d); 150 | }); 151 | }); 152 | 153 | async.series([ 154 | 155 | function(next) { 156 | // Blockchain Monitor 157 | var blockChainMonitor = new BlockchainMonitor(); 158 | blockChainMonitor.start(config, next); 159 | }, 160 | function(next) { 161 | // Email Service 162 | if (config.emailOpts) { 163 | var emailService = new EmailService(); 164 | emailService.start(config, next); 165 | } else { 166 | setImmediate(next); 167 | } 168 | }, 169 | function(next) { 170 | self._startWalletService(config, next); 171 | } 172 | ], done); 173 | 174 | }; 175 | 176 | /** 177 | * Called by node to stop the service 178 | */ 179 | Service.prototype.stop = function(done) { 180 | setImmediate(function() { 181 | done(); 182 | }); 183 | }; 184 | 185 | Service.prototype.getAPIMethods = function() { 186 | return []; 187 | }; 188 | 189 | Service.prototype.getPublishEvents = function() { 190 | return []; 191 | }; 192 | 193 | module.exports = Service; 194 | -------------------------------------------------------------------------------- /bws.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | 6 | var ExpressApp = require('./lib/expressapp'); 7 | var config = require('./config'); 8 | var log = require('npmlog'); 9 | log.debug = log.verbose; 10 | log.disableColor(); 11 | 12 | 13 | 14 | 15 | var port = process.env.BWS_PORT || config.port || 3232; 16 | 17 | var cluster = require('cluster'); 18 | var http = require('http'); 19 | var numCPUs = require('os').cpus().length; 20 | var clusterInstances = config.clusterInstances || numCPUs; 21 | var serverModule = config.https ? require('https') : require('http'); 22 | 23 | var serverOpts = {}; 24 | 25 | if (config.https) { 26 | serverOpts.key = fs.readFileSync(config.privateKeyFile || './ssl/privatekey.pem'); 27 | serverOpts.cert = fs.readFileSync(config.certificateFile || './ssl/certificate.pem'); 28 | if (config.ciphers) { 29 | serverOpts.ciphers = config.ciphers; 30 | serverOpts.honorCipherOrder = true; 31 | }; 32 | 33 | // This sets the intermediate CA certs only if they have all been designated in the config.js 34 | if (config.CAinter1 && config.CAinter2 && config.CAroot) { 35 | serverOpts.ca = [fs.readFileSync(config.CAinter1), 36 | fs.readFileSync(config.CAinter2), 37 | fs.readFileSync(config.CAroot) 38 | ]; 39 | }; 40 | } 41 | 42 | if (config.cluster && !config.lockOpts.lockerServer) 43 | throw 'When running in cluster mode, locker server need to be configured'; 44 | 45 | if (config.cluster && !config.messageBrokerOpts.messageBrokerServer) 46 | throw 'When running in cluster mode, message broker server need to be configured'; 47 | 48 | var expressApp = new ExpressApp(); 49 | 50 | function startInstance(cb) { 51 | var server = config.https ? serverModule.createServer(serverOpts, expressApp.app) : serverModule.Server(expressApp.app); 52 | 53 | server.on('connection', function(socket) { 54 | socket.setTimeout(300 * 1000); 55 | }) 56 | 57 | expressApp.start(config, function(err) { 58 | if (err) { 59 | log.error('Could not start BWS instance', err); 60 | return; 61 | } 62 | 63 | server.listen(port); 64 | 65 | var instanceInfo = cluster.worker ? ' [Instance:' + cluster.worker.id + ']' : ''; 66 | log.info('BWS running ' + instanceInfo); 67 | return; 68 | }); 69 | }; 70 | 71 | if (config.cluster && cluster.isMaster) { 72 | 73 | // Count the machine's CPUs 74 | var instances = config.clusterInstances || require('os').cpus().length; 75 | 76 | log.info('Starting ' + instances + ' instances'); 77 | 78 | // Create a worker for each CPU 79 | for (var i = 0; i < instances; i += 1) { 80 | cluster.fork(); 81 | } 82 | 83 | // Listen for dying workers 84 | cluster.on('exit', function(worker) { 85 | // Replace the dead worker, 86 | log.error('Worker ' + worker.id + ' died :('); 87 | cluster.fork(); 88 | }); 89 | // Code to run if we're in a worker process 90 | } else { 91 | log.info('Listening on port: ' + port); 92 | startInstance(); 93 | }; 94 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | basePath: '/bws/api', 3 | disableLogs: false, 4 | port: 3232, 5 | 6 | // Uncomment to make BWS a forking server 7 | // cluster: true, 8 | 9 | // Uncomment to set the number or process (will use the nr of availalbe CPUs by default) 10 | // clusterInstances: 4, 11 | 12 | // https: true, 13 | // privateKeyFile: 'private.pem', 14 | // certificateFile: 'cert.pem', 15 | ////// The following is only for certs which are not 16 | ////// trusted by nodejs 'https' by default 17 | ////// CAs like Verisign do not require this 18 | // CAinter1: '', // ex. 'COMODORSADomainValidationSecureServerCA.crt' 19 | // CAinter2: '', // ex. 'COMODORSAAddTrustCA.crt' 20 | // CAroot: '', // ex. 'AddTrustExternalCARoot.crt' 21 | 22 | storageOpts: { 23 | mongoDb: { 24 | uri: 'mongodb://localhost:27017/bws', 25 | }, 26 | }, 27 | lockOpts: { 28 | // To use locker-server, uncomment this: 29 | lockerServer: { 30 | host: 'localhost', 31 | port: 3231, 32 | }, 33 | }, 34 | messageBrokerOpts: { 35 | // To use message broker server, uncomment this: 36 | messageBrokerServer: { 37 | url: 'http://localhost:3380', 38 | }, 39 | }, 40 | blockchainExplorerOpts: { 41 | btc: { 42 | livenet: { 43 | provider: 'insight', 44 | url: 'https://insight.bitpay.com:443', 45 | }, 46 | testnet: { 47 | provider: 'insight', 48 | url: 'https://test-insight.bitpay.com:443', 49 | // url: 'http://localhost:3001', 50 | // Multiple servers (in priority order) 51 | // url: ['http://a.b.c', 'https://test-insight.bitpay.com:443'], 52 | }, 53 | }, 54 | bch: { 55 | livenet: { 56 | provider: 'insight', 57 | url: 'https://cashexplorer.bitcoin.com', 58 | }, 59 | }, 60 | }, 61 | pushNotificationsOpts: { 62 | templatePath: './lib/templates', 63 | defaultLanguage: 'en', 64 | defaultUnit: 'btc', 65 | subjectPrefix: '', 66 | pushServerUrl: 'https://fcm.googleapis.com/fcm', 67 | authorizationKey: '', 68 | }, 69 | fiatRateServiceOpts: { 70 | defaultProvider: 'BitPay', 71 | fetchInterval: 60, // in minutes 72 | }, 73 | // To use email notifications uncomment this: 74 | // emailOpts: { 75 | // host: 'localhost', 76 | // port: 25, 77 | // ignoreTLS: true, 78 | // subjectPrefix: '[Wallet Service]', 79 | // from: 'wallet-service@bitcore.io', 80 | // templatePath: './lib/templates', 81 | // defaultLanguage: 'en', 82 | // defaultUnit: 'btc', 83 | // publicTxUrlTemplate: { 84 | // livenet: 'https://insight.bitpay.com/tx/{{txid}}', 85 | // testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', 86 | // }, 87 | //}, 88 | // 89 | // To use sendgrid: 90 | // var sgTransport = require('nodemail-sendgrid-transport'); 91 | // mailer:sgTransport({ 92 | // api_user: xxx, 93 | // api_key: xxx, 94 | // }); 95 | }; 96 | module.exports = config; 97 | -------------------------------------------------------------------------------- /db/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/bitcore-wallet-service/8825de4b1ff370893e952bae0d6fecb2bcc6fa0d/db/.empty -------------------------------------------------------------------------------- /emailservice/emailservice.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var log = require('npmlog'); 7 | log.debug = log.verbose; 8 | 9 | var config = require('../config'); 10 | var EmailService = require('../lib/emailservice'); 11 | 12 | var emailService = new EmailService(); 13 | emailService.start(config, function(err) { 14 | if (err) throw err; 15 | 16 | console.log('Email service started'); 17 | }); 18 | -------------------------------------------------------------------------------- /fiatrateservice/fiatrateservice.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var config = require('../config'); 6 | var FiatRateService = require('../lib/fiatrateservice'); 7 | 8 | var service = new FiatRateService(); 9 | service.init(config, function(err) { 10 | if (err) throw err; 11 | service.startCron(config, function(err) { 12 | if (err) throw err; 13 | 14 | console.log('Fiat rate service started'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var BWS = {}; 2 | 3 | BWS.ExpressApp = require('./lib/expressapp'); 4 | BWS.Storage = require('./lib/storage'); 5 | 6 | 7 | module.exports = BWS; 8 | -------------------------------------------------------------------------------- /installation.md: -------------------------------------------------------------------------------- 1 | The following document is a step-by-step guide to run BWS. 2 | 3 | ### Prerequisites 4 | Ensure MongoDB (2.6+) is installed and running. This document assumes that mongod is running at the default port 27017. 5 | See the configuration section to configure a different host/port. 6 | 7 | ### Install BWS from NPM 8 | Use the following steps to Install BWS from the npmjs repository and run it with defaults. 9 | ```bash 10 | npm install bitcore-wallet-service 11 | cd bitcore-wallet-service 12 | ``` 13 | To change configuration before running, see the Configuration section. 14 | ```bash 15 | npm start 16 | ``` 17 | 18 | ### Install BWS from github source 19 | Use the following steps to Install BWS from github source and run it with defaults. 20 | ```bash 21 | git clone https://github.com/bitpay/bitcore-wallet-service.git 22 | cd bitcore-wallet-service 23 | npm install 24 | ``` 25 | To change configuration before running, see the Configuration section. 26 | ```bash 27 | npm start 28 | ``` 29 | ### Configuration 30 | Configuration for all required modules can be specified in https://github.com/bitpay/bitcore-wallet-service/blob/master/config.js 31 | 32 | BWS is composed of 5 separate node services - 33 | Locker - locker/locker.js 34 | Message Broker - messagebroker/messagebroker.js 35 | Blockchain Monitor - bcmonitor/bcmonitor.js (This service talks to the Blockchain Explorer service configured under blockchainExplorerOpts - see Configure blockchain service below.) 36 | Email Service - emailservice/emailservice.js 37 | Bitcore Wallet Service - bws.js 38 | 39 | #### Configure MongoDB 40 | Example configuration for connecting to the MongoDB instance: 41 | ```javascript 42 | storageOpts: { 43 | mongoDb: { 44 | uri: 'mongodb://localhost:27017/bws', 45 | }, 46 | } 47 | ``` 48 | #### Configure Locker service 49 | Example configuration for connecting to locker service: 50 | ```javascript 51 | lockOpts: { 52 | lockerServer: { 53 | host: 'localhost', 54 | port: 3231, 55 | }, 56 | } 57 | ``` 58 | 59 | #### Configure Message Broker service 60 | Example configuration for connecting to message broker service: 61 | ```javascript 62 | messageBrokerOpts: { 63 | messageBrokerServer: { 64 | url: 'http://localhost:3380', 65 | }, 66 | } 67 | ``` 68 | 69 | #### Configure blockchain service 70 | Note: this service will be used by blockchain monitor service as well as by BWS itself. 71 | An example of this configuration is: 72 | ```javascript 73 | blockchainExplorerOpts: { 74 | livenet: { 75 | provider: 'insight', 76 | url: 'https://insight.bitpay.com:443', 77 | }, 78 | testnet: { 79 | provider: 'insight', 80 | url: 'https://test-insight.bitpay.com:443', 81 | }, 82 | } 83 | ``` 84 | 85 | #### Configure Email service 86 | Example configuration for connecting to email service (using postfix): 87 | ```javascript 88 | emailOpts: { 89 | host: 'localhost', 90 | port: 25, 91 | ignoreTLS: true, 92 | subjectPrefix: '[Wallet Service]', 93 | from: 'wallet-service@bitcore.io', 94 | } 95 | ``` 96 | 97 | #### Enable clustering 98 | Change `config.js` file to enable and configure clustering: 99 | ```javascript 100 | { 101 | cluster: true, 102 | clusterInstances: 4, 103 | } 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /lib/bannerservice.js: -------------------------------------------------------------------------------- 1 | var banners; 2 | var fs = require('fs'); 3 | var log = require('npmlog'); 4 | 5 | /** 6 | * Creates an instance of the Banner Service. 7 | * @constructor 8 | */ 9 | function BannerService() { 10 | this.banners = []; 11 | } 12 | 13 | BannerService.initialize = function() { 14 | var self = this; 15 | 16 | // Read settings file on initialization 17 | this.readSettingsFile(); 18 | 19 | // Watch for changes to banners.json 20 | fs.watch(__dirname+'/../assets/banners/', function (event, filename) { 21 | if (filename === 'banners.json') { 22 | log.info("Change detected on banners.json."); 23 | self.readSettingsFile(); 24 | } 25 | }); 26 | }; 27 | 28 | BannerService.readSettingsFile = function() { 29 | try { 30 | this.banners = []; 31 | var bannerObj = JSON.parse(fs.readFileSync(__dirname+'/../assets/banners/banners.json', 'utf8')); 32 | for (var i in bannerObj) { 33 | if (bannerObj[i].enabled) { 34 | this.banners.push(bannerObj[i]); 35 | } 36 | } 37 | log.info("New banners.json successfully loaded. "+bannerObj.length+" found."); 38 | } catch (err) { 39 | log.warn("Error while parsing banners.json.."); 40 | this.banners = []; 41 | } 42 | }; 43 | 44 | module.exports = BannerService; -------------------------------------------------------------------------------- /lib/blockchainexplorer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var $ = require('preconditions').singleton(); 5 | var log = require('npmlog'); 6 | log.debug = log.verbose; 7 | 8 | var Insight = require('./blockchainexplorers/insight'); 9 | var Common = require('./common'); 10 | var Constants = Common.Constants, 11 | Defaults = Common.Defaults, 12 | Utils = Common.Utils; 13 | 14 | var PROVIDERS = { 15 | 'insight': { 16 | 'btc': { 17 | 'livenet': 'https://insight.bitcoin.com:443', 18 | 'testnet': 'https://test-insight.bitpay.com:443', 19 | }, 20 | 'bch': { 21 | 'livenet': 'https://cashexplorer.bitcoin.com:443' 22 | }, 23 | }, 24 | }; 25 | 26 | function BlockChainExplorer(opts) { 27 | $.checkArgument(opts); 28 | 29 | var provider = opts.provider || 'insight'; 30 | var coin = opts.coin || Defaults.COIN; 31 | var network = opts.network || 'livenet'; 32 | 33 | $.checkState(PROVIDERS[provider], 'Provider ' + provider + ' not supported'); 34 | $.checkState(_.contains(_.keys(PROVIDERS[provider]), coin), 'Coin ' + coin + ' not supported by this provider'); 35 | $.checkState(_.contains(_.keys(PROVIDERS[provider][coin]), network), 'Network ' + network + ' not supported by this provider for coin ' + coin); 36 | 37 | var url = opts.url || PROVIDERS[provider][coin][network]; 38 | 39 | switch (provider) { 40 | case 'insight': 41 | return new Insight({ 42 | coin: coin, 43 | network: network, 44 | url: url, 45 | apiPrefix: opts.apiPrefix, 46 | userAgent: opts.userAgent, 47 | }); 48 | default: 49 | throw new Error('Provider ' + provider + ' not supported.'); 50 | }; 51 | }; 52 | 53 | module.exports = BlockChainExplorer; 54 | -------------------------------------------------------------------------------- /lib/blockchainexplorers/insight.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var $ = require('preconditions').singleton(); 5 | var log = require('npmlog'); 6 | log.debug = log.verbose; 7 | var io = require('socket.io-client'); 8 | var requestList = require('./request-list'); 9 | var Common = require('../common'); 10 | var Constants = Common.Constants, 11 | Defaults = Common.Defaults, 12 | Utils = Common.Utils; 13 | 14 | function Insight(opts) { 15 | $.checkArgument(opts); 16 | $.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS)); 17 | $.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS)); 18 | $.checkArgument(opts.url); 19 | 20 | this.apiPrefix = opts.apiPrefix || '/api'; 21 | this.coin = opts.coin || Defaults.COIN; 22 | this.network = opts.network || 'livenet'; 23 | this.hosts = opts.url; 24 | this.userAgent = opts.userAgent || 'bws'; 25 | }; 26 | 27 | 28 | var _parseErr = function(err, res) { 29 | if (err) { 30 | log.warn('Insight error: ', err); 31 | return "Insight Error"; 32 | } 33 | log.warn("Insight " + res.request.href + " Returned Status: " + res.statusCode); 34 | return "Error querying the blockchain"; 35 | }; 36 | 37 | Insight.prototype._doRequest = function(args, cb) { 38 | var opts = { 39 | hosts: this.hosts, 40 | headers: { 41 | 'User-Agent': this.userAgent, 42 | } 43 | }; 44 | requestList(_.defaults(args, opts), cb); 45 | }; 46 | 47 | Insight.prototype.getConnectionInfo = function() { 48 | return 'Insight (' + this.coin + '/' + this.network + ') @ ' + this.hosts; 49 | }; 50 | 51 | /** 52 | * Retrieve a list of unspent outputs associated with an address or set of addresses 53 | */ 54 | Insight.prototype.getUtxos = function(addresses, cb) { 55 | var url = this.url + this.apiPrefix + '/addrs/utxo'; 56 | var args = { 57 | method: 'POST', 58 | path: this.apiPrefix + '/addrs/utxo', 59 | json: { 60 | addrs: _.uniq([].concat(addresses)).join(',') 61 | }, 62 | }; 63 | 64 | this._doRequest(args, function(err, res, unspent) { 65 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 66 | return cb(null, unspent); 67 | }); 68 | }; 69 | 70 | /** 71 | * Broadcast a transaction to the bitcoin network 72 | */ 73 | Insight.prototype.broadcast = function(rawTx, cb) { 74 | var args = { 75 | method: 'POST', 76 | path: this.apiPrefix + '/tx/send', 77 | json: { 78 | rawtx: rawTx 79 | }, 80 | }; 81 | 82 | this._doRequest(args, function(err, res, body) { 83 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 84 | return cb(null, body ? body.txid : null); 85 | }); 86 | }; 87 | 88 | Insight.prototype.getTransaction = function(txid, cb) { 89 | var args = { 90 | method: 'GET', 91 | path: this.apiPrefix + '/tx/' + txid, 92 | json: true, 93 | }; 94 | 95 | this._doRequest(args, function(err, res, tx) { 96 | if (res && res.statusCode == 404) return cb(); 97 | if (err || res.statusCode !== 200) 98 | return cb(_parseErr(err, res)); 99 | 100 | return cb(null, tx); 101 | }); 102 | }; 103 | 104 | Insight.prototype.getTransactions = function(addresses, from, to, cb) { 105 | var qs = []; 106 | var total; 107 | if (_.isNumber(from)) qs.push('from=' + from); 108 | if (_.isNumber(to)) qs.push('to=' + to); 109 | 110 | // Trim output 111 | qs.push('noAsm=1'); 112 | qs.push('noScriptSig=1'); 113 | qs.push('noSpent=1'); 114 | 115 | var args = { 116 | method: 'POST', 117 | path: this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''), 118 | json: { 119 | addrs: _.uniq([].concat(addresses)).join(',') 120 | }, 121 | timeout: 120000, 122 | }; 123 | 124 | this._doRequest(args, function(err, res, txs) { 125 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 126 | 127 | if (_.isObject(txs)) { 128 | if (txs.totalItems) 129 | total = txs.totalItems; 130 | 131 | if (txs.items) 132 | txs = txs.items; 133 | } 134 | 135 | // NOTE: Whenever Insight breaks communication with bitcoind, it returns invalid data but no error code. 136 | if (!_.isArray(txs) || (txs.length != _.compact(txs).length)) return cb(new Error('Could not retrieve transactions from blockchain. Request was:' + JSON.stringify(args))); 137 | 138 | return cb(null, txs, total); 139 | }); 140 | }; 141 | 142 | Insight.prototype.getAddressActivity = function(address, cb) { 143 | var self = this; 144 | 145 | var args = { 146 | method: 'GET', 147 | path: self.apiPrefix + '/addr/' + address, 148 | json: true, 149 | }; 150 | 151 | this._doRequest(args, function(err, res, result) { 152 | if (res && res.statusCode == 404) return cb(); 153 | if (err || res.statusCode !== 200) 154 | return cb(_parseErr(err, res)); 155 | 156 | var nbTxs = result.unconfirmedTxApperances + result.txApperances; 157 | return cb(null, nbTxs > 0); 158 | }); 159 | }; 160 | 161 | Insight.prototype.estimateFee = function(nbBlocks, cb) { 162 | var path = this.apiPrefix + '/utils/estimatefee'; 163 | if (nbBlocks) { 164 | path += '?nbBlocks=' + [].concat(nbBlocks).join(','); 165 | } 166 | 167 | var args = { 168 | method: 'GET', 169 | path: path, 170 | json: true, 171 | }; 172 | this._doRequest(args, function(err, res, body) { 173 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 174 | return cb(null, body); 175 | }); 176 | }; 177 | 178 | Insight.prototype.getBlockchainHeight = function(cb) { 179 | var path = this.apiPrefix + '/sync'; 180 | 181 | var args = { 182 | method: 'GET', 183 | path: path, 184 | json: true, 185 | }; 186 | this._doRequest(args, function(err, res, body) { 187 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 188 | return cb(null, body.blockChainHeight); 189 | }); 190 | }; 191 | 192 | Insight.prototype.getTxidsInBlock = function(blockHash, cb) { 193 | var self = this; 194 | 195 | var args = { 196 | method: 'GET', 197 | path: this.apiPrefix + '/block/' + blockHash, 198 | json: true, 199 | }; 200 | 201 | this._doRequest(args, function(err, res, body) { 202 | if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); 203 | return cb(null, body.tx); 204 | }); 205 | }; 206 | 207 | Insight.prototype.initSocket = function() { 208 | 209 | // sockets always use the first server on the pull 210 | var socket = io.connect(_.first([].concat(this.hosts)), { 211 | 'reconnection': true, 212 | }); 213 | return socket; 214 | }; 215 | 216 | module.exports = Insight; 217 | -------------------------------------------------------------------------------- /lib/blockchainexplorers/request-list.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | var $ = require('preconditions').singleton(); 4 | 5 | var log = require('npmlog'); 6 | log.debug = log.verbose; 7 | 8 | 9 | var DEFAULT_TIMEOUT= 60000; // 60 s 10 | /** 11 | * Query a server, using one of the given options 12 | * 13 | * @param {Object} opts 14 | * @param {Array} opts.hosts Array of hosts to query. Until the first success one. 15 | * @param {Array} opts.path Path to request in each server 16 | */ 17 | var requestList = function(args, cb) { 18 | $.checkArgument(args.hosts); 19 | request = args.request || require('request'); 20 | 21 | if (!_.isArray(args.hosts)) 22 | args.hosts = [args.hosts]; 23 | 24 | args.timeout = args.timeout || DEFAULT_TIMEOUT; 25 | 26 | var urls = _.map(args.hosts, function(x) { 27 | return (x + args.path); 28 | }); 29 | var nextUrl, result, success; 30 | 31 | async.whilst( 32 | function() { 33 | nextUrl = urls.shift(); 34 | return nextUrl && !success; 35 | }, 36 | function(a_cb) { 37 | args.uri = nextUrl; 38 | request(args, function(err, res, body) { 39 | if (err) { 40 | log.warn('REQUEST FAIL: ' + nextUrl + ' ERROR: ' + err); 41 | } 42 | 43 | if (res) { 44 | success = !!res.statusCode.toString().match(/^[1234]../); 45 | if (!success) { 46 | log.warn('REQUEST FAIL: ' + nextUrl + ' STATUS CODE: ' + res.statusCode); 47 | } 48 | } 49 | 50 | result = [err, res, body]; 51 | return a_cb(); 52 | }); 53 | }, 54 | function(err) { 55 | if (err) return cb(err); 56 | return cb(result[0], result[1], result[2]); 57 | } 58 | ); 59 | }; 60 | 61 | module.exports = requestList; 62 | -------------------------------------------------------------------------------- /lib/blockchainmonitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('preconditions').singleton(); 4 | var _ = require('lodash'); 5 | var async = require('async'); 6 | var log = require('npmlog'); 7 | log.debug = log.verbose; 8 | 9 | var BlockchainExplorer = require('./blockchainexplorer'); 10 | var Storage = require('./storage'); 11 | var MessageBroker = require('./messagebroker'); 12 | var Lock = require('./lock'); 13 | 14 | var Notification = require('./model/notification'); 15 | 16 | var WalletService = require('./server'); 17 | var Constants = require('./common/constants'); 18 | 19 | function BlockchainMonitor() {}; 20 | 21 | BlockchainMonitor.prototype.start = function(opts, cb) { 22 | opts = opts || {}; 23 | 24 | var self = this; 25 | 26 | async.parallel([ 27 | 28 | function(done) { 29 | self.explorers = { 30 | btc: {}, 31 | bch: {}, 32 | }; 33 | 34 | var coinNetworkPairs = []; 35 | _.each(_.values(Constants.COINS), function(coin) { 36 | _.each(_.values(Constants.NETWORKS), function(network) { 37 | coinNetworkPairs.push({ 38 | coin: coin, 39 | network: network 40 | }); 41 | }); 42 | }); 43 | _.each(coinNetworkPairs, function(pair) { 44 | var explorer; 45 | if (opts.blockchainExplorers && opts.blockchainExplorers[pair.coin] && opts.blockchainExplorers[pair.coin][pair.network]) { 46 | explorer = opts.blockchainExplorers[pair.coin][pair.network]; 47 | } else { 48 | var config = {} 49 | if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[pair.coin] && opts.blockchainExplorerOpts[pair.coin][pair.network]) { 50 | config = opts.blockchainExplorerOpts[pair.coin][pair.network]; 51 | } else { 52 | return; 53 | } 54 | var explorer = new BlockchainExplorer({ 55 | provider: config.provider, 56 | coin: pair.coin, 57 | network: pair.network, 58 | url: config.url, 59 | userAgent: WalletService.getServiceVersion(), 60 | }); 61 | } 62 | $.checkState(explorer); 63 | self._initExplorer(pair.coin, pair.network, explorer); 64 | self.explorers[pair.coin][pair.network] = explorer; 65 | }); 66 | done(); 67 | }, 68 | function(done) { 69 | if (opts.storage) { 70 | self.storage = opts.storage; 71 | done(); 72 | } else { 73 | self.storage = new Storage(); 74 | self.storage.connect(opts.storageOpts, done); 75 | } 76 | }, 77 | function(done) { 78 | self.messageBroker = opts.messageBroker || new MessageBroker(opts.messageBrokerOpts); 79 | done(); 80 | }, 81 | function(done) { 82 | self.lock = opts.lock || new Lock(opts.lockOpts); 83 | done(); 84 | }, 85 | ], function(err) { 86 | if (err) { 87 | log.error(err); 88 | } 89 | return cb(err); 90 | }); 91 | }; 92 | 93 | BlockchainMonitor.prototype._initExplorer = function(coin, network, explorer) { 94 | var self = this; 95 | 96 | var socket = explorer.initSocket(); 97 | 98 | socket.on('connect', function() { 99 | log.info('Connected to ' + explorer.getConnectionInfo()); 100 | socket.emit('subscribe', 'inv'); 101 | }); 102 | socket.on('connect_error', function() { 103 | log.error('Error connecting to ' + explorer.getConnectionInfo()); 104 | }); 105 | socket.on('tx', _.bind(self._handleIncomingTx, self, coin, network)); 106 | socket.on('block', _.bind(self._handleNewBlock, self, coin, network)); 107 | }; 108 | 109 | BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, processIt) { 110 | var self = this; 111 | if (!data || !data.txid) return; 112 | 113 | self.storage.fetchTxByHash(data.txid, function(err, txp) { 114 | if (err) { 115 | log.error('Could not fetch tx from the db'); 116 | return; 117 | } 118 | if (!txp || txp.status != 'accepted') return; 119 | 120 | var walletId = txp.walletId; 121 | 122 | if (!processIt) { 123 | log.info('Detected broadcast ' + data.txid + ' of an accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]'); 124 | return setTimeout(self._handleThirdPartyBroadcasts.bind(self, data, true), 20 * 1000); 125 | } 126 | 127 | log.info('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]'); 128 | 129 | txp.setBroadcasted(); 130 | 131 | self.storage.softResetTxHistoryCache(walletId, function() { 132 | self.storage.storeTx(self.walletId, txp, function(err) { 133 | if (err) 134 | log.error('Could not save TX'); 135 | 136 | var args = { 137 | txProposalId: txp.id, 138 | txid: data.txid, 139 | amount: txp.getTotalAmount(), 140 | }; 141 | 142 | var notification = Notification.create({ 143 | type: 'NewOutgoingTxByThirdParty', 144 | data: args, 145 | walletId: walletId, 146 | }); 147 | self._storeAndBroadcastNotification(notification); 148 | }); 149 | }); 150 | }); 151 | }; 152 | 153 | var incomingTxIds = {}; 154 | 155 | BlockchainMonitor.prototype._handleIncomingPayments = function(coin, network, data) { 156 | var self = this; 157 | 158 | if (!data || !data.vout) return; 159 | 160 | var outs = _.compact(_.map(data.vout, function(v) { 161 | var addr = _.keys(v)[0]; 162 | 163 | return { 164 | address: addr, 165 | amount: +v[addr] 166 | }; 167 | })); 168 | if (_.isEmpty(outs)) return; 169 | if (!data.txid) return; 170 | 171 | if (incomingTxIds[data.txid]) { 172 | return; 173 | } else { 174 | incomingTxIds[data.txid] = true; 175 | } 176 | 177 | async.each(outs, function(out, next) { 178 | self.storage.fetchAddressByCoin(coin, out.address, function(err, address) { 179 | if (err) { 180 | log.error('Could not fetch addresses from the db'); 181 | return next(err); 182 | } 183 | if (!address || address.isChange) return next(); 184 | 185 | var walletId = address.walletId; 186 | log.info('Incoming tx for wallet ' + walletId + ' [' + out.amount + 'sat -> ' + out.address + ']'); 187 | 188 | var fromTs = Date.now() - 24 * 3600 * 1000; 189 | self.storage.fetchNotifications(walletId, null, fromTs, function(err, notifications) { 190 | if (err) return next(err); 191 | var alreadyNotified = _.any(notifications, function(n) { 192 | return n.type == 'NewIncomingTx' && n.data && n.data.txid == data.txid; 193 | }); 194 | if (alreadyNotified) { 195 | log.info('The incoming tx ' + data.txid + ' was already notified'); 196 | return next(); 197 | } 198 | 199 | var notification = Notification.create({ 200 | type: 'NewIncomingTx', 201 | data: { 202 | txid: data.txid, 203 | address: out.address, 204 | amount: out.amount, 205 | }, 206 | walletId: walletId, 207 | }); 208 | self.storage.softResetTxHistoryCache(walletId, function() { 209 | self._updateActiveAddresses(address, function() { 210 | self._storeAndBroadcastNotification(notification, function() { 211 | delete incomingTxIds[data.txid]; 212 | return next(); 213 | }); 214 | }); 215 | }); 216 | }); 217 | }); 218 | }, function(err) { 219 | return; 220 | }); 221 | }; 222 | 223 | BlockchainMonitor.prototype._updateActiveAddresses = function(address, cb) { 224 | var self = this; 225 | 226 | self.storage.storeActiveAddresses(address.walletId, address.address, function(err) { 227 | if (err) { 228 | log.warn('Could not update wallet cache', err); 229 | } 230 | return cb(err); 231 | }); 232 | }; 233 | 234 | BlockchainMonitor.prototype._handleIncomingTx = function(coin, network, data) { 235 | this._handleThirdPartyBroadcasts(data); 236 | this._handleIncomingPayments(coin, network, data); 237 | }; 238 | 239 | BlockchainMonitor.prototype._notifyNewBlock = function(coin, network, hash) { 240 | var self = this; 241 | 242 | log.info('New ' + network + ' block: ' + hash); 243 | var notification = Notification.create({ 244 | type: 'NewBlock', 245 | walletId: network, // use network name as wallet id for global notifications 246 | data: { 247 | hash: hash, 248 | coin: coin, 249 | network: network, 250 | }, 251 | }); 252 | 253 | self.storage.softResetAllTxHistoryCache(function() { 254 | self._storeAndBroadcastNotification(notification, function(err) { 255 | return; 256 | }); 257 | }); 258 | }; 259 | 260 | BlockchainMonitor.prototype._handleTxConfirmations = function(coin, network, hash) { 261 | var self = this; 262 | 263 | function processTriggeredSubs(subs, cb) { 264 | async.each(subs, function(sub) { 265 | log.info('New tx confirmation ' + sub.txid); 266 | sub.isActive = false; 267 | self.storage.storeTxConfirmationSub(sub, function(err) { 268 | if (err) return cb(err); 269 | 270 | /* 271 | var notification = Notification.create({ 272 | type: 'TxConfirmation', 273 | walletId: sub.walletId, 274 | creatorId: sub.copayerId, 275 | data: { 276 | txid: sub.txid, 277 | coin: coin, 278 | network: network, 279 | // TODO: amount 280 | }, 281 | }); 282 | self._storeAndBroadcastNotification(notification, cb); 283 | */ 284 | }); 285 | }); 286 | }; 287 | 288 | var explorer = self.explorers[coin][network]; 289 | if (!explorer) return; 290 | 291 | explorer.getTxidsInBlock(hash, function(err, txids) { 292 | if (err) { 293 | log.error('Could not fetch txids from block ' + hash, err); 294 | return; 295 | } 296 | 297 | self.storage.fetchActiveTxConfirmationSubs(null, function(err, subs) { 298 | if (err) return; 299 | if (_.isEmpty(subs)) return; 300 | var indexedSubs = _.indexBy(subs, 'txid'); 301 | var triggered = []; 302 | _.each(txids, function(txid) { 303 | if (indexedSubs[txid]) triggered.push(indexedSubs[txid]); 304 | }); 305 | processTriggeredSubs(triggered, function(err) { 306 | if (err) { 307 | log.error('Could not process tx confirmations', err); 308 | } 309 | return; 310 | }); 311 | }); 312 | }); 313 | }; 314 | 315 | BlockchainMonitor.prototype._handleNewBlock = function(coin, network, hash) { 316 | this._notifyNewBlock(coin, network, hash); 317 | this._handleTxConfirmations(coin, network, hash); 318 | }; 319 | 320 | BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) { 321 | var self = this; 322 | 323 | self.storage.storeNotification(notification.walletId, notification, function() { 324 | self.messageBroker.send(notification) 325 | if (cb) return cb(); 326 | }); 327 | }; 328 | 329 | module.exports = BlockchainMonitor; 330 | -------------------------------------------------------------------------------- /lib/common/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Constants = {}; 4 | 5 | Constants.COINS = { 6 | BTC: 'btc', 7 | BCH: 'bch', 8 | }; 9 | 10 | Constants.NETWORKS = { 11 | LIVENET: 'livenet', 12 | TESTNET: 'testnet', 13 | }; 14 | 15 | Constants.SCRIPT_TYPES = { 16 | P2SH: 'P2SH', 17 | P2PKH: 'P2PKH', 18 | }; 19 | Constants.DERIVATION_STRATEGIES = { 20 | BIP44: 'BIP44', 21 | BIP45: 'BIP45', 22 | }; 23 | 24 | Constants.PATHS = { 25 | REQUEST_KEY: "m/1'/0", 26 | TXPROPOSAL_KEY: "m/1'/1", 27 | REQUEST_KEY_AUTH: "m/2", // relative to BASE 28 | }; 29 | 30 | Constants.BIP45_SHARED_INDEX = 0x80000000 - 1; 31 | 32 | module.exports = Constants; 33 | -------------------------------------------------------------------------------- /lib/common/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Defaults = {}; 4 | 5 | Defaults.MIN_FEE_PER_KB = 0; 6 | Defaults.MAX_FEE_PER_KB = 1000000; 7 | Defaults.MIN_TX_FEE = 0; 8 | Defaults.MAX_TX_FEE = 0.1 * 1e8; 9 | Defaults.MAX_TX_SIZE_IN_KB = 100; 10 | 11 | Defaults.MAX_KEYS = 100; 12 | 13 | // Time after which a tx proposal can be erased by any copayer. in seconds 14 | Defaults.DELETE_LOCKTIME = 600; 15 | 16 | // Allowed consecutive txp rejections before backoff is applied. 17 | Defaults.BACKOFF_OFFSET = 10; 18 | 19 | // Time a copayer need to wait to create a new tx after her previous proposal was rejected. in seconds. 20 | Defaults.BACKOFF_TIME = 600; 21 | 22 | Defaults.MAX_MAIN_ADDRESS_GAP = 20; 23 | 24 | // TODO: should allow different gap sizes for external/internal chains 25 | Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP + 20; 26 | 27 | Defaults.FEE_LEVELS = { 28 | btc: [{ 29 | name: 'urgent', 30 | nbBlocks: 2, 31 | multiplier: 1.5, 32 | defaultValue: 150000, 33 | }, { 34 | name: 'priority', 35 | nbBlocks: 2, 36 | defaultValue: 100000 37 | }, { 38 | name: 'normal', 39 | nbBlocks: 3, 40 | defaultValue: 80000 41 | }, { 42 | name: 'economy', 43 | nbBlocks: 6, 44 | defaultValue: 50000 45 | }, { 46 | name: 'superEconomy', 47 | nbBlocks: 24, 48 | defaultValue: 20000 49 | }], 50 | bch: [{ 51 | name: 'normal', 52 | nbBlocks: 2, 53 | defaultValue: 2000, 54 | }] 55 | }; 56 | 57 | // How many levels to fallback to if the value returned by the network for a given nbBlocks is -1 58 | Defaults.FEE_LEVELS_FALLBACK = 2; 59 | 60 | // Minimum nb of addresses a wallet must have to start using 2-step balance optimization 61 | Defaults.TWO_STEP_BALANCE_THRESHOLD = 100; 62 | 63 | Defaults.FIAT_RATE_PROVIDER = 'BitPay'; 64 | Defaults.FIAT_RATE_FETCH_INTERVAL = 10; // In minutes 65 | Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME = 120; // In minutes 66 | 67 | Defaults.HISTORY_LIMIT = 50; 68 | 69 | // The maximum amount of an UTXO to be considered too big to be used in the tx before exploring smaller 70 | // alternatives (proportinal to tx amount). 71 | Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR = 2; 72 | 73 | // The minimum amount an UTXO need to contribute proportional to tx amount. 74 | Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR = 0.1; 75 | 76 | // The maximum threshold to consider fees non-significant in relation to tx amount. 77 | Defaults.UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR = 0.05; 78 | 79 | // The maximum amount to pay for using small inputs instead of one big input 80 | // when fees are significant (proportional to how much we would pay for using that big input only). 81 | Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR = 5; 82 | 83 | // Minimum allowed amount for tx outputs (including change) in SAT 84 | Defaults.MIN_OUTPUT_AMOUNT = 5000; 85 | 86 | // Number of confirmations from which tx in history will be cached 87 | // (ie we consider them inmutables) 88 | Defaults.CONFIRMATIONS_TO_START_CACHING = 6 * 6; // ~ 6hrs 89 | 90 | // Number of addresses from which tx history is enabled in a wallet 91 | Defaults.HISTORY_CACHE_ADDRESS_THRESOLD = 100; 92 | 93 | // Cache time for blockchain height (in seconds) 94 | Defaults.BLOCKHEIGHT_CACHE_TIME = 10 * 60; 95 | 96 | 97 | // Max allowed timespan for notification queries in seconds 98 | Defaults.MAX_NOTIFICATIONS_TIMESPAN = 60 * 60 * 24 * 14; // ~ 2 weeks 99 | Defaults.NOTIFICATIONS_TIMESPAN = 60; 100 | 101 | Defaults.SESSION_EXPIRATION = 1 * 60 * 60; // 1 hour to session expiration 102 | 103 | Defaults.RateLimit = { 104 | createWallet: { 105 | windowMs: 60 * 60 * 1000, // hour window 106 | delayAfter: 10, // begin slowing down responses after the 3rd request 107 | delayMs: 3000, // slow down subsequent responses by 3 seconds per request 108 | max: 20, // start blocking after 20 request 109 | message: "Too many wallets created from this IP, please try again after an hour" 110 | }, 111 | // otherPosts: { 112 | // windowMs: 60 * 60 * 1000, // 1 hour window 113 | // max: 1200 , // 1 post every 3 sec average, max. 114 | // }, 115 | }; 116 | 117 | Defaults.COIN = 'btc'; 118 | 119 | module.exports = Defaults; 120 | -------------------------------------------------------------------------------- /lib/common/index.js: -------------------------------------------------------------------------------- 1 | var Common = {}; 2 | 3 | Common.Constants = require('./constants'); 4 | Common.Defaults = require('./defaults'); 5 | Common.Utils = require('./utils'); 6 | 7 | module.exports = Common; 8 | -------------------------------------------------------------------------------- /lib/common/utils.js: -------------------------------------------------------------------------------- 1 | var $ = require('preconditions').singleton(); 2 | var _ = require('lodash'); 3 | 4 | var bitcore = require('bitcore-lib'); 5 | var crypto = bitcore.crypto; 6 | var encoding = bitcore.encoding; 7 | var secp256k1 = require('secp256k1'); 8 | 9 | var Utils = {}; 10 | var Bitcore = require('bitcore-lib'); 11 | var Bitcore_ = { 12 | btc: Bitcore, 13 | bch: require('bitcore-lib-cash') 14 | }; 15 | 16 | 17 | 18 | Utils.getMissingFields = function(obj, args) { 19 | args = [].concat(args); 20 | if (!_.isObject(obj)) return args; 21 | var missing = _.filter(args, function(arg) { 22 | return !obj.hasOwnProperty(arg); 23 | }); 24 | return missing; 25 | }; 26 | 27 | /** 28 | * 29 | * @desc rounds a JAvascript number 30 | * @param number 31 | * @return {number} 32 | */ 33 | Utils.strip = function(number) { 34 | return parseFloat(number.toPrecision(12)); 35 | } 36 | 37 | /* TODO: It would be nice to be compatible with bitcoind signmessage. How 38 | * the hash is calculated there? */ 39 | Utils.hashMessage = function(text, noReverse) { 40 | $.checkArgument(text); 41 | var buf = new Buffer(text); 42 | var ret = crypto.Hash.sha256sha256(buf); 43 | if (!noReverse) { 44 | ret = new bitcore.encoding.BufferReader(ret).readReverse(); 45 | } 46 | return ret; 47 | }; 48 | 49 | Utils.verifyMessage = function(text, signature, publicKey) { 50 | $.checkArgument(text); 51 | 52 | var hash = Utils.hashMessage(text, true); 53 | 54 | var sig = this._tryImportSignature(signature); 55 | if (!sig) { 56 | return false; 57 | } 58 | 59 | var publicKeyBuffer = this._tryImportPublicKey(publicKey); 60 | if (!publicKeyBuffer) { 61 | return false; 62 | } 63 | 64 | return this._tryVerifyMessage(hash, sig, publicKeyBuffer); 65 | }; 66 | 67 | Utils._tryImportPublicKey = function(publicKey) { 68 | var publicKeyBuffer = publicKey; 69 | try { 70 | if (!Buffer.isBuffer(publicKey)) { 71 | publicKeyBuffer = new Buffer(publicKey, 'hex'); 72 | } 73 | return publicKeyBuffer; 74 | } catch (e) { 75 | return false; 76 | } 77 | }; 78 | 79 | Utils._tryImportSignature = function(signature) { 80 | try { 81 | var signatureBuffer = signature; 82 | if (!Buffer.isBuffer(signature)) { 83 | signatureBuffer = new Buffer(signature, 'hex'); 84 | } 85 | return secp256k1.signatureImport(signatureBuffer); 86 | } catch (e) { 87 | return false; 88 | } 89 | }; 90 | 91 | Utils._tryVerifyMessage = function(hash, sig, publicKeyBuffer) { 92 | try { 93 | return secp256k1.verify(hash, sig, publicKeyBuffer); 94 | } catch (e) { 95 | return false; 96 | } 97 | }; 98 | 99 | Utils.formatAmount = function(satoshis, unit, opts) { 100 | var UNITS = { 101 | btc: { 102 | toSatoshis: 100000000, 103 | maxDecimals: 6, 104 | minDecimals: 2, 105 | }, 106 | bit: { 107 | toSatoshis: 100, 108 | maxDecimals: 0, 109 | minDecimals: 0, 110 | }, 111 | sat: { 112 | toSatoshis: 1, 113 | maxDecimals: 0, 114 | minDecimals: 0, 115 | }, 116 | bch: { 117 | toSatoshis: 100000000, 118 | maxDecimals: 6, 119 | minDecimals: 2, 120 | }, 121 | }; 122 | 123 | $.shouldBeNumber(satoshis); 124 | $.checkArgument(_.contains(_.keys(UNITS), unit)); 125 | 126 | function addSeparators(nStr, thousands, decimal, minDecimals) { 127 | nStr = nStr.replace('.', decimal); 128 | var x = nStr.split(decimal); 129 | var x0 = x[0]; 130 | var x1 = x[1]; 131 | 132 | x1 = _.dropRightWhile(x1, function(n, i) { 133 | return n == '0' && i >= minDecimals; 134 | }).join(''); 135 | var x2 = x.length > 1 ? decimal + x1 : ''; 136 | 137 | x0 = x0.replace(/\B(?=(\d{3})+(?!\d))/g, thousands); 138 | return x0 + x2; 139 | } 140 | 141 | opts = opts || {}; 142 | 143 | var u = _.assign(UNITS[unit], opts); 144 | var amount = (satoshis / u.toSatoshis).toFixed(u.maxDecimals); 145 | return addSeparators(amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u.minDecimals); 146 | }; 147 | 148 | Utils.formatAmountInBtc = function(amount) { 149 | return Utils.formatAmount(amount, 'btc', { 150 | minDecimals: 8, 151 | maxDecimals: 8, 152 | }) + 'btc'; 153 | }; 154 | 155 | Utils.formatUtxos = function(utxos) { 156 | if (_.isEmpty(utxos)) return 'none'; 157 | return _.map([].concat(utxos), function(i) { 158 | var amount = Utils.formatAmountInBtc(i.satoshis); 159 | var confirmations = i.confirmations ? i.confirmations + 'c' : 'u'; 160 | return amount + '/' + confirmations; 161 | }).join(', '); 162 | }; 163 | 164 | Utils.formatRatio = function(ratio) { 165 | return (ratio * 100.).toFixed(4) + '%'; 166 | }; 167 | 168 | Utils.formatSize = function(size) { 169 | return (size / 1000.).toFixed(4) + 'kB'; 170 | }; 171 | 172 | Utils.parseVersion = function(version) { 173 | var v = {}; 174 | 175 | if (!version) return null; 176 | 177 | var x = version.split('-'); 178 | if (x.length != 2) { 179 | v.agent = version; 180 | return v; 181 | } 182 | v.agent = _.contains(['bwc', 'bws'], x[0]) ? 'bwc' : x[0]; 183 | x = x[1].split('.'); 184 | v.major = parseInt(x[0]); 185 | v.minor = parseInt(x[1]); 186 | v.patch = parseInt(x[2]); 187 | 188 | return v; 189 | }; 190 | 191 | Utils.checkValueInCollection = function(value, collection) { 192 | if (!value || !_.isString(value)) return false; 193 | return _.contains(_.values(collection), value); 194 | }; 195 | 196 | 197 | Utils.getAddressCoin = function(address) { 198 | try { 199 | new Bitcore_['btc'].Address(address); 200 | return 'btc'; 201 | } catch (e) { 202 | try { 203 | new Bitcore_['bch'].Address(address); 204 | return 'bch'; 205 | } catch (e) { 206 | return; 207 | } 208 | } 209 | }; 210 | 211 | Utils.translateAddress = function(address, coin) { 212 | var origCoin = Utils.getAddressCoin(address); 213 | var origAddress = new Bitcore_[origCoin].Address(address); 214 | var origObj = origAddress.toObject(); 215 | 216 | var result = Bitcore_[coin].Address.fromObject(origObj) 217 | return result.toString(); 218 | }; 219 | 220 | 221 | 222 | module.exports = Utils; 223 | -------------------------------------------------------------------------------- /lib/errors/clienterror.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ClientError() { 4 | var args = Array.prototype.slice.call(arguments); 5 | 6 | switch (args.length) { 7 | case 0: 8 | this.code = 'BADREQUEST'; 9 | this.message = 'Bad request'; 10 | break; 11 | case 1: 12 | this.code = 'BADREQUEST'; 13 | this.message = args[0]; 14 | break; 15 | default: 16 | case 2: 17 | this.code = args[0]; 18 | this.message = args[1]; 19 | break; 20 | } 21 | }; 22 | 23 | ClientError.prototype.toString = function() { 24 | return ''; 25 | }; 26 | 27 | module.exports = ClientError; 28 | -------------------------------------------------------------------------------- /lib/errors/errordefinitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var ClientError = require('./clienterror'); 6 | 7 | var errors = { 8 | BAD_SIGNATURES: 'Bad signatures', 9 | COPAYER_DATA_MISMATCH: 'Copayer data mismatch', 10 | COPAYER_IN_WALLET: 'Copayer already in wallet', 11 | COPAYER_REGISTERED: 'Copayer ID already registered on server', 12 | COPAYER_VOTED: 'Copayer already voted on this transaction proposal', 13 | DUST_AMOUNT: 'Amount below dust threshold', 14 | INCORRECT_ADDRESS_NETWORK: 'Incorrect address network', 15 | INSUFFICIENT_FUNDS: 'Insufficient funds', 16 | INSUFFICIENT_FUNDS_FOR_FEE: 'Insufficient funds for fee', 17 | INVALID_ADDRESS: 'Invalid address', 18 | INVALID_CHANGE_ADDRESS: 'Invalid change address', 19 | KEY_IN_COPAYER: 'Key already registered', 20 | LOCKED_FUNDS: 'Funds are locked by pending transaction proposals', 21 | HISTORY_LIMIT_EXCEEDED: 'Requested page limit is above allowed maximum', 22 | MAIN_ADDRESS_GAP_REACHED: 'Maximum number of consecutive addresses without activity reached', 23 | NOT_AUTHORIZED: 'Not authorized', 24 | TOO_MANY_KEYS: 'Too many keys registered', 25 | TX_ALREADY_BROADCASTED: 'The transaction proposal is already broadcasted', 26 | TX_CANNOT_CREATE: 'Cannot create TX proposal during backoff time', 27 | TX_CANNOT_REMOVE: 'Cannot remove this tx proposal during locktime', 28 | TX_MAX_SIZE_EXCEEDED: 'TX exceeds maximum allowed size', 29 | TX_NOT_ACCEPTED: 'The transaction proposal is not accepted', 30 | TX_NOT_FOUND: 'Transaction proposal not found', 31 | TX_NOT_PENDING: 'The transaction proposal is not pending', 32 | UNAVAILABLE_UTXOS: 'Unavailable unspent outputs', 33 | UPGRADE_NEEDED: 'Client app needs to be upgraded', 34 | WALLET_ALREADY_EXISTS: 'Wallet already exists', 35 | WALLET_FULL: 'Wallet full', 36 | WALLET_LOCKED: 'Wallet is locked', 37 | WALLET_NOT_COMPLETE: 'Wallet is not complete', 38 | WALLET_NOT_FOUND: 'Wallet not found', 39 | }; 40 | 41 | var errorObjects = _.zipObject(_.map(errors, function(msg, code) { 42 | return [code, new ClientError(code, msg)]; 43 | })); 44 | 45 | errorObjects.codes = _.mapValues(errors, function(v, k) { 46 | return k; 47 | }); 48 | 49 | module.exports = errorObjects; 50 | -------------------------------------------------------------------------------- /lib/fiatrateproviders/bitpay.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var provider = { 4 | name: 'BitPay', 5 | url: 'https://bitpay.com/api/rates/', 6 | parseFn: function(raw) { 7 | var rates = _.compact(_.map(raw, function(d) { 8 | if (!d.code || !d.rate) return null; 9 | return { 10 | code: d.code, 11 | value: d.rate, 12 | }; 13 | })); 14 | return rates; 15 | }, 16 | }; 17 | 18 | module.exports = provider; 19 | -------------------------------------------------------------------------------- /lib/fiatrateproviders/bitstamp.js: -------------------------------------------------------------------------------- 1 | var provider = { 2 | name: 'Bitstamp', 3 | url: 'https://www.bitstamp.net/api/ticker/', 4 | parseFn: function(raw) { 5 | return [{ 6 | code: 'USD', 7 | value: parseFloat(raw.last) 8 | }]; 9 | } 10 | }; 11 | 12 | module.exports = provider; 13 | -------------------------------------------------------------------------------- /lib/fiatrateproviders/index.js: -------------------------------------------------------------------------------- 1 | var Providers = { 2 | BitPay: require('./bitpay'), 3 | Bitstamp: require('./bitstamp'), 4 | } 5 | 6 | module.exports = Providers; 7 | -------------------------------------------------------------------------------- /lib/fiatrateservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var $ = require('preconditions').singleton(); 5 | var async = require('async'); 6 | var log = require('npmlog'); 7 | log.debug = log.verbose; 8 | var request = require('request'); 9 | 10 | var Common = require('./common'); 11 | var Defaults = Common.Defaults; 12 | 13 | var Storage = require('./storage'); 14 | var Model = require('./model'); 15 | 16 | function FiatRateService() {}; 17 | 18 | FiatRateService.prototype.init = function(opts, cb) { 19 | var self = this; 20 | 21 | opts = opts || {}; 22 | 23 | self.request = opts.request || request; 24 | self.defaultProvider = opts.defaultProvider || Defaults.FIAT_RATE_PROVIDER; 25 | 26 | async.parallel([ 27 | 28 | function(done) { 29 | if (opts.storage) { 30 | self.storage = opts.storage; 31 | done(); 32 | } else { 33 | self.storage = new Storage(); 34 | self.storage.connect(opts.storageOpts, done); 35 | } 36 | }, 37 | ], function(err) { 38 | if (err) { 39 | log.error("Error fiatrateservice.js line 39: " + err); 40 | } 41 | return cb(err); 42 | }); 43 | }; 44 | 45 | FiatRateService.prototype.startCron = function(opts, cb) { 46 | var self = this; 47 | 48 | opts = opts || {}; 49 | 50 | self.providers = _.values(require('./fiatrateproviders')); 51 | 52 | var interval = opts.fetchInterval || Defaults.FIAT_RATE_FETCH_INTERVAL; 53 | if (interval) { 54 | self._fetch(); 55 | setInterval(function() { 56 | self._fetch(); 57 | }, interval * 60 * 1000); 58 | } 59 | 60 | return cb(); 61 | }; 62 | 63 | FiatRateService.prototype._fetch = function(cb) { 64 | var self = this; 65 | 66 | cb = cb || function() {}; 67 | 68 | async.each(self.providers, function(provider, next) { 69 | self._retrieve(provider, function(err, res) { 70 | if (err) { 71 | log.warn('Error retrieving data for ' + provider.name, err); 72 | return next(); 73 | } 74 | self.storage.storeFiatRate(provider.name, res, function(err) { 75 | if (err) { 76 | log.warn('Error storing data for ' + provider.name, err); 77 | } 78 | return next(); 79 | }); 80 | }); 81 | }, cb); 82 | }; 83 | 84 | FiatRateService.prototype._retrieve = function(provider, cb) { 85 | var self = this; 86 | 87 | log.debug('Fetching data for ' + provider.name); 88 | self.request.get({ 89 | url: provider.url, 90 | json: true, 91 | }, function(err, res, body) { 92 | if (err || !body) { 93 | return cb(err); 94 | } 95 | 96 | log.debug('Data for ' + provider.name + ' fetched successfully'); 97 | 98 | if (!provider.parseFn) { 99 | return cb(new Error('No parse function for provider ' + provider.name)); 100 | } 101 | var rates = provider.parseFn(body); 102 | 103 | return cb(null, rates); 104 | }); 105 | }; 106 | 107 | 108 | FiatRateService.prototype.getRate = function(opts, cb) { 109 | var self = this; 110 | 111 | $.shouldBeFunction(cb); 112 | 113 | opts = opts || {}; 114 | 115 | var now = Date.now(); 116 | var provider = opts.provider || self.defaultProvider; 117 | var ts = (_.isNumber(opts.ts) || _.isArray(opts.ts)) ? opts.ts : now; 118 | 119 | async.map([].concat(ts), function(ts, cb) { 120 | self.storage.fetchFiatRate(provider, opts.code, ts, function(err, rate) { 121 | if (err) return cb(err); 122 | if (rate && (ts - rate.ts) > Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME * 60 * 1000) rate = null; 123 | 124 | return cb(null, { 125 | ts: +ts, 126 | rate: rate ? rate.value : undefined, 127 | fetchedOn: rate ? rate.ts : undefined, 128 | }); 129 | }); 130 | }, function(err, res) { 131 | if (err) return cb(err); 132 | if (!_.isArray(ts)) res = res[0]; 133 | return cb(null, res); 134 | }); 135 | }; 136 | 137 | 138 | module.exports = FiatRateService; 139 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/bitcore-wallet-service/8825de4b1ff370893e952bae0d6fecb2bcc6fa0d/lib/index.js -------------------------------------------------------------------------------- /lib/locallock.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var $ = require('preconditions').singleton(); 3 | 4 | function Lock() { 5 | this.tasks = {}; 6 | }; 7 | 8 | Lock.prototype._release = function(token, task) { 9 | if (!task.running) return; 10 | task.running = false; 11 | this.tasks[token] = _.without(this.tasks[token], task); 12 | this._runOne(token); 13 | }; 14 | 15 | Lock.prototype._runOne = function(token) { 16 | var self = this; 17 | 18 | if (_.any(self.tasks[token], { 19 | running: true 20 | })) return; 21 | 22 | var task = _.first(self.tasks[token]); 23 | if (!task) return; 24 | 25 | task.running = true; 26 | 27 | if (task.timeout > 0) { 28 | setTimeout(function() { 29 | self._release(token, task); 30 | }, task.timeout); 31 | } 32 | 33 | task.fn(null, function() { 34 | self._release(token, task); 35 | }); 36 | }; 37 | 38 | Lock.prototype.locked = function(token, wait, max, userTask) { 39 | var self = this; 40 | 41 | if (_.isUndefined(self.tasks[token])) { 42 | self.tasks[token] = []; 43 | } 44 | 45 | var task = { 46 | timeout: max, 47 | running: false, 48 | fn: userTask, 49 | }; 50 | self.tasks[token].push(task); 51 | 52 | if (wait > 0) { 53 | setTimeout(function() { 54 | if (task.running || !_.contains(self.tasks[token], task)) return; 55 | self.tasks[token] = _.without(self.tasks[token], task); 56 | task.fn(new Error('Could not acquire lock ' + token)); 57 | }, wait); 58 | } 59 | 60 | self._runOne(token); 61 | }; 62 | 63 | module.exports = Lock; 64 | -------------------------------------------------------------------------------- /lib/lock.js: -------------------------------------------------------------------------------- 1 | var $ = require('preconditions').singleton(); 2 | var _ = require('lodash'); 3 | var log = require('npmlog'); 4 | log.debug = log.verbose; 5 | log.disableColor(); 6 | 7 | var LocalLock = require('./locallock'); 8 | var RemoteLock = require('locker'); 9 | 10 | var Errors = require('./errors/errordefinitions'); 11 | 12 | function Lock(opts) { 13 | opts = opts || {}; 14 | if (opts.lockerServer) { 15 | this.lock = new RemoteLock(opts.lockerServer.port, opts.lockerServer.host); 16 | 17 | log.info('Using locker server:' + opts.lockerServer.host + ':' + opts.lockerServer.port); 18 | 19 | this.lock.on('reset', function() { 20 | log.debug('Locker server reset'); 21 | }); 22 | this.lock.on('error', function(error) { 23 | log.error('Locker server threw error', error); 24 | }); 25 | } else { 26 | this.lock = new LocalLock(); 27 | } 28 | }; 29 | 30 | Lock.prototype.runLocked = function(token, cb, task) { 31 | $.shouldBeDefined(token); 32 | 33 | this.lock.locked(token, 5 * 1000, 5 * 60 * 1000, function(err, release) { 34 | if (err) return cb(Errors.WALLET_LOCKED); 35 | var _cb = function() { 36 | cb.apply(null, arguments); 37 | release(); 38 | }; 39 | task(_cb); 40 | }); 41 | }; 42 | 43 | module.exports = Lock; 44 | -------------------------------------------------------------------------------- /lib/messagebroker.js: -------------------------------------------------------------------------------- 1 | var $ = require('preconditions').singleton(); 2 | var _ = require('lodash'); 3 | var inherits = require('inherits'); 4 | var events = require('events'); 5 | var nodeutil = require('util'); 6 | var log = require('npmlog'); 7 | log.debug = log.verbose; 8 | log.disableColor(); 9 | 10 | function MessageBroker(opts) { 11 | var self = this; 12 | 13 | opts = opts || {}; 14 | if (opts.messageBrokerServer) { 15 | var url = opts.messageBrokerServer.url; 16 | 17 | this.remote = true; 18 | this.mq = require('socket.io-client').connect(url); 19 | this.mq.on('connect', function() {}); 20 | this.mq.on('connect_error', function() { 21 | log.warn('Error connecting to message broker server @ ' + url); 22 | }); 23 | 24 | this.mq.on('msg', function(data) { 25 | self.emit('msg', data); 26 | }); 27 | 28 | log.info('Using message broker server at ' + url); 29 | } 30 | }; 31 | 32 | nodeutil.inherits(MessageBroker, events.EventEmitter); 33 | 34 | MessageBroker.prototype.send = function(data) { 35 | if (this.remote) { 36 | this.mq.emit('msg', data); 37 | } else { 38 | this.emit('msg', data); 39 | } 40 | }; 41 | 42 | MessageBroker.prototype.onMessage = function(handler) { 43 | this.on('msg', handler); 44 | }; 45 | 46 | module.exports = MessageBroker; 47 | -------------------------------------------------------------------------------- /lib/model/address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('npmlog'); 4 | var $ = require('preconditions').singleton(); 5 | var _ = require('lodash'); 6 | 7 | var Bitcore = { 8 | 'btc': require('bitcore-lib'), 9 | 'bch': require('bitcore-lib-cash'), 10 | }; 11 | var Common = require('../common'); 12 | var Constants = Common.Constants, 13 | Defaults = Common.Defaults, 14 | Utils = Common.Utils; 15 | 16 | function Address() {}; 17 | 18 | Address.create = function(opts) { 19 | opts = opts || {}; 20 | 21 | var x = new Address(); 22 | 23 | $.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS)); 24 | 25 | x.version = '1.0.0'; 26 | x.createdOn = Math.floor(Date.now() / 1000); 27 | x.address = opts.address; 28 | x.walletId = opts.walletId; 29 | x.isChange = opts.isChange; 30 | x.path = opts.path; 31 | x.publicKeys = opts.publicKeys; 32 | x.coin = opts.coin; 33 | x.network = Bitcore[opts.coin].Address(x.address).toObject().network; 34 | x.type = opts.type || Constants.SCRIPT_TYPES.P2SH; 35 | x.hasActivity = undefined; 36 | return x; 37 | }; 38 | 39 | Address.fromObj = function(obj) { 40 | var x = new Address(); 41 | 42 | x.version = obj.version; 43 | x.createdOn = obj.createdOn; 44 | x.address = obj.address; 45 | x.walletId = obj.walletId; 46 | x.coin = obj.coin || Defaults.COIN; 47 | x.network = obj.network; 48 | x.isChange = obj.isChange; 49 | x.path = obj.path; 50 | x.publicKeys = obj.publicKeys; 51 | x.type = obj.type || Constants.SCRIPT_TYPES.P2SH; 52 | x.hasActivity = obj.hasActivity; 53 | return x; 54 | }; 55 | 56 | Address._deriveAddress = function(scriptType, publicKeyRing, path, m, coin, network) { 57 | $.checkArgument(Utils.checkValueInCollection(scriptType, Constants.SCRIPT_TYPES)); 58 | 59 | var publicKeys = _.map(publicKeyRing, function(item) { 60 | var xpub = new Bitcore[coin].HDPublicKey(item.xPubKey); 61 | return xpub.deriveChild(path).publicKey; 62 | }); 63 | 64 | var bitcoreAddress; 65 | switch (scriptType) { 66 | case Constants.SCRIPT_TYPES.P2SH: 67 | bitcoreAddress = Bitcore[coin].Address.createMultisig(publicKeys, m, network); 68 | break; 69 | case Constants.SCRIPT_TYPES.P2PKH: 70 | $.checkState(_.isArray(publicKeys) && publicKeys.length == 1); 71 | bitcoreAddress = Bitcore[coin].Address.fromPublicKey(publicKeys[0], network); 72 | break; 73 | } 74 | 75 | return { 76 | address: bitcoreAddress.toString(), 77 | path: path, 78 | publicKeys: _.invoke(publicKeys, 'toString'), 79 | }; 80 | }; 81 | 82 | Address.derive = function(walletId, scriptType, publicKeyRing, path, m, coin, network, isChange) { 83 | var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, coin, network); 84 | return Address.create(_.extend(raw, { 85 | coin: coin, 86 | walletId: walletId, 87 | type: scriptType, 88 | isChange: isChange, 89 | })); 90 | }; 91 | 92 | 93 | module.exports = Address; 94 | -------------------------------------------------------------------------------- /lib/model/addressmanager.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var $ = require('preconditions').singleton(); 3 | 4 | var Constants = require('../common/constants'); 5 | var Utils = require('../common/utils'); 6 | 7 | function AddressManager() {}; 8 | 9 | AddressManager.create = function(opts) { 10 | opts = opts || {}; 11 | 12 | var x = new AddressManager(); 13 | 14 | x.version = 2; 15 | x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 16 | $.checkState(Utils.checkValueInCollection(x.derivationStrategy, Constants.DERIVATION_STRATEGIES)); 17 | 18 | x.receiveAddressIndex = 0; 19 | x.changeAddressIndex = 0; 20 | x.copayerIndex = _.isNumber(opts.copayerIndex) ? opts.copayerIndex : Constants.BIP45_SHARED_INDEX; 21 | 22 | return x; 23 | }; 24 | 25 | AddressManager.fromObj = function(obj) { 26 | var x = new AddressManager(); 27 | 28 | x.version = obj.version; 29 | x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 30 | x.receiveAddressIndex = obj.receiveAddressIndex; 31 | x.changeAddressIndex = obj.changeAddressIndex; 32 | x.copayerIndex = obj.copayerIndex; 33 | 34 | return x; 35 | }; 36 | 37 | AddressManager.supportsCopayerBranches = function(derivationStrategy) { 38 | return derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45; 39 | }; 40 | 41 | AddressManager.prototype._incrementIndex = function(isChange) { 42 | if (isChange) { 43 | this.changeAddressIndex++; 44 | } else { 45 | this.receiveAddressIndex++; 46 | } 47 | }; 48 | 49 | AddressManager.prototype.rewindIndex = function(isChange, n) { 50 | n = _.isUndefined(n) ? 1 : n; 51 | if (isChange) { 52 | this.changeAddressIndex = Math.max(0, this.changeAddressIndex - n); 53 | } else { 54 | this.receiveAddressIndex = Math.max(0, this.receiveAddressIndex - n); 55 | } 56 | }; 57 | 58 | AddressManager.prototype.getCurrentAddressPath = function(isChange) { 59 | return 'm/' + 60 | (this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') + 61 | (isChange ? 1 : 0) + '/' + 62 | (isChange ? this.changeAddressIndex : this.receiveAddressIndex); 63 | }; 64 | 65 | AddressManager.prototype.getNewAddressPath = function(isChange) { 66 | var ret = this.getCurrentAddressPath(isChange); 67 | this._incrementIndex(isChange); 68 | return ret; 69 | }; 70 | 71 | module.exports = AddressManager; 72 | -------------------------------------------------------------------------------- /lib/model/copayer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('preconditions').singleton(); 4 | var _ = require('lodash'); 5 | var util = require('util'); 6 | var Uuid = require('uuid'); 7 | var sjcl = require('sjcl'); 8 | 9 | var Address = require('./address'); 10 | var AddressManager = require('./addressmanager'); 11 | var Bitcore = require('bitcore-lib'); 12 | 13 | var Common = require('../common'); 14 | var Constants = Common.Constants, 15 | Defaults = Common.Defaults, 16 | Utils = Common.Utils; 17 | 18 | function Copayer() {}; 19 | 20 | Copayer._xPubToCopayerId = function(coin, xpub) { 21 | var str = coin == Defaults.COIN ? xpub : coin + xpub; 22 | var hash = sjcl.hash.sha256.hash(str); 23 | return sjcl.codec.hex.fromBits(hash); 24 | }; 25 | 26 | Copayer.create = function(opts) { 27 | opts = opts || {}; 28 | $.checkArgument(opts.xPubKey, 'Missing copayer extended public key') 29 | .checkArgument(opts.requestPubKey, 'Missing copayer request public key') 30 | .checkArgument(opts.signature, 'Missing copayer request public key signature'); 31 | 32 | $.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS)); 33 | 34 | opts.copayerIndex = opts.copayerIndex || 0; 35 | 36 | var x = new Copayer(); 37 | 38 | x.version = 2; 39 | x.createdOn = Math.floor(Date.now() / 1000); 40 | x.coin = opts.coin; 41 | x.xPubKey = opts.xPubKey; 42 | x.id = Copayer._xPubToCopayerId(opts.coin, x.xPubKey); 43 | x.name = opts.name; 44 | x.requestPubKey = opts.requestPubKey; 45 | x.signature = opts.signature; 46 | x.requestPubKeys = [{ 47 | key: opts.requestPubKey, 48 | signature: opts.signature, 49 | }]; 50 | 51 | var derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 52 | if (AddressManager.supportsCopayerBranches(derivationStrategy)) { 53 | x.addressManager = AddressManager.create({ 54 | derivationStrategy: derivationStrategy, 55 | copayerIndex: opts.copayerIndex, 56 | }); 57 | } 58 | 59 | x.customData = opts.customData; 60 | 61 | return x; 62 | }; 63 | 64 | Copayer.fromObj = function(obj) { 65 | var x = new Copayer(); 66 | 67 | x.version = obj.version; 68 | x.createdOn = obj.createdOn; 69 | x.coin = obj.coin || Defaults.COIN; 70 | x.id = obj.id; 71 | x.name = obj.name; 72 | x.xPubKey = obj.xPubKey; 73 | x.requestPubKey = obj.requestPubKey; 74 | x.signature = obj.signature; 75 | 76 | if (parseInt(x.version) == 1) { 77 | x.requestPubKeys = [{ 78 | key: x.requestPubKey, 79 | signature: x.signature, 80 | }]; 81 | x.version = 2; 82 | } else { 83 | x.requestPubKeys = obj.requestPubKeys; 84 | } 85 | 86 | if (obj.addressManager) { 87 | x.addressManager = AddressManager.fromObj(obj.addressManager); 88 | } 89 | x.customData = obj.customData; 90 | 91 | return x; 92 | }; 93 | 94 | Copayer.prototype.createAddress = function(wallet, isChange) { 95 | $.checkState(wallet.isComplete()); 96 | 97 | var path = this.addressManager.getNewAddressPath(isChange); 98 | var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.coin, wallet.network, isChange); 99 | return address; 100 | }; 101 | 102 | module.exports = Copayer; 103 | -------------------------------------------------------------------------------- /lib/model/email.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Uuid = require('uuid'); 5 | 6 | function Email() {}; 7 | 8 | Email.create = function(opts) { 9 | opts = opts || {}; 10 | 11 | var x = new Email(); 12 | 13 | x.version = 2; 14 | var now = Date.now(); 15 | x.createdOn = Math.floor(now / 1000); 16 | x.id = _.padLeft(now, 14, '0') + Uuid.v4(); 17 | x.walletId = opts.walletId; 18 | x.copayerId = opts.copayerId; 19 | x.from = opts.from; 20 | x.to = opts.to; 21 | x.subject = opts.subject; 22 | x.bodyPlain = opts.bodyPlain; 23 | x.bodyHtml = opts.bodyHtml; 24 | x.status = 'pending'; 25 | x.attempts = 0; 26 | x.lastAttemptOn = null; 27 | x.notificationId = opts.notificationId; 28 | x.language = opts.language || 'en'; 29 | return x; 30 | }; 31 | 32 | Email.fromObj = function(obj) { 33 | var x = new Email(); 34 | 35 | x.version = obj.version; 36 | x.createdOn = obj.createdOn; 37 | x.id = obj.id; 38 | x.walletId = obj.walletId; 39 | x.copayerId = obj.copayerId; 40 | x.from = obj.from; 41 | x.to = obj.to; 42 | x.subject = obj.subject; 43 | if (parseInt(x.version) == 1) { 44 | x.bodyPlain = obj.body; 45 | x.version = 2; 46 | } else { 47 | x.bodyPlain = obj.bodyPlain; 48 | } 49 | x.bodyHtml = obj.bodyHtml; 50 | x.status = obj.status; 51 | x.attempts = obj.attempts; 52 | x.lastAttemptOn = obj.lastAttemptOn; 53 | x.notificationId = obj.notificationId; 54 | x.language = obj.language; 55 | return x; 56 | }; 57 | 58 | Email.prototype._logAttempt = function(result) { 59 | this.attempts++; 60 | this.lastAttemptOn = Math.floor(Date.now() / 1000); 61 | this.status = result; 62 | }; 63 | 64 | Email.prototype.setSent = function() { 65 | this._logAttempt('sent'); 66 | }; 67 | 68 | Email.prototype.setFail = function() { 69 | this._logAttempt('fail'); 70 | }; 71 | 72 | 73 | module.exports = Email; 74 | -------------------------------------------------------------------------------- /lib/model/index.js: -------------------------------------------------------------------------------- 1 | var Model = {}; 2 | 3 | Model.Wallet = require('./wallet'); 4 | Model.Copayer = require('./copayer'); 5 | Model.TxProposal = require('./txproposal'); 6 | Model.Address = require('./address'); 7 | Model.Notification = require('./notification'); 8 | Model.Preferences = require('./preferences'); 9 | Model.Email = require('./email'); 10 | Model.TxNote = require('./txnote'); 11 | Model.Session = require('./session'); 12 | Model.PushNotificationSub = require('./pushnotificationsub'); 13 | Model.TxConfirmationSub = require('./txconfirmationsub'); 14 | 15 | module.exports = Model; 16 | -------------------------------------------------------------------------------- /lib/model/notification.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Uuid = require('uuid'); 3 | 4 | /* 5 | * notifications examples 6 | * 7 | * NewCopayer - 8 | * NewAddress - 9 | * NewTxProposal - (amount) 10 | * TxProposalAcceptedBy - (txProposalId, copayerId) 11 | * TxProposalRejectedBy - (txProposalId, copayerId) 12 | * txProposalFinallyRejected - txProposalId 13 | * txProposalFinallyAccepted - txProposalId 14 | * 15 | * NewIncomingTx (address, txid) 16 | * NewOutgoingTx - (txProposalId, txid) 17 | * 18 | * data Examples: 19 | * { amount: 'xxx', address: 'xxx'} 20 | * { txProposalId: 'xxx', copayerId: 'xxx' } 21 | * 22 | * Data is meant to provide only the needed information 23 | * to notify the user 24 | * 25 | */ 26 | function Notification() {}; 27 | 28 | Notification.create = function(opts) { 29 | opts = opts || {}; 30 | 31 | var x = new Notification(); 32 | 33 | x.version = '1.0.0'; 34 | var now = Date.now(); 35 | 36 | x.createdOn = Math.floor(now / 1000); 37 | x.id = _.padLeft(now, 14, '0') + _.padLeft(opts.ticker || 0, 4, '0'); 38 | x.type = opts.type || 'general'; 39 | x.data = opts.data; 40 | x.walletId = opts.walletId; 41 | x.creatorId = opts.creatorId; 42 | 43 | return x; 44 | }; 45 | 46 | Notification.fromObj = function(obj) { 47 | var x = new Notification(); 48 | 49 | x.version = obj.version; 50 | x.createdOn = obj.createdOn; 51 | x.id = obj.id; 52 | x.type = obj.type, 53 | x.data = obj.data; 54 | x.walletId = obj.walletId; 55 | x.creatorId = obj.creatorId; 56 | 57 | return x; 58 | }; 59 | 60 | module.exports = Notification; 61 | -------------------------------------------------------------------------------- /lib/model/preferences.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Preferences() {}; 4 | 5 | Preferences.create = function(opts) { 6 | opts = opts || {}; 7 | 8 | var x = new Preferences(); 9 | 10 | x.version = '1.0.0'; 11 | x.createdOn = Math.floor(Date.now() / 1000); 12 | x.walletId = opts.walletId; 13 | x.copayerId = opts.copayerId; 14 | x.email = opts.email; 15 | x.language = opts.language; 16 | x.unit = opts.unit; 17 | return x; 18 | }; 19 | 20 | Preferences.fromObj = function(obj) { 21 | var x = new Preferences(); 22 | 23 | x.version = obj.version; 24 | x.createdOn = obj.createdOn; 25 | x.walletId = obj.walletId; 26 | x.copayerId = obj.copayerId; 27 | x.email = obj.email; 28 | x.language = obj.language; 29 | x.unit = obj.unit; 30 | return x; 31 | }; 32 | 33 | 34 | module.exports = Preferences; 35 | -------------------------------------------------------------------------------- /lib/model/pushnotificationsub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function PushNotificationSub() {}; 4 | 5 | PushNotificationSub.create = function(opts) { 6 | opts = opts || {}; 7 | 8 | var x = new PushNotificationSub(); 9 | 10 | x.version = '1.0.0'; 11 | x.createdOn = Math.floor(Date.now() / 1000); 12 | x.copayerId = opts.copayerId; 13 | x.token = opts.token; 14 | x.packageName = opts.packageName; 15 | x.platform = opts.platform; 16 | return x; 17 | }; 18 | 19 | PushNotificationSub.fromObj = function(obj) { 20 | var x = new PushNotificationSub(); 21 | 22 | x.version = obj.version; 23 | x.createdOn = obj.createdOn; 24 | x.copayerId = obj.copayerId; 25 | x.token = obj.token; 26 | x.packageName = obj.packageName; 27 | x.platform = obj.platform; 28 | return x; 29 | }; 30 | 31 | 32 | module.exports = PushNotificationSub; 33 | -------------------------------------------------------------------------------- /lib/model/session.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Uuid = require('uuid'); 3 | 4 | var Defaults = require('../common/defaults'); 5 | 6 | function Session() {}; 7 | 8 | Session.create = function(opts) { 9 | opts = opts || {}; 10 | 11 | var now = Math.floor(Date.now() / 1000); 12 | 13 | var x = new Session(); 14 | 15 | x.id = Uuid.v4(); 16 | x.version = 1; 17 | x.createdOn = now; 18 | x.updatedOn = now; 19 | x.copayerId = opts.copayerId; 20 | x.walletId = opts.walletId; 21 | 22 | return x; 23 | }; 24 | 25 | Session.fromObj = function(obj) { 26 | var x = new Session(); 27 | 28 | x.id = obj.id; 29 | x.version = obj.version; 30 | x.createdOn = obj.createdOn; 31 | x.updatedOn = obj.updatedOn; 32 | x.copayerId = obj.copayerId; 33 | x.walletId = obj.walletId; 34 | 35 | return x; 36 | }; 37 | 38 | Session.prototype.toObject = function() { 39 | return this; 40 | }; 41 | 42 | Session.prototype.isValid = function() { 43 | var now = Math.floor(Date.now() / 1000); 44 | return (now - this.updatedOn) <= Defaults.SESSION_EXPIRATION; 45 | }; 46 | 47 | Session.prototype.touch = function() { 48 | var now = Math.floor(Date.now() / 1000); 49 | this.updatedOn = now; 50 | }; 51 | 52 | module.exports = Session; 53 | -------------------------------------------------------------------------------- /lib/model/txconfirmationsub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function TxConfirmationSub() {}; 4 | 5 | TxConfirmationSub.create = function(opts) { 6 | opts = opts || {}; 7 | 8 | var x = new TxConfirmationSub(); 9 | 10 | x.version = 1; 11 | x.createdOn = Math.floor(Date.now() / 1000); 12 | x.walletId = opts.walletId; 13 | x.copayerId = opts.copayerId; 14 | x.txid = opts.txid; 15 | x.isActive = true; 16 | return x; 17 | }; 18 | 19 | TxConfirmationSub.fromObj = function(obj) { 20 | var x = new TxConfirmationSub(); 21 | 22 | x.version = obj.version; 23 | x.createdOn = obj.createdOn; 24 | x.walletId = obj.walletId; 25 | x.copayerId = obj.copayerId; 26 | x.txid = obj.txid; 27 | x.isActive = obj.isActive; 28 | return x; 29 | }; 30 | 31 | 32 | module.exports = TxConfirmationSub; 33 | -------------------------------------------------------------------------------- /lib/model/txnote.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Uuid = require('uuid'); 3 | 4 | function TxNote() {}; 5 | 6 | TxNote.create = function(opts) { 7 | opts = opts || {}; 8 | 9 | var now = Math.floor(Date.now() / 1000); 10 | 11 | var x = new TxNote(); 12 | 13 | x.version = 1; 14 | x.createdOn = now; 15 | x.walletId = opts.walletId; 16 | x.txid = opts.txid; 17 | x.body = opts.body; 18 | x.editedOn = now; 19 | x.editedBy = opts.copayerId; 20 | 21 | return x; 22 | }; 23 | 24 | TxNote.fromObj = function(obj) { 25 | var x = new TxNote(); 26 | 27 | x.version = obj.version; 28 | x.createdOn = obj.createdOn; 29 | x.walletId = obj.walletId; 30 | x.txid = obj.txid; 31 | x.body = obj.body; 32 | x.editedOn = obj.editedOn; 33 | x.editedBy = obj.editedBy; 34 | 35 | return x; 36 | }; 37 | 38 | TxNote.prototype.edit = function(body, copayerId) { 39 | this.body = body; 40 | this.editedBy = copayerId; 41 | this.editedOn = Math.floor(Date.now() / 1000); 42 | }; 43 | 44 | TxNote.prototype.toObject = function() { 45 | return this; 46 | }; 47 | 48 | module.exports = TxNote; 49 | -------------------------------------------------------------------------------- /lib/model/txproposal_legacy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var $ = require('preconditions').singleton(); 5 | var log = require('npmlog'); 6 | log.debug = log.verbose; 7 | log.disableColor(); 8 | 9 | var Bitcore = require('bitcore-lib'); 10 | 11 | var Common = require('../common'); 12 | var Constants = Common.Constants; 13 | var Defaults = Common.Defaults; 14 | 15 | var TxProposalAction = require('./txproposalaction'); 16 | 17 | function TxProposal() {}; 18 | 19 | TxProposal.Types = { 20 | SIMPLE: 'simple', 21 | MULTIPLEOUTPUTS: 'multiple_outputs', 22 | EXTERNAL: 'external' 23 | }; 24 | 25 | TxProposal.fromObj = function(obj) { 26 | var x = new TxProposal(); 27 | 28 | x.version = obj.version; 29 | if (obj.version === '1.0.0') { 30 | x.type = TxProposal.Types.SIMPLE; 31 | } else { 32 | x.type = obj.type; 33 | } 34 | x.createdOn = obj.createdOn; 35 | x.id = obj.id; 36 | x.walletId = obj.walletId; 37 | x.creatorId = obj.creatorId; 38 | x.outputs = obj.outputs; 39 | x.toAddress = obj.toAddress; 40 | x.amount = obj.amount; 41 | x.message = obj.message; 42 | x.payProUrl = obj.payProUrl; 43 | x.proposalSignature = obj.proposalSignature; 44 | x.changeAddress = obj.changeAddress; 45 | x.inputs = obj.inputs; 46 | x.requiredSignatures = obj.requiredSignatures; 47 | x.requiredRejections = obj.requiredRejections; 48 | x.walletN = obj.walletN; 49 | x.status = obj.status; 50 | x.txid = obj.txid; 51 | x.broadcastedOn = obj.broadcastedOn; 52 | x.inputPaths = obj.inputPaths; 53 | x.actions = _.map(obj.actions, function(action) { 54 | return TxProposalAction.fromObj(action); 55 | }); 56 | x.outputOrder = obj.outputOrder; 57 | x.coin = obj.coin || Defaults.COIN; 58 | x.network = obj.network; 59 | x.fee = obj.fee; 60 | x.feePerKb = obj.feePerKb; 61 | x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; 62 | x.proposalSignaturePubKey = obj.proposalSignaturePubKey; 63 | x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; 64 | x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; 65 | x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 66 | x.customData = obj.customData; 67 | 68 | return x; 69 | }; 70 | 71 | function throwUnsupportedError() { 72 | var msg = 'Unsupported operation on this transaction proposal'; 73 | log.warn('DEPRECATED: ' + msg); 74 | throw new Error(msg); 75 | }; 76 | 77 | TxProposal.prototype.toObject = function() { 78 | var x = _.cloneDeep(this); 79 | x.isPending = this.isPending(); 80 | return x; 81 | }; 82 | 83 | TxProposal.prototype._updateStatus = function() { 84 | if (this.status != 'pending') return; 85 | 86 | if (this.isRejected()) { 87 | this.status = 'rejected'; 88 | } else if (this.isAccepted()) { 89 | this.status = 'accepted'; 90 | } 91 | }; 92 | 93 | TxProposal.prototype.getBitcoreTx = function() { 94 | throwUnsupportedError(); 95 | }; 96 | 97 | TxProposal.prototype.getRawTx = function() { 98 | throwUnsupportedError(); 99 | }; 100 | 101 | TxProposal.prototype.getTotalAmount = function() { 102 | if (this.type == TxProposal.Types.MULTIPLEOUTPUTS || this.type == TxProposal.Types.EXTERNAL) { 103 | return _.pluck(this.outputs, 'amount') 104 | .reduce(function(total, n) { 105 | return total + n; 106 | }, 0); 107 | } else { 108 | return this.amount; 109 | } 110 | }; 111 | 112 | TxProposal.prototype.getActors = function() { 113 | return _.pluck(this.actions, 'copayerId'); 114 | }; 115 | 116 | TxProposal.prototype.getApprovers = function() { 117 | return _.pluck( 118 | _.filter(this.actions, { 119 | type: 'accept' 120 | }), 'copayerId'); 121 | }; 122 | 123 | TxProposal.prototype.getActionBy = function(copayerId) { 124 | return _.find(this.actions, { 125 | copayerId: copayerId 126 | }); 127 | }; 128 | 129 | TxProposal.prototype.addAction = function(copayerId, type, comment, signatures, xpub) { 130 | var action = TxProposalAction.create({ 131 | copayerId: copayerId, 132 | type: type, 133 | signatures: signatures, 134 | xpub: xpub, 135 | comment: comment, 136 | }); 137 | this.actions.push(action); 138 | this._updateStatus(); 139 | }; 140 | 141 | TxProposal.prototype.sign = function() { 142 | throwUnsupportedError(); 143 | }; 144 | 145 | TxProposal.prototype.reject = function(copayerId, reason) { 146 | this.addAction(copayerId, 'reject', reason); 147 | }; 148 | 149 | TxProposal.prototype.isPending = function() { 150 | return !_.contains(['broadcasted', 'rejected'], this.status); 151 | }; 152 | 153 | TxProposal.prototype.isAccepted = function() { 154 | var votes = _.countBy(this.actions, 'type'); 155 | return votes['accept'] >= this.requiredSignatures; 156 | }; 157 | 158 | TxProposal.prototype.isRejected = function() { 159 | var votes = _.countBy(this.actions, 'type'); 160 | return votes['reject'] >= this.requiredRejections; 161 | }; 162 | 163 | TxProposal.prototype.isBroadcasted = function() { 164 | return this.status == 'broadcasted'; 165 | }; 166 | 167 | TxProposal.prototype.setBroadcasted = function() { 168 | $.checkState(this.txid); 169 | this.status = 'broadcasted'; 170 | this.broadcastedOn = Math.floor(Date.now() / 1000); 171 | }; 172 | 173 | module.exports = TxProposal; 174 | -------------------------------------------------------------------------------- /lib/model/txproposalaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function TxProposalAction() {}; 4 | 5 | TxProposalAction.create = function(opts) { 6 | opts = opts || {}; 7 | 8 | var x = new TxProposalAction(); 9 | 10 | x.version = '1.0.0'; 11 | x.createdOn = Math.floor(Date.now() / 1000); 12 | x.copayerId = opts.copayerId; 13 | x.type = opts.type; 14 | x.signatures = opts.signatures; 15 | x.xpub = opts.xpub; 16 | x.comment = opts.comment; 17 | 18 | return x; 19 | }; 20 | 21 | TxProposalAction.fromObj = function(obj) { 22 | var x = new TxProposalAction(); 23 | 24 | x.version = obj.version; 25 | x.createdOn = obj.createdOn; 26 | x.copayerId = obj.copayerId; 27 | x.type = obj.type; 28 | x.signatures = obj.signatures; 29 | x.xpub = obj.xpub; 30 | x.comment = obj.comment; 31 | 32 | return x; 33 | }; 34 | 35 | module.exports = TxProposalAction; 36 | -------------------------------------------------------------------------------- /lib/model/wallet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var util = require('util'); 5 | var $ = require('preconditions').singleton(); 6 | var Uuid = require('uuid'); 7 | 8 | var Address = require('./address'); 9 | var Copayer = require('./copayer'); 10 | var AddressManager = require('./addressmanager'); 11 | 12 | var Common = require('../common'); 13 | var Constants = Common.Constants, 14 | Defaults = Common.Defaults, 15 | Utils = Common.Utils; 16 | 17 | function Wallet() {}; 18 | 19 | Wallet.create = function(opts) { 20 | opts = opts || {}; 21 | 22 | var x = new Wallet(); 23 | 24 | $.shouldBeNumber(opts.m); 25 | $.shouldBeNumber(opts.n); 26 | $.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS)); 27 | $.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS)); 28 | 29 | x.version = '1.0.0'; 30 | x.createdOn = Math.floor(Date.now() / 1000); 31 | x.id = opts.id || Uuid.v4(); 32 | x.name = opts.name; 33 | x.m = opts.m; 34 | x.n = opts.n; 35 | x.singleAddress = !!opts.singleAddress; 36 | x.status = 'pending'; 37 | x.publicKeyRing = []; 38 | x.addressIndex = 0; 39 | x.copayers = []; 40 | x.pubKey = opts.pubKey; 41 | x.coin = opts.coin; 42 | x.network = opts.network; 43 | x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 44 | x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH; 45 | 46 | x.addressManager = AddressManager.create({ 47 | derivationStrategy: x.derivationStrategy, 48 | }); 49 | x.scanStatus = null; 50 | 51 | return x; 52 | }; 53 | 54 | Wallet.fromObj = function(obj) { 55 | var x = new Wallet(); 56 | 57 | $.shouldBeNumber(obj.m); 58 | $.shouldBeNumber(obj.n); 59 | 60 | x.version = obj.version; 61 | x.createdOn = obj.createdOn; 62 | x.id = obj.id; 63 | x.name = obj.name; 64 | x.m = obj.m; 65 | x.n = obj.n; 66 | x.singleAddress = !!obj.singleAddress; 67 | x.status = obj.status; 68 | x.publicKeyRing = obj.publicKeyRing; 69 | x.copayers = _.map(obj.copayers, function(copayer) { 70 | return Copayer.fromObj(copayer); 71 | }); 72 | x.pubKey = obj.pubKey; 73 | x.coin = obj.coin || Defaults.COIN; 74 | x.network = obj.network; 75 | x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; 76 | x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; 77 | x.addressManager = AddressManager.fromObj(obj.addressManager); 78 | x.scanStatus = obj.scanStatus; 79 | 80 | return x; 81 | }; 82 | 83 | Wallet.prototype.toObject = function() { 84 | var x = _.cloneDeep(this); 85 | x.isShared = this.isShared(); 86 | return x; 87 | }; 88 | 89 | /** 90 | * Get the maximum allowed number of required copayers. 91 | * This is a limit imposed by the maximum allowed size of the scriptSig. 92 | * @param {number} totalCopayers - the total number of copayers 93 | * @return {number} 94 | */ 95 | Wallet.getMaxRequiredCopayers = function(totalCopayers) { 96 | return Wallet.COPAYER_PAIR_LIMITS[totalCopayers]; 97 | }; 98 | 99 | Wallet.verifyCopayerLimits = function(m, n) { 100 | return (n >= 1 && n <= 15) && (m >= 1 && m <= n); 101 | }; 102 | 103 | Wallet.prototype.isShared = function() { 104 | return this.n > 1; 105 | }; 106 | 107 | 108 | Wallet.prototype._updatePublicKeyRing = function() { 109 | this.publicKeyRing = _.map(this.copayers, function(copayer) { 110 | return _.pick(copayer, ['xPubKey', 'requestPubKey']); 111 | }); 112 | }; 113 | 114 | Wallet.prototype.addCopayer = function(copayer) { 115 | $.checkState(copayer.coin == this.coin); 116 | 117 | this.copayers.push(copayer); 118 | if (this.copayers.length < this.n) return; 119 | 120 | this.status = 'complete'; 121 | this._updatePublicKeyRing(); 122 | }; 123 | 124 | Wallet.prototype.addCopayerRequestKey = function(copayerId, requestPubKey, signature, restrictions, name) { 125 | $.checkState(this.copayers.length == this.n); 126 | 127 | var c = this.getCopayer(copayerId); 128 | 129 | //new ones go first 130 | c.requestPubKeys.unshift({ 131 | key: requestPubKey.toString(), 132 | signature: signature, 133 | selfSigned: true, 134 | restrictions: restrictions || {}, 135 | name: name || null, 136 | }); 137 | }; 138 | 139 | Wallet.prototype.getCopayer = function(copayerId) { 140 | return _.find(this.copayers, { 141 | id: copayerId 142 | }); 143 | }; 144 | 145 | Wallet.prototype.isComplete = function() { 146 | return this.status == 'complete'; 147 | }; 148 | 149 | Wallet.prototype.isScanning = function() { 150 | return this.scanning; 151 | }; 152 | 153 | Wallet.prototype.createAddress = function(isChange) { 154 | $.checkState(this.isComplete()); 155 | 156 | var self = this; 157 | 158 | var path = this.addressManager.getNewAddressPath(isChange); 159 | var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.coin, this.network, isChange); 160 | return address; 161 | }; 162 | 163 | 164 | module.exports = Wallet; 165 | -------------------------------------------------------------------------------- /lib/notificationbroadcaster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('npmlog'); 4 | log.debug = log.verbose; 5 | var inherits = require('inherits'); 6 | var events = require('events'); 7 | var nodeutil = require('util'); 8 | 9 | function NotificationBroadcaster() {}; 10 | 11 | nodeutil.inherits(NotificationBroadcaster, events.EventEmitter); 12 | 13 | NotificationBroadcaster.prototype.broadcast = function(eventName, notification, walletService) { 14 | this.emit(eventName, notification, walletService); 15 | }; 16 | 17 | var _instance; 18 | NotificationBroadcaster.singleton = function() { 19 | if (!_instance) { 20 | _instance = new NotificationBroadcaster(); 21 | } 22 | return _instance; 23 | }; 24 | 25 | module.exports = NotificationBroadcaster.singleton(); 26 | -------------------------------------------------------------------------------- /lib/stats.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var $ = require('preconditions').singleton(); 7 | var async = require('async'); 8 | var log = require('npmlog'); 9 | log.debug = log.verbose; 10 | log.disableColor(); 11 | var mongodb = require('mongodb'); 12 | var moment = require('moment'); 13 | 14 | var config = require('../config'); 15 | var storage = require('./storage'); 16 | 17 | 18 | var INITIAL_DATE = '2015-01-01'; 19 | 20 | function Stats(opts) { 21 | opts = opts || {}; 22 | 23 | this.network = opts.network || 'livenet'; 24 | this.from = moment(opts.from || INITIAL_DATE); 25 | this.to = moment(opts.to); 26 | this.fromTs = this.from.startOf('day').valueOf(); 27 | this.toTs = this.to.endOf('day').valueOf(); 28 | }; 29 | 30 | Stats.prototype.run = function(cb) { 31 | var self = this; 32 | 33 | var uri = config.storageOpts.mongoDb.uri; 34 | mongodb.MongoClient.connect(uri, function(err, db) { 35 | if (err) { 36 | log.error('Unable to connect to the mongoDB', err); 37 | return cb(err, null); 38 | } 39 | log.info('Connection established to ' + uri); 40 | self.db = db; 41 | self._getStats(function(err, stats) { 42 | if (err) return cb(err); 43 | return cb(null, stats); 44 | }); 45 | }); 46 | }; 47 | 48 | Stats.prototype._getStats = function(cb) { 49 | var self = this; 50 | var result = {}; 51 | async.parallel([ 52 | 53 | function(next) { 54 | self._getNewWallets(next); 55 | }, 56 | function(next) { 57 | self._getTxProposals(next); 58 | }, 59 | ], function(err, results) { 60 | if (err) return cb(err); 61 | 62 | result.newWallets = results[0]; 63 | result.txProposals = results[1]; 64 | return cb(null, result); 65 | }); 66 | }; 67 | 68 | Stats.prototype._getNewWallets = function(cb) { 69 | var self = this; 70 | 71 | function getLastDate(cb) { 72 | self.db.collection('stats_wallets') 73 | .find({}) 74 | .sort({ 75 | '_id.day': -1 76 | }) 77 | .limit(1) 78 | .toArray(function(err, lastRecord) { 79 | if (_.isEmpty(lastRecord)) return cb(null, moment(INITIAL_DATE)); 80 | return cb(null, moment(lastRecord[0]._id.day)); 81 | }); 82 | }; 83 | 84 | function updateStats(from, cb) { 85 | var to = moment().subtract(1, 'day').endOf('day'); 86 | var map = function() { 87 | var day = new Date(this.createdOn * 1000); 88 | day.setHours(0); 89 | day.setMinutes(0); 90 | day.setSeconds(0); 91 | var key = { 92 | day: +day, 93 | network: this.network, 94 | }; 95 | var value = { 96 | count: 1 97 | }; 98 | emit(key, value); 99 | }; 100 | var reduce = function(k, v) { 101 | var count = 0; 102 | for (var i = 0; i < v.length; i++) { 103 | count += v[i].count; 104 | } 105 | return { 106 | count: count, 107 | }; 108 | }; 109 | var opts = { 110 | query: { 111 | createdOn: { 112 | $gt: from.unix(), 113 | $lte: to.unix(), 114 | }, 115 | }, 116 | out: { 117 | merge: 'stats_wallets', 118 | } 119 | }; 120 | self.db.collection(storage.collections.WALLETS) 121 | .mapReduce(map, reduce, opts, function(err, collection, stats) { 122 | return cb(err); 123 | }); 124 | }; 125 | 126 | function queryStats(cb) { 127 | self.db.collection('stats_wallets') 128 | .find({ 129 | '_id.network': self.network, 130 | '_id.day': { 131 | $gte: self.fromTs, 132 | $lte: self.toTs, 133 | }, 134 | }) 135 | .sort({ 136 | '_id.day': 1 137 | }) 138 | .toArray(function(err, results) { 139 | if (err) return cb(err); 140 | var stats = {}; 141 | stats.byDay = _.map(results, function(record) { 142 | var day = moment(record._id.day).format('YYYYMMDD'); 143 | return { 144 | day: day, 145 | count: record.value.count, 146 | }; 147 | }); 148 | return cb(null, stats); 149 | }); 150 | }; 151 | 152 | async.series([ 153 | 154 | function(next) { 155 | getLastDate(function(err, lastDate) { 156 | if (err) return next(err); 157 | 158 | lastDate = lastDate.startOf('day'); 159 | var yesterday = moment().subtract(1, 'day').startOf('day'); 160 | if (lastDate.isBefore(yesterday)) { 161 | // Needs update 162 | return updateStats(lastDate, next); 163 | } 164 | next(); 165 | }); 166 | }, 167 | function(next) { 168 | queryStats(next); 169 | }, 170 | ], 171 | function(err, res) { 172 | if (err) { 173 | log.error(err); 174 | } 175 | return cb(err, res[1]); 176 | }); 177 | }; 178 | 179 | Stats.prototype._getTxProposals = function(cb) { 180 | var self = this; 181 | 182 | function getLastDate(cb) { 183 | self.db.collection('stats_txps') 184 | .find({}) 185 | .sort({ 186 | '_id.day': -1 187 | }) 188 | .limit(1) 189 | .toArray(function(err, lastRecord) { 190 | if (_.isEmpty(lastRecord)) return cb(null, moment(INITIAL_DATE)); 191 | return cb(null, moment(lastRecord[0]._id.day)); 192 | }); 193 | }; 194 | 195 | function updateStats(from, cb) { 196 | var to = moment().subtract(1, 'day').endOf('day'); 197 | var map = function() { 198 | var day = new Date(this.broadcastedOn * 1000); 199 | day.setHours(0); 200 | day.setMinutes(0); 201 | day.setSeconds(0); 202 | var key = { 203 | day: +day, 204 | network: this.network, 205 | }; 206 | var value = { 207 | count: 1, 208 | amount: this.amount 209 | }; 210 | emit(key, value); 211 | }; 212 | var reduce = function(k, v) { 213 | var count = 0, 214 | amount = 0; 215 | for (var i = 0; i < v.length; i++) { 216 | count += v[i].count; 217 | amount += v[i].amount; 218 | } 219 | return { 220 | count: count, 221 | amount: amount, 222 | }; 223 | }; 224 | var opts = { 225 | query: { 226 | status: 'broadcasted', 227 | broadcastedOn: { 228 | $gt: from.unix(), 229 | $lte: to.unix(), 230 | }, 231 | }, 232 | out: { 233 | merge: 'stats_txps', 234 | } 235 | }; 236 | self.db.collection(storage.collections.TXS) 237 | .mapReduce(map, reduce, opts, function(err, collection, stats) { 238 | return cb(err); 239 | }); 240 | }; 241 | 242 | function queryStats(cb) { 243 | self.db.collection('stats_txps') 244 | .find({ 245 | '_id.network': self.network, 246 | '_id.day': { 247 | $gte: self.fromTs, 248 | $lte: self.toTs, 249 | }, 250 | }) 251 | .sort({ 252 | '_id.day': 1 253 | }) 254 | .toArray(function(err, results) { 255 | if (err) return cb(err); 256 | 257 | var stats = { 258 | nbByDay: [], 259 | amountByDay: [] 260 | }; 261 | _.each(results, function(record) { 262 | var day = moment(record._id.day).format('YYYYMMDD'); 263 | stats.nbByDay.push({ 264 | day: day, 265 | count: record.value.count, 266 | }); 267 | stats.amountByDay.push({ 268 | day: day, 269 | amount: record.value.amount, 270 | }); 271 | }); 272 | return cb(null, stats); 273 | }); 274 | }; 275 | 276 | async.series([ 277 | 278 | function(next) { 279 | getLastDate(function(err, lastDate) { 280 | if (err) return next(err); 281 | 282 | lastDate = lastDate.startOf('day'); 283 | var yesterday = moment().subtract(1, 'day').startOf('day'); 284 | if (lastDate.isBefore(yesterday)) { 285 | // Needs update 286 | return updateStats(lastDate, next); 287 | } 288 | next(); 289 | }); 290 | }, 291 | function(next) { 292 | queryStats(next); 293 | }, 294 | ], 295 | function(err, res) { 296 | if (err) { 297 | log.error(err); 298 | } 299 | return cb(err, res[1]); 300 | }); 301 | }; 302 | 303 | module.exports = Stats; 304 | -------------------------------------------------------------------------------- /lib/templates/en/new_copayer.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}New copayer 2 | A new copayer just joined your wallet. -------------------------------------------------------------------------------- /lib/templates/en/new_incoming_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}New payment received 2 | A payment of {{amount}} has been received into your wallet. -------------------------------------------------------------------------------- /lib/templates/en/new_outgoing_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Payment sent 2 | A Payment of {{amount}} has been sent from your wallet. -------------------------------------------------------------------------------- /lib/templates/en/new_tx_proposal.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}New payment proposal 2 | A new payment proposal has been created in your wallet. -------------------------------------------------------------------------------- /lib/templates/en/tx_confirmation.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}} Transaction confirmed 2 | The transaction you were waiting for has been confirmed. -------------------------------------------------------------------------------- /lib/templates/en/txp_finally_rejected.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Payment proposal rejected 2 | A payment proposal in your wallet has been rejected. -------------------------------------------------------------------------------- /lib/templates/en/wallet_complete.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Wallet complete 2 | Your wallet is complete. -------------------------------------------------------------------------------- /lib/templates/es/new_copayer.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nuevo copayer 2 | Un nuevo copayer ha ingresado a su billetera. -------------------------------------------------------------------------------- /lib/templates/es/new_incoming_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nuevo pago recibido 2 | Un pago de {{amount}} fue recibido en su billetera. -------------------------------------------------------------------------------- /lib/templates/es/new_outgoing_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Pago enviado 2 | Un pago de {{amount}} ha sido enviado de su billetera. -------------------------------------------------------------------------------- /lib/templates/es/new_tx_proposal.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nueva propuesta de pago 2 | Una nueva propuesta de pago ha sido creada en su billetera. -------------------------------------------------------------------------------- /lib/templates/es/tx_confirmation.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}} Transacción confirmada 2 | La transacción que estabas esperando se ha confirmado. -------------------------------------------------------------------------------- /lib/templates/es/txp_finally_rejected.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Propuesta de pago rechazada 2 | Una propuesta de pago en su billetera ha sido rechazada. -------------------------------------------------------------------------------- /lib/templates/es/wallet_complete.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Billetera completa 2 | Su billetera está completa. -------------------------------------------------------------------------------- /lib/templates/fr/new_copayer.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nouveau copayer 2 | Un nouveau copayer vient de rejoindre votre portefeuille. -------------------------------------------------------------------------------- /lib/templates/fr/new_incoming_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nouveau paiement reçu 2 | Un paiement de {{amount}} a été reçu dans votre portefeuille. -------------------------------------------------------------------------------- /lib/templates/fr/new_outgoing_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Paiement envoyé 2 | Un paiement de {{amount}} a été envoyé de votre portefeuille. -------------------------------------------------------------------------------- /lib/templates/fr/new_tx_proposal.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Nouvelle proposition de paiement 2 | Une nouvelle proposition de paiement a été créée dans votre portefeuille. -------------------------------------------------------------------------------- /lib/templates/fr/txp_finally_rejected.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Proposition de paiement rejetée 2 | Une proposition de paiement dans votre portefeuille a été rejetée. -------------------------------------------------------------------------------- /lib/templates/fr/wallet_complete.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}Portefeuille terminé 2 | Votre portefeuille est terminé. -------------------------------------------------------------------------------- /lib/templates/ja/new_copayer.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}ウォレットメンバー参加の知らせ 2 | 「{{walletName}}」のウォレットに新しいメンバーが加わりました。 -------------------------------------------------------------------------------- /lib/templates/ja/new_incoming_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}着金確認の知らせ 2 | {{amount}} のビットコインがウォレット「{{walletName}}」に着金しました。 -------------------------------------------------------------------------------- /lib/templates/ja/new_outgoing_tx.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}送金のお知らせ 2 | {{amount}}のビットコインがウォレット「{{walletName}}」から送金されました。 -------------------------------------------------------------------------------- /lib/templates/ja/new_tx_proposal.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}送金の新規提案のお知らせ 2 | 「{{walletName}}」のウォレットにおいて {{copayerName}} さんが送金の提案をしました。 -------------------------------------------------------------------------------- /lib/templates/ja/txp_finally_rejected.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}送金提案の却下のお知らせ 2 | 「{{walletName}}」のウォレットにおいて {{rejectorsNames}} さんが送金の提案を却下しました。 -------------------------------------------------------------------------------- /lib/templates/ja/wallet_complete.plain: -------------------------------------------------------------------------------- 1 | {{subjectPrefix}}ウォレット作成完了 2 | あなたの新しいウォレット「{{walletName}}」が完成されました。 -------------------------------------------------------------------------------- /locker/locker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var PORT = 3231; 4 | 5 | console.log('Server started at port ' + PORT + '...'); 6 | var Locker = require('locker-server'), 7 | locker = new Locker(); 8 | 9 | locker.listen(PORT); 10 | -------------------------------------------------------------------------------- /messagebroker/messagebroker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var $ = require('preconditions').singleton(); 6 | var io = require('socket.io'); 7 | var log = require('npmlog'); 8 | log.debug = log.verbose; 9 | 10 | var DEFAULT_PORT = 3380; 11 | 12 | var opts = { 13 | port: parseInt(process.argv[2]) || DEFAULT_PORT, 14 | }; 15 | 16 | var server = io(opts.port); 17 | server.on('connection', function(socket) { 18 | socket.on('msg', function(data) { 19 | server.emit('msg', data); 20 | }); 21 | }); 22 | 23 | console.log('Message broker server listening on port ' + opts.port) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcore-wallet-service", 3 | "description": "A service for Mutisig HD Bitcoin Wallets", 4 | "author": "BitPay Inc", 5 | "version": "2.1.0", 6 | "licence": "MIT", 7 | "keywords": [ 8 | "bitcoin", 9 | "copay", 10 | "multisig", 11 | "wallet", 12 | "bitcore", 13 | "BWS" 14 | ], 15 | "repository": { 16 | "url": "git@github.com:Bitcoin-com/bitcore-wallet-service.git", 17 | "type": "git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Bitcoin-com/bitcore-wallet-service/issues" 21 | }, 22 | "dependencies": { 23 | "async": "^0.9.2", 24 | "bitcore-lib": "0.14.0", 25 | "bitcore-lib-cash": "https://github.com/Bitcoin-com/bitcore-lib-cash.git", 26 | "body-parser": "^1.11.0", 27 | "compression": "^1.6.2", 28 | "coveralls": "^2.11.2", 29 | "email-validator": "^1.0.1", 30 | "express": "^4.10.0", 31 | "express-rate-limit": "^2.6.0", 32 | "inherits": "^2.0.1", 33 | "json-stable-stringify": "^1.0.0", 34 | "locker": "^0.1.0", 35 | "locker-server": "^0.1.3", 36 | "lodash": "^3.10.1", 37 | "mocha-lcov-reporter": "0.0.1", 38 | "moment": "^2.10.3", 39 | "mongodb": "^2.0.27", 40 | "morgan": "*", 41 | "mustache": "^2.1.0", 42 | "nodemailer": "^1.3.4", 43 | "nodemailer-sendgrid-transport": "^0.2.0", 44 | "npmlog": "^0.1.1", 45 | "preconditions": "^1.0.7", 46 | "read": "^1.0.5", 47 | "request": "^2.53.0", 48 | "secp256k1": "^3.1.0", 49 | "sjcl": "^1.0.2", 50 | "socket.io": "^1.3.5", 51 | "socket.io-client": "^1.3.5", 52 | "sticky-session": "^0.1.0", 53 | "uuid": "*" 54 | }, 55 | "devDependencies": { 56 | "chai": "^1.9.1", 57 | "istanbul": "*", 58 | "jsdoc": "^3.3.0-beta1", 59 | "memdown": "^1.0.0", 60 | "mocha": "^1.18.2", 61 | "proxyquire": "^1.7.2", 62 | "sinon": "1.10.3", 63 | "supertest": "*", 64 | "tingodb": "^0.3.4" 65 | }, 66 | "scripts": { 67 | "start": "./start.sh", 68 | "stop": "./stop.sh", 69 | "coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --reporter spec test", 70 | "test": "./node_modules/.bin/mocha", 71 | "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 72 | }, 73 | "bitcoreNode": "./bitcorenode", 74 | "contributors": [ 75 | { 76 | "name": "Braydon Fuller", 77 | "email": "braydon@bitpay.com" 78 | }, 79 | { 80 | "name": "Ivan Socolsky", 81 | "email": "ivan@bitpay.com" 82 | }, 83 | { 84 | "name": "Matias Alejo Garcia", 85 | "email": "ematiu@gmail.com" 86 | } 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /pushnotificationsservice/pushnotificationsservice.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var log = require('npmlog'); 6 | log.debug = log.verbose; 7 | log.level = 'debug'; 8 | 9 | var config = require('../config'); 10 | var PushNotificationsService = require('../lib/pushnotificationsservice'); 11 | 12 | var pushNotificationsService = new PushNotificationsService(); 13 | pushNotificationsService.start(config, function(err) { 14 | if (err) throw err; 15 | 16 | log.debug('Push Notification Service started'); 17 | }); 18 | -------------------------------------------------------------------------------- /scripts/clean_db.mongodb: -------------------------------------------------------------------------------- 1 | db.email_queue.remove({createdOn: {$lt: Date.now()/1000-86400*10 }}); 2 | db.notifications.remove({createdOn: {$lt: Date.now()/1000-86400*10 }}); 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/level2mongo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LevelStorage = require('../lib/storage_leveldb'); 4 | var MongoStorage = require('../lib/storage'); 5 | var Bitcore = require('bitcore-lib'); 6 | 7 | var level = new LevelStorage({ 8 | dbPath: './db', 9 | }); 10 | 11 | var mongo = new MongoStorage(); 12 | mongo.connect({ 13 | mongoDb: { 14 | uri: 'mongodb://localhost:27017/bws', 15 | } 16 | }, 17 | function(err) { 18 | if (err) throw err; 19 | run(function(err) { 20 | if (err) throw err; 21 | console.log('All data successfully migrated'); 22 | process.exit(0); 23 | // mongo._dump(function() { 24 | // process.exit(0); 25 | // }); 26 | }); 27 | }); 28 | 29 | 30 | function run(cb) { 31 | var pending = 0, 32 | ended = false; 33 | level.db.readStream() 34 | .on('data', function(data) { 35 | pending++; 36 | migrate(data.key, data.value, function(err) { 37 | if (err) throw err; 38 | pending--; 39 | if (pending == 0 && ended) { 40 | return cb(); 41 | } 42 | }); 43 | }) 44 | .on('error', function(err) { 45 | return cb(err); 46 | }) 47 | .on('end', function() { 48 | console.log('All old data read') 49 | ended = true; 50 | if (!pending) { 51 | return cb(); 52 | } 53 | }); 54 | }; 55 | 56 | function migrate(key, value, cb) { 57 | if (key.match(/^copayer!/)) { 58 | value.copayerId = key.substring(key.indexOf('!') + 1); 59 | mongo.db.collection('copayers_lookup').insert(value, cb); 60 | } else if (key.match(/!addr!/)) { 61 | value.walletId = key.substring(2, key.indexOf('!addr')); 62 | value.network = Bitcore.Address(value.address).toObject().network; 63 | mongo.db.collection('addresses').insert(value, cb); 64 | } else if (key.match(/!not!/)) { 65 | mongo.db.collection('notifications').insert(value, cb); 66 | } else if (key.match(/!p?txp!/)) { 67 | value.isPending = key.indexOf('!ptxp!') != -1; 68 | value.network = Bitcore.Address(value.toAddress).toObject().network; 69 | mongo.db.collection('txs').insert(value, cb); 70 | } else if (key.match(/!main$/)) { 71 | mongo.db.collection('wallets').insert(value, cb); 72 | } else { 73 | return cb(new Error('Invalid key ' + key)); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p logs 4 | mkdir -p pids 5 | 6 | # run_program (nodefile, pidfile, logfile) 7 | run_program () 8 | { 9 | nodefile=$1 10 | pidfile=$2 11 | logfile=$3 12 | 13 | if [ -e "$pidfile" ] 14 | then 15 | echo "$nodefile is already running. Run 'npm stop' if you wish to restart." 16 | return 0 17 | fi 18 | 19 | nohup node $nodefile >> $logfile 2>&1 & 20 | PID=$! 21 | if [ $? -eq 0 ] 22 | then 23 | echo "Successfully started $nodefile. PID=$PID. Logs are at $logfile" 24 | echo $PID > $pidfile 25 | return 0 26 | else 27 | echo "Could not start $nodefile - check logs at $logfile" 28 | exit 1 29 | fi 30 | } 31 | 32 | run_program locker/locker.js pids/locker.pid logs/locker.log 33 | run_program messagebroker/messagebroker.js pids/messagebroker.pid logs/messagebroker.log 34 | run_program bcmonitor/bcmonitor.js pids/bcmonitor.pid logs/bcmonitor.log 35 | run_program emailservice/emailservice.js pids/emailservice.pid logs/emailservice.log 36 | run_program pushnotificationsservice/pushnotificationsservice.js pids/pushnotificationsservice.pid logs/pushnotificationsservice.log 37 | run_program fiatrateservice/fiatrateservice.js pids/fiatrateservice.pid logs/fiatrateservice.log 38 | run_program bws.js pids/bws.pid logs/bws.log 39 | 40 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | stop_program () 4 | { 5 | pidfile=$1 6 | 7 | echo "Stopping Process - $pidfile. PID=$(cat $pidfile)" 8 | kill -9 $(cat $pidfile) 9 | rm $pidfile 10 | 11 | } 12 | 13 | stop_program pids/bws.pid 14 | stop_program pids/fiatrateservice.pid 15 | stop_program pids/emailservice.pid 16 | stop_program pids/bcmonitor.pid 17 | stop_program pids/pushnotificationsservice.pid 18 | stop_program pids/messagebroker.pid 19 | stop_program pids/locker.pid 20 | 21 | -------------------------------------------------------------------------------- /test/bitcorenode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var proxyquire = require('proxyquire'); 5 | var bitcore = require('bitcore-lib'); 6 | var sinon = require('sinon'); 7 | var Service = require('../bitcorenode'); 8 | 9 | describe('Bitcore Node Service', function() { 10 | describe('#constructor', function() { 11 | it('https settings from node', function() { 12 | var node = { 13 | https: true, 14 | httpsOptions: { 15 | key: 'key', 16 | cert: 'cert' 17 | } 18 | }; 19 | var options = { 20 | node: node 21 | }; 22 | var service = new Service(options); 23 | service.node.should.equal(node); 24 | service.https.should.equal(true); 25 | service.httpsOptions.should.deep.equal({ 26 | key: 'key', 27 | cert: 'cert' 28 | }); 29 | service.bwsPort.should.equal(3232); 30 | service.messageBrokerPort.should.equal(3380); 31 | service.lockerPort.should.equal(3231); 32 | }); 33 | it('direct https options', function() { 34 | var node = {}; 35 | var options = { 36 | node: node, 37 | https: true, 38 | httpsOptions: { 39 | key: 'key', 40 | cert: 'cert' 41 | } 42 | }; 43 | var service = new Service(options); 44 | service.https.should.equal(true); 45 | service.httpsOptions.should.deep.equal({ 46 | key: 'key', 47 | cert: 'cert' 48 | }); 49 | service.bwsPort.should.equal(3232); 50 | service.messageBrokerPort.should.equal(3380); 51 | service.lockerPort.should.equal(3231); 52 | }); 53 | it('can set custom ports', function() { 54 | var node = {}; 55 | var options = { 56 | node: node, 57 | bwsPort: 1000, 58 | messageBrokerPort: 1001, 59 | lockerPort: 1002 60 | }; 61 | var service = new Service(options); 62 | service.bwsPort.should.equal(1000); 63 | service.messageBrokerPort.should.equal(1001); 64 | service.lockerPort.should.equal(1002); 65 | }); 66 | }); 67 | describe('#readHttpsOptions', function() { 68 | var TestService = proxyquire('../bitcorenode', { 69 | fs: { 70 | readFileSync: function(arg) { 71 | return arg; 72 | } 73 | } 74 | }); 75 | it('will create server options from httpsOptions', function() { 76 | var options = { 77 | node: { 78 | https: true, 79 | httpsOptions: { 80 | key: 'key', 81 | cert: 'cert', 82 | CAinter1: 'CAinter1', 83 | CAinter2: 'CAinter2', 84 | CAroot: 'CAroot' 85 | } 86 | } 87 | }; 88 | var service = new TestService(options); 89 | var serverOptions = service._readHttpsOptions(); 90 | serverOptions.key.should.equal('key'); 91 | serverOptions.cert.should.equal('cert'); 92 | serverOptions.ca[0].should.equal('CAinter1'); 93 | serverOptions.ca[1].should.equal('CAinter2'); 94 | serverOptions.ca[2].should.equal('CAroot'); 95 | }); 96 | }); 97 | describe('#_getConfiguration', function() { 98 | it('will throw with an unknown network', function() { 99 | var options = { 100 | node: { 101 | network: 'unknown' 102 | } 103 | }; 104 | var service = new Service(options); 105 | (function() { 106 | service._getConfiguration(); 107 | }).should.throw('Unknown network'); 108 | }); 109 | it('livenet local insight', function() { 110 | var options = { 111 | node: { 112 | network: bitcore.Networks.livenet, 113 | port: 3001 114 | } 115 | }; 116 | var service = new Service(options); 117 | var config = service._getConfiguration(); 118 | config.blockchainExplorerOpts.livenet.should.deep.equal({ 119 | 'apiPrefix': '/insight-api', 120 | 'provider': 'insight', 121 | 'url': 'http://localhost:3001' 122 | }); 123 | }); 124 | it('testnet local insight', function() { 125 | var options = { 126 | node: { 127 | network: bitcore.Networks.testnet, 128 | port: 3001 129 | } 130 | }; 131 | var service = new Service(options); 132 | var config = service._getConfiguration(); 133 | config.blockchainExplorerOpts.testnet.should.deep.equal({ 134 | 'apiPrefix': '/insight-api', 135 | 'provider': 'insight', 136 | 'url': 'http://localhost:3001' 137 | }); 138 | }); 139 | }); 140 | describe('#_startWalletService', function() { 141 | it('error from express', function(done) { 142 | function TestExpressApp() {} 143 | TestExpressApp.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); 144 | function TestWSApp() {} 145 | TestWSApp.prototype.start = sinon.stub().callsArg(2); 146 | var listen = sinon.stub().callsArg(1); 147 | var TestService = proxyquire('../bitcorenode', { 148 | '../lib/expressapp': TestExpressApp, 149 | '../lib/wsapp': TestWSApp, 150 | 'http': { 151 | Server: sinon.stub().returns({ 152 | listen: listen 153 | }) 154 | } 155 | }); 156 | var options = { 157 | node: { 158 | bwsPort: 3232 159 | } 160 | }; 161 | var service = new TestService(options); 162 | var config = {}; 163 | service._startWalletService(config, function(err) { 164 | err.message.should.equal('test'); 165 | done(); 166 | }); 167 | }); 168 | it('error from server.listen', function(done) { 169 | var app = {}; 170 | function TestExpressApp() { 171 | this.app = app; 172 | } 173 | TestExpressApp.prototype.start = sinon.stub().callsArg(1); 174 | function TestWSApp() {} 175 | TestWSApp.prototype.start = sinon.stub().callsArg(2); 176 | var listen = sinon.stub().callsArgWith(1, new Error('test')); 177 | var TestService = proxyquire('../bitcorenode', { 178 | '../lib/expressapp': TestExpressApp, 179 | '../lib/wsapp': TestWSApp, 180 | 'http': { 181 | Server: function() { 182 | arguments[0].should.equal(app); 183 | return { 184 | listen: listen 185 | }; 186 | } 187 | } 188 | }); 189 | var options = { 190 | node: { 191 | bwsPort: 3232 192 | } 193 | }; 194 | var service = new TestService(options); 195 | var config = {}; 196 | service._startWalletService(config, function(err) { 197 | err.message.should.equal('test'); 198 | done(); 199 | }); 200 | }); 201 | it('will enable https', function(done) { 202 | var app = {}; 203 | function TestExpressApp() { 204 | this.app = app; 205 | } 206 | TestExpressApp.prototype.start = sinon.stub().callsArg(1); 207 | function TestWSApp() {} 208 | TestWSApp.prototype.start = sinon.stub().callsArg(2); 209 | var listen = sinon.stub().callsArg(1); 210 | var httpsOptions = {}; 211 | var createServer = function() { 212 | arguments[0].should.equal(httpsOptions); 213 | arguments[1].should.equal(app); 214 | return { 215 | listen: listen 216 | }; 217 | }; 218 | var TestService = proxyquire('../bitcorenode', { 219 | '../lib/expressapp': TestExpressApp, 220 | '../lib/wsapp': TestWSApp, 221 | 'https': { 222 | createServer: createServer 223 | } 224 | }); 225 | var options = { 226 | node: { 227 | https: true, 228 | bwsPort: 3232 229 | } 230 | }; 231 | var service = new TestService(options); 232 | service._readHttpsOptions = sinon.stub().returns(httpsOptions); 233 | var config = {}; 234 | service._startWalletService(config, function(err) { 235 | service._readHttpsOptions.callCount.should.equal(1); 236 | listen.callCount.should.equal(1); 237 | done(); 238 | }); 239 | }); 240 | }); 241 | describe('#start', function(done) { 242 | it('error from configuration', function(done) { 243 | var options = { 244 | node: {} 245 | }; 246 | var service = new Service(options); 247 | service._getConfiguration = function() { 248 | throw new Error('test'); 249 | }; 250 | service.start(function(err) { 251 | err.message.should.equal('test'); 252 | done(); 253 | }); 254 | }); 255 | it('error from blockchain monitor', function(done) { 256 | var app = {}; 257 | function TestBlockchainMonitor() {} 258 | TestBlockchainMonitor.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); 259 | function TestLocker() {} 260 | TestLocker.prototype.listen = sinon.stub(); 261 | function TestEmailService() {} 262 | TestEmailService.prototype.start = sinon.stub(); 263 | var TestService = proxyquire('../bitcorenode', { 264 | '../lib/blockchainmonitor': TestBlockchainMonitor, 265 | '../lib/emailservice': TestEmailService, 266 | 'socket.io': sinon.stub().returns({ 267 | on: sinon.stub() 268 | }), 269 | 'locker-server': TestLocker, 270 | }); 271 | var options = { 272 | node: {} 273 | }; 274 | var service = new TestService(options); 275 | var config = {}; 276 | service._getConfiguration = sinon.stub().returns(config); 277 | service._startWalletService = sinon.stub().callsArg(1); 278 | service.start(function(err) { 279 | err.message.should.equal('test'); 280 | done(); 281 | }); 282 | }); 283 | it('error from email service', function(done) { 284 | var app = {}; 285 | function TestBlockchainMonitor() {} 286 | TestBlockchainMonitor.prototype.start = sinon.stub().callsArg(1); 287 | function TestLocker() {} 288 | TestLocker.prototype.listen = sinon.stub(); 289 | function TestEmailService() {} 290 | TestEmailService.prototype.start = sinon.stub().callsArgWith(1, new Error('test')); 291 | var TestService = proxyquire('../bitcorenode', { 292 | '../lib/blockchainmonitor': TestBlockchainMonitor, 293 | '../lib/emailservice': TestEmailService, 294 | 'socket.io': sinon.stub().returns({ 295 | on: sinon.stub() 296 | }), 297 | 'locker-server': TestLocker, 298 | }); 299 | var options = { 300 | node: {} 301 | }; 302 | var service = new TestService(options); 303 | service._getConfiguration = sinon.stub().returns({ 304 | emailOpts: {} 305 | }); 306 | var config = {}; 307 | service._startWalletService = sinon.stub().callsArg(1); 308 | service.start(function(err) { 309 | err.message.should.equal('test'); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /test/blockchainexplorer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var BlockchainExplorer = require('../lib/blockchainexplorer'); 8 | 9 | describe('Blockchain explorer', function() { 10 | describe('#constructor', function() { 11 | it('should return a blockchain explorer with basic methods', function() { 12 | var exp = new BlockchainExplorer({ 13 | provider: 'insight', 14 | network: 'testnet', 15 | }); 16 | should.exist(exp); 17 | exp.should.respondTo('broadcast'); 18 | exp.should.respondTo('getUtxos'); 19 | exp.should.respondTo('getTransactions'); 20 | exp.should.respondTo('getAddressActivity'); 21 | exp.should.respondTo('estimateFee'); 22 | exp.should.respondTo('initSocket'); 23 | var exp = new BlockchainExplorer({ 24 | provider: 'insight', 25 | network: 'livenet', 26 | }); 27 | should.exist(exp); 28 | }); 29 | it('should fail on unsupported provider', function() { 30 | (function() { 31 | var exp = new BlockchainExplorer({ 32 | provider: 'dummy', 33 | }); 34 | }).should.throw('not supported'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/expressapp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var sinon = require('sinon'); 5 | var request = require('request'); 6 | var http = require('http'); 7 | var should = chai.should(); 8 | var proxyquire = require('proxyquire'); 9 | var config = require('../config.js'); 10 | 11 | var Common = require('../lib/common'); 12 | var Defaults = Common.Defaults; 13 | 14 | 15 | 16 | describe('ExpressApp', function() { 17 | describe('#constructor', function() { 18 | it('will set an express app', function() { 19 | var TestExpressApp = proxyquire('../lib/expressapp', {}); 20 | var express = new TestExpressApp(); 21 | should.exist(express.app); 22 | should.exist(express.app.use); 23 | should.exist(express.app.enable); 24 | }); 25 | }); 26 | describe('#start', function() { 27 | it('will listen at the specified port', function(done) { 28 | var initialize = sinon.stub().callsArg(1); 29 | var TestExpressApp = proxyquire('../lib/expressapp', { 30 | './server': { 31 | initialize: initialize 32 | } 33 | }); 34 | var app = new TestExpressApp(); 35 | var options = {}; 36 | app.start(config, function(err) { 37 | should.not.exist(err); 38 | initialize.callCount.should.equal(1); 39 | done(); 40 | }); 41 | }); 42 | 43 | describe('Routes', function() { 44 | var testPort = 3239; 45 | var testHost = 'http://127.0.0.1'; 46 | var httpServer; 47 | 48 | function start(ExpressApp, done) { 49 | var app = new ExpressApp(); 50 | httpServer = http.Server(app.app); 51 | 52 | app.start(config, function(err) { 53 | should.not.exist(err); 54 | httpServer.listen(testPort); 55 | done(); 56 | }); 57 | }; 58 | 59 | afterEach(function() { 60 | httpServer.close(); 61 | }); 62 | 63 | it('/v2/wallets', function(done) { 64 | var server = { 65 | getStatus: sinon.stub().callsArgWith(1, null, {}), 66 | }; 67 | var TestExpressApp = proxyquire('../lib/expressapp', { 68 | './server': { 69 | initialize: sinon.stub().callsArg(1), 70 | getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), 71 | } 72 | }); 73 | start(TestExpressApp, function() { 74 | var requestOptions = { 75 | url: testHost + ':' + testPort + config.basePath + '/v2/wallets', 76 | headers: { 77 | 'x-identity': 'identity', 78 | 'x-signature': 'signature' 79 | } 80 | }; 81 | request(requestOptions, function(err, res, body) { 82 | should.not.exist(err); 83 | should.exist(res.headers['x-service-version']); 84 | res.headers['x-service-version'].should.equal('bws-' + require('../package').version); 85 | res.statusCode.should.equal(200); 86 | body.should.equal('{}'); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | 92 | it('/v1/addresses', function(done) { 93 | var server = { 94 | getMainAddresses: sinon.stub().callsArgWith(1, null, {}), 95 | }; 96 | var TestExpressApp = proxyquire('../lib/expressapp', { 97 | './server': { 98 | initialize: sinon.stub().callsArg(1), 99 | getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), 100 | } 101 | }); 102 | start(TestExpressApp, function() { 103 | var requestOptions = { 104 | url: testHost + ':' + testPort + config.basePath + '/v1/addresses?limit=4&reverse=1', 105 | headers: { 106 | 'x-identity': 'identity', 107 | 'x-signature': 'signature' 108 | } 109 | }; 110 | request(requestOptions, function(err, res, body) { 111 | should.not.exist(err); 112 | res.statusCode.should.equal(200); 113 | var args = server.getMainAddresses.getCalls()[0].args[0]; 114 | args.limit.should.equal(4); 115 | args.reverse.should.be.true; 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | it('/v1/sendmaxinfo', function(done) { 122 | var server = { 123 | getSendMaxInfo: sinon.stub().callsArgWith(1, null, { 124 | amount: 123 125 | }), 126 | }; 127 | var TestExpressApp = proxyquire('../lib/expressapp', { 128 | './server': { 129 | initialize: sinon.stub().callsArg(1), 130 | getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), 131 | } 132 | }); 133 | start(TestExpressApp, function() { 134 | var requestOptions = { 135 | url: testHost + ':' + testPort + config.basePath + '/v1/sendmaxinfo?feePerKb=10000&returnInputs=1', 136 | headers: { 137 | 'x-identity': 'identity', 138 | 'x-signature': 'signature' 139 | } 140 | }; 141 | request(requestOptions, function(err, res, body) { 142 | should.not.exist(err); 143 | res.statusCode.should.equal(200); 144 | var args = server.getSendMaxInfo.getCalls()[0].args[0]; 145 | args.feePerKb.should.equal(10000); 146 | args.returnInputs.should.be.true; 147 | JSON.parse(body).amount.should.equal(123); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | 153 | describe('Balance', function() { 154 | it('should handle cache argument', function(done) { 155 | var server = { 156 | getBalance: sinon.stub().callsArgWith(1, null, {}), 157 | }; 158 | var TestExpressApp = proxyquire('../lib/expressapp', { 159 | './server': { 160 | initialize: sinon.stub().callsArg(1), 161 | getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), 162 | } 163 | }); 164 | start(TestExpressApp, function() { 165 | var reqOpts = { 166 | url: testHost + ':' + testPort + config.basePath + '/v1/balance', 167 | headers: { 168 | 'x-identity': 'identity', 169 | 'x-signature': 'signature' 170 | } 171 | }; 172 | request(reqOpts, function(err, res, body) { 173 | should.not.exist(err); 174 | res.statusCode.should.equal(200); 175 | var args = server.getBalance.getCalls()[0].args[0]; 176 | should.not.exist(args.twoStep); 177 | 178 | reqOpts.url += '?twoStep=1'; 179 | request(reqOpts, function(err, res, body) { 180 | should.not.exist(err); 181 | res.statusCode.should.equal(200); 182 | var args = server.getBalance.getCalls()[1].args[0]; 183 | args.twoStep.should.equal(true); 184 | done(); 185 | }); 186 | }); 187 | }); 188 | }); 189 | }); 190 | 191 | describe('/v1/notifications', function(done) { 192 | var server, TestExpressApp, clock; 193 | beforeEach(function() { 194 | clock = sinon.useFakeTimers(2000000000, 'Date'); 195 | 196 | server = { 197 | getNotifications: sinon.stub().callsArgWith(1, null, {}) 198 | }; 199 | TestExpressApp = proxyquire('../lib/expressapp', { 200 | './server': { 201 | initialize: sinon.stub().callsArg(1), 202 | getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server), 203 | } 204 | }); 205 | }); 206 | afterEach(function() { 207 | clock.restore(); 208 | }); 209 | 210 | it('should fetch notifications from a specified id', function(done) { 211 | start(TestExpressApp, function() { 212 | var requestOptions = { 213 | url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?notificationId=123', 214 | headers: { 215 | 'x-identity': 'identity', 216 | 'x-signature': 'signature' 217 | } 218 | }; 219 | request(requestOptions, function(err, res, body) { 220 | should.not.exist(err); 221 | res.statusCode.should.equal(200); 222 | body.should.equal('{}'); 223 | server.getNotifications.calledWith({ 224 | notificationId: '123', 225 | minTs: +Date.now() - Defaults.NOTIFICATIONS_TIMESPAN * 1000, 226 | }).should.be.true; 227 | done(); 228 | }); 229 | }); 230 | }); 231 | it('should allow custom minTs within limits', function(done) { 232 | start(TestExpressApp, function() { 233 | var requestOptions = { 234 | url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?timeSpan=30', 235 | headers: { 236 | 'x-identity': 'identity', 237 | 'x-signature': 'signature' 238 | } 239 | }; 240 | request(requestOptions, function(err, res, body) { 241 | should.not.exist(err); 242 | res.statusCode.should.equal(200); 243 | server.getNotifications.calledWith({ 244 | notificationId: undefined, 245 | minTs: +Date.now() - 30000, 246 | }).should.be.true; 247 | done(); 248 | }); 249 | }); 250 | }); 251 | it('should limit minTs to Defaults.MAX_NOTIFICATIONS_TIMESPAN', function(done) { 252 | start(TestExpressApp, function() { 253 | var overLimit = Defaults.MAX_NOTIFICATIONS_TIMESPAN * 2; 254 | var requestOptions = { 255 | url: testHost + ':' + testPort + config.basePath + '/v1/notifications' + '?timeSpan=' + overLimit , 256 | headers: { 257 | 'x-identity': 'identity', 258 | 'x-signature': 'signature' 259 | } 260 | }; 261 | request(requestOptions, function(err, res, body) { 262 | should.not.exist(err); 263 | res.statusCode.should.equal(200); 264 | body.should.equal('{}'); 265 | 266 | server.getNotifications.calledWith({ 267 | notificationId: undefined, 268 | minTs: Date.now() - Defaults.MAX_NOTIFICATIONS_TIMESPAN * 1000, // override minTs argument 269 | }).should.be.true; 270 | done(); 271 | }); 272 | }); 273 | }); 274 | }); 275 | }); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /test/integration/bcmonitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | 6 | var chai = require('chai'); 7 | var sinon = require('sinon'); 8 | var should = chai.should(); 9 | var log = require('npmlog'); 10 | log.debug = log.verbose; 11 | log.level = 'info'; 12 | 13 | var WalletService = require('../../lib/server'); 14 | var BlockchainMonitor = require('../../lib/blockchainmonitor'); 15 | 16 | var TestData = require('../testdata'); 17 | var helpers = require('./helpers'); 18 | var storage, blockchainExplorer; 19 | 20 | var socket = { 21 | handlers: {}, 22 | }; 23 | socket.on = function(eventName, handler) { 24 | this.handlers[eventName] = handler; 25 | }; 26 | 27 | describe('Blockchain monitor', function() { 28 | var server, wallet; 29 | 30 | before(function(done) { 31 | helpers.before(done); 32 | }); 33 | after(function(done) { 34 | helpers.after(done); 35 | }); 36 | beforeEach(function(done) { 37 | helpers.beforeEach(function(res) { 38 | storage = res.storage; 39 | blockchainExplorer = res.blockchainExplorer; 40 | blockchainExplorer.initSocket = sinon.stub().returns(socket); 41 | 42 | helpers.createAndJoinWallet(2, 3, function(s, w) { 43 | server = s; 44 | wallet = w; 45 | 46 | var bcmonitor = new BlockchainMonitor(); 47 | bcmonitor.start({ 48 | lockOpts: {}, 49 | messageBroker: server.messageBroker, 50 | storage: storage, 51 | blockchainExplorers: { 52 | 'btc': { 53 | 'testnet': blockchainExplorer, 54 | 'livenet': blockchainExplorer 55 | } 56 | }, 57 | }, function(err) { 58 | should.not.exist(err); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | it('should notify copayers of incoming txs', function(done) { 66 | server.createAddress({}, function(err, address) { 67 | should.not.exist(err); 68 | 69 | var incoming = { 70 | txid: '123', 71 | vout: [{}], 72 | }; 73 | incoming.vout[0][address.address] = 1500; 74 | socket.handlers['tx'](incoming); 75 | 76 | setTimeout(function() { 77 | server.getNotifications({}, function(err, notifications) { 78 | should.not.exist(err); 79 | var notification = _.find(notifications, { 80 | type: 'NewIncomingTx' 81 | }); 82 | should.exist(notification); 83 | notification.walletId.should.equal(wallet.id); 84 | notification.data.txid.should.equal('123'); 85 | notification.data.address.should.equal(address.address); 86 | notification.data.amount.should.equal(1500); 87 | done(); 88 | }); 89 | }, 100); 90 | }); 91 | }); 92 | 93 | it('should not notify copayers of incoming txs more than once', function(done) { 94 | server.createAddress({}, function(err, address) { 95 | should.not.exist(err); 96 | 97 | var incoming = { 98 | txid: '123', 99 | vout: [{}], 100 | }; 101 | incoming.vout[0][address.address] = 1500; 102 | socket.handlers['tx'](incoming); 103 | setTimeout(function() { 104 | socket.handlers['tx'](incoming); 105 | 106 | setTimeout(function() { 107 | server.getNotifications({}, function(err, notifications) { 108 | should.not.exist(err); 109 | var notification = _.filter(notifications, { 110 | type: 'NewIncomingTx' 111 | }); 112 | notification.length.should.equal(1); 113 | done(); 114 | }); 115 | }, 100); 116 | }, 50); 117 | }); 118 | }); 119 | 120 | it('should notify copayers of tx confirmation', function(done) { 121 | server.createAddress({}, function(err, address) { 122 | should.not.exist(err); 123 | 124 | var incoming = { 125 | txid: '123', 126 | vout: [{}], 127 | }; 128 | incoming.vout[0][address.address] = 1500; 129 | 130 | server.txConfirmationSubscribe({ 131 | txid: '123' 132 | }, function(err) { 133 | should.not.exist(err); 134 | 135 | blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']); 136 | socket.handlers['block']('block1'); 137 | 138 | setTimeout(function() { 139 | blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']); 140 | socket.handlers['block']('block2'); 141 | 142 | setTimeout(function() { 143 | server.getNotifications({}, function(err, notifications) { 144 | should.not.exist(err); 145 | var notifications = _.filter(notifications, { 146 | type: 'TxConfirmation' 147 | }); 148 | notifications.length.should.equal(1); 149 | var n = notifications[0]; 150 | n.walletId.should.equal(wallet.id); 151 | n.creatorId.should.equal(server.copayerId); 152 | n.data.txid.should.equal('123'); 153 | done(); 154 | }); 155 | }, 50); 156 | }, 50); 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/integration/fiatrateservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | 6 | var chai = require('chai'); 7 | var sinon = require('sinon'); 8 | var should = chai.should(); 9 | var log = require('npmlog'); 10 | log.debug = log.verbose; 11 | log.level = 'info'; 12 | 13 | var helpers = require('./helpers'); 14 | 15 | var FiatRateService = require('../../lib/fiatrateservice'); 16 | 17 | describe('Fiat rate service', function() { 18 | var service, request; 19 | 20 | before(function(done) { 21 | helpers.before(done); 22 | }); 23 | after(function(done) { 24 | helpers.after(done); 25 | }); 26 | beforeEach(function(done) { 27 | helpers.beforeEach(function() { 28 | service = new FiatRateService(); 29 | request = sinon.stub(); 30 | request.get = sinon.stub(); 31 | service.init({ 32 | storage: helpers.getStorage(), 33 | request: request, 34 | }, function(err) { 35 | should.not.exist(err); 36 | service.startCron({}, done); 37 | }); 38 | }); 39 | }); 40 | describe('#getRate', function() { 41 | it('should get current rate', function(done) { 42 | service.storage.storeFiatRate('BitPay', [{ 43 | code: 'USD', 44 | value: 123.45, 45 | }], function(err) { 46 | should.not.exist(err); 47 | service.getRate({ 48 | code: 'USD' 49 | }, function(err, res) { 50 | should.not.exist(err); 51 | res.rate.should.equal(123.45); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | it('should get current rate for different currency', function(done) { 57 | service.storage.storeFiatRate('BitPay', [{ 58 | code: 'USD', 59 | value: 123.45, 60 | }], function(err) { 61 | should.not.exist(err); 62 | service.storage.storeFiatRate('BitPay', [{ 63 | code: 'EUR', 64 | value: 345.67, 65 | }], function(err) { 66 | should.not.exist(err); 67 | service.getRate({ 68 | code: 'EUR' 69 | }, function(err, res) { 70 | should.not.exist(err); 71 | res.rate.should.equal(345.67); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | it('should get current rate for different provider', function(done) { 79 | service.storage.storeFiatRate('BitPay', [{ 80 | code: 'USD', 81 | value: 100.00, 82 | }], function(err) { 83 | should.not.exist(err); 84 | service.storage.storeFiatRate('Bitstamp', [{ 85 | code: 'USD', 86 | value: 200.00, 87 | }], function(err) { 88 | should.not.exist(err); 89 | service.getRate({ 90 | code: 'USD' 91 | }, function(err, res) { 92 | should.not.exist(err); 93 | res.rate.should.equal(100.00, 'Should use default provider'); 94 | service.getRate({ 95 | code: 'USD', 96 | provider: 'Bitstamp', 97 | }, function(err, res) { 98 | should.not.exist(err); 99 | res.rate.should.equal(200.00); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | it('should get rate for specific ts', function(done) { 108 | var clock = sinon.useFakeTimers(0, 'Date'); 109 | clock.tick(20); 110 | service.storage.storeFiatRate('BitPay', [{ 111 | code: 'USD', 112 | value: 123.45, 113 | }], function(err) { 114 | should.not.exist(err); 115 | clock.tick(100); 116 | service.storage.storeFiatRate('BitPay', [{ 117 | code: 'USD', 118 | value: 345.67, 119 | }], function(err) { 120 | should.not.exist(err); 121 | service.getRate({ 122 | code: 'USD', 123 | ts: 50, 124 | }, function(err, res) { 125 | should.not.exist(err); 126 | res.ts.should.equal(50); 127 | res.rate.should.equal(123.45); 128 | res.fetchedOn.should.equal(20); 129 | clock.restore(); 130 | done(); 131 | }); 132 | }); 133 | }); 134 | }); 135 | 136 | it('should get rates for a series of ts', function(done) { 137 | var clock = sinon.useFakeTimers(0, 'Date'); 138 | async.each([1.00, 2.00, 3.00, 4.00], function(value, next) { 139 | clock.tick(100); 140 | service.storage.storeFiatRate('BitPay', [{ 141 | code: 'USD', 142 | value: value, 143 | }, { 144 | code: 'EUR', 145 | value: value, 146 | }], next); 147 | }, function(err) { 148 | should.not.exist(err); 149 | service.getRate({ 150 | code: 'USD', 151 | ts: [50, 100, 199, 500], 152 | }, function(err, res) { 153 | should.not.exist(err); 154 | res.length.should.equal(4); 155 | 156 | res[0].ts.should.equal(50); 157 | should.not.exist(res[0].rate); 158 | should.not.exist(res[0].fetchedOn); 159 | 160 | res[1].ts.should.equal(100); 161 | res[1].rate.should.equal(1.00); 162 | res[1].fetchedOn.should.equal(100); 163 | 164 | res[2].ts.should.equal(199); 165 | res[2].rate.should.equal(1.00); 166 | res[2].fetchedOn.should.equal(100); 167 | 168 | res[3].ts.should.equal(500); 169 | res[3].rate.should.equal(4.00); 170 | res[3].fetchedOn.should.equal(400); 171 | 172 | clock.restore(); 173 | done(); 174 | }); 175 | }); 176 | }); 177 | 178 | it('should not get rate older than 2hs', function(done) { 179 | var clock = sinon.useFakeTimers(0, 'Date'); 180 | service.storage.storeFiatRate('BitPay', [{ 181 | code: 'USD', 182 | value: 123.45, 183 | }], function(err) { 184 | should.not.exist(err); 185 | clock.tick(24 * 3600 * 1000); // Some time in the future 186 | service.getRate({ 187 | ts: 2 * 3600 * 1000 - 1, // almost 2 hours 188 | code: 'USD', 189 | }, function(err, res) { 190 | should.not.exist(err); 191 | res.rate.should.equal(123.45); 192 | res.fetchedOn.should.equal(0); 193 | service.getRate({ 194 | ts: 2 * 3600 * 1000 + 1, // just past 2 hours 195 | code: 'USD', 196 | }, function(err, res) { 197 | should.not.exist(err); 198 | should.not.exist(res.rate); 199 | clock.restore(); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | 206 | }); 207 | 208 | describe('#fetch', function() { 209 | it('should fetch rates from all providers', function(done) { 210 | var clock = sinon.useFakeTimers(100, 'Date'); 211 | var bitpay = [{ 212 | code: 'USD', 213 | rate: 123.45, 214 | }, { 215 | code: 'EUR', 216 | rate: 234.56, 217 | }]; 218 | var bitstamp = { 219 | last: 120.00, 220 | }; 221 | request.get.withArgs({ 222 | url: 'https://bitpay.com/api/rates/', 223 | json: true 224 | }).yields(null, null, bitpay); 225 | request.get.withArgs({ 226 | url: 'https://www.bitstamp.net/api/ticker/', 227 | json: true 228 | }).yields(null, null, bitstamp); 229 | 230 | service._fetch(function(err) { 231 | should.not.exist(err); 232 | service.getRate({ 233 | code: 'USD' 234 | }, function(err, res) { 235 | should.not.exist(err); 236 | res.fetchedOn.should.equal(100); 237 | res.rate.should.equal(123.45); 238 | service.getRate({ 239 | code: 'USD', 240 | provider: 'Bitstamp', 241 | }, function(err, res) { 242 | should.not.exist(err); 243 | res.fetchedOn.should.equal(100); 244 | res.rate.should.equal(120.00); 245 | service.getRate({ 246 | code: 'EUR' 247 | }, function(err, res) { 248 | should.not.exist(err); 249 | res.fetchedOn.should.equal(100); 250 | res.rate.should.equal(234.56); 251 | clock.restore(); 252 | done(); 253 | }); 254 | }); 255 | }); 256 | }); 257 | }); 258 | 259 | it('should not stop when failing to fetch provider', function(done) { 260 | var clock = sinon.useFakeTimers(100, 'Date'); 261 | var bitstamp = { 262 | last: 120.00, 263 | }; 264 | request.get.withArgs({ 265 | url: 'https://bitpay.com/api/rates/', 266 | json: true 267 | }).yields('dummy error', null, null); 268 | request.get.withArgs({ 269 | url: 'https://www.bitstamp.net/api/ticker/', 270 | json: true 271 | }).yields(null, null, bitstamp); 272 | 273 | service._fetch(function(err) { 274 | should.not.exist(err); 275 | service.getRate({ 276 | code: 'USD' 277 | }, function(err, res) { 278 | should.not.exist(err); 279 | res.ts.should.equal(100); 280 | should.not.exist(res.rate) 281 | should.not.exist(res.fetchedOn) 282 | service.getRate({ 283 | code: 'USD', 284 | provider: 'Bitstamp' 285 | }, function(err, res) { 286 | should.not.exist(err); 287 | res.fetchedOn.should.equal(100); 288 | res.rate.should.equal(120.00); 289 | clock.restore(); 290 | done(); 291 | }); 292 | }); 293 | }); 294 | }); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /test/locallock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var Lock = require('../lib/locallock'); 8 | 9 | 10 | describe('Local locks', function() { 11 | var lock; 12 | beforeEach(function() { 13 | this.clock = sinon.useFakeTimers(); 14 | lock = new Lock(); 15 | }); 16 | afterEach(function() { 17 | this.clock.restore(); 18 | }); 19 | it('should lock tasks using the same token', function() { 20 | var a = false, 21 | b = false; 22 | lock.locked('123', 0, 0, function(err, release) { 23 | should.not.exist(err); 24 | a = true; 25 | setTimeout(function() { 26 | release(); 27 | }, 5); 28 | lock.locked('123', 0, 0, function(err, release) { 29 | should.not.exist(err); 30 | b = true; 31 | release(); 32 | }); 33 | }); 34 | a.should.equal(true); 35 | b.should.equal(false); 36 | this.clock.tick(10); 37 | a.should.equal(true); 38 | b.should.equal(true); 39 | }); 40 | it('should not lock tasks using different tokens', function() { 41 | var i = 0; 42 | lock.locked('123', 0, 0, function(err, release) { 43 | should.not.exist(err); 44 | i++; 45 | setTimeout(function() { 46 | release(); 47 | }, 5); 48 | lock.locked('456', 0, 0, function(err, release) { 49 | should.not.exist(err); 50 | i++; 51 | release(); 52 | }); 53 | }); 54 | i.should.equal(2); 55 | }); 56 | it('should return error if unable to acquire lock', function() { 57 | lock.locked('123', 0, 0, function(err, release) { 58 | should.not.exist(err); 59 | setTimeout(function() { 60 | release(); 61 | }, 5); 62 | lock.locked('123', 1, 0, function(err, release) { 63 | should.exist(err); 64 | err.toString().should.contain('Could not acquire lock 123'); 65 | }); 66 | }); 67 | this.clock.tick(2); 68 | }); 69 | it('should release lock if acquired for a long time', function() { 70 | var i = 0; 71 | lock.locked('123', 0, 3, function(err, release) { 72 | should.not.exist(err); 73 | i++; 74 | lock.locked('123', 20, 0, function(err, release) { 75 | should.not.exist(err); 76 | i++; 77 | release(); 78 | }); 79 | }); 80 | i.should.equal(1); 81 | this.clock.tick(1); 82 | i.should.equal(1); 83 | this.clock.tick(10); 84 | i.should.equal(2); 85 | }); 86 | it('should only release one pending task on lock timeout', function() { 87 | var i = 0; 88 | lock.locked('123', 0, 3, function(err, release) { 89 | should.not.exist(err); 90 | i++; 91 | lock.locked('123', 5, 0, function(err, release) { 92 | should.not.exist(err); 93 | i++; 94 | setTimeout(function() { 95 | release(); 96 | }, 5); 97 | }); 98 | lock.locked('123', 20, 0, function(err, release) { 99 | should.not.exist(err); 100 | i++; 101 | release(); 102 | }); 103 | }); 104 | i.should.equal(1); 105 | this.clock.tick(4); 106 | i.should.equal(2) 107 | this.clock.tick(7); 108 | i.should.equal(3) 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | -------------------------------------------------------------------------------- /test/model/address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | 8 | var Address = require('../../lib/model/address'); 9 | 10 | describe('Address', function() { 11 | describe('#create', function() { 12 | it('should create livenet address', function() { 13 | var x = Address.create({ 14 | address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg', 15 | coin: 'btc', 16 | walletId: '123', 17 | isChange: false, 18 | path: 'm/0/1', 19 | publicKeys: ['123', '456'], 20 | }); 21 | should.exist(x.createdOn); 22 | x.network.should.equal('livenet'); 23 | }); 24 | it('should create testnet address', function() { 25 | var x = Address.create({ 26 | address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj', 27 | coin: 'btc', 28 | walletId: '123', 29 | isChange: false, 30 | path: 'm/0/1', 31 | publicKeys: ['123', '456'], 32 | }); 33 | x.network.should.equal('testnet'); 34 | }); 35 | }); 36 | describe('#derive', function() { 37 | it('should derive multi-sig P2SH address', function() { 38 | var address = Address.derive('wallet-id', 'P2SH', [{ 39 | xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' 40 | // PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd 41 | }, { 42 | xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY' 43 | // PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171 44 | }], 'm/0/0', 1, 'btc', 'livenet', false); 45 | should.exist(address); 46 | address.walletId.should.equal('wallet-id'); 47 | address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw'); 48 | address.network.should.equal('livenet'); 49 | address.isChange.should.be.false; 50 | address.path.should.equal('m/0/0'); 51 | address.type.should.equal('P2SH'); 52 | }); 53 | it('should derive 1-of-1 P2SH address', function() { 54 | var address = Address.derive('wallet-id', 'P2SH', [{ 55 | xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' 56 | // PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd 57 | }], 'm/0/0', 1, 'btc', 'livenet', false); 58 | should.exist(address); 59 | address.walletId.should.equal('wallet-id'); 60 | address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH'); 61 | address.network.should.equal('livenet'); 62 | address.isChange.should.be.false; 63 | address.path.should.equal('m/0/0'); 64 | address.type.should.equal('P2SH'); 65 | }); 66 | it('should derive 1-of-1 P2PKH address', function() { 67 | var address = Address.derive('wallet-id', 'P2PKH', [{ 68 | xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' 69 | // PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7 70 | }], 'm/1/2', 1, 'btc', 'livenet', true); 71 | should.exist(address); 72 | address.walletId.should.equal('wallet-id'); 73 | address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT'); 74 | address.network.should.equal('livenet'); 75 | address.isChange.should.be.true; 76 | address.path.should.equal('m/1/2'); 77 | address.type.should.equal('P2PKH'); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/model/addressmanager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var AddressManager = require('../../lib/model/addressmanager'); 8 | 9 | 10 | describe('AddressManager', function() { 11 | describe('#create', function() { 12 | it('should create BIP45 address manager by default', function() { 13 | var am = AddressManager.create(); 14 | am.derivationStrategy.should.equal('BIP45'); 15 | }); 16 | }); 17 | describe('#fromObj', function() { 18 | it('should assume legacy address manager uses BIP45', function() { 19 | var obj = { 20 | version: '1.0.0', 21 | receiveAddressIndex: 2, 22 | changeAddressIndex: 0, 23 | copayerIndex: 4 24 | }; 25 | var am = AddressManager.fromObj(obj); 26 | am.derivationStrategy.should.equal('BIP45'); 27 | am.getCurrentAddressPath(false).should.equal('m/4/0/2'); 28 | }); 29 | }); 30 | describe('#supportsCopayerBranches', function() { 31 | it('should return true for BIP45 & false for BIP44', function() { 32 | AddressManager.supportsCopayerBranches('BIP45').should.be.true; 33 | AddressManager.supportsCopayerBranches('BIP44').should.be.false; 34 | }); 35 | }); 36 | describe('BIP45', function() { 37 | describe('#getCurrentAddressPath', function() { 38 | it('should return a valid BIP32 path for given index', function() { 39 | var am = AddressManager.create({ 40 | copayerIndex: 4, 41 | }); 42 | am.getCurrentAddressPath(false).should.equal('m/4/0/0'); 43 | am.getCurrentAddressPath(true).should.equal('m/4/1/0'); 44 | }); 45 | }); 46 | it('should return a valid BIP32 path for defaut Index', function() { 47 | var am = AddressManager.create({}); 48 | am.getCurrentAddressPath(false).should.equal('m/2147483647/0/0'); 49 | am.getCurrentAddressPath(true).should.equal('m/2147483647/1/0'); 50 | }); 51 | describe('#getNewAddressPath', function() { 52 | it('should return a new valid BIP32 path for given index', function() { 53 | var am = AddressManager.create({ 54 | copayerIndex: 2, 55 | }); 56 | am.getNewAddressPath(false).should.equal('m/2/0/0'); 57 | am.getNewAddressPath(true).should.equal('m/2/1/0'); 58 | am.getNewAddressPath(false).should.equal('m/2/0/1'); 59 | am.getNewAddressPath(true).should.equal('m/2/1/1'); 60 | }); 61 | }); 62 | describe('#rewindIndex', function() { 63 | it('should rewind main index', function() { 64 | var am = AddressManager.create({}); 65 | am.getNewAddressPath(false).should.equal('m/2147483647/0/0'); 66 | am.getNewAddressPath(false).should.equal('m/2147483647/0/1'); 67 | am.getNewAddressPath(false).should.equal('m/2147483647/0/2'); 68 | am.rewindIndex(false, 2); 69 | am.getNewAddressPath(false).should.equal('m/2147483647/0/1'); 70 | }); 71 | it('should rewind change index', function() { 72 | var am = AddressManager.create({}); 73 | am.getNewAddressPath(true).should.equal('m/2147483647/1/0'); 74 | am.rewindIndex(false, 1); 75 | am.getNewAddressPath(true).should.equal('m/2147483647/1/1'); 76 | am.rewindIndex(true, 2); 77 | am.getNewAddressPath(true).should.equal('m/2147483647/1/0'); 78 | }); 79 | it('should stop at 0', function() { 80 | var am = AddressManager.create({}); 81 | am.getNewAddressPath(false).should.equal('m/2147483647/0/0'); 82 | am.rewindIndex(false, 20); 83 | am.getNewAddressPath(false).should.equal('m/2147483647/0/0'); 84 | }); 85 | }); 86 | }); 87 | describe('BIP44', function() { 88 | describe('#getCurrentAddressPath', function() { 89 | it('should return first address path', function() { 90 | var am = AddressManager.create({ 91 | derivationStrategy: 'BIP44', 92 | }); 93 | am.getCurrentAddressPath(false).should.equal('m/0/0'); 94 | am.getCurrentAddressPath(true).should.equal('m/1/0'); 95 | }); 96 | it('should return address path independently of copayerIndex', function() { 97 | var am = AddressManager.create({ 98 | derivationStrategy: 'BIP44', 99 | copayerIndex: 4, 100 | }); 101 | am.getCurrentAddressPath(false).should.equal('m/0/0'); 102 | am.getCurrentAddressPath(true).should.equal('m/1/0'); 103 | }); 104 | }); 105 | describe('#getNewAddressPath', function() { 106 | it('should return a new path', function() { 107 | var am = AddressManager.create({ 108 | derivationStrategy: 'BIP44', 109 | }); 110 | am.getNewAddressPath(false).should.equal('m/0/0'); 111 | am.getNewAddressPath(true).should.equal('m/1/0'); 112 | am.getNewAddressPath(false).should.equal('m/0/1'); 113 | am.getNewAddressPath(true).should.equal('m/1/1'); 114 | }); 115 | }); 116 | describe('#rewindIndex', function() { 117 | it('should rewind main index', function() { 118 | var am = AddressManager.create({ 119 | derivationStrategy: 'BIP44', 120 | }); 121 | am.getNewAddressPath(false).should.equal('m/0/0'); 122 | am.getNewAddressPath(false).should.equal('m/0/1'); 123 | am.getNewAddressPath(false).should.equal('m/0/2'); 124 | am.rewindIndex(false, 2); 125 | am.getNewAddressPath(false).should.equal('m/0/1'); 126 | }); 127 | it('should rewind change index', function() { 128 | var am = AddressManager.create({ 129 | derivationStrategy: 'BIP44', 130 | }); 131 | am.getNewAddressPath(true).should.equal('m/1/0'); 132 | am.rewindIndex(false, 1); 133 | am.getNewAddressPath(true).should.equal('m/1/1'); 134 | am.rewindIndex(true, 2); 135 | am.getNewAddressPath(true).should.equal('m/1/0'); 136 | }); 137 | it('should stop at 0', function() { 138 | var am = AddressManager.create({ 139 | derivationStrategy: 'BIP44', 140 | }); 141 | am.getNewAddressPath(false).should.equal('m/0/0'); 142 | am.rewindIndex(false, 20); 143 | am.getNewAddressPath(false).should.equal('m/0/0'); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/model/copayer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var Wallet = require('../../lib/model/wallet'); 8 | var Copayer = require('../../lib/model/copayer'); 9 | 10 | 11 | describe('Copayer', function() { 12 | 13 | describe('#fromObj', function() { 14 | it('read a copayer', function() { 15 | var c = Copayer.fromObj(testWallet.copayers[0]); 16 | c.name.should.equal('copayer 1'); 17 | }); 18 | }); 19 | describe('#createAddress', function() { 20 | it('should create an address', function() { 21 | var w = Wallet.fromObj(testWallet); 22 | var c = Copayer.fromObj(testWallet.copayers[2]); 23 | should.exist(c.requestPubKeys); 24 | c.requestPubKeys.length.should.equal(1); 25 | var a1 = c.createAddress(w, true); 26 | a1.address.should.equal('3AXmDe2FkWY9g5LpRaTs1U7pXKtkNm3NBf'); 27 | a1.path.should.equal('m/2/1/0'); 28 | a1.createdOn.should.be.above(1); 29 | var a2 = c.createAddress(w, true); 30 | a2.path.should.equal('m/2/1/1'); 31 | }); 32 | }); 33 | }); 34 | 35 | 36 | var testWallet = { 37 | addressManager: { 38 | receiveAddressIndex: 0, 39 | changeAddressIndex: 0, 40 | copayerIndex: 2147483647, 41 | }, 42 | createdOn: 1422904188, 43 | id: '123', 44 | name: '123 wallet', 45 | network: 'livenet', 46 | m: 2, 47 | n: 3, 48 | status: 'complete', 49 | publicKeyRing: [{ 50 | xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', 51 | requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc' 52 | }, { 53 | xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', 54 | requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60' 55 | }, { 56 | xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', 57 | requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a' 58 | }, ], 59 | copayers: [{ 60 | addressManager: { 61 | receiveAddressIndex: 0, 62 | changeAddressIndex: 0, 63 | copayerIndex: 0, 64 | }, 65 | createdOn: 1422904189, 66 | id: '1', 67 | name: 'copayer 1', 68 | xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', 69 | requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc', 70 | signature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', 71 | version: '1.0.0', 72 | }, { 73 | addressManager: { 74 | receiveAddressIndex: 0, 75 | changeAddressIndex: 0, 76 | copayerIndex: 1, 77 | }, 78 | createdOn: 1422904189, 79 | id: '2', 80 | name: 'copayer 2', 81 | xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', 82 | requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60', 83 | signature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', 84 | version: '1.0.0', 85 | }, { 86 | addressManager: { 87 | receiveAddressIndex: 0, 88 | changeAddressIndex: 0, 89 | copayerIndex: 2, 90 | }, 91 | createdOn: 1422904189, 92 | id: '3', 93 | name: 'copayer 3', 94 | xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', 95 | requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a', 96 | signature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', 97 | version: '1.0.0', 98 | }], 99 | version: '1.0.0', 100 | pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}', 101 | isTestnet: false 102 | }; 103 | -------------------------------------------------------------------------------- /test/model/txproposal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var TxProposal = require('../../lib/model/txproposal'); 8 | var Bitcore = require('bitcore-lib'); 9 | 10 | describe('TxProposal', function() { 11 | describe('#create', function() { 12 | it('should create a TxProposal', function() { 13 | var txp = TxProposal.create(aTxpOpts()); 14 | should.exist(txp); 15 | txp.outputs.length.should.equal(2); 16 | txp.amount.should.equal(30000000); 17 | txp.network.should.equal('livenet'); 18 | }); 19 | }); 20 | 21 | describe('#fromObj', function() { 22 | it('should copy a TxProposal', function() { 23 | var txp = TxProposal.fromObj(aTXP()); 24 | should.exist(txp); 25 | txp.amount.should.equal(aTXP().amount); 26 | }); 27 | it('should default to BTC coin', function() { 28 | var txp = TxProposal.fromObj(aTXP()); 29 | should.exist(txp); 30 | txp.coin.should.equal('btc'); 31 | }); 32 | }); 33 | 34 | describe('#getBitcoreTx', function() { 35 | it('should create a valid bitcore TX', function() { 36 | var txp = TxProposal.fromObj(aTXP()); 37 | var t = txp.getBitcoreTx(); 38 | should.exist(t); 39 | }); 40 | it('should order outputs as specified by outputOrder', function() { 41 | var txp = TxProposal.fromObj(aTXP()); 42 | 43 | txp.outputOrder = [0, 1, 2]; 44 | var t = txp.getBitcoreTx(); 45 | t.getChangeOutput().should.deep.equal(t.outputs[2]); 46 | 47 | txp.outputOrder = [2, 0, 1]; 48 | var t = txp.getBitcoreTx(); 49 | t.getChangeOutput().should.deep.equal(t.outputs[0]); 50 | }); 51 | }); 52 | 53 | describe('#getTotalAmount', function() { 54 | it('should compute total amount', function() { 55 | var x = TxProposal.fromObj(aTXP()); 56 | var total = x.getTotalAmount(); 57 | total.should.equal(x.amount); 58 | }); 59 | }); 60 | 61 | describe('#getEstimatedSize', function() { 62 | it('should return estimated size in bytes', function() { 63 | var x = TxProposal.fromObj(aTXP()); 64 | x.getEstimatedSize().should.equal(396); 65 | }); 66 | }); 67 | 68 | describe('#sign', function() { 69 | it('should sign 2-2', function() { 70 | var txp = TxProposal.fromObj(aTXP()); 71 | txp.sign('1', theSignatures, theXPub); 72 | txp.isAccepted().should.equal(false); 73 | txp.isRejected().should.equal(false); 74 | txp.sign('2', theSignatures, theXPub); 75 | txp.isAccepted().should.equal(true); 76 | txp.isRejected().should.equal(false); 77 | }); 78 | }); 79 | 80 | describe('#getRawTx', function() { 81 | it('should generate correct raw transaction for signed 2-2', function() { 82 | var txp = TxProposal.fromObj(aTXP()); 83 | txp.sign('1', theSignatures, theXPub); 84 | txp.getRawTx().should.equal(theRawTx); 85 | }); 86 | }); 87 | 88 | describe('#reject', function() { 89 | it('should reject 2-2', function() { 90 | var txp = TxProposal.fromObj(aTXP()); 91 | txp.reject('1'); 92 | txp.isAccepted().should.equal(false); 93 | txp.isRejected().should.equal(true); 94 | }); 95 | }); 96 | 97 | describe('#reject & #sign', function() { 98 | it('should finally reject', function() { 99 | var txp = TxProposal.fromObj(aTXP()); 100 | txp.sign('1', theSignatures); 101 | txp.isAccepted().should.equal(false); 102 | txp.isRejected().should.equal(false); 103 | txp.reject('2'); 104 | txp.isAccepted().should.equal(false); 105 | txp.isRejected().should.equal(true); 106 | }); 107 | }); 108 | 109 | }); 110 | 111 | var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e'; 112 | var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9'; 113 | var theSignatures = ['304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6']; 114 | var theRawTx = '0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e08000000910047304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0380969800000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac002d3101000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70f62b040000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000'; 115 | 116 | var aTxpOpts = function() { 117 | var opts = { 118 | coin: 'btc', 119 | network: 'livenet', 120 | message: 'some message' 121 | }; 122 | opts.outputs = [{ 123 | toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", 124 | amount: 10000000, 125 | message: "first message" 126 | }, { 127 | toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", 128 | amount: 20000000, 129 | message: "second message" 130 | }, ]; 131 | 132 | return opts; 133 | }; 134 | 135 | var aTXP = function() { 136 | var txp = { 137 | "version": 3, 138 | "createdOn": 1423146231, 139 | "id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e", 140 | "walletId": "1", 141 | "creatorId": "1", 142 | "network": "livenet", 143 | "amount": 30000000, 144 | "message": 'some message', 145 | "proposalSignature": '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9', 146 | "changeAddress": { 147 | "version": '1.0.0', 148 | "createdOn": 1424372337, 149 | "address": '3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH', 150 | "path": 'm/2147483647/1/0', 151 | "publicKeys": ['030562cb099e6043dc499eb359dd97c9d500a3586498e4bcf0228a178cc20e6f16', 152 | '0367027d17dbdfc27b5e31f8ed70e14d47949f0fa392261e977db0851c8b0d6fac', 153 | '0315ae1e8aa866794ae603389fb2b8549153ebf04e7cdf74501dadde5c75ddad11' 154 | ] 155 | }, 156 | "inputs": [{ 157 | "txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab", 158 | "vout": 8, 159 | "satoshis": 100000000, 160 | "scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887", 161 | "address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA", 162 | "path": "m/2147483647/0/1", 163 | "publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"] 164 | }], 165 | "inputPaths": ["m/2147483647/0/1"], 166 | "requiredSignatures": 2, 167 | "requiredRejections": 1, 168 | "walletN": 2, 169 | "addressType": "P2SH", 170 | "status": "pending", 171 | "actions": [], 172 | "fee": 10000, 173 | "outputs": [{ 174 | "toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", 175 | "amount": 10000000, 176 | "message": "first message" 177 | }, { 178 | "toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", 179 | "amount": 20000000, 180 | "message": "second message" 181 | }, ], 182 | "outputOrder": [0, 1, 2] 183 | }; 184 | 185 | return txp; 186 | }; 187 | -------------------------------------------------------------------------------- /test/model/wallet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var Wallet = require('../../lib/model/wallet'); 8 | 9 | 10 | describe('Wallet', function() { 11 | 12 | describe('#create', function() { 13 | it('will throw with an invalid string argument for "m" or "n"', function() { 14 | (function() { 15 | Wallet.create({ 16 | m: '2', 17 | n: 2 18 | }); 19 | }).should.throw('Variable should be a Number.'); 20 | (function() { 21 | Wallet.create({ 22 | m: 2, 23 | n: '2' 24 | }); 25 | }).should.throw('Variable should be a Number.'); 26 | }); 27 | }); 28 | 29 | describe('#fromObj', function() { 30 | it('will throw with an invalid string argument for "m" or "n"', function() { 31 | (function() { 32 | Wallet.fromObj({ 33 | m: '2', 34 | n: 2 35 | }); 36 | }).should.throw('Variable should be a Number.'); 37 | (function() { 38 | Wallet.fromObj({ 39 | m: 2, 40 | n: '2' 41 | }); 42 | }).should.throw('Variable should be a Number.'); 43 | }); 44 | it('read a wallet', function() { 45 | var w = Wallet.fromObj(testWallet); 46 | w.isComplete().should.be.true; 47 | }); 48 | }); 49 | describe('#createAddress', function() { 50 | it('create an address', function() { 51 | var w = Wallet.fromObj(testWallet); 52 | var a = w.createAddress(false); 53 | a.address.should.equal('3HPJYvQZuTVY6pPBz17fFVz2YPoMBVT34i'); 54 | a.path.should.equal('m/2147483647/0/0'); 55 | a.createdOn.should.be.above(1); 56 | }); 57 | }); 58 | }); 59 | 60 | 61 | var testWallet = { 62 | addressManager: { 63 | receiveAddressIndex: 0, 64 | changeAddressIndex: 0, 65 | copayerIndex: 2147483647, 66 | }, 67 | createdOn: 1422904188, 68 | id: '123', 69 | name: '123 wallet', 70 | m: 2, 71 | n: 3, 72 | status: 'complete', 73 | publicKeyRing: [{ 74 | xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', 75 | requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc' 76 | }, { 77 | xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', 78 | requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60' 79 | }, { 80 | xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', 81 | requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a' 82 | }, ], 83 | copayers: [{ 84 | addressManager: { 85 | receiveAddressIndex: 0, 86 | changeAddressIndex: 0, 87 | copayerIndex: 0, 88 | }, 89 | createdOn: 1422904189, 90 | id: '1', 91 | name: 'copayer 1', 92 | xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', 93 | requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc', 94 | signature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', 95 | version: '1.0.0', 96 | }, { 97 | addressManager: { 98 | receiveAddressIndex: 0, 99 | changeAddressIndex: 0, 100 | copayerIndex: 1, 101 | }, 102 | createdOn: 1422904189, 103 | id: '2', 104 | name: 'copayer 2', 105 | xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', 106 | requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60', 107 | signature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', 108 | version: '1.0.0', 109 | }, { 110 | addressManager: { 111 | receiveAddressIndex: 0, 112 | changeAddressIndex: 0, 113 | copayerIndex: 2, 114 | }, 115 | createdOn: 1422904189, 116 | id: '3', 117 | name: 'copayer 3', 118 | xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', 119 | requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a', 120 | signature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', 121 | version: '1.0.0', 122 | }], 123 | version: '1.0.0', 124 | pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}', 125 | isTestnet: false 126 | }; 127 | -------------------------------------------------------------------------------- /test/request-list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var prequest = require('../lib/blockchainexplorers/request-list'); 8 | 9 | describe('request-list', function() { 10 | var request; 11 | 12 | beforeEach(function() { 13 | request = sinon.stub(); 14 | }); 15 | it('should support url as string', function(done) { 16 | 17 | request.yields(null, { 18 | statusCode: 200 19 | }, 'abc'); 20 | 21 | prequest({ 22 | hosts: 'url1', 23 | request: request, 24 | }, function(err, res, body) { 25 | should.not.exist(err); 26 | body.should.be.equal('abc'); 27 | res.statusCode.should.be.equal(200); 28 | done(); 29 | }); 30 | }); 31 | it('should support url as string (500 response)', function(done) { 32 | request.yields(null, { 33 | statusCode: 500 34 | }); 35 | prequest({ 36 | hosts: 'url1', 37 | request: request, 38 | }, function(err, res, body) { 39 | should.not.exist(err); 40 | res.statusCode.should.be.equal(500); 41 | done(); 42 | }); 43 | }); 44 | it('should support url as array of strings', function(done) { 45 | request.yields(null, { 46 | statusCode: 200 47 | }, 'abc'); 48 | prequest({ 49 | hosts: ['url1', 'url2'], 50 | request: request, 51 | }, function(err, res, body) { 52 | should.not.exist(err); 53 | body.should.be.equal('abc'); 54 | done(); 55 | }); 56 | }); 57 | it('should try 2nd url if first is unsuccessful (5xx)', function(done) { 58 | request.onCall(0).yields(null, { 59 | statusCode: 500 60 | }); 61 | request.onCall(1).yields(null, { 62 | statusCode: 550 63 | }); 64 | prequest({ 65 | hosts: ['url1', 'url2'], 66 | request: request, 67 | }, function(err, res, body) { 68 | should.not.exist(err); 69 | res.statusCode.should.be.equal(550); 70 | done(); 71 | }); 72 | }); 73 | it('should query 3th url if first 2 are unsuccessful (5xx)', function(done) { 74 | request.onCall(0).yields(null, { 75 | statusCode: 500 76 | }); 77 | request.onCall(1).yields(null, { 78 | statusCode: 550 79 | }); 80 | request.onCall(2).yields(null, { 81 | statusCode: 200, 82 | }, 'abc'); 83 | prequest({ 84 | hosts: ['url1', 'url2', 'url3'], 85 | request: request, 86 | }, function(err, res, body) { 87 | should.not.exist(err); 88 | body.should.be.equal('abc'); 89 | done(); 90 | }); 91 | }); 92 | it('should query only the first url if response is 404', function(done) { 93 | request.onCall(0).yields(null, { 94 | statusCode: 404 95 | }); 96 | request.onCall(1).yields(null, { 97 | statusCode: 550 98 | }); 99 | prequest({ 100 | hosts: ['url1', 'url2'], 101 | request: request, 102 | }, function(err, res, body) { 103 | should.not.exist(err); 104 | res.statusCode.should.be.equal(404); 105 | done(); 106 | }); 107 | }); 108 | it('should query only the first 2 urls if the second is successfull (5xx)', function(done) { 109 | request.onCall(0).yields(null, { 110 | statusCode: 500 111 | }); 112 | request.onCall(1).yields(null, { 113 | statusCode: 200, 114 | }, '2nd'); 115 | request.onCall(2).yields(null, { 116 | statusCode: 200, 117 | }, 'abc'); 118 | prequest({ 119 | hosts: ['url1', 'url2', 'url3'], 120 | request: request, 121 | }, function(err, res, body) { 122 | should.not.exist(err); 123 | body.should.be.equal('2nd'); 124 | res.statusCode.should.be.equal(200); 125 | done(); 126 | }); 127 | }); 128 | it('should query only the first 2 urls if the second is successfull (timeout)', function(done) { 129 | request.onCall(0).yields({ 130 | code: 'ETIMEDOUT', 131 | connect: true 132 | }); 133 | request.onCall(1).yields(null, { 134 | statusCode: 200, 135 | }, '2nd'); 136 | request.onCall(2).yields(null, { 137 | statusCode: 200, 138 | }, 'abc'); 139 | prequest({ 140 | hosts: ['url1', 'url2', 'url3'], 141 | request: request, 142 | }, function(err, res, body) { 143 | should.not.exist(err); 144 | body.should.be.equal('2nd'); 145 | res.statusCode.should.be.equal(200); 146 | done(); 147 | }); 148 | 149 | }); 150 | it('should use the latest response if all requests are unsuccessfull', function(done) { 151 | request.onCall(0).yields({ 152 | code: 'ETIMEDOUT', 153 | connect: true 154 | }); 155 | request.onCall(1).yields(null, { 156 | statusCode: 505, 157 | }, '2nd'); 158 | request.onCall(2).yields(null, { 159 | statusCode: 510, 160 | }, 'abc'); 161 | prequest({ 162 | hosts: ['url1', 'url2', 'url3'], 163 | request: request, 164 | }, function(err, res, body) { 165 | should.not.exist(err); 166 | res.statusCode.should.be.equal(510); 167 | done(); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var chai = require('chai'); 6 | var sinon = require('sinon'); 7 | var should = chai.should(); 8 | var tingodb = require('tingodb')({ 9 | memStore: true 10 | }); 11 | 12 | var Storage = require('../lib/storage'); 13 | var Model = require('../lib/model'); 14 | 15 | var db, storage; 16 | 17 | function openDb(cb) { 18 | db = new tingodb.Db('./db/test', {}); 19 | // HACK: There appears to be a bug in TingoDB's close function where the callback is not being executed 20 | db.__close = db.close; 21 | db.close = function(force, cb) { 22 | this.__close(force, cb); 23 | return cb(); 24 | }; 25 | return cb(); 26 | }; 27 | 28 | 29 | function resetDb(cb) { 30 | if (!db) return cb(); 31 | db.dropDatabase(function(err) { 32 | return cb(); 33 | }); 34 | }; 35 | 36 | 37 | describe('Storage', function() { 38 | before(function(done) { 39 | openDb(function() { 40 | storage = new Storage({ 41 | db: db 42 | }); 43 | done(); 44 | }); 45 | }); 46 | beforeEach(function(done) { 47 | resetDb(done); 48 | }); 49 | 50 | describe('Store & fetch wallet', function() { 51 | it('should correctly store and fetch wallet', function(done) { 52 | var wallet = Model.Wallet.create({ 53 | id: '123', 54 | name: 'my wallet', 55 | m: 2, 56 | n: 3, 57 | coin: 'btc', 58 | network: 'livenet', 59 | }); 60 | should.exist(wallet); 61 | storage.storeWallet(wallet, function(err) { 62 | should.not.exist(err); 63 | storage.fetchWallet('123', function(err, w) { 64 | should.not.exist(err); 65 | should.exist(w); 66 | w.id.should.equal(wallet.id); 67 | w.name.should.equal(wallet.name); 68 | w.m.should.equal(wallet.m); 69 | w.n.should.equal(wallet.n); 70 | done(); 71 | }) 72 | }); 73 | }); 74 | it('should not return error if wallet not found', function(done) { 75 | storage.fetchWallet('123', function(err, w) { 76 | should.not.exist(err); 77 | should.not.exist(w); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Copayer lookup', function() { 84 | it('should correctly store and fetch copayer lookup', function(done) { 85 | var wallet = Model.Wallet.create({ 86 | id: '123', 87 | name: 'my wallet', 88 | m: 2, 89 | n: 3, 90 | coin: 'btc', 91 | network: 'livenet', 92 | }); 93 | _.each(_.range(3), function(i) { 94 | var copayer = Model.Copayer.create({ 95 | coin: 'btc', 96 | name: 'copayer ' + i, 97 | xPubKey: 'xPubKey ' + i, 98 | requestPubKey: 'requestPubKey ' + i, 99 | signature: 'xxx', 100 | }); 101 | wallet.addCopayer(copayer); 102 | }); 103 | 104 | should.exist(wallet); 105 | storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { 106 | should.not.exist(err); 107 | storage.fetchCopayerLookup(wallet.copayers[1].id, function(err, lookup) { 108 | should.not.exist(err); 109 | should.exist(lookup); 110 | lookup.walletId.should.equal('123'); 111 | lookup.requestPubKeys[0].key.should.equal('requestPubKey 1'); 112 | lookup.requestPubKeys[0].signature.should.equal('xxx'); 113 | done(); 114 | }) 115 | }); 116 | }); 117 | it('should not return error if copayer not found', function(done) { 118 | storage.fetchCopayerLookup('2', function(err, lookup) { 119 | should.not.exist(err); 120 | should.not.exist(lookup); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('Transaction proposals', function() { 127 | var wallet, proposals; 128 | 129 | beforeEach(function(done) { 130 | wallet = Model.Wallet.create({ 131 | id: '123', 132 | name: 'my wallet', 133 | m: 2, 134 | n: 3, 135 | coin: 'btc', 136 | network: 'livenet', 137 | }); 138 | _.each(_.range(3), function(i) { 139 | var copayer = Model.Copayer.create({ 140 | coin: 'btc', 141 | name: 'copayer ' + i, 142 | xPubKey: 'xPubKey ' + i, 143 | requestPubKey: 'requestPubKey ' + i, 144 | signature: 'signarture ' + i, 145 | }); 146 | wallet.addCopayer(copayer); 147 | }); 148 | should.exist(wallet); 149 | storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { 150 | should.not.exist(err); 151 | 152 | proposals = _.map(_.range(4), function(i) { 153 | var tx = Model.TxProposal.create({ 154 | walletId: '123', 155 | coin: 'btc', 156 | network: 'livenet', 157 | outputs: [{ 158 | toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 159 | amount: i + 100, 160 | }], 161 | feePerKb: 100e2, 162 | creatorId: wallet.copayers[0].id, 163 | }); 164 | if (i % 2 == 0) { 165 | tx.status = 'pending'; 166 | tx.isPending().should.be.true; 167 | } else { 168 | tx.status = 'rejected'; 169 | tx.isPending().should.be.false; 170 | } 171 | tx.txid = 'txid' + i; 172 | return tx; 173 | }); 174 | async.each(proposals, function(tx, next) { 175 | storage.storeTx('123', tx, next); 176 | }, function(err) { 177 | should.not.exist(err); 178 | done(); 179 | }); 180 | }); 181 | }); 182 | it('should fetch tx', function(done) { 183 | storage.fetchTx('123', proposals[0].id, function(err, tx) { 184 | should.not.exist(err); 185 | should.exist(tx); 186 | tx.id.should.equal(proposals[0].id); 187 | tx.walletId.should.equal(proposals[0].walletId); 188 | tx.creatorName.should.equal('copayer 0'); 189 | done(); 190 | }); 191 | }); 192 | it('should fetch tx by hash', function(done) { 193 | storage.fetchTxByHash('txid0', function(err, tx) { 194 | should.not.exist(err); 195 | should.exist(tx); 196 | tx.id.should.equal(proposals[0].id); 197 | tx.walletId.should.equal(proposals[0].walletId); 198 | tx.creatorName.should.equal('copayer 0'); 199 | done(); 200 | }); 201 | }); 202 | 203 | it('should fetch all pending txs', function(done) { 204 | storage.fetchPendingTxs('123', function(err, txs) { 205 | should.not.exist(err); 206 | should.exist(txs); 207 | txs.length.should.equal(2); 208 | txs = _.sortBy(txs, 'amount'); 209 | txs[0].amount.should.equal(100); 210 | txs[1].amount.should.equal(102); 211 | done(); 212 | }); 213 | }); 214 | it('should remove tx', function(done) { 215 | storage.removeTx('123', proposals[0].id, function(err) { 216 | should.not.exist(err); 217 | storage.fetchTx('123', proposals[0].id, function(err, tx) { 218 | should.not.exist(err); 219 | should.not.exist(tx); 220 | storage.fetchTxs('123', {}, function(err, txs) { 221 | should.not.exist(err); 222 | should.exist(txs); 223 | txs.length.should.equal(3); 224 | _.any(txs, { 225 | id: proposals[0].id 226 | }).should.be.false; 227 | done(); 228 | }); 229 | }); 230 | }); 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var should = chai.should(); 7 | var Utils = require('../lib/common/utils'); 8 | 9 | describe('Utils', function() { 10 | describe('#getMissingFields', function() { 11 | it('should check required fields', function() { 12 | var obj = { 13 | id: 'id', 14 | name: 'name', 15 | array: ['a', 'b'], 16 | }; 17 | var fixtures = [{ 18 | args: 'id', 19 | check: [], 20 | }, { 21 | args: ['id'], 22 | check: [] 23 | }, { 24 | args: ['id, name'], 25 | check: ['id, name'], 26 | }, { 27 | args: ['id', 'name'], 28 | check: [] 29 | }, { 30 | args: 'array', 31 | check: [] 32 | }, { 33 | args: 'dummy', 34 | check: ['dummy'] 35 | }, { 36 | args: ['dummy1', 'dummy2'], 37 | check: ['dummy1', 'dummy2'] 38 | }, { 39 | args: ['id', 'dummy'], 40 | check: ['dummy'] 41 | }, ]; 42 | _.each(fixtures, function(f) { 43 | Utils.getMissingFields(obj, f.args).should.deep.equal(f.check); 44 | }); 45 | }); 46 | it('should fail to check required fields on non-object', function() { 47 | var obj = 'dummy'; 48 | Utils.getMissingFields(obj, 'name').should.deep.equal(['name']); 49 | }); 50 | }); 51 | 52 | describe('#hashMessage', function() { 53 | it('should create a hash', function() { 54 | var res = Utils.hashMessage('hola'); 55 | res.toString('hex').should.equal('4102b8a140ec642feaa1c645345f714bc7132d4fd2f7f6202db8db305a96172f'); 56 | }); 57 | }); 58 | 59 | describe('#verifyMessage', function() { 60 | it('should fail to verify a malformed signature', function() { 61 | var res = Utils.verifyMessage('hola', 'badsignature', '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); 62 | should.exist(res); 63 | res.should.equal(false); 64 | }); 65 | it('should fail to verify a null signature', function() { 66 | var res = Utils.verifyMessage('hola', null, '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); 67 | should.exist(res); 68 | res.should.equal(false); 69 | }); 70 | it('should fail to verify with wrong pubkey', function() { 71 | var res = Utils.verifyMessage('hola', '3045022100d6186930e4cd9984e3168e15535e2297988555838ad10126d6c20d4ac0e74eb502201095a6319ea0a0de1f1e5fb50f7bf10b8069de10e0083e23dbbf8de9b8e02785', '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); 72 | should.exist(res); 73 | res.should.equal(false); 74 | }); 75 | it('should verify', function() { 76 | var res = Utils.verifyMessage('hola', '3045022100d6186930e4cd9984e3168e15535e2297988555838ad10126d6c20d4ac0e74eb502201095a6319ea0a0de1f1e5fb50f7bf10b8069de10e0083e23dbbf8de9b8e02785', '03bec86ad4a8a91fe7c11ec06af27246ec55094db3d86098b7d8b2f12afe47627f'); 77 | should.exist(res); 78 | res.should.equal(true); 79 | }); 80 | }); 81 | 82 | describe('#formatAmount', function() { 83 | it('should successfully format amount', function() { 84 | var cases = [{ 85 | args: [1, 'bit'], 86 | expected: '0', 87 | }, { 88 | args: [1, 'btc'], 89 | expected: '0.00', 90 | }, { 91 | args: [0, 'bit'], 92 | expected: '0', 93 | }, { 94 | args: [12345678, 'bit'], 95 | expected: '123,457', 96 | }, { 97 | args: [12345678, 'btc'], 98 | expected: '0.123457', 99 | }, { 100 | args: [12345611, 'btc'], 101 | expected: '0.123456', 102 | }, { 103 | args: [1234, 'btc'], 104 | expected: '0.000012', 105 | }, { 106 | args: [1299, 'btc'], 107 | expected: '0.000013', 108 | }, { 109 | args: [1234567899999, 'btc'], 110 | expected: '12,345.679', 111 | }, { 112 | args: [12345678, 'bit', { 113 | thousandsSeparator: '.' 114 | }], 115 | expected: '123.457', 116 | }, { 117 | args: [12345678, 'btc', { 118 | decimalSeparator: ',' 119 | }], 120 | expected: '0,123457', 121 | }, { 122 | args: [1234567899999, 'btc', { 123 | thousandsSeparator: ' ', 124 | decimalSeparator: ',' 125 | }], 126 | expected: '12 345,679', 127 | }, ]; 128 | 129 | _.each(cases, function(testCase) { 130 | Utils.formatAmount.apply(this, testCase.args).should.equal(testCase.expected); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('#getAddressCoin', function() { 136 | it('should identify btc as coin for 1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', function() { 137 | Utils.getAddressCoin('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA').should.equal('btc'); 138 | }); 139 | it('should identify bch as coin for CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz', function() { 140 | Utils.getAddressCoin('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz').should.equal('bch'); 141 | }); 142 | it('should return null for 1L', function() { 143 | should.not.exist(Utils.getAddressCoin('1L')); 144 | }); 145 | }); 146 | 147 | 148 | describe('#translateAddress', function() { 149 | it('should translate address from btc to bch', function() { 150 | var res = Utils.translateAddress('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', 'bch'); 151 | res.should.equal('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz'); 152 | }); 153 | it('should translate address from bch to btc', function() { 154 | var res = Utils.translateAddress('HBf8isgS8EXG1r3X6GP89FmooUmiJ42wHS', 'btc'); 155 | res.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r'); 156 | }); 157 | 158 | it('should keep the address if there is nothing to do (bch)', function() { 159 | var res = Utils.translateAddress('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz', 'bch'); 160 | res.should.equal('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz'); 161 | }); 162 | it('should keep the address if there is nothing to do (btc)', function() { 163 | var res = Utils.translateAddress('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', 'btc'); 164 | should.exist(res); 165 | res.should.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA'); 166 | }); 167 | 168 | 169 | 170 | }); 171 | 172 | }); 173 | --------------------------------------------------------------------------------