Compare commits
211 Commits
Author | SHA1 | Date |
---|---|---|
|
6064e2a8bc | |
|
9f27a33f0e | |
|
01bedc939f | |
|
547e984b2c | |
|
52c26ced2e | |
|
b5b3c51d98 | |
|
8f35e78b57 | |
|
58447bbceb | |
|
0f0d605a3f | |
|
5b63d8ff92 | |
|
1f46848b03 | |
|
771cf5d82c | |
|
d4ef5345f6 | |
|
b9836f6069 | |
|
267cfdba8a | |
|
1ab4e27bde | |
|
4a19302a89 | |
|
41af549ed2 | |
|
242c98ebd3 | |
|
696b5b298b | |
|
1c271cfff3 | |
|
77f776a6b1 | |
|
41683bf10a | |
|
8e483953c9 | |
|
1333dd6ab7 | |
|
072bb336c5 | |
|
b40a24691a | |
|
ff89650337 | |
|
1d629c5674 | |
|
23b183714a | |
|
031e47ad5e | |
|
938bd05685 | |
|
36f88ec891 | |
|
6b293a64d6 | |
|
0e82c99e07 | |
|
7272b5234e | |
|
2605bfeca5 | |
|
89d2e21b29 | |
|
82fd50be9d | |
|
9c5abaf8bc | |
|
35d1fc3e12 | |
|
2f93137767 | |
|
c62c464d45 | |
|
d5bcb182d6 | |
|
c5259a6b0d | |
|
6a916bdf17 | |
|
80d609c014 | |
|
869840eae4 | |
|
acb14a1877 | |
|
9f248357e6 | |
|
72608f64f3 | |
|
616a8c40a7 | |
|
a62cb3da6f | |
|
89ab474d2a | |
|
920680b35f | |
|
55ce16b552 | |
|
391898a06e | |
|
3718493277 | |
|
a00572a194 | |
|
b3558ae1e5 | |
|
fe02071a38 | |
|
b12634f1d8 | |
|
f9ec2e1d5b | |
|
71d146bc9e | |
|
2692f579e2 | |
|
ead5ddb817 | |
|
7e788e202c | |
|
bdc0ba9699 | |
|
2493dbb98d | |
|
a7cd4e6171 | |
|
64e5a292e4 | |
|
6361006b8d | |
|
a1ec459311 | |
|
5c22852328 | |
|
23278a06fb | |
|
1db21481d6 | |
|
bf2e281787 | |
|
cc9e0ba403 | |
|
bd24613261 | |
|
9140f89944 | |
|
545dd4e51a | |
|
a04ac1383c | |
|
0d839a1c74 | |
|
e4d7f3ee7b | |
|
8bffde15a2 | |
|
b8c52a814c | |
|
0b6e5771c4 | |
|
2d46dca887 | |
|
f911ac3cc6 | |
|
002f9c77a6 | |
|
3a75b38232 | |
|
85156a8898 | |
|
1949924c1c | |
|
dac3d1ab0b | |
|
74f71230d8 | |
|
9177033ac0 | |
|
7a12b1761b | |
|
f15d69f7cf | |
|
ec47ec0b61 | |
|
ad772b7f6e | |
|
f07bda2d22 | |
|
46bee919da | |
|
e87521149a | |
|
13229b8aa9 | |
|
9d6af8cd0e | |
|
15de4716f7 | |
|
622f9cf7c4 | |
|
ffea739a19 | |
|
51f66c7f8d | |
|
12dee6fd59 | |
|
357419b53c | |
|
a9e56dd3be | |
|
5186392558 | |
|
d8e6964587 | |
|
570c8c193f | |
|
c6269a39a5 | |
|
f0a48e1217 | |
|
03a61f68f2 | |
|
8dd76a0dba | |
|
fd9a2ad839 | |
|
f81b5484c5 | |
|
5c3b0f4d37 | |
|
5ba71f1641 | |
|
3d967298f0 | |
|
d25e21b0d5 | |
|
05982ba166 | |
|
5cab2a722b | |
|
b74318862e | |
|
c4f87ccb17 | |
|
7bbc6aea82 | |
|
87ba10de84 | |
|
5dffb5f958 | |
|
5babd07201 | |
|
b2845456d3 | |
|
d3bb52ccf6 | |
|
f16b33908f | |
|
e2edfd394f | |
|
30494a8133 | |
|
98bdaebbfb | |
|
c125950891 | |
|
398c5ccc64 | |
|
fbe2040cbb | |
|
9ef45b4409 | |
|
acdf99a34f | |
|
cb4ebe8c50 | |
|
371d315138 | |
|
5fbb262ace | |
|
ba6fbab240 | |
|
58cb560b79 | |
|
06b63e0311 | |
|
4d7a5ee3d5 | |
|
daad0367d7 | |
|
827e05e08e | |
|
b6d9048001 | |
|
b47dfaeb95 | |
|
5f68cb27f2 | |
|
bad1826ec0 | |
|
5f3295722b | |
|
0ed05ee808 | |
|
d416284fc3 | |
|
279d2ecc68 | |
|
565bc01339 | |
|
2ab93cea1d | |
|
41c82e9e76 | |
|
c5d9f44b2d | |
|
3be1e616e7 | |
|
7cbfe71a79 | |
|
88ecc303bb | |
|
eabf3c978f | |
|
a2f1e0e40e | |
|
954b88aceb | |
|
ca0bd7fbcd | |
|
3aaa2bf82e | |
|
acd75cd62c | |
|
dd1c90f2bc | |
|
da6e61b7e7 | |
|
8df8327395 | |
|
c621d1c573 | |
|
c87350d7ef | |
|
b856d1cb02 | |
|
7803e4576b | |
|
fdea614d81 | |
|
300102e053 | |
|
0dbdfa1857 | |
|
13472e147a | |
|
7587540113 | |
|
4be0937e5b | |
|
a9b44ee5f1 | |
|
a2944af07e | |
|
cb3ee35deb | |
|
bbb6890fd5 | |
|
be430ace03 | |
|
8e60cf01de | |
|
11c5bd92a7 | |
|
4fd339da29 | |
|
2d3df0a12e | |
|
785fc4676e | |
|
1d4cf86a56 | |
|
d94290181e | |
|
3eea8ccfc4 | |
|
e2f497a6e5 | |
|
f1d9d2c499 | |
|
98165ab5a7 | |
|
ea54fe5a58 | |
|
752f86029a | |
|
d13fe55b66 | |
|
ba2fea93d7 | |
|
3a0431fac6 | |
|
aac8b274eb | |
|
dee50e8d30 | |
|
dba1601560 |
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"devel": false,
|
||||
"eqeqeq": true,
|
||||
"eqnull": false,
|
||||
"freeze": true,
|
||||
"funcscope": false,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": "nofunc",
|
||||
"maxcomplexity": 10,
|
||||
"maxdepth": 4,
|
||||
"maxerr": 99999,
|
||||
"maxlen": 120,
|
||||
"maxparams": 4,
|
||||
"maxstatements": 15,
|
||||
"mocha": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"node": true,
|
||||
"noempty": true,
|
||||
"nonew": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"smarttabs": false,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"predef": [
|
||||
"after",
|
||||
"afterEach",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"describe",
|
||||
"exports",
|
||||
"it",
|
||||
"xit",
|
||||
"module",
|
||||
"require"
|
||||
]
|
||||
}
|
|
@ -12,7 +12,7 @@ addons:
|
|||
- g++-4.8
|
||||
- clang
|
||||
node_js:
|
||||
- '4'
|
||||
- '8'
|
||||
before_install:
|
||||
- export CXX="g++-4.8" CC="gcc-4.8"
|
||||
install:
|
||||
|
|
77
README.md
77
README.md
|
@ -13,18 +13,19 @@ Bitcore Wallet Service facilitates multisig HD wallets creation and operation th
|
|||
|
||||
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.
|
||||
|
||||
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 relays on BWS.
|
||||
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.
|
||||
|
||||
BWS is been used in production enviroments for [Copay Wallet](https://copay.io), [Bitpay App wallet](https://bitpay.com/wallet) and others.
|
||||
|
||||
More about BWS at https://blog.bitpay.com/announcing-the-bitcore-wallet-suite/
|
||||
|
||||
# Install
|
||||
# Getting Started
|
||||
```
|
||||
npm install bitcore-wallet-service
|
||||
npm start
|
||||
git clone https://github.com/bitpay/bitcore-wallet-service.git
|
||||
cd bitcore-wallet-service && npm start
|
||||
```
|
||||
|
||||
|
||||
This will launch the BWS service (with default settings) at `http://localhost:3232/bws/api`.
|
||||
|
||||
BWS needs mongoDB. You can configure the connection at `config.js`
|
||||
|
@ -33,6 +34,14 @@ BWS supports SSL and Clustering. For a detailed guide on installing BWS with ext
|
|||
|
||||
BWS uses by default a Request Rate Limitation to CreateWallet endpoint. If you need to modify it, check defaults.js' `Defaults.RateLimit`
|
||||
|
||||
# Using BWS with PM2
|
||||
|
||||
BWS can be used with PM2 with the provided `app.js` script:
|
||||
|
||||
```
|
||||
pm2 start app.js --name "bitcoin-wallet-service"
|
||||
```
|
||||
|
||||
# Security Considerations
|
||||
* Private keys are never sent to BWS. Copayers store them locally.
|
||||
* Extended public keys are stored on BWS. This allows BWS to easily check wallet balance, send offline notifications to copayers, etc.
|
||||
|
@ -42,7 +51,29 @@ BWS uses by default a Request Rate Limitation to CreateWallet endpoint. If you n
|
|||
* Addresses and change addresses are derived independently and locally by the copayers from their local data.
|
||||
* TX Proposals templates are signed by copayers and verified by others, so the BWS cannot create or tamper with them.
|
||||
|
||||
# Using SSL
|
||||
|
||||
You can add your certificates at the config.js using:
|
||||
|
||||
``` json
|
||||
https: true,
|
||||
privateKeyFile: 'private.pem',
|
||||
certificateFile: 'cert.pem',
|
||||
////// The following is only for certs which are not
|
||||
////// trusted by nodejs 'https' by default
|
||||
////// CAs like Verisign do not require this
|
||||
// CAinter1: '', // ex. 'COMODORSADomainValidationSecureServerCA.crt'
|
||||
// CAinter2: '', // ex. 'COMODORSAAddTrustCA.crt'
|
||||
// CAroot: '', // ex. 'AddTrustExternalCARoot.crt'
|
||||
```
|
||||
|
||||
@dabura667 made a report about how to use letsencrypt with BWS: https://github.com/bitpay/bitcore-wallet-service/issues/423
|
||||
|
||||
|
||||
# REST API
|
||||
|
||||
Note: all currency amounts are in units of satoshis (1/100,000,000 of a bitcoin).
|
||||
|
||||
## Authentication
|
||||
|
||||
In order to access a wallet, clients are required to send the headers:
|
||||
|
@ -102,6 +133,18 @@ Returns:
|
|||
* byAddress array ['address', 'path', 'amount']: A list of addresses holding funds.
|
||||
* totalKbToSendMax: An estimation of the number of KiB required to include all available UTXOs in a tx (including unconfirmed).
|
||||
|
||||
`/v1/txnotes/:txid`: Get user notes associated to the specified transaction.
|
||||
Returns:
|
||||
* The note associated to the `txid` as a string.
|
||||
|
||||
`/v1/fiatrates/:code`: Get the fiat rate for the specified ISO 4217 code.
|
||||
Optional Arguments:
|
||||
* provider: An identifier representing the source of the rates.
|
||||
* ts: The timestamp for the fiat rate (defaults to now).
|
||||
|
||||
Returns:
|
||||
* The fiat exchange rate.
|
||||
|
||||
## POST Endpoints
|
||||
`/v1/wallets/`: Create a new Wallet
|
||||
|
||||
|
@ -117,6 +160,7 @@ Returns:
|
|||
|
||||
|
||||
`/v1/wallets/:id/copayers/`: Join a Wallet in creation
|
||||
|
||||
Required Arguments:
|
||||
* walletId: Id of the wallet to join
|
||||
* name: Copayer Name
|
||||
|
@ -129,10 +173,11 @@ Returns:
|
|||
* wallet: Object with wallet's information
|
||||
|
||||
`/v1/txproposals/`: Add a new transaction proposal
|
||||
|
||||
Required Arguments:
|
||||
* toAddress: RCPT Bitcoin address.
|
||||
* amount: amount (in satoshis) of the mount proposed to be transfered
|
||||
* proposalsSignature: Signature of the proposal by the creator peer, using prososalSigningKey.
|
||||
* proposalsSignature: Signature of the proposal by the creator peer, using proposalSigningKey.
|
||||
* (opt) message: Encrypted private message to peers.
|
||||
* (opt) payProUrl: Paypro URL for peers to verify TX
|
||||
* (opt) feePerKb: Use an alternative fee per KB for this TX.
|
||||
|
@ -142,7 +187,7 @@ Returns:
|
|||
* 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.
|
||||
|
||||
|
||||
`/v1/addresses/`: Request a new main address from wallet
|
||||
`/v3/addresses/`: Request a new main address from wallet . (creates an address on normal conditions)
|
||||
|
||||
Returns:
|
||||
* 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.
|
||||
|
@ -170,6 +215,13 @@ Returns:
|
|||
Optional Arguments:
|
||||
* includeCopayerBranches: Scan all copayer branches following BIP45 recommendation (defaults to false).
|
||||
|
||||
`/v1/txconfirmations/`: Subscribe to receive push notifications when the specified transaction gets confirmed.
|
||||
Required Arguments:
|
||||
* txid: The transaction to subscribe to.
|
||||
|
||||
## PUT Endpoints
|
||||
`/v1/txnotes/:txid/`: Modify a note for a tx.
|
||||
|
||||
|
||||
## DELETE Endpoints
|
||||
`/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
|
||||
|
@ -177,13 +229,10 @@ Returns:
|
|||
Returns:
|
||||
* 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.
|
||||
|
||||
`/v1/txconfirmations/:txid`: Unsubscribe from transaction `txid` and no longer listen to its confirmation.
|
||||
|
||||
|
||||
# Push Notifications
|
||||
## Installation
|
||||
|
||||
In order to use push notifications service, you need install:
|
||||
|
||||
* [node-pushserver](https://www.npmjs.com/package/node-pushserver)
|
||||
|
||||
Recomended to complete config.js file:
|
||||
|
||||
* [GCM documentation to get your API key](https://developers.google.com/cloud-messaging/gcm)
|
||||
|
@ -194,8 +243,8 @@ Returns:
|
|||
`/v1/pushnotifications/subscriptions/`: Adds subscriptions for push notifications service at database.
|
||||
|
||||
|
||||
## DELETE Endopints
|
||||
`/v1/pushnotifications/subscriptions/`: Remove subscriptions for push notifications service from database.
|
||||
## DELETE Endpoints
|
||||
`/v2/pushnotifications/subscriptions/`: Remove subscriptions for push notifications service from database.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var spawn = require('child_process').spawn;
|
||||
var async = require('async');
|
||||
|
||||
var scripts = ['locker/locker.js', 'messagebroker/messagebroker.js',
|
||||
'bcmonitor/bcmonitor.js', 'emailservice/emailservice.js',
|
||||
'pushnotificationsservice/pushnotificationsservice.js',
|
||||
'fiatrateservice/fiatrateservice.js', 'bws.js'];
|
||||
|
||||
async.eachSeries(scripts, function(script, callback) {
|
||||
console.log(`Spawning ${script}`);
|
||||
|
||||
var node = spawn('node', [script]);
|
||||
node.stdout.on('data', (data) => {
|
||||
console.log(`${data}`);
|
||||
});
|
||||
node.stderr.on('data', (data) => {
|
||||
console.error(`${data}`);
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
21
config.js
21
config.js
|
@ -38,18 +38,33 @@ var config = {
|
|||
},
|
||||
},
|
||||
blockchainExplorerOpts: {
|
||||
btc: {
|
||||
livenet: {
|
||||
provider: 'insight',
|
||||
url: 'https://insight.bitpay.com:443',
|
||||
url: 'https://explorer.btcprivate.org:443',
|
||||
},
|
||||
testnet: {
|
||||
provider: 'insight',
|
||||
url: 'https://test-insight.bitpay.com:443',
|
||||
// url: 'http://localhost:3001',
|
||||
url: 'https://explorer.testnet.btcprivate.org:443',
|
||||
// Multiple servers (in priority order)
|
||||
// url: ['http://a.b.c', 'https://test-insight.bitpay.com:443'],
|
||||
},
|
||||
},
|
||||
bch: {
|
||||
livenet: {
|
||||
provider: 'insight',
|
||||
//url: 'https://cashexplorer.bitcoin.com',
|
||||
url: 'https://bch-insight.bitpay.com:443',
|
||||
addressFormat: 'cashaddr', // copay, cashaddr, or legacy
|
||||
},
|
||||
testnet: {
|
||||
provider: 'insight',
|
||||
url: 'https://test-bch-insight.bitpay.com:443',
|
||||
addressFormat: 'cashaddr', // copay, cashaddr, or legacy
|
||||
},
|
||||
|
||||
},
|
||||
},
|
||||
pushNotificationsOpts: {
|
||||
templatePath: './lib/templates',
|
||||
defaultLanguage: 'en',
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
var Bitcore_ = {
|
||||
btc: require('bitcore-lib'),
|
||||
bch: require('bitcore-lib-cash')
|
||||
};
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
function BCHAddressTranslator() {
|
||||
};
|
||||
|
||||
|
||||
BCHAddressTranslator.getAddressCoin = function(address) {
|
||||
try {
|
||||
new Bitcore_['btc'].Address(address);
|
||||
return 'legacy';
|
||||
} catch (e) {
|
||||
try {
|
||||
var a= new Bitcore_['bch'].Address(address);
|
||||
if (a.toString() == address) return 'copay';
|
||||
return 'cashaddr';
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Supports 3 formats: legacy (1xxx, mxxxx); Copay: (Cxxx, Hxxx), Cashaddr(qxxx);
|
||||
BCHAddressTranslator.translate = function(addresses, to, from) {
|
||||
var wasArray = true;
|
||||
if (!_.isArray(addresses)) {
|
||||
wasArray = false;
|
||||
addresses = [addresses];
|
||||
}
|
||||
|
||||
|
||||
from = from || BCHAddressTranslator.getAddressCoin(addresses[0]);
|
||||
if (from == to) return addresses;
|
||||
var ret = _.map(addresses, function(x) {
|
||||
|
||||
var bitcore = Bitcore_[from == 'legacy' ? 'btc' : 'bch'];
|
||||
var orig = new bitcore.Address(x).toObject();
|
||||
|
||||
if (to == 'cashaddr') {
|
||||
return Bitcore_['bch'].Address.fromObject(orig).toCashAddress(true);
|
||||
} else if (to == 'copay') {
|
||||
return Bitcore_['bch'].Address.fromObject(orig).toString();
|
||||
} else if (to == 'legacy') {
|
||||
return Bitcore_['btc'].Address.fromObject(orig).toString();
|
||||
}
|
||||
});
|
||||
|
||||
if (wasArray)
|
||||
return ret;
|
||||
else
|
||||
return ret[0];
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = BCHAddressTranslator;
|
|
@ -6,11 +6,21 @@ var log = require('npmlog');
|
|||
log.debug = log.verbose;
|
||||
|
||||
var Insight = require('./blockchainexplorers/insight');
|
||||
var Common = require('./common');
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
var PROVIDERS = {
|
||||
'insight': {
|
||||
'livenet': 'https://insight.bitpay.com:443',
|
||||
'testnet': 'https://test-insight.bitpay.com:443',
|
||||
'btc': {
|
||||
'livenet': 'https://explorer.btcprivate.org:443',
|
||||
'testnet': 'https://explorer.testnet.btcprivate.org:443',
|
||||
},
|
||||
'bch': {
|
||||
'livenet': 'https://bch-insight.bitpay.com:443',
|
||||
'testnet': 'https://test-bch-insight.bitpay.com:443',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -18,20 +28,32 @@ function BlockChainExplorer(opts) {
|
|||
$.checkArgument(opts);
|
||||
|
||||
var provider = opts.provider || 'insight';
|
||||
var coin = opts.coin || Defaults.COIN;
|
||||
var network = opts.network || 'livenet';
|
||||
|
||||
$.checkState(PROVIDERS[provider], 'Provider ' + provider + ' not supported');
|
||||
$.checkState(_.contains(_.keys(PROVIDERS[provider]), network), 'Network ' + network + ' not supported by this provider');
|
||||
$.checkState(_.contains(_.keys(PROVIDERS[provider]), coin), 'Coin ' + coin + ' not supported by this provider');
|
||||
$.checkState(_.contains(_.keys(PROVIDERS[provider][coin]), network), 'Network ' + network + ' not supported by this provider for coin ' + coin);
|
||||
|
||||
var url = opts.url || PROVIDERS[provider][coin][network];
|
||||
|
||||
|
||||
if (coin != 'bch' && opts.addressFormat)
|
||||
throw new Error('addressFormat only supported for bch');
|
||||
|
||||
if (coin == 'bch' && !opts.addressFormat)
|
||||
opts.addressFormat = 'cashaddr';
|
||||
|
||||
var url = opts.url || PROVIDERS[provider][network];
|
||||
|
||||
switch (provider) {
|
||||
case 'insight':
|
||||
return new Insight({
|
||||
coin: coin,
|
||||
network: network,
|
||||
url: url,
|
||||
apiPrefix: opts.apiPrefix,
|
||||
userAgent: opts.userAgent,
|
||||
addressFormat: opts.addressFormat,
|
||||
});
|
||||
default:
|
||||
throw new Error('Provider ' + provider + ' not supported.');
|
||||
|
|
|
@ -1,23 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
var $ = require('preconditions').singleton();
|
||||
var log = require('npmlog');
|
||||
log.debug = log.verbose;
|
||||
var io = require('socket.io-client');
|
||||
var requestList = require('./request-list');
|
||||
var Common = require('../common');
|
||||
var BCHAddressTranslator = require('../bchaddresstranslator');
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
function Insight(opts) {
|
||||
$.checkArgument(opts);
|
||||
$.checkArgument(_.contains(['livenet', 'testnet'], opts.network));
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
|
||||
$.checkArgument(opts.url);
|
||||
|
||||
this.apiPrefix = opts.apiPrefix || '/api';
|
||||
this.apiPrefix = _.isUndefined(opts.apiPrefix)? '/api' : opts.apiPrefix;
|
||||
this.coin = opts.coin || Defaults.COIN;
|
||||
this.network = opts.network || 'livenet';
|
||||
this.hosts = opts.url;
|
||||
this.userAgent = opts.userAgent || 'bws';
|
||||
};
|
||||
|
||||
if (opts.addressFormat) {
|
||||
$.checkArgument(Constants.ADDRESS_FORMATS.includes(opts.addressFormat), 'Unkown addr format:' + opts.addressFormat);
|
||||
this.addressFormat = opts.addressFormat != 'copay' ? opts.addressFormat : null;
|
||||
}
|
||||
|
||||
this.requestQueue = async.queue(this._doRequest.bind(this), Defaults.INSIGHT_REQUEST_POOL_SIZE);
|
||||
|
||||
}
|
||||
|
||||
var _parseErr = function(err, res) {
|
||||
if (err) {
|
||||
|
@ -28,6 +43,42 @@ var _parseErr = function(err, res) {
|
|||
return "Error querying the blockchain";
|
||||
};
|
||||
|
||||
|
||||
// Translate Request Address query
|
||||
Insight.prototype.translateQueryAddresses = function(addresses) {
|
||||
if (!this.addressFormat) return addresses;
|
||||
return BCHAddressTranslator.translate(addresses, this.addressFormat, 'copay');
|
||||
};
|
||||
|
||||
|
||||
// Translate Result Address
|
||||
Insight.prototype.translateResultAddresses = function(addresses) {
|
||||
if (!this.addressFormat) return addresses;
|
||||
|
||||
return BCHAddressTranslator.translate(addresses, 'copay', this.addressFormat);
|
||||
};
|
||||
|
||||
|
||||
Insight.prototype.translateTx = function(tx) {
|
||||
var self = this;
|
||||
if (!this.addressFormat) return tx;
|
||||
|
||||
_.each(tx.vin, function(x){
|
||||
if (x.addr) {
|
||||
x.addr = self.translateResultAddresses(x.addr);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
_.each(tx.vout, function(x){
|
||||
if (x.scriptPubKey && x.scriptPubKey.addresses) {
|
||||
x.scriptPubKey.addresses = self.translateResultAddresses(x.scriptPubKey.addresses);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
Insight.prototype._doRequest = function(args, cb) {
|
||||
var opts = {
|
||||
hosts: this.hosts,
|
||||
|
@ -35,28 +86,43 @@ Insight.prototype._doRequest = function(args, cb) {
|
|||
'User-Agent': this.userAgent,
|
||||
}
|
||||
};
|
||||
|
||||
var s = JSON.stringify(args);
|
||||
// if ( s.length > 100 )
|
||||
// s= s.substr(0,100) + '...';
|
||||
log.debug('', 'Insight Q: %s', s);
|
||||
|
||||
requestList(_.defaults(args, opts), cb);
|
||||
};
|
||||
|
||||
Insight.prototype.getConnectionInfo = function() {
|
||||
return 'Insight (' + this.network + ') @ ' + this.hosts;
|
||||
return 'Insight (' + this.coin + '/' + this.network + ') @ ' + this.hosts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a list of unspent outputs associated with an address or set of addresses
|
||||
*/
|
||||
Insight.prototype.getUtxos = function(addresses, cb) {
|
||||
var self = this;
|
||||
|
||||
var url = this.url + this.apiPrefix + '/addrs/utxo';
|
||||
var args = {
|
||||
method: 'POST',
|
||||
path: this.apiPrefix + '/addrs/utxo',
|
||||
json: {
|
||||
addrs: [].concat(addresses).join(',')
|
||||
addrs: this.translateQueryAddresses(_.uniq([].concat(addresses))).join(',')
|
||||
},
|
||||
};
|
||||
|
||||
this._doRequest(args, function(err, res, unspent) {
|
||||
this.requestQueue.push(args, function(err, res, unspent) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
|
||||
if (self.addressFormat) {
|
||||
|
||||
_.each(unspent, function(x) {
|
||||
x.address = self.translateResultAddresses(x.address);
|
||||
});
|
||||
}
|
||||
return cb(null, unspent);
|
||||
});
|
||||
};
|
||||
|
@ -73,29 +139,35 @@ Insight.prototype.broadcast = function(rawTx, cb) {
|
|||
},
|
||||
};
|
||||
|
||||
this._doRequest(args, function(err, res, body) {
|
||||
this.requestQueue.push(args, function(err, res, body) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
return cb(null, body ? body.txid : null);
|
||||
});
|
||||
};
|
||||
|
||||
Insight.prototype.getTransaction = function(txid, cb) {
|
||||
var self = this;
|
||||
var args = {
|
||||
method: 'GET',
|
||||
path: this.apiPrefix + '/tx/' + txid,
|
||||
json: true,
|
||||
};
|
||||
|
||||
this._doRequest(args, function(err, res, tx) {
|
||||
this.requestQueue.push(args, function(err, res, tx) {
|
||||
if (res && res.statusCode == 404) return cb();
|
||||
if (err || res.statusCode !== 200)
|
||||
return cb(_parseErr(err, res));
|
||||
|
||||
self.translateTx(tx);
|
||||
|
||||
return cb(null, tx);
|
||||
});
|
||||
};
|
||||
|
||||
Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
||||
var self = this;
|
||||
|
||||
|
||||
var qs = [];
|
||||
var total;
|
||||
if (_.isNumber(from)) qs.push('from=' + from);
|
||||
|
@ -110,12 +182,13 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
|||
method: 'POST',
|
||||
path: this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''),
|
||||
json: {
|
||||
addrs: [].concat(addresses).join(',')
|
||||
addrs: this.translateQueryAddresses(_.uniq([].concat(addresses))).join(',')
|
||||
},
|
||||
timeout: 120000,
|
||||
};
|
||||
|
||||
this._doRequest(args, function(err, res, txs) {
|
||||
|
||||
this.requestQueue.push(args, function(err, res, txs) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
|
||||
if (_.isObject(txs)) {
|
||||
|
@ -129,6 +202,13 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
|||
// NOTE: Whenever Insight breaks communication with bitcoind, it returns invalid data but no error code.
|
||||
if (!_.isArray(txs) || (txs.length != _.compact(txs).length)) return cb(new Error('Could not retrieve transactions from blockchain. Request was:' + JSON.stringify(args)));
|
||||
|
||||
if (self.addressFormat) {
|
||||
|
||||
_.each(txs, function(tx){
|
||||
self.translateTx(tx);
|
||||
});
|
||||
}
|
||||
|
||||
return cb(null, txs, total);
|
||||
});
|
||||
};
|
||||
|
@ -138,15 +218,17 @@ Insight.prototype.getAddressActivity = function(address, cb) {
|
|||
|
||||
var args = {
|
||||
method: 'GET',
|
||||
path: self.apiPrefix + '/addr/' + address,
|
||||
path: self.apiPrefix + '/addr/' + this.translateQueryAddresses(address),
|
||||
json: true,
|
||||
};
|
||||
|
||||
this._doRequest(args, function(err, res, result) {
|
||||
this.requestQueue.push(args, function(err, res, result) {
|
||||
if (res && res.statusCode == 404) return cb();
|
||||
if (err || res.statusCode !== 200)
|
||||
return cb(_parseErr(err, res));
|
||||
|
||||
// note: result.addrStr is not translated, but not used.
|
||||
|
||||
var nbTxs = result.unconfirmedTxApperances + result.txApperances;
|
||||
return cb(null, nbTxs > 0);
|
||||
});
|
||||
|
@ -163,7 +245,7 @@ Insight.prototype.estimateFee = function(nbBlocks, cb) {
|
|||
path: path,
|
||||
json: true,
|
||||
};
|
||||
this._doRequest(args, function(err, res, body) {
|
||||
this.requestQueue.push(args, function(err, res, body) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
return cb(null, body);
|
||||
});
|
||||
|
@ -177,12 +259,27 @@ Insight.prototype.getBlockchainHeight = function(cb) {
|
|||
path: path,
|
||||
json: true,
|
||||
};
|
||||
this._doRequest(args, function(err, res, body) {
|
||||
this.requestQueue.push(args, function(err, res, body) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
return cb(null, body.blockChainHeight);
|
||||
});
|
||||
};
|
||||
|
||||
Insight.prototype.getTxidsInBlock = function(blockHash, cb) {
|
||||
var self = this;
|
||||
|
||||
var args = {
|
||||
method: 'GET',
|
||||
path: this.apiPrefix + '/block/' + blockHash,
|
||||
json: true,
|
||||
};
|
||||
|
||||
this.requestQueue.push(args, function(err, res, body) {
|
||||
if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
|
||||
return cb(null, body.tx);
|
||||
});
|
||||
};
|
||||
|
||||
Insight.prototype.initSocket = function() {
|
||||
|
||||
// sockets always use the first server on the pull
|
||||
|
|
|
@ -31,11 +31,23 @@ var requestList = function(args, cb) {
|
|||
async.whilst(
|
||||
function() {
|
||||
nextUrl = urls.shift();
|
||||
if (!nextUrl && success === 'false')
|
||||
log.warn('no more servers to test for the request');
|
||||
return nextUrl && !success;
|
||||
},
|
||||
function(a_cb) {
|
||||
args.uri = nextUrl;
|
||||
|
||||
var time = 0;
|
||||
var interval = setInterval(function() {
|
||||
time += 10;
|
||||
log.debug('', 'Delayed insight query: %s, time: %d s', args.uri, time);
|
||||
}, 10000);
|
||||
|
||||
request(args, function(err, res, body) {
|
||||
clearInterval(interval);
|
||||
sucess = false;
|
||||
|
||||
if (err) {
|
||||
log.warn('REQUEST FAIL: ' + nextUrl + ' ERROR: ' + err);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ var Lock = require('./lock');
|
|||
var Notification = require('./model/notification');
|
||||
|
||||
var WalletService = require('./server');
|
||||
var Common = require('./common');
|
||||
var Constants = Common.Constants;
|
||||
var Utils = Common.Utils;
|
||||
|
||||
function BlockchainMonitor() {};
|
||||
|
||||
|
@ -25,25 +28,42 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
|||
async.parallel([
|
||||
|
||||
function(done) {
|
||||
self.explorers = _.map(['livenet', 'testnet'], function(network) {
|
||||
self.explorers = {
|
||||
btc: {},
|
||||
bch: {},
|
||||
};
|
||||
|
||||
var coinNetworkPairs = [];
|
||||
_.each(_.values(Constants.COINS), function(coin) {
|
||||
_.each(_.values(Constants.NETWORKS), function(network) {
|
||||
coinNetworkPairs.push({
|
||||
coin: coin,
|
||||
network: network
|
||||
});
|
||||
});
|
||||
});
|
||||
_.each(coinNetworkPairs, function(pair) {
|
||||
var explorer;
|
||||
if (opts.blockchainExplorers) {
|
||||
explorer = opts.blockchainExplorers[network];
|
||||
if (opts.blockchainExplorers && opts.blockchainExplorers[pair.coin] && opts.blockchainExplorers[pair.coin][pair.network]) {
|
||||
explorer = opts.blockchainExplorers[pair.coin][pair.network];
|
||||
} else {
|
||||
var config = {}
|
||||
if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[network]) {
|
||||
config = opts.blockchainExplorerOpts[network];
|
||||
if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[pair.coin] && opts.blockchainExplorerOpts[pair.coin][pair.network]) {
|
||||
config = opts.blockchainExplorerOpts[pair.coin][pair.network];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
var explorer = new BlockchainExplorer({
|
||||
provider: config.provider,
|
||||
network: network,
|
||||
coin: pair.coin,
|
||||
network: pair.network,
|
||||
url: config.url,
|
||||
userAgent: WalletService.getServiceVersion(),
|
||||
});
|
||||
}
|
||||
$.checkState(explorer);
|
||||
self._initExplorer(explorer);
|
||||
return explorer;
|
||||
self._initExplorer(pair.coin, pair.network, explorer);
|
||||
self.explorers[pair.coin][pair.network] = explorer;
|
||||
});
|
||||
done();
|
||||
},
|
||||
|
@ -72,7 +92,7 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._initExplorer = function(explorer) {
|
||||
BlockchainMonitor.prototype._initExplorer = function(coin, network, explorer) {
|
||||
var self = this;
|
||||
|
||||
var socket = explorer.initSocket();
|
||||
|
@ -84,11 +104,11 @@ BlockchainMonitor.prototype._initExplorer = function(explorer) {
|
|||
socket.on('connect_error', function() {
|
||||
log.error('Error connecting to ' + explorer.getConnectionInfo());
|
||||
});
|
||||
socket.on('tx', _.bind(self._handleIncomingTx, self));
|
||||
socket.on('block', _.bind(self._handleNewBlock, self, explorer.network));
|
||||
socket.on('tx', _.bind(self._handleIncomingTx, self, coin, network));
|
||||
socket.on('block', _.bind(self._handleNewBlock, self, coin, network));
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
||||
BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, processIt) {
|
||||
var self = this;
|
||||
if (!data || !data.txid) return;
|
||||
|
||||
|
@ -103,7 +123,7 @@ BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
|||
|
||||
if (!processIt) {
|
||||
log.info('Detected broadcast ' + data.txid + ' of an accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
||||
return setTimeout(self._handleTxId.bind(self, data, true), 20 * 1000);
|
||||
return setTimeout(self._handleThirdPartyBroadcasts.bind(self, data, true), 20 * 1000);
|
||||
}
|
||||
|
||||
log.info('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
|
||||
|
@ -132,25 +152,30 @@ BlockchainMonitor.prototype._handleTxId = function(data, processIt) {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
BlockchainMonitor.prototype._handleTxOuts = function(data) {
|
||||
BlockchainMonitor.prototype._handleIncomingPayments = function(coin, network, data) {
|
||||
var self = this;
|
||||
|
||||
if (!data || !data.vout) return;
|
||||
|
||||
var outs = _.compact(_.map(data.vout, function(v) {
|
||||
var addr = _.keys(v)[0];
|
||||
var amount = +v[addr];
|
||||
|
||||
// This is because a bug on insight, that always return no copay addr
|
||||
if (coin == 'bch' && Utils.getAddressCoin(addr) !='bch') {
|
||||
addr = Utils.translateAddress(addr, coin);
|
||||
}
|
||||
|
||||
return {
|
||||
address: addr,
|
||||
amount: +v[addr]
|
||||
amount: amount,
|
||||
};
|
||||
}));
|
||||
if (_.isEmpty(outs)) return;
|
||||
|
||||
async.each(outs, function(out, next) {
|
||||
self.storage.fetchAddress(out.address, function(err, address) {
|
||||
|
||||
// toDo, remove coin here: no more same address for diff coins
|
||||
self.storage.fetchAddressByCoin(coin, out.address, function(err, address) {
|
||||
if (err) {
|
||||
log.error('Could not fetch addresses from the db');
|
||||
return next(err);
|
||||
|
@ -160,6 +185,17 @@ BlockchainMonitor.prototype._handleTxOuts = function(data) {
|
|||
var walletId = address.walletId;
|
||||
log.info('Incoming tx for wallet ' + walletId + ' [' + out.amount + 'sat -> ' + out.address + ']');
|
||||
|
||||
var fromTs = Date.now() - 24 * 3600 * 1000;
|
||||
self.storage.fetchNotifications(walletId, null, fromTs, function(err, notifications) {
|
||||
if (err) return next(err);
|
||||
var alreadyNotified = _.any(notifications, function(n) {
|
||||
return n.type == 'NewIncomingTx' && n.data && n.data.txid == data.txid;
|
||||
});
|
||||
if (alreadyNotified) {
|
||||
log.info('The incoming tx ' + data.txid + ' was already notified');
|
||||
return next();
|
||||
}
|
||||
|
||||
var notification = Notification.create({
|
||||
type: 'NewIncomingTx',
|
||||
data: {
|
||||
|
@ -170,41 +206,58 @@ BlockchainMonitor.prototype._handleTxOuts = function(data) {
|
|||
walletId: walletId,
|
||||
});
|
||||
self.storage.softResetTxHistoryCache(walletId, function() {
|
||||
self._updateActiveAddresses(address, function() {
|
||||
self._updateAddressesWithBalance(address, function() {
|
||||
self._storeAndBroadcastNotification(notification, next);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, function(err) {
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._updateActiveAddresses = function(address, cb) {
|
||||
BlockchainMonitor.prototype._updateAddressesWithBalance = function(address, cb) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self.storage.storeActiveAddresses(address.walletId, address.address, function(err) {
|
||||
self.storage.fetchAddressesWithBalance(address.walletId, function(err, result) {
|
||||
if (err) {
|
||||
log.warn('Could not update wallet cache', err);
|
||||
return cb(err);
|
||||
}
|
||||
var addresses = _.map(result,'address');
|
||||
|
||||
if (_.indexOf(addresses, address.address) >= 0) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
addresses.push(address.address);
|
||||
log.info('Activating address ' + address.address);
|
||||
self.storage.storeAddressesWithBalance(address.walletId, addresses, function(err) {
|
||||
if (err) {
|
||||
log.warn('Could not update wallet cache', err);
|
||||
}
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._handleIncomingTx = function(data) {
|
||||
this._handleTxId(data);
|
||||
this._handleTxOuts(data);
|
||||
BlockchainMonitor.prototype._handleIncomingTx = function(coin, network, data) {
|
||||
this._handleThirdPartyBroadcasts(data);
|
||||
this._handleIncomingPayments(coin, network, data);
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
|
||||
BlockchainMonitor.prototype._notifyNewBlock = function(coin, network, hash) {
|
||||
var self = this;
|
||||
|
||||
log.info('New ' + network + ' block: ', hash);
|
||||
log.info('New ' + network + ' block: ' + hash);
|
||||
var notification = Notification.create({
|
||||
type: 'NewBlock',
|
||||
walletId: network, // use network name as wallet id for global notifications
|
||||
data: {
|
||||
hash: hash,
|
||||
coin: coin,
|
||||
network: network,
|
||||
},
|
||||
});
|
||||
|
@ -216,6 +269,64 @@ BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
|
|||
});
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._handleTxConfirmations = function(coin, network, hash) {
|
||||
var self = this;
|
||||
|
||||
function processTriggeredSubs(subs, cb) {
|
||||
async.each(subs, function(sub) {
|
||||
log.info('New tx confirmation ' + sub.txid);
|
||||
sub.isActive = false;
|
||||
self.storage.storeTxConfirmationSub(sub, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var notification = Notification.create({
|
||||
type: 'TxConfirmation',
|
||||
walletId: sub.walletId,
|
||||
creatorId: sub.copayerId,
|
||||
data: {
|
||||
txid: sub.txid,
|
||||
coin: coin,
|
||||
network: network,
|
||||
// TODO: amount
|
||||
},
|
||||
});
|
||||
self._storeAndBroadcastNotification(notification, cb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var explorer = self.explorers[coin][network];
|
||||
if (!explorer) return;
|
||||
|
||||
explorer.getTxidsInBlock(hash, function(err, txids) {
|
||||
if (err) {
|
||||
log.error('Could not fetch txids from block ' + hash, err);
|
||||
return;
|
||||
}
|
||||
|
||||
self.storage.fetchActiveTxConfirmationSubs(null, function(err, subs) {
|
||||
if (err) return;
|
||||
if (_.isEmpty(subs)) return;
|
||||
var indexedSubs = _.indexBy(subs, 'txid');
|
||||
var triggered = [];
|
||||
_.each(txids, function(txid) {
|
||||
if (indexedSubs[txid]) triggered.push(indexedSubs[txid]);
|
||||
});
|
||||
processTriggeredSubs(triggered, function(err) {
|
||||
if (err) {
|
||||
log.error('Could not process tx confirmations', err);
|
||||
}
|
||||
return;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._handleNewBlock = function(coin, network, hash) {
|
||||
this._notifyNewBlock(coin, network, hash);
|
||||
this._handleTxConfirmations(coin, network, hash);
|
||||
};
|
||||
|
||||
BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) {
|
||||
var self = this;
|
||||
|
||||
|
|
|
@ -2,11 +2,18 @@
|
|||
|
||||
var Constants = {};
|
||||
|
||||
Constants.COINS = {
|
||||
BTC: 'btc',
|
||||
BCH: 'bch',
|
||||
};
|
||||
|
||||
Constants.NETWORKS = {
|
||||
LIVENET: 'livenet',
|
||||
TESTNET: 'testnet',
|
||||
};
|
||||
|
||||
Constants.ADDRESS_FORMATS = ['copay', 'cashaddr', 'legacy'];
|
||||
|
||||
Constants.SCRIPT_TYPES = {
|
||||
P2SH: 'P2SH',
|
||||
P2PKH: 'P2PKH',
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
var Defaults = {};
|
||||
|
||||
Defaults.MIN_FEE_PER_KB = 0;
|
||||
Defaults.MAX_FEE_PER_KB = 1000000;
|
||||
Defaults.MAX_FEE_PER_KB = 10000 * 1000; // 10k sat/b
|
||||
Defaults.MIN_TX_FEE = 0;
|
||||
Defaults.MAX_TX_FEE = 0.1 * 1e8;
|
||||
Defaults.MAX_TX_SIZE_IN_KB = 100;
|
||||
|
@ -24,30 +24,35 @@ Defaults.MAX_MAIN_ADDRESS_GAP = 20;
|
|||
// TODO: should allow different gap sizes for external/internal chains
|
||||
Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP + 20;
|
||||
|
||||
Defaults.FEE_LEVELS = [{
|
||||
Defaults.FEE_LEVELS = {
|
||||
btc: [{
|
||||
name: 'urgent',
|
||||
nbBlocks: 2,
|
||||
multiplier: 1.5,
|
||||
defaultValue: 150000,
|
||||
}, {
|
||||
}, {
|
||||
name: 'priority',
|
||||
nbBlocks: 2,
|
||||
defaultValue: 100000
|
||||
}, {
|
||||
}, {
|
||||
name: 'normal',
|
||||
nbBlocks: 3,
|
||||
defaultValue: 80000
|
||||
}, {
|
||||
}, {
|
||||
name: 'economy',
|
||||
nbBlocks: 6,
|
||||
defaultValue: 50000
|
||||
}, {
|
||||
}, {
|
||||
name: 'superEconomy',
|
||||
nbBlocks: 24,
|
||||
defaultValue: 20000
|
||||
}];
|
||||
|
||||
Defaults.DEFAULT_FEE_PER_KB = Defaults.FEE_LEVELS[1].defaultValue;
|
||||
}],
|
||||
bch: [{
|
||||
name: 'normal',
|
||||
nbBlocks: 2,
|
||||
defaultValue: 2000,
|
||||
}]
|
||||
};
|
||||
|
||||
// How many levels to fallback to if the value returned by the network for a given nbBlocks is -1
|
||||
Defaults.FEE_LEVELS_FALLBACK = 2;
|
||||
|
@ -55,6 +60,12 @@ Defaults.FEE_LEVELS_FALLBACK = 2;
|
|||
// Minimum nb of addresses a wallet must have to start using 2-step balance optimization
|
||||
Defaults.TWO_STEP_BALANCE_THRESHOLD = 100;
|
||||
|
||||
// Age Limit for addresses to be considered 'active' always
|
||||
Defaults.TWO_STEP_CREATION_HOURS = 24;
|
||||
|
||||
// Time to prevent re-quering inactive addresses (MIN)
|
||||
Defaults.TWO_STEP_INACTIVE_CLEAN_DURATION_MIN = 60;
|
||||
|
||||
Defaults.FIAT_RATE_PROVIDER = 'BitPay';
|
||||
Defaults.FIAT_RATE_FETCH_INTERVAL = 10; // In minutes
|
||||
Defaults.FIAT_RATE_MAX_LOOK_BACK_TIME = 120; // In minutes
|
||||
|
@ -85,6 +96,12 @@ Defaults.CONFIRMATIONS_TO_START_CACHING = 6 * 6; // ~ 6hrs
|
|||
// Number of addresses from which tx history is enabled in a wallet
|
||||
Defaults.HISTORY_CACHE_ADDRESS_THRESOLD = 100;
|
||||
|
||||
// Number of addresses from which balance in cache for a few seconds
|
||||
Defaults.BALANCE_CACHE_ADDRESS_THRESOLD = Defaults.HISTORY_CACHE_ADDRESS_THRESOLD;
|
||||
|
||||
Defaults.BALANCE_CACHE_DIRECT_DURATION = 60;
|
||||
Defaults.BALANCE_CACHE_DURATION = 10;
|
||||
|
||||
// Cache time for blockchain height (in seconds)
|
||||
Defaults.BLOCKHEIGHT_CACHE_TIME = 10 * 60;
|
||||
|
||||
|
@ -98,15 +115,26 @@ Defaults.SESSION_EXPIRATION = 1 * 60 * 60; // 1 hour to session expiration
|
|||
Defaults.RateLimit = {
|
||||
createWallet: {
|
||||
windowMs: 60 * 60 * 1000, // hour window
|
||||
delayAfter: 10, // begin slowing down responses after the 3rd request
|
||||
delayAfter: 8, // begin slowing down responses after the 3rd request
|
||||
delayMs: 3000, // slow down subsequent responses by 3 seconds per request
|
||||
max: 20, // start blocking after 20 request
|
||||
message: "Too many wallets created from this IP, please try again after an hour"
|
||||
max: 15, // start blocking after 20 request
|
||||
message: 'Too many wallets created from this IP, please try again after an hour',
|
||||
},
|
||||
estimateFee: {
|
||||
windowMs: 60 * 10 *1000, // 10 min window
|
||||
delayAfter: 5, // begin slowing down responses after the 3rd request
|
||||
delayMs: 300, // slow down subsequent responses by 3 seconds per request
|
||||
|
||||
max: 10, // start blocking after 200 request
|
||||
message: 'Too many request',
|
||||
},
|
||||
|
||||
// otherPosts: {
|
||||
// windowMs: 60 * 60 * 1000, // 1 hour window
|
||||
// max: 1200 , // 1 post every 3 sec average, max.
|
||||
// },
|
||||
};
|
||||
|
||||
Defaults.COIN = 'btc';
|
||||
Defaults.INSIGHT_REQUEST_POOL_SIZE = 20;
|
||||
module.exports = Defaults;
|
||||
|
|
|
@ -7,6 +7,13 @@ var encoding = bitcore.encoding;
|
|||
var secp256k1 = require('secp256k1');
|
||||
|
||||
var Utils = {};
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var Bitcore_ = {
|
||||
btc: Bitcore,
|
||||
bch: require('bitcore-lib-cash')
|
||||
};
|
||||
|
||||
|
||||
|
||||
Utils.getMissingFields = function(obj, args) {
|
||||
args = [].concat(args);
|
||||
|
@ -64,7 +71,7 @@ Utils._tryImportPublicKey = function(publicKey) {
|
|||
publicKeyBuffer = new Buffer(publicKey, 'hex');
|
||||
}
|
||||
return publicKeyBuffer;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -76,7 +83,7 @@ Utils._tryImportSignature = function(signature) {
|
|||
signatureBuffer = new Buffer(signature, 'hex');
|
||||
}
|
||||
return secp256k1.signatureImport(signatureBuffer);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -84,7 +91,7 @@ Utils._tryImportSignature = function(signature) {
|
|||
Utils._tryVerifyMessage = function(hash, sig, publicKeyBuffer) {
|
||||
try {
|
||||
return secp256k1.verify(hash, sig, publicKeyBuffer);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -105,7 +112,12 @@ Utils.formatAmount = function(satoshis, unit, opts) {
|
|||
toSatoshis: 1,
|
||||
maxDecimals: 0,
|
||||
minDecimals: 0,
|
||||
}
|
||||
},
|
||||
bch: {
|
||||
toSatoshis: 100000000,
|
||||
maxDecimals: 6,
|
||||
minDecimals: 2,
|
||||
},
|
||||
};
|
||||
|
||||
$.shouldBeNumber(satoshis);
|
||||
|
@ -176,5 +188,35 @@ Utils.parseVersion = function(version) {
|
|||
return v;
|
||||
};
|
||||
|
||||
Utils.checkValueInCollection = function(value, collection) {
|
||||
if (!value || !_.isString(value)) return false;
|
||||
return _.contains(_.values(collection), value);
|
||||
};
|
||||
|
||||
|
||||
Utils.getAddressCoin = function(address) {
|
||||
try {
|
||||
new Bitcore_['btc'].Address(address);
|
||||
return 'btc';
|
||||
} catch (e) {
|
||||
try {
|
||||
new Bitcore_['bch'].Address(address);
|
||||
return 'bch';
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Utils.translateAddress = function(address, coin) {
|
||||
var origCoin = Utils.getAddressCoin(address);
|
||||
var origAddress = new Bitcore_[origCoin].Address(address);
|
||||
var origObj = origAddress.toObject();
|
||||
|
||||
var result = Bitcore_[coin].Address.fromObject(origObj)
|
||||
return result.toString();
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = Utils;
|
||||
|
|
|
@ -11,6 +11,7 @@ var path = require('path');
|
|||
var nodemailer = require('nodemailer');
|
||||
|
||||
var Utils = require('./common/utils');
|
||||
var Defaults = require('./common/defaults');
|
||||
var Storage = require('./storage');
|
||||
var MessageBroker = require('./messagebroker');
|
||||
var Lock = require('./lock');
|
||||
|
@ -21,26 +22,37 @@ var EMAIL_TYPES = {
|
|||
'NewCopayer': {
|
||||
filename: 'new_copayer',
|
||||
notifyDoer: false,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'WalletComplete': {
|
||||
filename: 'wallet_complete',
|
||||
notifyDoer: true,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'NewTxProposal': {
|
||||
filename: 'new_tx_proposal',
|
||||
notifyDoer: false,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'NewOutgoingTx': {
|
||||
filename: 'new_outgoing_tx',
|
||||
notifyDoer: true,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'NewIncomingTx': {
|
||||
filename: 'new_incoming_tx',
|
||||
notifyDoer: true,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'TxProposalFinallyRejected': {
|
||||
filename: 'txp_finally_rejected',
|
||||
notifyDoer: false,
|
||||
notifyOthers: true,
|
||||
},
|
||||
'TxConfirmation': {
|
||||
filename: 'tx_confirmation',
|
||||
notifyDoer: true,
|
||||
notifyOthers: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -162,6 +174,9 @@ EmailService.prototype._applyTemplate = function(template, data, cb) {
|
|||
EmailService.prototype._getRecipientsList = function(notification, emailType, cb) {
|
||||
var self = this;
|
||||
|
||||
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) {
|
||||
if (err) return cb(err);
|
||||
if (_.isEmpty(preferences)) return cb(null, []);
|
||||
|
@ -172,6 +187,7 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb
|
|||
|
||||
usedEmails[p.email] = true;
|
||||
if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return;
|
||||
if (notification.creatorId != p.copayerId && !emailType.notifyOthers) return;
|
||||
if (!_.contains(self.availableLanguages, p.language)) {
|
||||
if (p.language) {
|
||||
log.warn('Language for email "' + p.language + '" not available.');
|
||||
|
@ -179,16 +195,24 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb
|
|||
p.language = self.defaultLanguage;
|
||||
}
|
||||
|
||||
var unit;
|
||||
if (wallet.coin != Defaults.COIN) {
|
||||
unit = wallet.coin;
|
||||
} else {
|
||||
unit = p.unit || self.defaultUnit;
|
||||
}
|
||||
|
||||
return {
|
||||
copayerId: p.copayerId,
|
||||
emailAddress: p.email,
|
||||
language: p.language,
|
||||
unit: p.unit || self.defaultUnit,
|
||||
unit: unit,
|
||||
};
|
||||
}));
|
||||
|
||||
return cb(null, recipients);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
EmailService.prototype._getDataForTemplate = function(notification, recipient, cb) {
|
||||
|
@ -197,7 +221,8 @@ EmailService.prototype._getDataForTemplate = function(notification, recipient, c
|
|||
// TODO: Declare these in BWU
|
||||
var UNIT_LABELS = {
|
||||
btc: 'BTC',
|
||||
bit: 'bits'
|
||||
bit: 'bits',
|
||||
bch: 'BCH',
|
||||
};
|
||||
|
||||
var data = _.cloneDeep(notification.data);
|
||||
|
|
|
@ -33,9 +33,10 @@ var errors = {
|
|||
UPGRADE_NEEDED: 'Client app needs to be upgraded',
|
||||
WALLET_ALREADY_EXISTS: 'Wallet already exists',
|
||||
WALLET_FULL: 'Wallet full',
|
||||
WALLET_LOCKED: 'Wallet is locked',
|
||||
WALLET_BUSY: 'Wallet is busy, try later',
|
||||
WALLET_NOT_COMPLETE: 'Wallet is not complete',
|
||||
WALLET_NOT_FOUND: 'Wallet not found',
|
||||
WALLET_NEED_SCAN: 'Wallet needs addresses scan',
|
||||
};
|
||||
|
||||
var errorObjects = _.zipObject(_.map(errors, function(msg, code) {
|
||||
|
|
|
@ -17,7 +17,7 @@ var Stats = require('./stats');
|
|||
|
||||
log.disableColor();
|
||||
log.debug = log.verbose;
|
||||
log.level = 'info';
|
||||
log.level = 'verbose';
|
||||
|
||||
var ExpressApp = function() {
|
||||
this.app = express();
|
||||
|
@ -39,7 +39,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
this.app.use(function(req, res, next) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,X-Requested-With,Content-Type,Authorization');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,x-wallet-id,X-Requested-With,Content-Type,Authorization');
|
||||
res.setHeader('x-service-version', WalletService.getServiceVersion());
|
||||
next();
|
||||
});
|
||||
|
@ -54,6 +54,16 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
this.app.use(allowCORS);
|
||||
this.app.enable('trust proxy');
|
||||
|
||||
|
||||
|
||||
// handle `abort` https://nodejs.org/api/http.html#http_event_abort
|
||||
this.app.use(function(req, res, next) {
|
||||
req.on('abort', function() {
|
||||
log.warn('Request aborted by the client');
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
var POST_LIMIT = 1024 * 100 /* Max POST 100 kb */ ;
|
||||
|
||||
this.app.use(bodyParser.json({
|
||||
|
@ -65,14 +75,10 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
} else {
|
||||
var morgan = require('morgan');
|
||||
morgan.token('walletId', function getId(req) {
|
||||
return req.walletId
|
||||
return req.walletId ? '<' + req.walletId + '>' : '<>';
|
||||
});
|
||||
|
||||
morgan.token('copayerId', function getId(req) {
|
||||
return req.copayerId
|
||||
});
|
||||
|
||||
var logFormat = ':remote-addr :date[iso] ":method :url" :status :res[content-length] :response-time ":user-agent" :walletId :copayerId';
|
||||
var logFormat = ':walletId :remote-addr :date[iso] ":method :url" :status :res[content-length] :response-time ":user-agent" ';
|
||||
var logOpts = {
|
||||
skip: function(req, res) {
|
||||
if (res.statusCode != 200) return false;
|
||||
|
@ -90,7 +96,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
|
||||
var status = (err.code == 'NOT_AUTHORIZED') ? 401 : 400;
|
||||
if (!opts.disableLogs)
|
||||
log.info('Client Err: ' + status + ' ' + req.url + ' ' + err);
|
||||
log.info('Client Err: ' + status + ' ' + req.url + ' ' + JSON.stringify(err));
|
||||
|
||||
res.status(status).json({
|
||||
code: err.code,
|
||||
|
@ -155,6 +161,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body),
|
||||
signature: credentials.signature,
|
||||
clientVersion: req.header('x-client-version'),
|
||||
walletId: req.header('x-wallet-id'),
|
||||
};
|
||||
if (opts.allowSession) {
|
||||
auth.session = credentials.session;
|
||||
|
@ -162,10 +169,15 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
WalletService.getInstanceWithAuth(auth, function(err, server) {
|
||||
if (err) return returnError(err, res, req);
|
||||
|
||||
if (opts.onlySupportStaff && !server.copayerIsSupportStaff) {
|
||||
return returnError(new WalletService.ClientError({
|
||||
code: 'NOT_AUTHORIZED'
|
||||
}), res, req);
|
||||
}
|
||||
|
||||
// For logging
|
||||
req.walletId = server.walletId;
|
||||
req.copayerId = server.copayerId;
|
||||
|
||||
return cb(server);
|
||||
});
|
||||
};
|
||||
|
@ -184,7 +196,6 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
}
|
||||
|
||||
// DEPRECATED
|
||||
|
||||
router.post('/v1/wallets/', createWalletLimiter, function(req, res) {
|
||||
logDeprecated(req);
|
||||
var server;
|
||||
|
@ -290,6 +301,29 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/v1/wallets/:identifier/', function(req, res) {
|
||||
getServerWithAuth(req, res, {
|
||||
onlySupportStaff: true
|
||||
}, function(server) {
|
||||
var opts = {
|
||||
identifier: req.params['identifier'],
|
||||
};
|
||||
server.getWalletFromIdentifier(opts, function(err, wallet) {
|
||||
if (err) return returnError(err, res, req);
|
||||
if (!wallet) return res.end();
|
||||
|
||||
server.walletId = wallet.id;
|
||||
var opts = {};
|
||||
if (req.query.includeExtendedInfo == '1') opts.includeExtendedInfo = true;
|
||||
if (req.query.twoStep == '1') opts.twoStep = true;
|
||||
server.getStatus(opts, function(err, status) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(status);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v1/preferences/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.getPreferences({}, function(err, preferences) {
|
||||
|
@ -383,6 +417,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
router.get('/v1/balance/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
var opts = {};
|
||||
if (req.query.coin) opts.coin = req.query.coin;
|
||||
if (req.query.twoStep == '1') opts.twoStep = true;
|
||||
server.getBalance(opts, function(err, balance) {
|
||||
if (err) return returnError(err, res, req);
|
||||
|
@ -391,8 +426,21 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
var estimateFeeLimiter;
|
||||
|
||||
if (Defaults.RateLimit.estimateFee && !opts.ignoreRateLimiter) {
|
||||
log.info('', 'Limiting estimate fee per IP: %d req/h', (Defaults.RateLimit.estimateFee.max / Defaults.RateLimit.estimateFee.windowMs * 60 * 60 * 1000).toFixed(2))
|
||||
estimateFeeLimiter = new RateLimit(Defaults.RateLimit.estimateFee);
|
||||
// router.use(/\/v\d+\/wallets\/$/, createWalletLimiter)
|
||||
} else {
|
||||
estimateFeeLimiter = function(req, res, next) {
|
||||
next()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// DEPRECATED
|
||||
router.get('/v1/feelevels/', function(req, res) {
|
||||
router.get('/v1/feelevels/', estimateFeeLimiter, function(req, res) {
|
||||
logDeprecated(req);
|
||||
var opts = {};
|
||||
if (req.query.network) opts.network = req.query.network;
|
||||
|
@ -412,9 +460,11 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/v2/feelevels/', function(req, res) {
|
||||
router.get('/v2/feelevels/', estimateFeeLimiter, function(req, res) {
|
||||
var opts = {};
|
||||
if (req.query.coin) opts.coin = req.query.coin;
|
||||
if (req.query.network) opts.network = req.query.network;
|
||||
|
||||
var server;
|
||||
try {
|
||||
server = getServer(req, res);
|
||||
|
@ -561,6 +611,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
router.get('/v1/stats/', function(req, res) {
|
||||
var opts = {};
|
||||
if (req.query.network) opts.network = req.query.network;
|
||||
if (req.query.coin) opts.coin = req.query.coin;
|
||||
if (req.query.from) opts.from = req.query.from;
|
||||
if (req.query.to) opts.to = req.query.to;
|
||||
|
||||
|
@ -650,18 +701,22 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
|
||||
router.get('/v1/fiatrates/:code/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
var server;
|
||||
var opts = {
|
||||
code: req.params['code'],
|
||||
source: req.query.source,
|
||||
provider: req.query.provider,
|
||||
ts: +req.query.ts,
|
||||
};
|
||||
try {
|
||||
server = getServer(req, res);
|
||||
} catch (ex) {
|
||||
return returnError(ex, res, req);
|
||||
}
|
||||
server.getFiatRate(opts, function(err, rates) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(rates);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v1/pushnotifications/subscriptions/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
|
@ -697,6 +752,28 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/v1/txconfirmations/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.txConfirmationSubscribe(req.body, function(err, response) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/v1/txconfirmations/:txid', function(req, res) {
|
||||
var opts = {
|
||||
txid: req.params['txid'],
|
||||
};
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.txConfirmationUnsubscribe(opts, function(err, response) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.app.use(opts.basePath || '/bws/api', router);
|
||||
|
||||
WalletService.initialize(opts, cb);
|
||||
|
|
|
@ -27,11 +27,13 @@ function Lock(opts) {
|
|||
}
|
||||
};
|
||||
|
||||
Lock.prototype.runLocked = function(token, cb, task) {
|
||||
Lock.prototype.runLocked = function(token, cb, task, waitTime) {
|
||||
$.shouldBeDefined(token);
|
||||
|
||||
this.lock.locked(token, 5 * 1000, 5 * 60 * 1000, function(err, release) {
|
||||
if (err) return cb(Errors.WALLET_LOCKED);
|
||||
waitTime = waitTime || 5 * 1000;
|
||||
|
||||
this.lock.locked(token, waitTime , 5 * 60 * 1000, function(err, release) {
|
||||
if (err) return cb(Errors.WALLET_BUSY);
|
||||
var _cb = function() {
|
||||
cb.apply(null, arguments);
|
||||
release();
|
||||
|
|
|
@ -3,8 +3,14 @@
|
|||
var $ = require('preconditions').singleton();
|
||||
var _ = require('lodash');
|
||||
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var Constants = require('../common/constants');
|
||||
var Bitcore = {
|
||||
'btc': require('bitcore-lib'),
|
||||
'bch': require('bitcore-lib-cash'),
|
||||
};
|
||||
var Common = require('../common');
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
function Address() {};
|
||||
|
||||
|
@ -13,6 +19,8 @@ Address.create = function(opts) {
|
|||
|
||||
var x = new Address();
|
||||
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
|
||||
|
||||
x.version = '1.0.0';
|
||||
x.createdOn = Math.floor(Date.now() / 1000);
|
||||
x.address = opts.address;
|
||||
|
@ -20,7 +28,8 @@ Address.create = function(opts) {
|
|||
x.isChange = opts.isChange;
|
||||
x.path = opts.path;
|
||||
x.publicKeys = opts.publicKeys;
|
||||
x.network = Bitcore.Address(x.address).toObject().network;
|
||||
x.coin = opts.coin;
|
||||
x.network = Bitcore[opts.coin].Address(x.address).toObject().network;
|
||||
x.type = opts.type || Constants.SCRIPT_TYPES.P2SH;
|
||||
x.hasActivity = undefined;
|
||||
return x;
|
||||
|
@ -33,6 +42,7 @@ Address.fromObj = function(obj) {
|
|||
x.createdOn = obj.createdOn;
|
||||
x.address = obj.address;
|
||||
x.walletId = obj.walletId;
|
||||
x.coin = obj.coin || Defaults.COIN;
|
||||
x.network = obj.network;
|
||||
x.isChange = obj.isChange;
|
||||
x.path = obj.path;
|
||||
|
@ -42,22 +52,22 @@ Address.fromObj = function(obj) {
|
|||
return x;
|
||||
};
|
||||
|
||||
Address._deriveAddress = function(scriptType, publicKeyRing, path, m, network) {
|
||||
$.checkArgument(_.contains(_.values(Constants.SCRIPT_TYPES), scriptType));
|
||||
Address._deriveAddress = function(scriptType, publicKeyRing, path, m, coin, network) {
|
||||
$.checkArgument(Utils.checkValueInCollection(scriptType, Constants.SCRIPT_TYPES));
|
||||
|
||||
var publicKeys = _.map(publicKeyRing, function(item) {
|
||||
var xpub = new Bitcore.HDPublicKey(item.xPubKey);
|
||||
var xpub = new Bitcore[coin].HDPublicKey(item.xPubKey);
|
||||
return xpub.deriveChild(path).publicKey;
|
||||
});
|
||||
|
||||
var bitcoreAddress;
|
||||
switch (scriptType) {
|
||||
case Constants.SCRIPT_TYPES.P2SH:
|
||||
bitcoreAddress = Bitcore.Address.createMultisig(publicKeys, m, network);
|
||||
bitcoreAddress = Bitcore[coin].Address.createMultisig(publicKeys, m, network);
|
||||
break;
|
||||
case Constants.SCRIPT_TYPES.P2PKH:
|
||||
$.checkState(_.isArray(publicKeys) && publicKeys.length == 1);
|
||||
bitcoreAddress = Bitcore.Address.fromPublicKey(publicKeys[0], network);
|
||||
bitcoreAddress = Bitcore[coin].Address.fromPublicKey(publicKeys[0], network);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -68,9 +78,10 @@ Address._deriveAddress = function(scriptType, publicKeyRing, path, m, network) {
|
|||
};
|
||||
};
|
||||
|
||||
Address.derive = function(walletId, scriptType, publicKeyRing, path, m, network, isChange) {
|
||||
var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, network);
|
||||
Address.derive = function(walletId, scriptType, publicKeyRing, path, m, coin, network, isChange) {
|
||||
var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, coin, network);
|
||||
return Address.create(_.extend(raw, {
|
||||
coin: coin,
|
||||
walletId: walletId,
|
||||
type: scriptType,
|
||||
isChange: isChange,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
var _ = require('lodash');
|
||||
var $ = require('preconditions').singleton();
|
||||
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var Constants = require('../common/constants');
|
||||
var Utils = require('../common/utils');
|
||||
|
||||
function AddressManager() {};
|
||||
|
||||
|
@ -13,7 +13,7 @@ AddressManager.create = function(opts) {
|
|||
|
||||
x.version = 2;
|
||||
x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
|
||||
$.checkState(_.contains(_.values(Constants.DERIVATION_STRATEGIES), x.derivationStrategy));
|
||||
$.checkState(Utils.checkValueInCollection(x.derivationStrategy, Constants.DERIVATION_STRATEGIES));
|
||||
|
||||
x.receiveAddressIndex = 0;
|
||||
x.changeAddressIndex = 0;
|
||||
|
@ -55,6 +55,18 @@ AddressManager.prototype.rewindIndex = function(isChange, n) {
|
|||
}
|
||||
};
|
||||
|
||||
AddressManager.prototype.getCurrentIndex = function(isChange) {
|
||||
return isChange ? this.changeAddressIndex : this.receiveAddressIndex;
|
||||
};
|
||||
|
||||
|
||||
AddressManager.prototype.getBaseAddressPath = function(isChange) {
|
||||
return 'm/' +
|
||||
(this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') +
|
||||
(isChange ? 1 : 0) + '/' +
|
||||
0;
|
||||
};
|
||||
|
||||
AddressManager.prototype.getCurrentAddressPath = function(isChange) {
|
||||
return 'm/' +
|
||||
(this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') +
|
||||
|
|
|
@ -10,12 +10,16 @@ var Address = require('./address');
|
|||
var AddressManager = require('./addressmanager');
|
||||
var Bitcore = require('bitcore-lib');
|
||||
|
||||
var Constants = require('../common/constants');
|
||||
var Common = require('../common');
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
function Copayer() {};
|
||||
|
||||
Copayer._xPubToCopayerId = function(xpub) {
|
||||
var hash = sjcl.hash.sha256.hash(xpub);
|
||||
Copayer._xPubToCopayerId = function(coin, xpub) {
|
||||
var str = coin == Defaults.COIN ? xpub : coin + xpub;
|
||||
var hash = sjcl.hash.sha256.hash(str);
|
||||
return sjcl.codec.hex.fromBits(hash);
|
||||
};
|
||||
|
||||
|
@ -25,14 +29,17 @@ Copayer.create = function(opts) {
|
|||
.checkArgument(opts.requestPubKey, 'Missing copayer request public key')
|
||||
.checkArgument(opts.signature, 'Missing copayer request public key signature');
|
||||
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
|
||||
|
||||
opts.copayerIndex = opts.copayerIndex || 0;
|
||||
|
||||
var x = new Copayer();
|
||||
|
||||
x.version = 2;
|
||||
x.createdOn = Math.floor(Date.now() / 1000);
|
||||
x.coin = opts.coin;
|
||||
x.xPubKey = opts.xPubKey;
|
||||
x.id = Copayer._xPubToCopayerId(x.xPubKey);
|
||||
x.id = Copayer._xPubToCopayerId(opts.coin, x.xPubKey);
|
||||
x.name = opts.name;
|
||||
x.requestPubKey = opts.requestPubKey;
|
||||
x.signature = opts.signature;
|
||||
|
@ -59,6 +66,7 @@ Copayer.fromObj = function(obj) {
|
|||
|
||||
x.version = obj.version;
|
||||
x.createdOn = obj.createdOn;
|
||||
x.coin = obj.coin || Defaults.COIN;
|
||||
x.id = obj.id;
|
||||
x.name = obj.name;
|
||||
x.xPubKey = obj.xPubKey;
|
||||
|
@ -87,7 +95,7 @@ Copayer.prototype.createAddress = function(wallet, isChange) {
|
|||
$.checkState(wallet.isComplete());
|
||||
|
||||
var path = this.addressManager.getNewAddressPath(isChange);
|
||||
var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network, isChange);
|
||||
var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.coin, wallet.network, isChange);
|
||||
return address;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,5 +10,6 @@ Model.Email = require('./email');
|
|||
Model.TxNote = require('./txnote');
|
||||
Model.Session = require('./session');
|
||||
Model.PushNotificationSub = require('./pushnotificationsub');
|
||||
Model.TxConfirmationSub = require('./txconfirmationsub');
|
||||
|
||||
module.exports = Model;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
function TxConfirmationSub() {};
|
||||
|
||||
TxConfirmationSub.create = function(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var x = new TxConfirmationSub();
|
||||
|
||||
x.version = 1;
|
||||
x.createdOn = Math.floor(Date.now() / 1000);
|
||||
x.walletId = opts.walletId;
|
||||
x.copayerId = opts.copayerId;
|
||||
x.txid = opts.txid;
|
||||
x.isActive = true;
|
||||
return x;
|
||||
};
|
||||
|
||||
TxConfirmationSub.fromObj = function(obj) {
|
||||
var x = new TxConfirmationSub();
|
||||
|
||||
x.version = obj.version;
|
||||
x.createdOn = obj.createdOn;
|
||||
x.walletId = obj.walletId;
|
||||
x.copayerId = obj.copayerId;
|
||||
x.txid = obj.txid;
|
||||
x.isActive = obj.isActive;
|
||||
return x;
|
||||
};
|
||||
|
||||
|
||||
module.exports = TxConfirmationSub;
|
|
@ -7,11 +7,15 @@ var log = require('npmlog');
|
|||
log.debug = log.verbose;
|
||||
log.disableColor();
|
||||
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var Bitcore = {
|
||||
'btc': require('bitcore-lib'),
|
||||
'bch': require('bitcore-lib-cash'),
|
||||
};
|
||||
|
||||
var Common = require('../common');
|
||||
var Constants = Common.Constants;
|
||||
var Defaults = Common.Defaults;
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
var TxProposalLegacy = require('./txproposal_legacy');
|
||||
var TxProposalAction = require('./txproposalaction');
|
||||
|
@ -21,6 +25,9 @@ function TxProposal() {};
|
|||
TxProposal.create = function(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
|
||||
|
||||
var x = new TxProposal();
|
||||
|
||||
x.version = 3;
|
||||
|
@ -30,6 +37,8 @@ TxProposal.create = function(opts) {
|
|||
x.id = opts.id || Uuid.v4();
|
||||
x.walletId = opts.walletId;
|
||||
x.creatorId = opts.creatorId;
|
||||
x.coin = opts.coin;
|
||||
x.network = opts.network;
|
||||
x.message = opts.message;
|
||||
x.payProUrl = opts.payProUrl;
|
||||
x.changeAddress = opts.changeAddress;
|
||||
|
@ -51,15 +60,11 @@ TxProposal.create = function(opts) {
|
|||
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
|
||||
|
||||
x.addressType = opts.addressType || (x.walletN > 1 ? Constants.SCRIPT_TYPES.P2SH : Constants.SCRIPT_TYPES.P2PKH);
|
||||
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), x.addressType));
|
||||
$.checkState(Utils.checkValueInCollection(x.addressType, Constants.SCRIPT_TYPES));
|
||||
|
||||
x.customData = opts.customData;
|
||||
|
||||
x.amount = x.getTotalAmount();
|
||||
try {
|
||||
x.network = opts.network || Bitcore.Address(x.outputs[0].toAddress).toObject().network;
|
||||
} catch (ex) {}
|
||||
$.checkState(_.contains(_.values(Constants.NETWORKS), x.network));
|
||||
|
||||
x.setInputs(opts.inputs);
|
||||
x.fee = opts.fee;
|
||||
|
@ -79,6 +84,7 @@ TxProposal.fromObj = function(obj) {
|
|||
x.id = obj.id;
|
||||
x.walletId = obj.walletId;
|
||||
x.creatorId = obj.creatorId;
|
||||
x.coin = obj.coin || Defaults.COIN;
|
||||
x.network = obj.network;
|
||||
x.outputs = obj.outputs;
|
||||
x.amount = obj.amount;
|
||||
|
@ -136,9 +142,9 @@ TxProposal.prototype._updateStatus = function() {
|
|||
TxProposal.prototype._buildTx = function() {
|
||||
var self = this;
|
||||
|
||||
var t = new Bitcore.Transaction();
|
||||
var t = new Bitcore[self.coin].Transaction();
|
||||
|
||||
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), self.addressType));
|
||||
$.checkState(Utils.checkValueInCollection(self.addressType, Constants.SCRIPT_TYPES));
|
||||
|
||||
switch (self.addressType) {
|
||||
case Constants.SCRIPT_TYPES.P2SH:
|
||||
|
@ -155,7 +161,7 @@ TxProposal.prototype._buildTx = function() {
|
|||
_.each(self.outputs, function(o) {
|
||||
$.checkState(o.script || o.toAddress, 'Output should have either toAddress or script specified');
|
||||
if (o.script) {
|
||||
t.addOutput(new Bitcore.Transaction.Output({
|
||||
t.addOutput(new Bitcore[self.coin].Transaction.Output({
|
||||
script: o.script,
|
||||
satoshis: o.amount
|
||||
}));
|
||||
|
@ -220,10 +226,6 @@ TxProposal.prototype.getBitcoreTx = function() {
|
|||
return t;
|
||||
};
|
||||
|
||||
TxProposal.prototype.getNetworkName = function() {
|
||||
return this.network;
|
||||
};
|
||||
|
||||
TxProposal.prototype.getRawTx = function() {
|
||||
var t = this.getBitcoreTx();
|
||||
|
||||
|
@ -323,21 +325,23 @@ TxProposal.prototype.addAction = function(copayerId, type, comment, signatures,
|
|||
TxProposal.prototype._addSignaturesToBitcoreTx = function(tx, signatures, xpub) {
|
||||
var self = this;
|
||||
|
||||
var bitcore = Bitcore[self.coin];
|
||||
|
||||
if (signatures.length != this.inputs.length)
|
||||
throw new Error('Number of signatures does not match number of inputs');
|
||||
|
||||
var i = 0,
|
||||
x = new Bitcore.HDPublicKey(xpub);
|
||||
x = new bitcore.HDPublicKey(xpub);
|
||||
|
||||
_.each(signatures, function(signatureHex) {
|
||||
var input = self.inputs[i];
|
||||
try {
|
||||
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
|
||||
var signature = bitcore.crypto.Signature.fromString(signatureHex);
|
||||
var pub = x.deriveChild(self.inputPaths[i]).publicKey;
|
||||
var s = {
|
||||
inputIndex: i,
|
||||
signature: signature,
|
||||
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
|
||||
sigtype: bitcore.crypto.Signature.SIGHASH_ALL | bitcore.crypto.Signature.SIGHASH_FORKID,
|
||||
publicKey: pub,
|
||||
};
|
||||
tx.inputs[i].addSignature(tx, s);
|
||||
|
|
|
@ -54,6 +54,7 @@ TxProposal.fromObj = function(obj) {
|
|||
return TxProposalAction.fromObj(action);
|
||||
});
|
||||
x.outputOrder = obj.outputOrder;
|
||||
x.coin = obj.coin || Defaults.COIN;
|
||||
x.network = obj.network;
|
||||
x.fee = obj.fee;
|
||||
x.feePerKb = obj.feePerKb;
|
||||
|
@ -93,10 +94,6 @@ TxProposal.prototype.getBitcoreTx = function() {
|
|||
throwUnsupportedError();
|
||||
};
|
||||
|
||||
TxProposal.prototype.getNetworkName = function() {
|
||||
return Bitcore.Address(this.changeAddress.address).toObject().network;
|
||||
};
|
||||
|
||||
TxProposal.prototype.getRawTx = function() {
|
||||
throwUnsupportedError();
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var log = require('npmlog');
|
||||
var $ = require('preconditions').singleton();
|
||||
var Uuid = require('uuid');
|
||||
|
||||
|
@ -9,7 +10,10 @@ var Address = require('./address');
|
|||
var Copayer = require('./copayer');
|
||||
var AddressManager = require('./addressmanager');
|
||||
|
||||
var Constants = require('../common/constants');
|
||||
var Common = require('../common');
|
||||
var Constants = Common.Constants,
|
||||
Defaults = Common.Defaults,
|
||||
Utils = Common.Utils;
|
||||
|
||||
function Wallet() {};
|
||||
|
||||
|
@ -20,6 +24,8 @@ Wallet.create = function(opts) {
|
|||
|
||||
$.shouldBeNumber(opts.m);
|
||||
$.shouldBeNumber(opts.n);
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
|
||||
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
|
||||
|
||||
x.version = '1.0.0';
|
||||
x.createdOn = Math.floor(Date.now() / 1000);
|
||||
|
@ -33,6 +39,7 @@ Wallet.create = function(opts) {
|
|||
x.addressIndex = 0;
|
||||
x.copayers = [];
|
||||
x.pubKey = opts.pubKey;
|
||||
x.coin = opts.coin;
|
||||
x.network = opts.network;
|
||||
x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
|
||||
x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH;
|
||||
|
@ -64,6 +71,7 @@ Wallet.fromObj = function(obj) {
|
|||
return Copayer.fromObj(copayer);
|
||||
});
|
||||
x.pubKey = obj.pubKey;
|
||||
x.coin = obj.coin || Defaults.COIN;
|
||||
x.network = obj.network;
|
||||
x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
|
||||
x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH;
|
||||
|
@ -105,6 +113,7 @@ Wallet.prototype._updatePublicKeyRing = function() {
|
|||
};
|
||||
|
||||
Wallet.prototype.addCopayer = function(copayer) {
|
||||
$.checkState(copayer.coin == this.coin);
|
||||
|
||||
this.copayers.push(copayer);
|
||||
if (this.copayers.length < this.n) return;
|
||||
|
@ -134,10 +143,6 @@ Wallet.prototype.getCopayer = function(copayerId) {
|
|||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.getNetworkName = function() {
|
||||
return this.network;
|
||||
};
|
||||
|
||||
Wallet.prototype.isComplete = function() {
|
||||
return this.status == 'complete';
|
||||
};
|
||||
|
@ -152,7 +157,8 @@ Wallet.prototype.createAddress = function(isChange) {
|
|||
var self = this;
|
||||
|
||||
var path = this.addressManager.getNewAddressPath(isChange);
|
||||
var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.network, isChange);
|
||||
log.verbose('Deriving addr:' + path);
|
||||
var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.coin, this.network, isChange);
|
||||
return address;
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ var Storage = require('./storage');
|
|||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var Utils = require('./common/utils');
|
||||
var Defaults = require('./common/defaults');
|
||||
var Model = require('./model');
|
||||
var sjcl = require('sjcl');
|
||||
var log = require('npmlog');
|
||||
|
@ -33,6 +34,10 @@ var PUSHNOTIFICATIONS_TYPES = {
|
|||
'TxProposalFinallyRejected': {
|
||||
filename: 'txp_finally_rejected',
|
||||
},
|
||||
'TxConfirmation': {
|
||||
filename: 'tx_confirmation',
|
||||
notifyCreatorOnly: true,
|
||||
},
|
||||
};
|
||||
|
||||
function PushNotificationsService() {};
|
||||
|
@ -111,7 +116,7 @@ PushNotificationsService.prototype._sendPushNotifications = function(notificatio
|
|||
log.debug('Should send notification: ', should);
|
||||
if (!should) return cb();
|
||||
|
||||
self._getRecipientsList(notification, function(err, recipientsList) {
|
||||
self._getRecipientsList(notification, notifType, function(err, recipientsList) {
|
||||
if (err) return cb(err);
|
||||
|
||||
async.waterfall([
|
||||
|
@ -184,16 +189,21 @@ PushNotificationsService.prototype._checkShouldSendNotif = function(notification
|
|||
|
||||
if (notification.type != 'NewTxProposal') return cb(null, true);
|
||||
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||
return cb(err, wallet.m > 1);
|
||||
return cb(err, wallet && wallet.m > 1);
|
||||
});
|
||||
};
|
||||
|
||||
PushNotificationsService.prototype._getRecipientsList = function(notification, cb) {
|
||||
PushNotificationsService.prototype._getRecipientsList = function(notification, notificationType, cb) {
|
||||
var self = this;
|
||||
|
||||
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var unit;
|
||||
if (wallet.coin != Defaults.COIN) {
|
||||
unit = wallet.coin;
|
||||
}
|
||||
|
||||
self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) {
|
||||
|
||||
if (err) log.error(err);
|
||||
|
@ -210,22 +220,23 @@ PushNotificationsService.prototype._getRecipientsList = function(notification, c
|
|||
return {
|
||||
copayerId: p.copayerId,
|
||||
language: p.language,
|
||||
unit: p.unit,
|
||||
unit: unit || p.unit || self.defaultUnit,
|
||||
};
|
||||
}));
|
||||
|
||||
recipientPreferences = _.indexBy(recipientPreferences, 'copayerId');
|
||||
|
||||
var recipientsList = _.reject(_.map(wallet.copayers, function(copayer) {
|
||||
var recipientsList = _.compact(_.map(wallet.copayers, function(copayer) {
|
||||
if ((copayer.id == notification.creatorId && notificationType.notifyCreatorOnly) ||
|
||||
(copayer.id != notification.creatorId && !notificationType.notifyCreatorOnly)) {
|
||||
var p = recipientPreferences[copayer.id] || {};
|
||||
return {
|
||||
copayerId: copayer.id,
|
||||
language: p.language || self.defaultLanguage,
|
||||
unit: p.unit || self.defaultUnit,
|
||||
unit: unit || p.unit || self.defaultUnit,
|
||||
}
|
||||
}), {
|
||||
copayerId: notification.creatorId
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return cb(null, recipientsList);
|
||||
});
|
||||
|
@ -270,7 +281,8 @@ PushNotificationsService.prototype._getDataForTemplate = function(notification,
|
|||
var self = this;
|
||||
var UNIT_LABELS = {
|
||||
btc: 'BTC',
|
||||
bit: 'bits'
|
||||
bit: 'bits',
|
||||
bch: 'BCH',
|
||||
};
|
||||
|
||||
var data = _.cloneDeep(notification.data);
|
||||
|
@ -285,7 +297,7 @@ PushNotificationsService.prototype._getDataForTemplate = function(notification,
|
|||
}
|
||||
|
||||
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
if (err || !wallet) return cb(err);
|
||||
|
||||
data.walletId = wallet.id;
|
||||
data.walletName = wallet.name;
|
||||
|
|
862
lib/server.js
862
lib/server.js
File diff suppressed because it is too large
Load Diff
13
lib/stats.js
13
lib/stats.js
|
@ -15,12 +15,13 @@ var config = require('../config');
|
|||
var storage = require('./storage');
|
||||
|
||||
|
||||
var INITIAL_DATE = '2015-01-01';
|
||||
var INITIAL_DATE = '2018-01-01';
|
||||
|
||||
function Stats(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
this.network = opts.network || 'livenet';
|
||||
this.coin = opts.coin || 'btc';
|
||||
this.from = moment(opts.from || INITIAL_DATE);
|
||||
this.to = moment(opts.to);
|
||||
this.fromTs = this.from.startOf('day').valueOf();
|
||||
|
@ -70,7 +71,7 @@ Stats.prototype._getNewWallets = function(cb) {
|
|||
|
||||
function getLastDate(cb) {
|
||||
self.db.collection('stats_wallets')
|
||||
.find({})
|
||||
.find({'_id.coin': self.coin})
|
||||
.sort({
|
||||
'_id.day': -1
|
||||
})
|
||||
|
@ -91,6 +92,7 @@ Stats.prototype._getNewWallets = function(cb) {
|
|||
var key = {
|
||||
day: +day,
|
||||
network: this.network,
|
||||
coin: this.coin
|
||||
};
|
||||
var value = {
|
||||
count: 1
|
||||
|
@ -127,6 +129,7 @@ Stats.prototype._getNewWallets = function(cb) {
|
|||
self.db.collection('stats_wallets')
|
||||
.find({
|
||||
'_id.network': self.network,
|
||||
'_id.coin': self.coin,
|
||||
'_id.day': {
|
||||
$gte: self.fromTs,
|
||||
$lte: self.toTs,
|
||||
|
@ -142,6 +145,7 @@ Stats.prototype._getNewWallets = function(cb) {
|
|||
var day = moment(record._id.day).format('YYYYMMDD');
|
||||
return {
|
||||
day: day,
|
||||
coin: record._id.coin,
|
||||
count: record.value.count,
|
||||
};
|
||||
});
|
||||
|
@ -181,7 +185,7 @@ Stats.prototype._getTxProposals = function(cb) {
|
|||
|
||||
function getLastDate(cb) {
|
||||
self.db.collection('stats_txps')
|
||||
.find({})
|
||||
.find({'_id.coin': self.coin })
|
||||
.sort({
|
||||
'_id.day': -1
|
||||
})
|
||||
|
@ -202,6 +206,7 @@ Stats.prototype._getTxProposals = function(cb) {
|
|||
var key = {
|
||||
day: +day,
|
||||
network: this.network,
|
||||
coin: this.coin
|
||||
};
|
||||
var value = {
|
||||
count: 1,
|
||||
|
@ -243,6 +248,7 @@ Stats.prototype._getTxProposals = function(cb) {
|
|||
self.db.collection('stats_txps')
|
||||
.find({
|
||||
'_id.network': self.network,
|
||||
'_id.coin': self.coin,
|
||||
'_id.day': {
|
||||
$gte: self.fromTs,
|
||||
$lte: self.toTs,
|
||||
|
@ -262,6 +268,7 @@ Stats.prototype._getTxProposals = function(cb) {
|
|||
var day = moment(record._id.day).format('YYYYMMDD');
|
||||
stats.nbByDay.push({
|
||||
day: day,
|
||||
coin: record._id.coin,
|
||||
count: record.value.count,
|
||||
});
|
||||
stats.amountByDay.push({
|
||||
|
|
409
lib/storage.js
409
lib/storage.js
|
@ -7,7 +7,7 @@ var log = require('npmlog');
|
|||
log.debug = log.verbose;
|
||||
log.disableColor();
|
||||
var util = require('util');
|
||||
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var mongodb = require('mongodb');
|
||||
|
||||
var Model = require('./model');
|
||||
|
@ -25,6 +25,7 @@ var collections = {
|
|||
TX_NOTES: 'tx_notes',
|
||||
SESSIONS: 'sessions',
|
||||
PUSH_NOTIFICATION_SUBS: 'push_notification_subs',
|
||||
TX_CONFIRMATION_SUBS: 'tx_confirmation_subs',
|
||||
};
|
||||
|
||||
var Storage = function(opts) {
|
||||
|
@ -39,6 +40,9 @@ Storage.prototype._createIndexes = function() {
|
|||
this.db.collection(collections.COPAYERS_LOOKUP).createIndex({
|
||||
copayerId: 1
|
||||
});
|
||||
this.db.collection(collections.COPAYERS_LOOKUP).createIndex({
|
||||
walletId: 1
|
||||
});
|
||||
this.db.collection(collections.TXS).createIndex({
|
||||
walletId: 1,
|
||||
id: 1,
|
||||
|
@ -52,6 +56,9 @@ Storage.prototype._createIndexes = function() {
|
|||
walletId: 1,
|
||||
createdOn: -1,
|
||||
});
|
||||
this.db.collection(collections.TXS).createIndex({
|
||||
txid: 1
|
||||
});
|
||||
this.db.collection(collections.NOTIFICATIONS).createIndex({
|
||||
walletId: 1,
|
||||
id: 1,
|
||||
|
@ -63,6 +70,13 @@ Storage.prototype._createIndexes = function() {
|
|||
this.db.collection(collections.ADDRESSES).createIndex({
|
||||
address: 1,
|
||||
});
|
||||
this.db.collection(collections.ADDRESSES).createIndex({
|
||||
walletId: 1,
|
||||
address: 1,
|
||||
});
|
||||
this.db.collection(collections.EMAIL_QUEUE).createIndex({
|
||||
id: 1,
|
||||
});
|
||||
this.db.collection(collections.EMAIL_QUEUE).createIndex({
|
||||
notificationId: 1,
|
||||
});
|
||||
|
@ -75,9 +89,24 @@ Storage.prototype._createIndexes = function() {
|
|||
walletId: 1,
|
||||
txid: 1,
|
||||
});
|
||||
this.db.collection(collections.PREFERENCES).createIndex({
|
||||
walletId: 1
|
||||
});
|
||||
this.db.collection(collections.FIAT_RATES).createIndex({
|
||||
provider: 1,
|
||||
code: 1,
|
||||
ts: 1
|
||||
});
|
||||
this.db.collection(collections.PUSH_NOTIFICATION_SUBS).createIndex({
|
||||
copayerId: 1,
|
||||
});
|
||||
this.db.collection(collections.TX_CONFIRMATION_SUBS).createIndex({
|
||||
copayerId: 1,
|
||||
txid: 1,
|
||||
});
|
||||
this.db.collection(collections.SESSIONS).createIndex({
|
||||
copayerId: 1
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.connect = function(opts, cb) {
|
||||
|
@ -111,6 +140,8 @@ Storage.prototype.disconnect = function(cb) {
|
|||
};
|
||||
|
||||
Storage.prototype.fetchWallet = function(id, cb) {
|
||||
if (!this.db) return cb('not ready');
|
||||
|
||||
this.db.collection(collections.WALLETS).findOne({
|
||||
id: id
|
||||
}, function(err, result) {
|
||||
|
@ -199,6 +230,7 @@ Storage.prototype._completeTxData = function(walletId, txs, cb) {
|
|||
// TODO: remove walletId from signature
|
||||
Storage.prototype.fetchTx = function(walletId, txProposalId, cb) {
|
||||
var self = this;
|
||||
if (!this.db) return cb();
|
||||
|
||||
this.db.collection(collections.TXS).findOne({
|
||||
id: txProposalId,
|
||||
|
@ -212,6 +244,7 @@ Storage.prototype.fetchTx = function(walletId, txProposalId, cb) {
|
|||
|
||||
Storage.prototype.fetchTxByHash = function(hash, cb) {
|
||||
var self = this;
|
||||
if (!this.db) return cb();
|
||||
|
||||
this.db.collection(collections.TXS).findOne({
|
||||
txid: hash,
|
||||
|
@ -443,6 +476,28 @@ Storage.prototype.fetchAddresses = function(walletId, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype.fetchNewAddresses = function(walletId, fromTs, cb) {
|
||||
var self = this;
|
||||
|
||||
this.db.collection(collections.ADDRESSES).find({
|
||||
walletId: walletId,
|
||||
createdOn: {
|
||||
$gte: fromTs,
|
||||
},
|
||||
}).sort({
|
||||
createdOn: 1
|
||||
}).toArray(function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (!result) return cb();
|
||||
var addresses = _.map(result, function(address) {
|
||||
return Model.Address.fromObj(address);
|
||||
});
|
||||
return cb(null, addresses);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype.countAddresses = function(walletId, cb) {
|
||||
this.db.collection(collections.ADDRESSES).find({
|
||||
walletId: walletId,
|
||||
|
@ -453,6 +508,7 @@ Storage.prototype.storeAddress = function(address, cb) {
|
|||
var self = this;
|
||||
|
||||
self.db.collection(collections.ADDRESSES).update({
|
||||
walletId: address.walletId,
|
||||
address: address.address
|
||||
}, address, {
|
||||
w: 1,
|
||||
|
@ -464,37 +520,21 @@ Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
|
|||
var self = this;
|
||||
|
||||
var addresses = [].concat(addresses);
|
||||
if (addresses.length == 0) return cb();
|
||||
if (_.isEmpty(addresses)) return cb();
|
||||
|
||||
async.filter(addresses, function(address, next) {
|
||||
self.db.collection(collections.ADDRESSES).findOne({
|
||||
address: address.address,
|
||||
}, {
|
||||
walletId: true,
|
||||
}, function(err, result) {
|
||||
if (err || !result) return next(true);
|
||||
if (result.walletId != wallet.id) {
|
||||
log.warn('Address ' + address.address + ' exists in more than one wallet.');
|
||||
return next(true);
|
||||
}
|
||||
// Ignore if address was already in wallet
|
||||
return next(false);
|
||||
});
|
||||
}, function(newAddresses) {
|
||||
if (newAddresses.length == 0) return cb();
|
||||
self.db.collection(collections.ADDRESSES).insert(newAddresses, {
|
||||
self.db.collection(collections.ADDRESSES).insert(addresses, {
|
||||
w: 1
|
||||
}, function(err) {
|
||||
if (err) return cb(err);
|
||||
self.storeWallet(wallet, cb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.fetchAddress = function(address, cb) {
|
||||
Storage.prototype.fetchAddressByWalletId = function(walletId, address, cb) {
|
||||
var self = this;
|
||||
|
||||
this.db.collection(collections.ADDRESSES).findOne({
|
||||
walletId: walletId,
|
||||
address: address,
|
||||
}, function(err, result) {
|
||||
if (err) return cb(err);
|
||||
|
@ -504,6 +544,28 @@ Storage.prototype.fetchAddress = function(address, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Storage.prototype.fetchAddressByCoin = function(coin, address, cb) {
|
||||
var self = this;
|
||||
if (!this.db) return cb();
|
||||
|
||||
this.db.collection(collections.ADDRESSES).find({
|
||||
address: address,
|
||||
}).toArray(function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (!result || _.isEmpty(result)) return cb();
|
||||
if (result.length > 1) {
|
||||
result = _.find(result, function(address) {
|
||||
return coin == (address.coin || 'btc');
|
||||
});
|
||||
} else {
|
||||
result = _.first(result);
|
||||
}
|
||||
if (!result) return cb();
|
||||
|
||||
return cb(null, Model.Address.fromObj(result));
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) {
|
||||
this.db.collection(collections.PREFERENCES).find({
|
||||
walletId: walletId,
|
||||
|
@ -567,51 +629,91 @@ Storage.prototype.fetchEmailByNotification = function(notificationId, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Storage.prototype.cleanActiveAddresses = function(walletId, cb) {
|
||||
Storage.prototype.storeTwoStepCache = function(walletId, cacheStatus, cb) {
|
||||
var self = this;
|
||||
|
||||
async.series([
|
||||
|
||||
function(next) {
|
||||
self.db.collection(collections.CACHE).remove({
|
||||
self.db.collection(collections.CACHE).update( {
|
||||
walletId: walletId,
|
||||
type: 'activeAddresses',
|
||||
type: 'twoStep',
|
||||
key: null,
|
||||
}, {
|
||||
w: 1
|
||||
}, next);
|
||||
},
|
||||
function(next) {
|
||||
self.db.collection(collections.CACHE).insert({
|
||||
walletId: walletId,
|
||||
type: 'activeAddresses',
|
||||
key: null
|
||||
"$set":
|
||||
{
|
||||
addressCount: cacheStatus.addressCount,
|
||||
lastEmpty: cacheStatus.lastEmpty,
|
||||
}
|
||||
}, {
|
||||
w: 1
|
||||
}, next);
|
||||
},
|
||||
], cb);
|
||||
};
|
||||
|
||||
Storage.prototype.storeActiveAddresses = function(walletId, addresses, cb) {
|
||||
var self = this;
|
||||
|
||||
async.each(addresses, function(address, next) {
|
||||
var record = {
|
||||
walletId: walletId,
|
||||
type: 'activeAddresses',
|
||||
key: address,
|
||||
};
|
||||
self.db.collection(collections.CACHE).update({
|
||||
walletId: record.walletId,
|
||||
type: record.type,
|
||||
key: record.key,
|
||||
}, record, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, next);
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype.getTwoStepCache = function(walletId, cb) {
|
||||
var self = this;
|
||||
|
||||
|
||||
self.db.collection(collections.CACHE).findOne({
|
||||
walletId: walletId,
|
||||
type: 'twoStep',
|
||||
key: null
|
||||
}, function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (!result) return cb();
|
||||
return cb(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype.storeAddressesWithBalance = function(walletId, addresses, cb) {
|
||||
var self = this;
|
||||
|
||||
if (_.isEmpty(addresses))
|
||||
addresses = [];
|
||||
|
||||
self.db.collection(collections.CACHE).update({
|
||||
walletId: walletId,
|
||||
type: 'addressesWithBalance',
|
||||
key: null,
|
||||
}, {
|
||||
"$set":
|
||||
{
|
||||
addresses: addresses,
|
||||
}
|
||||
}, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype.fetchAddressesWithBalance = function(walletId, cb) {
|
||||
var self = this;
|
||||
|
||||
|
||||
self.db.collection(collections.CACHE).findOne({
|
||||
walletId: walletId,
|
||||
type: 'addressesWithBalance',
|
||||
key: null,
|
||||
}, function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (_.isEmpty(result)) return cb(null, []);
|
||||
|
||||
|
||||
self.db.collection(collections.ADDRESSES).find({
|
||||
walletId: walletId,
|
||||
address: { $in: result.addresses },
|
||||
}).toArray(function(err, result2) {
|
||||
if (err) return cb(err);
|
||||
if (!result2) return cb(null, []);
|
||||
|
||||
var addresses = _.map(result2, function(address) {
|
||||
return Model.Address.fromObj(address);
|
||||
});
|
||||
return cb(null, addresses);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// -------- --------------------------- Total
|
||||
// > Time >
|
||||
// ^to <= ^from
|
||||
|
@ -662,7 +764,7 @@ Storage.prototype.getTxHistoryCache = function(walletId, from, to, cb) {
|
|||
return cb();
|
||||
}
|
||||
|
||||
var txs = _.pluck(result, 'tx');
|
||||
var txs = _.map(result, 'tx');
|
||||
return cb(null, txs);
|
||||
});
|
||||
})
|
||||
|
@ -764,23 +866,6 @@ Storage.prototype.storeTxHistoryCache = function(walletId, totalItems, firstPosi
|
|||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Storage.prototype.fetchActiveAddresses = function(walletId, cb) {
|
||||
var self = this;
|
||||
|
||||
self.db.collection(collections.CACHE).find({
|
||||
walletId: walletId,
|
||||
type: 'activeAddresses',
|
||||
}).toArray(function(err, result) {
|
||||
if (err) return cb(err);
|
||||
if (_.isEmpty(result)) return cb();
|
||||
|
||||
return cb(null, _.compact(_.pluck(result, 'key')));
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.storeFiatRate = function(providerName, rates, cb) {
|
||||
var self = this;
|
||||
|
||||
|
@ -928,6 +1013,45 @@ Storage.prototype.removePushNotificationSub = function(copayerId, token, cb) {
|
|||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype.fetchActiveTxConfirmationSubs = function(copayerId, cb) {
|
||||
var filter = {
|
||||
isActive: true
|
||||
};
|
||||
if (copayerId) filter.copayerId = copayerId;
|
||||
|
||||
this.db.collection(collections.TX_CONFIRMATION_SUBS).find(filter)
|
||||
.toArray(function(err, result) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (!result) return cb();
|
||||
|
||||
var subs = _.map([].concat(result), function(r) {
|
||||
return Model.TxConfirmationSub.fromObj(r);
|
||||
});
|
||||
return cb(null, subs);
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.storeTxConfirmationSub = function(txConfirmationSub, cb) {
|
||||
this.db.collection(collections.TX_CONFIRMATION_SUBS).update({
|
||||
copayerId: txConfirmationSub.copayerId,
|
||||
txid: txConfirmationSub.txid,
|
||||
}, txConfirmationSub, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype.removeTxConfirmationSub = function(copayerId, txid, cb) {
|
||||
this.db.collection(collections.TX_CONFIRMATION_SUBS).remove({
|
||||
copayerId: copayerId,
|
||||
txid: txid,
|
||||
}, {
|
||||
w: 1
|
||||
}, cb);
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype._dump = function(cb, fn) {
|
||||
fn = fn || console.log;
|
||||
cb = cb || function() {};
|
||||
|
@ -946,5 +1070,140 @@ Storage.prototype._dump = function(cb, fn) {
|
|||
});
|
||||
};
|
||||
|
||||
Storage.prototype.fetchAddressIndexCache = function (walletId, key, cb) {
|
||||
this.db.collection(collections.CACHE).findOne({
|
||||
walletId: walletId,
|
||||
type: 'addressIndexCache',
|
||||
key: key,
|
||||
}, function(err, ret) {
|
||||
if (err) return cb(err);
|
||||
if (!ret) return cb();
|
||||
cb(null, ret.index);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
Storage.prototype.storeAddressIndexCache = function (walletId, key, index, cb) {
|
||||
this.db.collection(collections.CACHE).update({
|
||||
walletId: walletId,
|
||||
type: 'addressIndexCache',
|
||||
key: key,
|
||||
}, {
|
||||
"$set":
|
||||
{
|
||||
index: index,
|
||||
}
|
||||
}, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype._addressHash = function(addresses) {
|
||||
var all = addresses.join();
|
||||
return Bitcore.crypto.Hash.ripemd160(new Buffer(all)).toString('hex');
|
||||
};
|
||||
|
||||
Storage.prototype.checkAndUseBalanceCache = function(walletId, addresses, duration, cb) {
|
||||
var self = this;
|
||||
var key = self._addressHash(addresses);
|
||||
var now = Date.now();
|
||||
|
||||
|
||||
self.db.collection(collections.CACHE).findOne({
|
||||
walletId: walletId || key,
|
||||
type: 'balanceCache',
|
||||
key: key,
|
||||
}, function(err, ret) {
|
||||
if (err) return cb(err);
|
||||
if (!ret) return cb();
|
||||
|
||||
var validFor = ret.ts + duration * 1000 - now;
|
||||
|
||||
if (validFor > 0) {
|
||||
log.debug('','Using Balance Cache valid for %d ms more', validFor);
|
||||
cb(null, ret.result);
|
||||
return true;
|
||||
}
|
||||
cb();
|
||||
|
||||
log.debug('','Balance cache expired, deleting');
|
||||
self.db.collection(collections.CACHE).remove({
|
||||
walletId: walletId,
|
||||
type: 'balanceCache',
|
||||
key: key,
|
||||
}, {}, function() {});
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
Storage.prototype.storeBalanceCache = function (walletId, addresses, balance, cb) {
|
||||
var key = this._addressHash(addresses);
|
||||
var now = Date.now();
|
||||
|
||||
this.db.collection(collections.CACHE).update({
|
||||
walletId: walletId || key,
|
||||
type: 'balanceCache',
|
||||
key: key,
|
||||
}, {
|
||||
"$set":
|
||||
{
|
||||
ts: now,
|
||||
result: balance,
|
||||
}
|
||||
}, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
// FEE_LEVEL_DURATION = 5min
|
||||
var FEE_LEVEL_DURATION = 5 * 60 * 1000;
|
||||
Storage.prototype.checkAndUseFeeLevelsCache = function(opts, cb) {
|
||||
var self = this;
|
||||
var key = JSON.stringify(opts);
|
||||
var now = Date.now();
|
||||
|
||||
self.db.collection(collections.CACHE).findOne({
|
||||
walletId: null,
|
||||
type: 'feeLevels',
|
||||
key: key,
|
||||
}, function(err, ret) {
|
||||
if (err) return cb(err);
|
||||
if (!ret) return cb();
|
||||
|
||||
var validFor = ret.ts + FEE_LEVEL_DURATION - now;
|
||||
return cb(null, validFor > 0 ? ret.result : null);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Storage.prototype.storeFeeLevelsCache = function (opts, values, cb) {
|
||||
var key = JSON.stringify(opts);
|
||||
var now = Date.now();
|
||||
this.db.collection(collections.CACHE).update({
|
||||
walletId: null,
|
||||
type: 'feeLevels',
|
||||
key: key,
|
||||
}, {
|
||||
"$set":
|
||||
{
|
||||
ts: now,
|
||||
result: values,
|
||||
}
|
||||
}, {
|
||||
w: 1,
|
||||
upsert: true,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Storage.collections = collections;
|
||||
module.exports = Storage;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
{{subjectPrefix}} Transaction confirmed
|
||||
The transaction you were waiting for has been confirmed.
|
|
@ -0,0 +1,2 @@
|
|||
{{subjectPrefix}} Transacción confirmada
|
||||
La transacción que estabas esperando se ha confirmado.
|
25
package.json
25
package.json
|
@ -2,7 +2,7 @@
|
|||
"name": "bitcore-wallet-service",
|
||||
"description": "A service for Mutisig HD Bitcoin Wallets",
|
||||
"author": "BitPay Inc",
|
||||
"version": "1.15.0",
|
||||
"version": "2.4.0",
|
||||
"licence": "MIT",
|
||||
"keywords": [
|
||||
"bitcoin",
|
||||
|
@ -13,15 +13,16 @@
|
|||
"BWS"
|
||||
],
|
||||
"repository": {
|
||||
"url": "git@github.com:bitpay/bitcore-wallet-service.git",
|
||||
"url": "git@github.com:BTCPrivate/bitcore-wallet-service.git",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/bitpay/bitcore-wallet-service/issues"
|
||||
"url": "https://github.com/BTCPrivate/bitcore-wallet-service/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^0.9.2",
|
||||
"bitcore-lib": "^0.14.0",
|
||||
"bitcore-lib": "ch4ot1c/bitcore-lib",
|
||||
"bitcore-lib-cash": "^0.17.0",
|
||||
"body-parser": "^1.11.0",
|
||||
"compression": "^1.6.2",
|
||||
"coveralls": "^2.11.2",
|
||||
|
@ -54,13 +55,13 @@
|
|||
"devDependencies": {
|
||||
"chai": "^1.9.1",
|
||||
"istanbul": "*",
|
||||
"jsdoc": "^3.3.0-beta1",
|
||||
"jsdoc": "^3.5.5",
|
||||
"memdown": "^1.0.0",
|
||||
"mocha": "^1.18.2",
|
||||
"proxyquire": "^1.7.2",
|
||||
"sinon": "1.10.3",
|
||||
"supertest": "*",
|
||||
"tingodb": "^0.3.4"
|
||||
"tingodb": "^0.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./start.sh",
|
||||
|
@ -70,14 +71,18 @@
|
|||
"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"
|
||||
},
|
||||
"bitcoreNode": "./bitcorenode",
|
||||
"contributors": [{
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Braydon Fuller",
|
||||
"email": "braydon@bitpay.com"
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"name": "Ivan Socolsky",
|
||||
"email": "ivan@bitpay.com"
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"name": "Matias Alejo Garcia",
|
||||
"email": "ematiu@gmail.com"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
// json support
|
||||
a='f42c8c47-0f7e-4cb0-9056-6c50bb821d7d';
|
||||
b= {'walletId':a};
|
||||
|
||||
db.addresses.remove(b);
|
||||
db.cache.remove(b);
|
||||
db.copayers_lookup.remove(b);
|
||||
db.notifications.remove(b);
|
||||
db.preferences.remove(b);
|
||||
db.sessions.remove(b);
|
||||
db.tx_confirmation_subs.remove(b);
|
||||
db.txs.remove(b);
|
||||
db.tx_notes.remove(b);
|
||||
db.wallets.remove({'id':a});
|
||||
|
||||
db.push_notification_subs.remove(b);
|
||||
db.email_queue.remove(b);
|
|
@ -13,7 +13,6 @@ log.level = 'info';
|
|||
var WalletService = require('../../lib/server');
|
||||
var BlockchainMonitor = require('../../lib/blockchainmonitor');
|
||||
|
||||
var TestData = require('../testdata');
|
||||
var helpers = require('./helpers');
|
||||
var storage, blockchainExplorer;
|
||||
|
||||
|
@ -49,8 +48,10 @@ describe('Blockchain monitor', function() {
|
|||
messageBroker: server.messageBroker,
|
||||
storage: storage,
|
||||
blockchainExplorers: {
|
||||
'btc': {
|
||||
'testnet': blockchainExplorer,
|
||||
'livenet': blockchainExplorer
|
||||
}
|
||||
},
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
|
@ -87,4 +88,178 @@ describe('Blockchain monitor', function() {
|
|||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update addressWithBalance cache on 1 incoming tx', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
var incoming = {
|
||||
txid: '123',
|
||||
vout: [{}],
|
||||
};
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
_.isEmpty(ret).should.equal(true);
|
||||
incoming.vout[0][address.address] = 1500;
|
||||
socket.handlers['tx'](incoming);
|
||||
setTimeout(function() {
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
ret.length.should.equal(1);
|
||||
ret[0].address.should.equal(address.address);
|
||||
done();
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update addressWithBalance cache on 2 incoming tx, same address', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
_.isEmpty(ret).should.equal(true);
|
||||
|
||||
var incoming = {
|
||||
txid: '123',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming.vout[0][address.address] = 1500;
|
||||
|
||||
socket.handlers['tx'](incoming);
|
||||
setTimeout(function() {
|
||||
|
||||
|
||||
var incoming2 = {
|
||||
txid: '456',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming2.vout[0][address.address] = 2500;
|
||||
socket.handlers['tx'](incoming2);
|
||||
|
||||
setTimeout(function() {
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
ret.length.should.equal(1);
|
||||
ret[0].address.should.equal(address.address);
|
||||
done();
|
||||
});
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should update addressWithBalance cache on 2 incoming tx, different address', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
server.createAddress({}, function(err, address2) {
|
||||
should.not.exist(err);
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
_.isEmpty(ret).should.equal(true);
|
||||
|
||||
var incoming = {
|
||||
txid: '123',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming.vout[0][address.address] = 1500;
|
||||
socket.handlers['tx'](incoming);
|
||||
|
||||
setTimeout(function() {
|
||||
|
||||
var incoming2 = {
|
||||
txid: '456',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming2.vout[0][address2.address] = 500;
|
||||
socket.handlers['tx'](incoming2);
|
||||
|
||||
setTimeout(function() {
|
||||
server.storage.fetchAddressesWithBalance(wallet.id, function(err,ret) {
|
||||
should.not.exist(err);
|
||||
ret.length.should.equal(2);
|
||||
ret[0].address.should.equal(address.address);
|
||||
done();
|
||||
});
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
it('should not notify copayers of incoming txs more than once', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
|
||||
var incoming = {
|
||||
txid: '123',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming.vout[0][address.address] = 1500;
|
||||
socket.handlers['tx'](incoming);
|
||||
setTimeout(function() {
|
||||
socket.handlers['tx'](incoming);
|
||||
|
||||
setTimeout(function() {
|
||||
server.getNotifications({}, function(err, notifications) {
|
||||
should.not.exist(err);
|
||||
var notification = _.filter(notifications, {
|
||||
type: 'NewIncomingTx'
|
||||
});
|
||||
notification.length.should.equal(1);
|
||||
done();
|
||||
});
|
||||
}, 100);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
it('should notify copayers of tx confirmation', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
|
||||
var incoming = {
|
||||
txid: '123',
|
||||
vout: [{}],
|
||||
};
|
||||
incoming.vout[0][address.address] = 1500;
|
||||
|
||||
server.txConfirmationSubscribe({
|
||||
txid: '123'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
|
||||
blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']);
|
||||
socket.handlers['block']('block1');
|
||||
|
||||
setTimeout(function() {
|
||||
blockchainExplorer.getTxidsInBlock = sinon.stub().callsArgWith(1, null, ['123', '456']);
|
||||
socket.handlers['block']('block2');
|
||||
|
||||
setTimeout(function() {
|
||||
server.getNotifications({}, function(err, notifications) {
|
||||
should.not.exist(err);
|
||||
var notifications = _.filter(notifications, {
|
||||
type: 'TxConfirmation'
|
||||
});
|
||||
notifications.length.should.equal(1);
|
||||
var n = notifications[0];
|
||||
n.walletId.should.equal(wallet.id);
|
||||
n.creatorId.should.equal(server.copayerId);
|
||||
n.data.txid.should.equal('123');
|
||||
done();
|
||||
});
|
||||
}, 50);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -169,7 +169,7 @@ describe('Email notifications', function() {
|
|||
txp = t;
|
||||
async.eachSeries(_.range(2), function(i, next) {
|
||||
var copayer = TestData.copayers[i];
|
||||
helpers.getAuthServer(copayer.id44, function(server) {
|
||||
helpers.getAuthServer(copayer.id44btc, function(server) {
|
||||
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H);
|
||||
server.signTx({
|
||||
txProposalId: txp.id,
|
||||
|
@ -235,7 +235,7 @@ describe('Email notifications', function() {
|
|||
txpId = txp.id;
|
||||
async.eachSeries(_.range(1, 3), function(i, next) {
|
||||
var copayer = TestData.copayers[i];
|
||||
helpers.getAuthServer(copayer.id44, function(server) {
|
||||
helpers.getAuthServer(copayer.id44btc, function(server) {
|
||||
server.rejectTx({
|
||||
txProposalId: txp.id,
|
||||
}, next);
|
||||
|
@ -295,6 +295,38 @@ describe('Email notifications', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('should notify copayers when tx is confirmed if they are subscribed', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
|
||||
server.txConfirmationSubscribe({
|
||||
txid: '123'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
|
||||
// Simulate tx confirmation notification
|
||||
server._notify('TxConfirmation', {
|
||||
txid: '123',
|
||||
}, function(err) {
|
||||
setTimeout(function() {
|
||||
var calls = mailerStub.sendMail.getCalls();
|
||||
calls.length.should.equal(1);
|
||||
var email = calls[0].args[0];
|
||||
email.to.should.equal('copayer1@domain.com');
|
||||
email.from.should.equal('bws@dummy.net');
|
||||
email.subject.should.contain('Transaction confirmed');
|
||||
server.storage.fetchUnsentEmails(function(err, unsent) {
|
||||
should.not.exist(err);
|
||||
unsent.should.be.empty;
|
||||
done();
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should notify each email address only once', function(done) {
|
||||
// Set same email address for copayer1 and copayer2
|
||||
server.savePreferences({
|
||||
|
|
|
@ -13,6 +13,10 @@ var tingodb = require('tingodb')({
|
|||
});
|
||||
|
||||
var Bitcore = require('bitcore-lib');
|
||||
var Bitcore_ = {
|
||||
btc: Bitcore,
|
||||
bch: require('bitcore-lib-cash')
|
||||
};
|
||||
|
||||
var Common = require('../../lib/common');
|
||||
var Utils = Common.Utils;
|
||||
|
@ -95,6 +99,7 @@ helpers.signRequestPubKey = function(requestPubKey, xPrivKey) {
|
|||
helpers.getAuthServer = function(copayerId, cb) {
|
||||
var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature');
|
||||
verifyStub.returns(true);
|
||||
|
||||
WalletService.getInstanceWithAuth({
|
||||
copayerId: copayerId,
|
||||
message: 'dummy',
|
||||
|
@ -107,26 +112,40 @@ helpers.getAuthServer = function(copayerId, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
helpers._generateCopayersTestData = function(n) {
|
||||
helpers._generateCopayersTestData = function() {
|
||||
var xPrivKeys = ['xprv9s21ZrQH143K2n4rV4AtAJFptEmd1tNMKCcSyQBCSuN5eq1dCUhcv6KQJS49joRxu8NNdFxy8yuwTtzCPNYUZvVGC7EPRm2st2cvE7oyTbB',
|
||||
'xprv9s21ZrQH143K3BwkLceWNLUsgES15JoZuv8BZfnmDRcCGtDooUAPhY8KovhCWcRLXUun5AYL5vVtUNRrmPEibtfk9ongxAGLXZzEHifpvwZ',
|
||||
'xprv9s21ZrQH143K3xgLzxd6SuWqG5Zp1iUmyGgSsJVhdQNeTzAqBFvXXLZqZzFZqocTx4HD9vUVYU27At5i8q46LmBXXL97fo4H9C3tHm4BnjY',
|
||||
'xprv9s21ZrQH143K48nfuK14gKJtML7eQzV2dAH1RaqAMj8v2zs79uaavA9UTWMxpBdgbMH2mhJLeKGq8AFA6GDnFyWP4rLmknqZAfgFFV718vo',
|
||||
'xprv9s21ZrQH143K44Bb9G3EVNmLfAUKjTBAA2YtKxF4zc8SLV1o15JBoddhGHE9PGLXePMbEsSjCCvTvP3fUv6yMXZrnHigBboRBn2DmNoJkJg',
|
||||
'xprv9s21ZrQH143K48PpVxrh71KdViTFhAaiDSVtNFkmbWNYjwwwPbTrcqoVXsgBfue3Gq9b71hQeEbk67JgtTBcpYgKLF8pTwVnGz56f1BaCYt',
|
||||
'xprv9s21ZrQH143K3pgRcRBRnmcxNkNNLmJrpneMkEXY6o5TWBuJLMfdRpAWdb2cG3yxbL4DxfpUnQpjfQUmwPdVrRGoDJmtAf5u8cyqKCoDV97',
|
||||
'xprv9s21ZrQH143K3nvcmdjDDDZbDJHpfWZCUiunwraZdcamYcafHvUnZfV51fivH9FPyfo12NyKH5JDxGLsQePyWKtTiJx3pkEaiwxsMLkVapp',
|
||||
'xprv9s21ZrQH143K2uYgqtYtphEQkFAgiWSqahFUWjgCdKykJagiNDz6Lf7xRVQdtZ7MvkhX9V3pEcK3xTAWZ6Y6ecJqrXnCpzrH9GSHn8wyrT5',
|
||||
'xprv9s21ZrQH143K2wcRMP75tAEL5JnUx4xU2AbUBQzVVUDP7DHZJkjF3kaRE7tcnPLLLL9PGjYTWTJmCQPaQ4GGzgWEUFJ6snwJG9YnQHBFRNR'
|
||||
];
|
||||
|
||||
console.log('var copayers = [');
|
||||
_.each(_.range(n), function(c) {
|
||||
var xpriv = new Bitcore.HDPrivateKey();
|
||||
_.each(xPrivKeys, function(xPrivKeyStr, c) {
|
||||
var xpriv = Bitcore.HDPrivateKey(xPrivKeyStr);
|
||||
var xpub = Bitcore.HDPublicKey(xpriv);
|
||||
|
||||
var xpriv_45H = xpriv.deriveChild(45, true);
|
||||
var xpub_45H = Bitcore.HDPublicKey(xpriv_45H);
|
||||
var id45 = Copayer._xPubToCopayerId(xpub_45H.toString());
|
||||
var id45 = Model.Copayer._xPubToCopayerId('btc', xpub_45H.toString());
|
||||
|
||||
var xpriv_44H_0H_0H = xpriv.deriveChild(44, true).deriveChild(0, true).deriveChild(0, true);
|
||||
var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H);
|
||||
var id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString());
|
||||
var id44btc = Model.Copayer._xPubToCopayerId('btc', xpub_44H_0H_0H.toString());
|
||||
var id44bch = Model.Copayer._xPubToCopayerId('bch', xpub_44H_0H_0H.toString());
|
||||
|
||||
var xpriv_1H = xpriv.deriveChild(1, true);
|
||||
var xpub_1H = Bitcore.HDPublicKey(xpriv_1H);
|
||||
var priv = xpriv_1H.deriveChild(0).privateKey;
|
||||
var pub = xpub_1H.deriveChild(0).publicKey;
|
||||
|
||||
console.log('{id44: ', "'" + id44 + "',");
|
||||
console.log('{id44btc: ', "'" + id44btc + "',");
|
||||
console.log('id44bch: ', "'" + id44bch + "',");
|
||||
console.log('id45: ', "'" + id45 + "',");
|
||||
console.log('xPrivKey: ', "'" + xpriv.toString() + "',");
|
||||
console.log('xPubKey: ', "'" + xpub.toString() + "',");
|
||||
|
@ -165,6 +184,8 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
|
|||
n: n,
|
||||
pubKey: TestData.keyPair.pub,
|
||||
singleAddress: !!opts.singleAddress,
|
||||
coin: opts.coin || 'btc',
|
||||
network: opts.network || 'livenet',
|
||||
};
|
||||
if (_.isBoolean(opts.supportBIP44AndP2PKH))
|
||||
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
|
||||
|
@ -174,10 +195,18 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
|
|||
|
||||
async.each(_.range(n), function(i, cb) {
|
||||
var copayerData = TestData.copayers[i + offset];
|
||||
|
||||
|
||||
var pub = (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H;
|
||||
|
||||
if (opts.network == 'testnet')
|
||||
pub = copayerData.xPubKey_44H_0H_0Ht;
|
||||
|
||||
var copayerOpts = helpers.getSignedCopayerOpts({
|
||||
walletId: walletId,
|
||||
coin: opts.coin,
|
||||
name: 'copayer ' + (i + 1),
|
||||
xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H,
|
||||
xPubKey: pub,
|
||||
requestPubKey: copayerData.pubKey_1H_0,
|
||||
customData: 'custom data ' + (i + 1),
|
||||
});
|
||||
|
@ -185,6 +214,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
|
|||
copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
|
||||
|
||||
server.joinWallet(copayerOpts, function(err, result) {
|
||||
if (err) console.log(err);
|
||||
should.not.exist(err);
|
||||
copayerIds.push(result.copayerId);
|
||||
return cb(err);
|
||||
|
@ -256,6 +286,8 @@ helpers.stubUtxos = function(server, wallet, amounts, opts, cb) {
|
|||
|
||||
if (!helpers._utxos) helpers._utxos = {};
|
||||
|
||||
var S = Bitcore_[wallet.coin].Script;
|
||||
|
||||
async.waterfall([
|
||||
|
||||
function(next) {
|
||||
|
@ -277,10 +309,10 @@ helpers.stubUtxos = function(server, wallet, amounts, opts, cb) {
|
|||
var scriptPubKey;
|
||||
switch (wallet.addressType) {
|
||||
case Constants.SCRIPT_TYPES.P2SH:
|
||||
scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut();
|
||||
scriptPubKey = S.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut();
|
||||
break;
|
||||
case Constants.SCRIPT_TYPES.P2PKH:
|
||||
scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address);
|
||||
scriptPubKey = S.buildPublicKeyHashOut(address.address);
|
||||
break;
|
||||
}
|
||||
should.exist(scriptPubKey);
|
||||
|
@ -357,8 +389,22 @@ helpers.stubFeeLevels = function(levels) {
|
|||
};
|
||||
};
|
||||
|
||||
helpers.stubAddressActivity = function(activeAddresses) {
|
||||
|
||||
var stubAddressActivityFailsOn = null;
|
||||
var stubAddressActivityFailsOnCount=1;
|
||||
helpers.stubAddressActivity = function(activeAddresses, failsOn) {
|
||||
|
||||
stubAddressActivityFailsOnCount=1;
|
||||
|
||||
// could be null
|
||||
stubAddressActivityFailsOn = failsOn;
|
||||
|
||||
blockchainExplorer.getAddressActivity = function(address, cb) {
|
||||
if (stubAddressActivityFailsOnCount === stubAddressActivityFailsOn)
|
||||
return cb('failed on request');
|
||||
|
||||
stubAddressActivityFailsOnCount++;
|
||||
|
||||
return cb(null, _.contains(activeAddresses, address));
|
||||
};
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -110,6 +110,7 @@ describe('Push notifications', function() {
|
|||
calls.length.should.equal(1);
|
||||
args[0].body.notification.title.should.contain('New payment received');
|
||||
args[0].body.notification.body.should.contain('123,000');
|
||||
args[0].body.notification.body.should.contain('bits');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
@ -158,6 +159,29 @@ describe('Push notifications', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should notify copayers when tx is confirmed if they are subscribed', function(done) {
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
|
||||
server.txConfirmationSubscribe({
|
||||
txid: '123'
|
||||
}, function(err) {
|
||||
should.not.exist(err);
|
||||
|
||||
// Simulate tx confirmation notification
|
||||
server._notify('TxConfirmation', {
|
||||
txid: '123',
|
||||
}, function(err) {
|
||||
setTimeout(function() {
|
||||
var calls = requestStub.getCalls();
|
||||
calls.length.should.equal(1);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shared wallet', function() {
|
||||
|
@ -345,7 +369,7 @@ describe('Push notifications', function() {
|
|||
txpId = txp.id;
|
||||
async.eachSeries(_.range(1, 3), function(i, next) {
|
||||
var copayer = TestData.copayers[i];
|
||||
helpers.getAuthServer(copayer.id44, function(server) {
|
||||
helpers.getAuthServer(copayer.id44btc, function(server) {
|
||||
server.rejectTx({
|
||||
txProposalId: txp.id,
|
||||
}, next);
|
||||
|
@ -390,7 +414,7 @@ describe('Push notifications', function() {
|
|||
txp = t;
|
||||
async.eachSeries(_.range(1, 3), function(i, next) {
|
||||
var copayer = TestData.copayers[i];
|
||||
helpers.getAuthServer(copayer.id44, function(s) {
|
||||
helpers.getAuthServer(copayer.id44btc, function(s) {
|
||||
server = s;
|
||||
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H);
|
||||
server.signTx({
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@ describe('Address', function() {
|
|||
it('should create livenet address', function() {
|
||||
var x = Address.create({
|
||||
address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg',
|
||||
coin: 'btc',
|
||||
walletId: '123',
|
||||
isChange: false,
|
||||
path: 'm/0/1',
|
||||
|
@ -23,6 +24,7 @@ describe('Address', function() {
|
|||
it('should create testnet address', function() {
|
||||
var x = Address.create({
|
||||
address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj',
|
||||
coin: 'btc',
|
||||
walletId: '123',
|
||||
isChange: false,
|
||||
path: 'm/0/1',
|
||||
|
@ -39,7 +41,7 @@ describe('Address', function() {
|
|||
}, {
|
||||
xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY'
|
||||
// PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171
|
||||
}], 'm/0/0', 1, 'livenet', false);
|
||||
}], 'm/0/0', 1, 'btc', 'livenet', false);
|
||||
should.exist(address);
|
||||
address.walletId.should.equal('wallet-id');
|
||||
address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw');
|
||||
|
@ -52,7 +54,7 @@ describe('Address', function() {
|
|||
var address = Address.derive('wallet-id', 'P2SH', [{
|
||||
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
|
||||
// PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd
|
||||
}], 'm/0/0', 1, 'livenet', false);
|
||||
}], 'm/0/0', 1, 'btc', 'livenet', false);
|
||||
should.exist(address);
|
||||
address.walletId.should.equal('wallet-id');
|
||||
address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH');
|
||||
|
@ -65,7 +67,7 @@ describe('Address', function() {
|
|||
var address = Address.derive('wallet-id', 'P2PKH', [{
|
||||
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
|
||||
// PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7
|
||||
}], 'm/1/2', 1, 'livenet', true);
|
||||
}], 'm/1/2', 1, 'btc', 'livenet', true);
|
||||
should.exist(address);
|
||||
address.walletId.should.equal('wallet-id');
|
||||
address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT');
|
||||
|
|
|
@ -17,7 +17,7 @@ describe('Copayer', function() {
|
|||
});
|
||||
});
|
||||
describe('#createAddress', function() {
|
||||
it('create an address', function() {
|
||||
it('should create an address', function() {
|
||||
var w = Wallet.fromObj(testWallet);
|
||||
var c = Copayer.fromObj(testWallet.copayers[2]);
|
||||
should.exist(c.requestPubKeys);
|
||||
|
@ -42,6 +42,7 @@ var testWallet = {
|
|||
createdOn: 1422904188,
|
||||
id: '123',
|
||||
name: '123 wallet',
|
||||
network: 'livenet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
status: 'complete',
|
||||
|
|
|
@ -24,6 +24,11 @@ describe('TxProposal', function() {
|
|||
should.exist(txp);
|
||||
txp.amount.should.equal(aTXP().amount);
|
||||
});
|
||||
it('should default to BTC coin', function() {
|
||||
var txp = TxProposal.fromObj(aTXP());
|
||||
should.exist(txp);
|
||||
txp.coin.should.equal('btc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBitcoreTx', function() {
|
||||
|
@ -108,8 +113,10 @@ var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2
|
|||
var theSignatures = ['304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6'];
|
||||
var theRawTx = '0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e08000000910047304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0380969800000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac002d3101000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70f62b040000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000';
|
||||
|
||||
var aTxpOpts = function(type) {
|
||||
var aTxpOpts = function() {
|
||||
var opts = {
|
||||
coin: 'btc',
|
||||
network: 'livenet',
|
||||
message: 'some message'
|
||||
};
|
||||
opts.outputs = [{
|
||||
|
@ -125,7 +132,7 @@ var aTxpOpts = function(type) {
|
|||
return opts;
|
||||
};
|
||||
|
||||
var aTXP = function(type) {
|
||||
var aTXP = function() {
|
||||
var txp = {
|
||||
"version": 3,
|
||||
"createdOn": 1423146231,
|
||||
|
|
|
@ -54,6 +54,8 @@ describe('Storage', function() {
|
|||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
coin: 'btc',
|
||||
network: 'livenet',
|
||||
});
|
||||
should.exist(wallet);
|
||||
storage.storeWallet(wallet, function(err) {
|
||||
|
@ -85,9 +87,12 @@ describe('Storage', function() {
|
|||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
coin: 'btc',
|
||||
network: 'livenet',
|
||||
});
|
||||
_.each(_.range(3), function(i) {
|
||||
var copayer = Model.Copayer.create({
|
||||
coin: 'btc',
|
||||
name: 'copayer ' + i,
|
||||
xPubKey: 'xPubKey ' + i,
|
||||
requestPubKey: 'requestPubKey ' + i,
|
||||
|
@ -127,9 +132,12 @@ describe('Storage', function() {
|
|||
name: 'my wallet',
|
||||
m: 2,
|
||||
n: 3,
|
||||
coin: 'btc',
|
||||
network: 'livenet',
|
||||
});
|
||||
_.each(_.range(3), function(i) {
|
||||
var copayer = Model.Copayer.create({
|
||||
coin: 'btc',
|
||||
name: 'copayer ' + i,
|
||||
xPubKey: 'xPubKey ' + i,
|
||||
requestPubKey: 'requestPubKey ' + i,
|
||||
|
@ -144,6 +152,8 @@ describe('Storage', function() {
|
|||
proposals = _.map(_.range(4), function(i) {
|
||||
var tx = Model.TxProposal.create({
|
||||
walletId: '123',
|
||||
coin: 'btc',
|
||||
network: 'livenet',
|
||||
outputs: [{
|
||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||
amount: i + 100,
|
||||
|
|
|
@ -4,7 +4,8 @@ var keyPair = {
|
|||
};
|
||||
|
||||
var copayers = [{
|
||||
id44: '626452e5e0e35df4d9ae4d3e60653c9ae9a814f00c84dc40f5887069b18e2110',
|
||||
id44btc: '626452e5e0e35df4d9ae4d3e60653c9ae9a814f00c84dc40f5887069b18e2110',
|
||||
id44bch: '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065',
|
||||
id45: 'e7467366d5754be2b7d386c9737ab87214c26314bdc3489702e09c719be1bdb7',
|
||||
xPrivKey: 'xprv9s21ZrQH143K2n4rV4AtAJFptEmd1tNMKCcSyQBCSuN5eq1dCUhcv6KQJS49joRxu8NNdFxy8yuwTtzCPNYUZvVGC7EPRm2st2cvE7oyTbB',
|
||||
xPubKey: 'xpub661MyMwAqRbcFG9Kb5htXSCZSGc7RM6CgRY3mnap1Eu4XdLmk21sTtdt9iWAiL64KazU3QWrYEYSRAKgLvkMRe8JMxffDvt4AhCDzyMsnsT',
|
||||
|
@ -12,12 +13,18 @@ var copayers = [{
|
|||
xPubKey_45H: 'xpub68pKcb8jHWqWuTgPz2czjFSJJBJTsTNdd87Mgh5bVz4sNFBJBus5KyptGBWgA4V6LGCi12s4Mw4S1JC2GkqX4NJ4kfQ47XqRZLbyM2DY9Jd',
|
||||
xPrivKey_44H_0H_0H: 'xprv9zWRZ7CXrC4z9xA9RRBFXohmPKbyCajWaCNTHPtwNeJwTnysHG5QK7WMqpNLVtvqGxts7WNcNtqBLfdaFdCGknDPXjLKt2E2BUrPaFDqrLh',
|
||||
xPubKey_44H_0H_0H: 'xpub6DVmxcjRgZdHNSEcXSiFtweVwMSTc3TMwRJ45nJYvyqvLbK1poPerupqh87rSoz27wvckb1CKnGZoLmLXSZyNGZtVd7neqSvdwJL6fceQpe',
|
||||
|
||||
|
||||
xPrivKey_44H_0H_0Ht: 'tprv8ZgxMBicQKsPcxUEtgtQ2wKpkmuNKS6R2w3UmFTUHHURv4PKGE2aGkkbQEcQs9gGsoW4zPr7VM98xdbjQuWc3cZ6bkEyKy1sywhV9gLUcUi',
|
||||
xPubKey_44H_0H_0Ht: 'tpubD6NzVbkrYhZ4WRW2nLYzSLywKoRJUmHKcEeG3mVmhZGpkYe5tcrATFNTaQRAWM3dzL2QyXoctpjkaAXruDXyc6xkF4EDGu3eQdwZXFzoFSW',
|
||||
|
||||
xPrivKey_1H: 'xprv9upyD5bqT9HBkWym7TvTH3njEzTnjrtkLB2sg3DD2CxxA5hZKGee1sYJUtD8C4QaeATLXQ33TirRzRhuTGDBA6XRoYDMwfXAj1KSmGyNBio',
|
||||
xPubKey_1H: 'xpub68pKcb8jHWqUy14EDVTTeBjTo2JH9KcbhPxUURcpaYVw2t2hroxtZfrnLBw1bWzBrHbEJA48QmZ8DB9gTvhphKSitC15SiYx9k2ncGh55Hq',
|
||||
privKey_1H_0: 'a710be25950738a7d13637e2e09affd7f579a3479fd7cc024bd9459f8fba6659',
|
||||
pubKey_1H_0: '026e3020913420a5b9425952627f0a074c9235e7a329869b322061f786e997ae0d'
|
||||
}, {
|
||||
id44: '842c048066e7d10ae1bbf67edccf69f2e5ff9a754d0c2b5524f0d01a87d6acbb',
|
||||
id44btc: '842c048066e7d10ae1bbf67edccf69f2e5ff9a754d0c2b5524f0d01a87d6acbb',
|
||||
id44bch: '0d8f0c0ebfb11ad589002fd4539075c6fb625fb1725406ca442726c6bc6746b1',
|
||||
id45: 'ee75154b646277c8d0d256fc1a0aa0470e4c3435497f208092c865737040b55b',
|
||||
xPrivKey: 'xprv9s21ZrQH143K3BwkLceWNLUsgES15JoZuv8BZfnmDRcCGtDooUAPhY8KovhCWcRLXUun5AYL5vVtUNRrmPEibtfk9ongxAGLXZzEHifpvwZ',
|
||||
xPubKey: 'xpub661MyMwAqRbcFg2DSeBWjURcEGGVUmXRH93nN4CNmm9B9gYxM1UeFLSofD6gtMnRYeucgPjfrWNxaAEhiT4di6HLty8Un6aheCKev4REvhZ',
|
||||
|
@ -30,7 +37,8 @@ var copayers = [{
|
|||
privKey_1H_0: 'ee062ce6dc5ece50e8110646b5e858c98dba9315cdfdd19da85ab0d33dcac74a',
|
||||
pubKey_1H_0: '02c679bf169233a273dec87fae5a1830481866c4e96a350d56346ac267808c905d'
|
||||
}, {
|
||||
id44: '719f4ee61c691fbf0ebefa34e2151a1a3dbe39cf2fa4a498cb6af53600d30d1a',
|
||||
id44btc: '719f4ee61c691fbf0ebefa34e2151a1a3dbe39cf2fa4a498cb6af53600d30d1a',
|
||||
id44bch: '56ed2c8d04c4aa29e9d6408724197c27d1fa0b71e2c2a6b91a4cf9710f09eb0a',
|
||||
id45: 'acd666d7c677d9f2c85b55a5fad1610fe272eac46ef7a577c7aeeab0b1474e43',
|
||||
xPrivKey: 'xprv9s21ZrQH143K3xgLzxd6SuWqG5Zp1iUmyGgSsJVhdQNeTzAqBFvXXLZqZzFZqocTx4HD9vUVYU27At5i8q46LmBXXL97fo4H9C3tHm4BnjY',
|
||||
xPubKey: 'xpub661MyMwAqRbcGSkp6zA6p3TZp7QJRBCdLVc3fguKBjudLnVyioEn58tKRFPmGMkdGJWMX69mgZWHKrKmpQ3fwBXeFjLc5Sd2rnxcQthSW42',
|
||||
|
@ -43,7 +51,8 @@ var copayers = [{
|
|||
privKey_1H_0: '5009c8488e9a364fc24a999d99a81ae955271de1d06d46c2f2f09e20c6281b04',
|
||||
pubKey_1H_0: '03338a3b7c08e9d9832e1baff0758e08f9cc691497dd6e91d4c191cd960fb2f043'
|
||||
}, {
|
||||
id44: 'e225a29864060823df67b98432b070a40aad1bf9af517005b0b5fe09c96e29c9',
|
||||
id44btc: 'e225a29864060823df67b98432b070a40aad1bf9af517005b0b5fe09c96e29c9',
|
||||
id44bch: '2baf290be693407fd9c32597608b6fd90ba60f65b2b81b58e9fe9c960938de11',
|
||||
id45: 'c65a89f64794cb7e1886c7010a32dd6fa362d3e81710bac32e97e325b9109fd8',
|
||||
xPrivKey: 'xprv9s21ZrQH143K48nfuK14gKJtML7eQzV2dAH1RaqAMj8v2zs79uaavA9UTWMxpBdgbMH2mhJLeKGq8AFA6GDnFyWP4rLmknqZAfgFFV718vo',
|
||||
xPubKey: 'xpub661MyMwAqRbcGcs91LY53TFcuMx8pTCszPCcDyEmv4ftuoCFhStqTxTxJoy35yjp2H3qQtxDYGe1gtkZu4T7mR7ARK1MLYte2fptZVt6hkD',
|
||||
|
@ -56,7 +65,8 @@ var copayers = [{
|
|||
privKey_1H_0: '460ee692f05de66b5d8e2fa1d005a8b6bdb1442e2ce6b3facfcee2f9012c9474',
|
||||
pubKey_1H_0: '03d0e0c526619b158aac9a8de8082f439df43d389ec50cb54386c3d87cfde4c99b'
|
||||
}, {
|
||||
id44: '120416cd4c427a7e4d94213cebe242f56a06bc6dd5c5c6cae27dc920a0ddf1fb',
|
||||
id44btc: '120416cd4c427a7e4d94213cebe242f56a06bc6dd5c5c6cae27dc920a0ddf1fb',
|
||||
id44bch: '4abc36e7731c08e0a93483691c3cb451013463ccee1b676e4a20d98cd1de8af3',
|
||||
id45: '65ae087eb9efdc7e0ada3a7ef954285e9e5ba4b8c7ab2d36747ddd286f7a334f',
|
||||
xPrivKey: 'xprv9s21ZrQH143K44Bb9G3EVNmLfAUKjTBAA2YtKxF4zc8SLV1o15JBoddhGHE9PGLXePMbEsSjCCvTvP3fUv6yMXZrnHigBboRBn2DmNoJkJg',
|
||||
xPubKey: 'xpub661MyMwAqRbcGYG4FHaErWi5DCJp8uu1XFUV8LegYwfRDHLwYccSMRxB7Z3L1NgKychKdXQvbVEyDhSwNnNnnNKh9mBEAdQ5tv2guK8ywKU',
|
||||
|
@ -69,7 +79,8 @@ var copayers = [{
|
|||
privKey_1H_0: '7a5158b92d9ed4cb9644ddbd472b43428832a5f3bb91a481532a081908e62b2e',
|
||||
pubKey_1H_0: '02b47d5c977c93c883f369165ebc2b564d14a52712ec6892f7097fa99e0d36ca20'
|
||||
}, {
|
||||
id44: '85de9f025ee190fab7cb1bd9b6772c64df26188ce705d4f258c5adaf7bc610f9',
|
||||
id44btc: '85de9f025ee190fab7cb1bd9b6772c64df26188ce705d4f258c5adaf7bc610f9',
|
||||
id44bch: '0845739e508fb8f7b28e10bed9d827968a12d2dbd6ecbac3303305fcaf535bfe',
|
||||
id45: 'dacc5c350cef4449a3ca12939711c7449d0d6189e5e7f33cff60095a7a29b0f9',
|
||||
xPrivKey: 'xprv9s21ZrQH143K48PpVxrh71KdViTFhAaiDSVtNFkmbWNYjwwwPbTrcqoVXsgBfue3Gq9b71hQeEbk67JgtTBcpYgKLF8pTwVnGz56f1BaCYt',
|
||||
xPubKey: 'xpub661MyMwAqRbcGcUHbzPhU9GN3kHk6dJZafRVAeAP9quXckH5w8n7Ae7yP8e2Zh6SPPKFn2K6oE3GBpcz9QzfJTNRWXbY7w1L3nGLE5beZL1',
|
||||
|
@ -82,7 +93,8 @@ var copayers = [{
|
|||
privKey_1H_0: '3c49816d4e83d8758f89e8e104e3566a8a61426a9b7d4945b34212fbbb8e8290',
|
||||
pubKey_1H_0: '0307ab8c0d8eea1fe3c3781050a69e71f9e7c8cc8476a77103e08a461506a0e780'
|
||||
}, {
|
||||
id44: '4d0c1eaab0aafc08aea7328f9ed1d3fc2812791ad2ebb9cbc1a8537b51b18afa',
|
||||
id44btc: '4d0c1eaab0aafc08aea7328f9ed1d3fc2812791ad2ebb9cbc1a8537b51b18afa',
|
||||
id44bch: '63ed91d8b7c4f06028d4a795cbb30d91772d93c99e7cc612d9f0b33a4fa215de',
|
||||
id45: '9129a0454adcf659f4f9d65a9b4dc4f9793bd1f59664268b56a7ef73f29f1b8a',
|
||||
xPrivKey: 'xprv9s21ZrQH143K3pgRcRBRnmcxNkNNLmJrpneMkEXY6o5TWBuJLMfdRpAWdb2cG3yxbL4DxfpUnQpjfQUmwPdVrRGoDJmtAf5u8cyqKCoDV97',
|
||||
xPubKey: 'xpub661MyMwAqRbcGJktiSiS9uZgvnCrkE2iC1ZxYcw9f8cSNzESstysycUzUsDCU6KnnjR29VZ1eRAXDgEXfYxGw1B9E7VLSAcHa9UuifSozmy',
|
||||
|
@ -95,7 +107,8 @@ var copayers = [{
|
|||
privKey_1H_0: '87f8a2b92dd04d2782c3d40a34f09f2ab42076bd02b81fbe4a4a72f87ad2e6df',
|
||||
pubKey_1H_0: '02a0370d6f1213ab3390ac666585614ad71146f3f28ec326e2e779f999c1a497eb'
|
||||
}, {
|
||||
id44: '5ae7b75deb3b4d7e251f1fc5613904c9ef8548af7601d93ef668299be4f75ddd',
|
||||
id44btc: '5ae7b75deb3b4d7e251f1fc5613904c9ef8548af7601d93ef668299be4f75ddd',
|
||||
id44bch: '375a87b5614473ad359fee0385e9ffcb01d78c7880b34987e59da06eeac8029a',
|
||||
id45: '37b81e2544b43ce7f37a132a748426e1566ecbb758564d4d7d07b716fbe1b368',
|
||||
xPrivKey: 'xprv9s21ZrQH143K3nvcmdjDDDZbDJHpfWZCUiunwraZdcamYcafHvUnZfV51fivH9FPyfo12NyKH5JDxGLsQePyWKtTiJx3pkEaiwxsMLkVapp',
|
||||
xPubKey: 'xpub661MyMwAqRbcGH15sfGDaMWKmL8K4yH3qwqPkEzBBx7kRQuoqTo37ToYrvLJh7JpV5FQSverERMcdF4HcP1UCiie2ayeMXRq67zr75PzMKs',
|
||||
|
@ -108,7 +121,8 @@ var copayers = [{
|
|||
privKey_1H_0: '66230b6b8b65725162ea43313fcc233f4f0dd135cea00d04b73a84d3f681ef25',
|
||||
pubKey_1H_0: '03f148bde0784c80051acd159b28a30022e685aca56418f8f50100d9f8a0192c37'
|
||||
}, {
|
||||
id44: '98e78a9cb2ab340a245c5082897eadb28c367319f97b93e7b51b4d5ca5cdc68e',
|
||||
id44btc: '98e78a9cb2ab340a245c5082897eadb28c367319f97b93e7b51b4d5ca5cdc68e',
|
||||
id44bch: 'f390e03140593c0c724e0d3a2a9cf39d63319edc833024a149a72efacb368737',
|
||||
id45: 'e1557d3421a8884fe007674f3f0b6f0feafa76289a0edcc5ec736161b4d02257',
|
||||
xPrivKey: 'xprv9s21ZrQH143K2uYgqtYtphEQkFAgiWSqahFUWjgCdKykJagiNDz6Lf7xRVQdtZ7MvkhX9V3pEcK3xTAWZ6Y6ecJqrXnCpzrH9GSHn8wyrT5',
|
||||
xPubKey: 'xpub661MyMwAqRbcFPd9wv5uBqB9JH1B7yAgwvB5K85pBfWjBP1rumJLtTSSGnCdsJSXfwmTyexsRjbUhzB4J6LWfL8mC2Ka117JrnXetyCzk3r',
|
||||
|
@ -121,7 +135,8 @@ var copayers = [{
|
|||
privKey_1H_0: '9e215580c8e5876215ad101ded325bcacc5ab9d97b26e8fdfab89ef5bb6e0ab7',
|
||||
pubKey_1H_0: '0265d33caaa128a77cc38ab8751c7d730e0274a212f1f65b73f637eddb3a3fb151'
|
||||
}, {
|
||||
id44: 'f716dbeec58e44c698b34c2d81bae4699ed5a5a522281733ec50aa03caf76a19',
|
||||
id44btc: 'f716dbeec58e44c698b34c2d81bae4699ed5a5a522281733ec50aa03caf76a19',
|
||||
id44bch: 'da39b3d560d2d99d9557a5a70ca3dc4561c3930e2850748fa80bdcecb650a9bf',
|
||||
id45: '8a6d840580549a34422c9b150dbd1e96e369c5db69ee736caab95616f8abb22b',
|
||||
xPrivKey: 'xprv9s21ZrQH143K2wcRMP75tAEL5JnUx4xU2AbUBQzVVUDP7DHZJkjF3kaRE7tcnPLLLL9PGjYTWTJmCQPaQ4GGzgWEUFJ6snwJG9YnQHBFRNR',
|
||||
xPubKey: 'xpub661MyMwAqRbcFRgtTQe6FJB4dLcyMXgKPPX4yoQ73okMz1chrJ3VbYtu5PRTxMBGuXt6eyqwAuG2BEBzQPLc1x8gnSQiATS3GRzKi1BuQAR',
|
||||
|
@ -135,8 +150,8 @@ var copayers = [{
|
|||
pubKey_1H_0: '0266cdb57b8a4d7c1b5b20ddeea43705420c6e3aef2c2979a3768b7b585839a0d3'
|
||||
}, ];
|
||||
|
||||
var history = [
|
||||
{
|
||||
|
||||
var history = [{
|
||||
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
|
||||
vin: [{
|
||||
txid: "c8e221141e8bb60977896561b77fa59d6dacfcc10db82bf6f5f923048b11c70d",
|
||||
|
@ -175,8 +190,7 @@ var history = [
|
|||
valueOut: 0.01345753,
|
||||
valueIn: 0.01371235,
|
||||
fees: 0.00025482
|
||||
},
|
||||
{
|
||||
}, {
|
||||
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
|
||||
vin: [{
|
||||
txid: "c8e221141e8bb60977896561b77fa59d6dacfcc10db82bf6f5f923048b11c70d",
|
||||
|
|
|
@ -131,4 +131,42 @@ describe('Utils', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAddressCoin', function() {
|
||||
it('should identify btc as coin for 1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', function() {
|
||||
Utils.getAddressCoin('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA').should.equal('btc');
|
||||
});
|
||||
it('should identify bch as coin for CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz', function() {
|
||||
Utils.getAddressCoin('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz').should.equal('bch');
|
||||
});
|
||||
it('should return null for 1L', function() {
|
||||
should.not.exist(Utils.getAddressCoin('1L'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#translateAddress', function() {
|
||||
it('should translate address from btc to bch', function() {
|
||||
var res = Utils.translateAddress('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', 'bch');
|
||||
res.should.equal('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz');
|
||||
});
|
||||
it('should translate address from bch to btc', function() {
|
||||
var res = Utils.translateAddress('HBf8isgS8EXG1r3X6GP89FmooUmiJ42wHS', 'btc');
|
||||
res.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r');
|
||||
});
|
||||
|
||||
it('should keep the address if there is nothing to do (bch)', function() {
|
||||
var res = Utils.translateAddress('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz', 'bch');
|
||||
res.should.equal('CcJ4qUfyQ8x5NwhAeCQkrBSWVeXxXghcNz');
|
||||
});
|
||||
it('should keep the address if there is nothing to do (btc)', function() {
|
||||
var res = Utils.translateAddress('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', 'btc');
|
||||
should.exist(res);
|
||||
res.should.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA');
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue