mirror of https://github.com/BTCPrivate/copay.git
2501 lines
69 KiB
JavaScript
2501 lines
69 KiB
JavaScript
'use strict';
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var _ = require('underscore');
|
|
var async = require('async');
|
|
var preconditions = require('preconditions').singleton();
|
|
var inherits = require('inherits');
|
|
var events = require('events');
|
|
|
|
var bitcore = require('bitcore');
|
|
var bignum = bitcore.Bignum;
|
|
var coinUtil = bitcore.util;
|
|
var buffertools = bitcore.buffertools;
|
|
var Builder = bitcore.TransactionBuilder;
|
|
var SecureRandom = bitcore.SecureRandom;
|
|
var Base58Check = bitcore.Base58.base58Check;
|
|
var Address = bitcore.Address;
|
|
var PayPro = bitcore.PayPro;
|
|
var Transaction = bitcore.Transaction;
|
|
var log = require('../../log');
|
|
|
|
var HDParams = require('./HDParams');
|
|
var PublicKeyRing = require('./PublicKeyRing');
|
|
var TxProposal = require('./TxProposal');
|
|
var TxProposals = require('./TxProposals');
|
|
var PrivateKey = require('./PrivateKey');
|
|
var WalletLock = require('./WalletLock');
|
|
var copayConfig = require('../../../config');
|
|
|
|
/**
|
|
* @desc
|
|
* Wallet manages a private key for Copay, network, storage of the wallet for
|
|
* persistance, and blockchain information.
|
|
*
|
|
* @TODO: Split this leviathan.
|
|
*
|
|
* @param {Object} opts
|
|
* @param {Storage} opts.storage - an object that can persist the wallet
|
|
* @param {Network} opts.network - used to send and retrieve messages from
|
|
* copayers
|
|
* @param {Blockchain} opts.blockchain - source of truth for what happens in
|
|
* the blockchain (utxos, balances)
|
|
* @param {number} opts.requiredCopayers - number of required copayers to
|
|
* release funds
|
|
* @param {number} opts.totalCopayers - number of copayers in the wallet
|
|
* @param {boolean} opts.spendUnconfirmed - whether it's safe to spend
|
|
* unconfirmed outputs or not
|
|
* @param {PublicKeyRing} opts.publicKeyRing - an instance of {@link PublicKeyRing}
|
|
* @param {TxProposals} opts.txProposals - an instance of {@link TxProposals}
|
|
* @param {PrivateKey} opts.privateKey - an instance of {@link PrivateKey}
|
|
* @param {string} opts.version - the version of copay where this wallet was
|
|
* created
|
|
* @TODO: figure out if reconnectDelay is set in milliseconds
|
|
* @param {number} opts.reconnectDelay - amount of seconds to wait before
|
|
* attempting to reconnect
|
|
*/
|
|
function Wallet(opts) {
|
|
var self = this;
|
|
|
|
//required params
|
|
['storage', 'network', 'blockchain',
|
|
'requiredCopayers', 'totalCopayers', 'spendUnconfirmed',
|
|
'publicKeyRing', 'txProposals', 'privateKey', 'version',
|
|
'reconnectDelay'
|
|
].forEach(function(k) {
|
|
preconditions.checkArgument(!_.isUndefined(opts[k]), 'missing required option for Wallet: ' + k);
|
|
self[k] = opts[k];
|
|
});
|
|
preconditions.checkArgument(!copayConfig.forceNetwork || this.getNetworkName() === copayConfig.networkName,
|
|
'Network forced to ' + copayConfig.networkName +
|
|
' and tried to create a Wallet with network ' + this.getNetworkName());
|
|
|
|
this.id = opts.id || Wallet.getRandomId();
|
|
this.secretNumber = opts.secretNumber || Wallet.getRandomNumber();
|
|
this.lock = new WalletLock(this.storage, this.id, opts.lockTimeOutMin);
|
|
this.name = opts.name;
|
|
|
|
this.verbose = opts.verbose;
|
|
this.publicKeyRing.walletId = this.id;
|
|
this.txProposals.walletId = this.id;
|
|
this.network.maxPeers = this.totalCopayers;
|
|
this.network.secretNumber = this.secretNumber;
|
|
this.registeredPeerIds = [];
|
|
this.addressBook = opts.addressBook || {};
|
|
this.publicKey = this.privateKey.publicHex;
|
|
this.lastTimestamp = opts.lastTimestamp || undefined;
|
|
this.lastMessageFrom = {};
|
|
|
|
this.paymentRequests = opts.paymentRequests || {};
|
|
|
|
//network nonces are 8 byte buffers, representing a big endian number
|
|
//one nonce for oneself, and then one nonce for each copayer
|
|
this.network.setHexNonce(opts.networkNonce);
|
|
this.network.setHexNonces(opts.networkNonces);
|
|
}
|
|
|
|
inherits(Wallet, events.EventEmitter);
|
|
|
|
/**
|
|
* @TODO: Document this. Its usage is kind of weird
|
|
*
|
|
* @static
|
|
* @property lockTime
|
|
* @property signhash
|
|
* @property fee
|
|
* @property feeSat
|
|
*/
|
|
Wallet.builderOpts = {
|
|
lockTime: null,
|
|
signhash: bitcore.Transaction.SIGNHASH_ALL,
|
|
fee: null,
|
|
feeSat: null,
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve a random id for the wallet
|
|
* @TODO: Discuss changing to a UUID
|
|
* @return {string} 8 bytes, hexa encoded
|
|
*/
|
|
Wallet.getRandomId = function() {
|
|
var r = bitcore.SecureRandom.getPseudoRandomBuffer(8).toString('hex');
|
|
return r;
|
|
};
|
|
|
|
/**
|
|
* @desc Get a random 8 byte number and encode it as a hexa string
|
|
* @return {string}
|
|
*/
|
|
Wallet.getRandomNumber = function() {
|
|
var r = bitcore.SecureRandom.getPseudoRandomBuffer(5).toString('hex');
|
|
return r;
|
|
};
|
|
|
|
/**
|
|
* @desc Set the copayer id for the owner of this wallet
|
|
* @param {string} pubkey - the pubkey to set to the {@link Wallet#seededCopayerId} property
|
|
*/
|
|
Wallet.prototype.seedCopayer = function(pubKey) {
|
|
this.seededCopayerId = pubKey;
|
|
};
|
|
|
|
/**
|
|
* @desc Handles an 'indexes' message.
|
|
*
|
|
* Processes the data using {@link HDParams#fromList} and merges it with the
|
|
* {@link Wallet#publicKeyRing}.
|
|
*
|
|
* Triggers a {@link Wallet#store} if the internal state has changed.
|
|
*
|
|
* @param {string} senderId - the sender id
|
|
* @param {Object} data - the data recived, {@see HDParams#fromList}
|
|
* @emits {publicKeyRingUpdated}
|
|
*/
|
|
Wallet.prototype._onIndexes = function(senderId, data) {
|
|
log.debug('RECV INDEXES:', data);
|
|
var inIndexes = HDParams.fromList(data.indexes);
|
|
var hasChanged = this.publicKeyRing.mergeIndexes(inIndexes);
|
|
if (hasChanged) {
|
|
this.emit('publicKeyRingUpdated');
|
|
this.store();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Handles a 'PUBLICKEYRING' message from <tt>senderId</tt>.
|
|
*
|
|
* <tt>data.publicKeyRing</tt> is expected to be processed correctly by
|
|
* {@link PublicKeyRing#fromObj}.
|
|
*
|
|
* After successful deserialization, {@link Wallet#publicKeyRing} is merged
|
|
* with the received data, a call to {@link Wallet#store} is performed if the
|
|
* internal state has changed.
|
|
*
|
|
* This locks new incoming connections in case the public key ring is completed
|
|
*
|
|
* @param {string} senderId - the sender id
|
|
* @param {Object} data - the data recived, {@see HDParams#fromList}
|
|
* @param {Object} data.publicKeyRing - data to be deserialized into a {@link PublicKeyRing}
|
|
* using {@link PublicKeyRing#fromObj}
|
|
* @emits {publicKeyRingUpdated}
|
|
* @emits {connectionError}
|
|
*/
|
|
Wallet.prototype._onPublicKeyRing = function(senderId, data) {
|
|
log.debug('RECV PUBLICKEYRING:', data);
|
|
|
|
var inPKR = PublicKeyRing.fromObj(data.publicKeyRing);
|
|
var wasIncomplete = !this.publicKeyRing.isComplete();
|
|
var hasChanged;
|
|
|
|
try {
|
|
hasChanged = this.publicKeyRing.merge(inPKR, true);
|
|
} catch (e) {
|
|
log.debug('## WALLET ERROR', e);
|
|
this.emit('connectionError', e.message);
|
|
return;
|
|
}
|
|
|
|
if (hasChanged) {
|
|
if (wasIncomplete) {
|
|
this.sendPublicKeyRing();
|
|
}
|
|
if (this.publicKeyRing.isComplete()) {
|
|
this._lockIncomming();
|
|
}
|
|
this.emit('publicKeyRingUpdated');
|
|
this.store();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Demultiplexes calls to update TxProposal updates
|
|
*
|
|
* @param {string} senderId - the copayer that sent this event
|
|
* @param {Object} m - the data received
|
|
* @emits {txProposalEvent}
|
|
*/
|
|
Wallet.prototype._processProposalEvents = function(senderId, m) {
|
|
var ev;
|
|
if (m) {
|
|
if (m.new) {
|
|
ev = {
|
|
type: 'new',
|
|
cid: senderId
|
|
}
|
|
} else if (m.newCopayer) {
|
|
ev = {
|
|
type: 'signed',
|
|
cid: m.newCopayer
|
|
};
|
|
}
|
|
} else {
|
|
ev = {
|
|
type: 'corrupt',
|
|
cId: senderId,
|
|
};
|
|
}
|
|
|
|
if (ev)
|
|
this.emit('txProposalEvent', ev);
|
|
};
|
|
|
|
|
|
/* OTDO
|
|
events.push({
|
|
type: 'signed',
|
|
cId: k,
|
|
txId: ntxid
|
|
});
|
|
*/
|
|
/**
|
|
* @desc
|
|
* Retrieves a keymap from from a transaction proposal set extracts a maps from
|
|
* public key to cosignerId for each signed input of the transaction proposal.
|
|
*
|
|
* @param {TxProposals} txp - the transaction proposals
|
|
* @return {Object}
|
|
*/
|
|
Wallet.prototype._getKeyMap = function(txp) {
|
|
preconditions.checkArgument(txp);
|
|
var inSig0, keyMapAll = {};
|
|
|
|
for (var i in txp._inputSigners) {
|
|
var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSigners[i], txp.inputChainPaths);
|
|
|
|
if (_.size(keyMap) !== _.size(txp._inputSigners[i]))
|
|
throw new Error('Signature does not match known copayers');
|
|
|
|
for (var j in keyMap) {
|
|
keyMapAll[j] = keyMap[j];
|
|
}
|
|
|
|
// From here -> only to check that all inputs have the same sigs
|
|
var inSigArr = [];
|
|
_.each(keyMap, function(value, key) {
|
|
inSigArr.push(value);
|
|
});
|
|
var inSig = JSON.stringify(inSigArr.sort());
|
|
if (i === '0') {
|
|
inSig0 = inSig;
|
|
continue;
|
|
}
|
|
if (inSig !== inSig0)
|
|
throw new Error('found inputs with different signatures');
|
|
}
|
|
return keyMapAll;
|
|
};
|
|
|
|
/**
|
|
* @callback transactionCallback
|
|
* @param {false|Transaction} returnValue
|
|
*/
|
|
|
|
/**
|
|
* @desc
|
|
* Asyncchronously check with the blockchain if a given transaction was sent.
|
|
*
|
|
* @param {string} ntxid - the transaction
|
|
* @param {transactionCallback} cb
|
|
*/
|
|
Wallet.prototype._checkSentTx = function(ntxid, cb) {
|
|
var txp = this.txProposals.get(ntxid);
|
|
var tx = txp.builder.build();
|
|
var txid = bitcore.util.formatHashFull(tx.getHash());
|
|
|
|
this.blockchain.getTransaction(txid, function(err, tx) {
|
|
if (err) return cb(false);
|
|
txp.setSent(tx.txid);
|
|
cb(ret);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Handles a 'TXPROPOSAL' network message
|
|
*
|
|
* @param {string} senderId - the id of the sender
|
|
* @param {Object} data - the data received
|
|
* @param {Object} data.txProposal - first parameter for {@link TxProposals#merge}
|
|
* @emits txProposalsUpdated
|
|
*/
|
|
Wallet.prototype._onTxProposal = function(senderId, data) {
|
|
var self = this;
|
|
log.debug('RECV TXPROPOSAL: ', data);
|
|
var m;
|
|
|
|
try {
|
|
m = this.txProposals.merge(data.txProposal, Wallet.builderOpts);
|
|
var keyMap = this._getKeyMap(m.txp);
|
|
ret.newCopayer = m.txp.setCopayers(senderId, keyMap);
|
|
|
|
} catch (e) {
|
|
log.debug('Corrupt TX proposal received from:', senderId, e);
|
|
}
|
|
|
|
if (m) {
|
|
|
|
if (m.hasChanged) {
|
|
m.txp.setSeen(this.getMyCopayerId());
|
|
this.sendSeen(m.ntxid);
|
|
var tx = m.txp.builder.build();
|
|
if (tx.isComplete()) {
|
|
this._checkSentTx(m.ntxid, function(ret) {
|
|
if (ret) {
|
|
self.emit('txProposalsUpdated');
|
|
self.store();
|
|
}
|
|
});
|
|
} else {
|
|
this.sendTxProposal(m.ntxid);
|
|
}
|
|
}
|
|
this.emit('txProposalsUpdated');
|
|
this.store();
|
|
}
|
|
this._processProposalEvents(senderId, m);
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Handle a REJECT message received
|
|
*
|
|
* @param {string} senderId
|
|
* @param {Object} data
|
|
* @param {string} data.ntxid
|
|
* @emits txProposalsUpdated
|
|
* @emits txProposalEvent
|
|
*/
|
|
Wallet.prototype._onReject = function(senderId, data) {
|
|
preconditions.checkState(data.ntxid);
|
|
log.debug('RECV REJECT:', data);
|
|
|
|
var txp = this.txProposals.get(data.ntxid);
|
|
|
|
if (!txp)
|
|
throw new Error('Received Reject for an unknown TX from:' + senderId);
|
|
|
|
if (txp.signedBy[senderId])
|
|
throw new Error('Received Reject for an already signed TX from:' + senderId);
|
|
|
|
txp.setRejected(senderId);
|
|
this.store();
|
|
|
|
this.emit('txProposalsUpdated');
|
|
this.emit('txProposalEvent', {
|
|
type: 'rejected',
|
|
cId: senderId,
|
|
txId: data.ntxid,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Handle a SEEN message received
|
|
*
|
|
* @param {string} senderId
|
|
* @param {Object} data
|
|
* @param {string} data.ntxid
|
|
* @emits txProposalsUpdated
|
|
* @emits txProposalEvent
|
|
*/
|
|
Wallet.prototype._onSeen = function(senderId, data) {
|
|
preconditions.checkState(data.ntxid);
|
|
log.debug('RECV SEEN:', data);
|
|
|
|
var txp = this.txProposals.get(data.ntxid);
|
|
txp.setSeen(senderId);
|
|
this.store();
|
|
this.emit('txProposalsUpdated');
|
|
this.emit('txProposalEvent', {
|
|
type: 'seen',
|
|
cId: senderId,
|
|
txId: data.ntxid,
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* @desc
|
|
* Handle a ADDRESSBOOK message received
|
|
*
|
|
* {@see Wallet#verifyAddressbookEntry}
|
|
*
|
|
* @param {string} senderId
|
|
* @param {Object} data
|
|
* @param {Object} data.addressBook
|
|
* @emits addressBookUpdated
|
|
* @emits txProposalEvent
|
|
*/
|
|
Wallet.prototype._onAddressBook = function(senderId, data) {
|
|
preconditions.checkState(data.addressBook);
|
|
log.debug('RECV ADDRESSBOOK:', data);
|
|
var rcv = data.addressBook;
|
|
var hasChange;
|
|
for (var key in rcv) {
|
|
if (!this.addressBook[key]) {
|
|
var isVerified = this.verifyAddressbookEntry(rcv[key], senderId, key);
|
|
if (isVerified) {
|
|
this.addressBook[key] = rcv[key];
|
|
hasChange = true;
|
|
}
|
|
}
|
|
}
|
|
if (hasChange) {
|
|
this.emit('addressBookUpdated');
|
|
this.store();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc Updates the wallet's last modified timestamp and triggers a save
|
|
* @param {number} ts - the timestamp
|
|
*/
|
|
Wallet.prototype.updateTimestamp = function(ts) {
|
|
preconditions.checkArgument(ts);
|
|
preconditions.checkArgument(_.isNumber(ts));
|
|
this.lastTimestamp = ts;
|
|
this.store();
|
|
};
|
|
|
|
/**
|
|
* @desc Called when there are no messages in the server
|
|
* Triggers a call to {@link Wallet#sendWalletReady}
|
|
*/
|
|
Wallet.prototype._onNoMessages = function() {
|
|
log.debug('No messages at the server. Requesting sync'); //TODO
|
|
this.sendWalletReady();
|
|
};
|
|
|
|
/**
|
|
* @desc Demultiplexes a new message received through the wire
|
|
*
|
|
* @param {string} senderId - the sender id
|
|
* @param {Object} data - the received object
|
|
* @param {number} ts - the timestamp when this object was received
|
|
*
|
|
* @emits corrupt
|
|
*/
|
|
Wallet.prototype._onData = function(senderId, data, ts) {
|
|
preconditions.checkArgument(senderId);
|
|
preconditions.checkArgument(data);
|
|
preconditions.checkArgument(data.type);
|
|
preconditions.checkArgument(ts);
|
|
preconditions.checkArgument(_.isNumber(ts));
|
|
|
|
log.debug('RECV', senderId, data);
|
|
|
|
if (data.type !== 'walletId' && this.id !== data.walletId) {
|
|
this.emit('corrupt', senderId);
|
|
this.updateTimestamp(ts);
|
|
return;
|
|
}
|
|
|
|
|
|
switch (data.type) {
|
|
// This handler is repeaded on WalletFactory (#join). TODO
|
|
case 'walletId':
|
|
this.sendWalletReady(senderId);
|
|
break;
|
|
case 'walletReady':
|
|
if (this.lastMessageFrom[senderId] !== 'walletReady') {
|
|
this.sendPublicKeyRing(senderId);
|
|
this.sendAddressBook(senderId);
|
|
this.sendAllTxProposals(senderId); // send old txps
|
|
}
|
|
break;
|
|
case 'publicKeyRing':
|
|
this._onPublicKeyRing(senderId, data);
|
|
break;
|
|
case 'reject':
|
|
this._onReject(senderId, data);
|
|
break;
|
|
case 'seen':
|
|
this._onSeen(senderId, data);
|
|
break;
|
|
case 'txProposal':
|
|
this._onTxProposal(senderId, data);
|
|
break;
|
|
case 'indexes':
|
|
this._onIndexes(senderId, data);
|
|
break;
|
|
case 'addressbook':
|
|
this._onAddressBook(senderId, data);
|
|
break;
|
|
// unused messages
|
|
case 'disconnect':
|
|
//case 'an other unused message':
|
|
break;
|
|
default:
|
|
throw new Error('unknown message type received: ' + data.type + ' from: ' + senderId)
|
|
}
|
|
|
|
this.lastMessageFrom[senderId] = data.type;
|
|
this.updateTimestamp(ts);
|
|
};
|
|
|
|
/**
|
|
* @desc Handles a connect message
|
|
* @param {string} newCopayerId - the new copayer in the wallet
|
|
* @emits connect
|
|
*/
|
|
Wallet.prototype._onConnect = function(newCopayerId) {
|
|
if (newCopayerId) {
|
|
log.debug('#### Setting new COPAYER:', newCopayerId);
|
|
this.sendWalletId(newCopayerId);
|
|
}
|
|
var peerID = this.network.peerFromCopayer(newCopayerId)
|
|
this.emit('connect', peerID);
|
|
};
|
|
|
|
/**
|
|
* @desc Returns the network name for this wallet ('testnet' or 'livenet')
|
|
* @return {string}
|
|
*/
|
|
Wallet.prototype.getNetworkName = function() {
|
|
return this.publicKeyRing.network.name;
|
|
};
|
|
|
|
/**
|
|
* @desc Serialize options into an object
|
|
* @return {Object} with keys <tt>id</tt>, <tt>spendUnconfirmed</tt>,
|
|
* <tt>requiredCopayers</tt>, <tt>totalCopayers</tt>, <tt>name</tt>,
|
|
* <tt>version</tt>
|
|
*/
|
|
Wallet.prototype._optsToObj = function() {
|
|
var obj = {
|
|
id: this.id,
|
|
spendUnconfirmed: this.spendUnconfirmed,
|
|
requiredCopayers: this.requiredCopayers,
|
|
totalCopayers: this.totalCopayers,
|
|
name: this.name,
|
|
version: this.version,
|
|
};
|
|
|
|
return obj;
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve the copayerId pubkey for a given index
|
|
* @param {number=} index - the index of the copayer, ours by default
|
|
* @return {string} hex-encoded pubkey
|
|
*/
|
|
Wallet.prototype.getCopayerId = function(index) {
|
|
return this.publicKeyRing.getCopayerId(index || 0);
|
|
};
|
|
|
|
/**
|
|
* @desc Get my own pubkey
|
|
* @return {string} hex-encoded pubkey
|
|
*/
|
|
Wallet.prototype.getMyCopayerId = function() {
|
|
return this.getCopayerId(0); //copayer id is hex of a public key
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve my private key
|
|
* @return {string} hex-encoded private key
|
|
*/
|
|
Wallet.prototype.getMyCopayerIdPriv = function() {
|
|
return this.privateKey.getIdPriv(); //copayer idpriv is hex of a private key
|
|
};
|
|
|
|
/**
|
|
* @desc Returns the secret value for other users to join this wallet
|
|
* @return {string} my own pubkey, base58 encoded
|
|
*/
|
|
Wallet.prototype.getSecretNumber = function() {
|
|
if (this.secretNumber) return this.secretNumber;
|
|
this.secretNumber = Wallet.getRandomNumber();
|
|
return this.secretNumber;
|
|
};
|
|
|
|
/**
|
|
* @desc Returns the secret number used to prevent MitM attacks from Insight
|
|
* @return {string}
|
|
*/
|
|
Wallet.prototype.getSecret = function() {
|
|
var buf = new Buffer(this.getMyCopayerId() + this.getSecretNumber(), 'hex');
|
|
var str = Base58Check.encode(buf);
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* @desc Returns an object with a <tt>pubKey</tt> value, an hex representation
|
|
* of a public key
|
|
* @param {string} secretB - the secret to be base58-decoded
|
|
* @return {Object}
|
|
*/
|
|
Wallet.decodeSecret = function(secretB) {
|
|
var secret = Base58Check.decode(secretB);
|
|
var pubKeyBuf = secret.slice(0, 33);
|
|
var secretNumber = secret.slice(33, 38);
|
|
return {
|
|
pubKey: pubKeyBuf.toString('hex'),
|
|
secretNumber : secretNumber.toString('hex')
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc Locks other sessions from connecting to the wallet
|
|
* @see Async#lockIncommingConnections
|
|
*/
|
|
Wallet.prototype._lockIncomming = function() {
|
|
this.network.lockIncommingConnections(this.publicKeyRing.getAllCopayerIds());
|
|
};
|
|
|
|
/**
|
|
* @desc Sets up the networking with other peers.
|
|
*
|
|
* @emits connect
|
|
* @emits data
|
|
*
|
|
* @emits ready
|
|
* @emits publicKeyRingUpdated
|
|
* @emits txProposalsUpdated
|
|
*
|
|
* @TODO: FIX PROTOCOL -- emit with a space is shitty
|
|
* @emits no messages
|
|
*/
|
|
Wallet.prototype.netStart = function() {
|
|
var self = this;
|
|
var net = this.network;
|
|
|
|
net.removeAllListeners();
|
|
net.on('connect', self._onConnect.bind(self));
|
|
net.on('data', self._onData.bind(self));
|
|
net.on('no messages', self._onNoMessages.bind(self));
|
|
|
|
var myId = self.getMyCopayerId();
|
|
var myIdPriv = self.getMyCopayerIdPriv();
|
|
|
|
var startOpts = {
|
|
copayerId: myId,
|
|
privkey: myIdPriv,
|
|
maxPeers: self.totalCopayers,
|
|
lastTimestamp: this.lastTimestamp,
|
|
secretNumber: self.secretNumber,
|
|
};
|
|
|
|
if (this.publicKeyRing.isComplete()) {
|
|
this._lockIncomming();
|
|
}
|
|
|
|
net.start(startOpts, function() {
|
|
self.emit('ready', net.getPeer());
|
|
setTimeout(function() {
|
|
self.emit('publicKeyRingUpdated', true);
|
|
// no connection logic for now
|
|
self.emit('txProposalsUpdated');
|
|
}, 10);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieves the public keys of all the copayers in the ring
|
|
* @return {string[]} hex-encoded public keys of copayers
|
|
*/
|
|
Wallet.prototype.getRegisteredCopayerIds = function() {
|
|
var l = this.publicKeyRing.registeredCopayers();
|
|
var copayers = [];
|
|
for (var i = 0; i < l; i++) {
|
|
var cid = this.getCopayerId(i);
|
|
copayers.push(cid);
|
|
}
|
|
return copayers;
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieves the public keys of all the peers in the network
|
|
* @TODO: Isn't this deprecated? Now that we don't use peerjs
|
|
*
|
|
* @return {string[]} hex-encoded public keys of copayers
|
|
*/
|
|
Wallet.prototype.getRegisteredPeerIds = function() {
|
|
var l = this.publicKeyRing.registeredCopayers();
|
|
if (this.registeredPeerIds.length !== l) {
|
|
this.registeredPeerIds = [];
|
|
var copayers = this.getRegisteredCopayerIds();
|
|
for (var i = 0; i < l; i++) {
|
|
var cid = copayers[i];
|
|
var pid = this.network.peerFromCopayer(cid);
|
|
this.registeredPeerIds.push({
|
|
peerId: pid,
|
|
copayerId: cid,
|
|
nick: this.publicKeyRing.nicknameForCopayer(cid),
|
|
index: i,
|
|
});
|
|
}
|
|
}
|
|
return this.registeredPeerIds;
|
|
};
|
|
|
|
/**
|
|
* @TODO: Review design of this call
|
|
* @desc Send a keepalive to this wallet's {@link WalletLock} instance.
|
|
*
|
|
* @emits locked - in case the wallet is opened in another instance
|
|
*/
|
|
Wallet.prototype.keepAlive = function() {
|
|
try {
|
|
this.lock.keepAlive();
|
|
} catch (e) {
|
|
log.debug(e);
|
|
this.emit('locked', null, 'Wallet appears to be openned on other browser instance. Closing this one.');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc Store the wallet's state
|
|
*/
|
|
Wallet.prototype.store = function() {
|
|
this.keepAlive();
|
|
|
|
var wallet = this.toObj();
|
|
this.storage.setFromObj(this.id, wallet);
|
|
log.debug('Wallet stored');
|
|
};
|
|
|
|
/**
|
|
* @desc Serialize the wallet into a plain object.
|
|
* @return {Object}
|
|
*/
|
|
Wallet.prototype.toObj = function() {
|
|
var optsObj = this._optsToObj();
|
|
|
|
var networkNonce = this.network.getHexNonce();
|
|
var networkNonces = this.network.getHexNonces();
|
|
|
|
var walletObj = {
|
|
opts: optsObj,
|
|
networkNonce: networkNonce, //yours
|
|
networkNonces: networkNonces, //copayers
|
|
publicKeyRing: this.publicKeyRing.toObj(),
|
|
txProposals: this.txProposals.toObj(),
|
|
privateKey: this.privateKey ? this.privateKey.toObj() : undefined,
|
|
addressBook: this.addressBook,
|
|
lastTimestamp: this.lastTimestamp,
|
|
};
|
|
|
|
return walletObj;
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve the wallet state from a trusted object
|
|
*
|
|
* @param {Object} o
|
|
* @param {Object[]} o.addressBook - Stores known associations of bitcoin addresses to names
|
|
* @param {Object} o.privateKey - Private key to be deserialized by {@link PrivateKey#fromObj}
|
|
* @param {string} o.networkName - 'livenet' or 'testnet'
|
|
* @param {Object} o.publicKeyRing - PublicKeyRing to be deserialized by {@link PublicKeyRing#fromObj}
|
|
* @param {number} o.lastTimestamp - last time this wallet object was deserialized
|
|
* @param {Object} o.txProposals - TxProposals to be deserialized by {@link TxProposals#fromObj}
|
|
* @param {string} o.nickname - user's nickname
|
|
* @param {Storage} storage - a Storage instance to store the data of the wallet
|
|
* @param {Network} network - a Network instance to communicate with peers
|
|
* @param {Blockchain} blockchain - a Blockchain instance to retrieve state from the blockchain
|
|
*/
|
|
Wallet.fromObj = function(o, storage, network, blockchain) {
|
|
|
|
// TODO: What is this supposed to do?
|
|
var opts = JSON.parse(JSON.stringify(o.opts));
|
|
|
|
opts.addressBook = o.addressBook;
|
|
|
|
if (o.privateKey) {
|
|
opts.privateKey = PrivateKey.fromObj(o.privateKey);
|
|
} else {
|
|
opts.privateKey = new PrivateKey({
|
|
networkName: opts.networkName
|
|
});
|
|
}
|
|
|
|
if (o.publicKeyRing) {
|
|
opts.publicKeyRing = PublicKeyRing.fromObj(o.publicKeyRing);
|
|
} else {
|
|
opts.publicKeyRing = new PublicKeyRing({
|
|
networkName: opts.networkName,
|
|
requiredCopayers: opts.requiredCopayers,
|
|
totalCopayers: opts.totalCopayers,
|
|
});
|
|
opts.publicKeyRing.addCopayer(
|
|
opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(),
|
|
opts.nickname
|
|
);
|
|
}
|
|
|
|
if (o.txProposals) {
|
|
opts.txProposals = TxProposals.fromObj(o.txProposals, Wallet.builderOpts);
|
|
} else {
|
|
opts.txProposals = new TxProposals({
|
|
networkName: this.networkName,
|
|
});
|
|
}
|
|
|
|
opts.lastTimestamp = o.lastTimestamp;
|
|
|
|
opts.storage = storage;
|
|
opts.network = network;
|
|
opts.blockchain = blockchain;
|
|
|
|
return new Wallet(opts);
|
|
};
|
|
|
|
/**
|
|
* @desc Return a base64 encrypted version of the wallet
|
|
* @return {string} base64 encoded string
|
|
*/
|
|
Wallet.prototype.toEncryptedObj = function() {
|
|
var walletObj = this.toObj();
|
|
return this.storage.export(walletObj);
|
|
};
|
|
|
|
/**
|
|
* @desc Send a message to other peers
|
|
* @param {string[]} recipients - the pubkey of the recipients of the message
|
|
* @param {Object} obj - the data to be sent to them
|
|
*/
|
|
Wallet.prototype.send = function(recipients, obj) {
|
|
this.network.send(recipients, obj);
|
|
};
|
|
|
|
/**
|
|
* @desc Send the set of TxProposals to some peers
|
|
* @param {string[]} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendAllTxProposals = function(recipients) {
|
|
var ntxids = this.txProposals.getNtxids(),
|
|
that = this;
|
|
_.each(ntxids, function(ntxid, key) {
|
|
that.sendTxProposal(ntxid, recipients);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Send a TxProposal identified by transaction id to a set of recipients
|
|
* @param {string} ntxid - the transaction proposal id
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
|
|
preconditions.checkArgument(ntxid);
|
|
|
|
log.debug('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals);
|
|
this.send(recipients, {
|
|
type: 'txProposal',
|
|
txProposal: this.txProposals.get(ntxid).toObjTrim(),
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Notifies other peers that a transaction proposal was seen
|
|
* @param {string} ntxid
|
|
*/
|
|
Wallet.prototype.sendSeen = function(ntxid) {
|
|
preconditions.checkArgument(ntxid);
|
|
log.debug('### SENDING seen: ' + ntxid + ' TO: All');
|
|
this.send(null, {
|
|
type: 'seen',
|
|
ntxid: ntxid,
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Notifies other peers that a transaction proposal was rejected
|
|
* @param {string} ntxid
|
|
*/
|
|
Wallet.prototype.sendReject = function(ntxid) {
|
|
preconditions.checkArgument(ntxid);
|
|
log.debug('### SENDING reject: ' + ntxid + ' TO: All');
|
|
this.send(null, {
|
|
type: 'reject',
|
|
ntxid: ntxid,
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Notify other peers that a wallet has been backed up and it's ready to be used
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendWalletReady = function(recipients) {
|
|
log.debug('### SENDING WalletReady TO:', recipients || 'All');
|
|
|
|
this.send(recipients, {
|
|
type: 'walletReady',
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Notify other peers of the walletId
|
|
* @TODO: Why is this needed? Can't everybody just calculate the walletId?
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendWalletId = function(recipients) {
|
|
log.debug('### SENDING walletId TO:', recipients || 'All', this.id);
|
|
|
|
this.send(recipients, {
|
|
type: 'walletId',
|
|
walletId: this.id,
|
|
opts: this._optsToObj(),
|
|
networkName: this.getNetworkName(),
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Send the current PublicKeyRing to other recipients
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendPublicKeyRing = function(recipients) {
|
|
log.debug('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj());
|
|
var publicKeyRing = this.publicKeyRing.toObj();
|
|
|
|
this.send(recipients, {
|
|
type: 'publicKeyRing',
|
|
publicKeyRing: publicKeyRing,
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Send the current indexes of our public key ring to other peers
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendIndexes = function(recipients) {
|
|
var indexes = HDParams.serialize(this.publicKeyRing.indexes);
|
|
log.debug('### INDEXES TO:', recipients || 'All', indexes);
|
|
|
|
this.send(recipients, {
|
|
type: 'indexes',
|
|
indexes: indexes,
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Send our addressBook to other recipients
|
|
* @param {string[]=} recipients - the pubkeys of the recipients
|
|
*/
|
|
Wallet.prototype.sendAddressBook = function(recipients) {
|
|
log.debug('### SENDING addressBook TO:', recipients || 'All', this.addressBook);
|
|
this.send(recipients, {
|
|
type: 'addressbook',
|
|
addressBook: this.addressBook,
|
|
walletId: this.id,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve this wallet's name
|
|
* @return {string}
|
|
*/
|
|
Wallet.prototype.getName = function() {
|
|
return this.name || this.id;
|
|
};
|
|
|
|
/**
|
|
* @desc Generate a new address
|
|
* @param {boolean} isChange - whether to generate a change address or a receive address
|
|
* @return {string[]} a list of all the addresses generated so far for the wallet
|
|
*/
|
|
Wallet.prototype._doGenerateAddress = function(isChange) {
|
|
return this.publicKeyRing.generateAddress(isChange, this.publicKey);
|
|
};
|
|
|
|
/**
|
|
* @callback addressCallback
|
|
* @param {string} addr - all the addresses of the wallet
|
|
*/
|
|
/**
|
|
* @desc Generate a new address
|
|
* @param {boolean} isChange - whether to generate a change address or a receive address
|
|
* @param {addressCallback} cb
|
|
* @return {string[]} a list of all the addresses generated so far for the wallet
|
|
*/
|
|
Wallet.prototype.generateAddress = function(isChange, cb) {
|
|
var addr = this._doGenerateAddress(isChange);
|
|
this.sendIndexes();
|
|
this.store();
|
|
if (cb) return cb(addr);
|
|
return addr;
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieve all the Transaction proposals (see {@link TxProposals})
|
|
* @return {Object[]} each object returned represents a transaction proposal, with two additional
|
|
* booleans: <tt>signedByUs</tt> and <tt>rejectedByUs</tt>. An optional third boolean signals
|
|
* whether the transaction was finally rejected (<tt>finallyRejected</tt> set to true).
|
|
*/
|
|
Wallet.prototype.getTxProposals = function() {
|
|
var ret = [];
|
|
var copayers = this.getRegisteredCopayerIds();
|
|
for (var ntxid in this.txProposals.txps) {
|
|
var txp = this.txProposals.getTxProposal(ntxid, copayers);
|
|
txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false;
|
|
txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false;
|
|
if (this.totalCopayers - txp.rejectCount < this.requiredCopayers) {
|
|
txp.finallyRejected = true;
|
|
}
|
|
|
|
if (!txp.readonly || txp.finallyRejected || txp.sentTs) {
|
|
ret.push(txp);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* @desc Removes old transactions
|
|
* @param {boolean} deleteAll - if true, remove all the transactions
|
|
* @return {number} the number of deleted proposals
|
|
*/
|
|
Wallet.prototype.purgeTxProposals = function(deleteAll) {
|
|
var m = this.txProposals.length();
|
|
|
|
if (deleteAll) {
|
|
this.txProposals.deleteAll();
|
|
} else {
|
|
this.txProposals.deletePending(this.maxRejectCount());
|
|
}
|
|
this.store();
|
|
|
|
var n = this.txProposals.length();
|
|
return m - n;
|
|
};
|
|
|
|
/**
|
|
* @desc Reject a proposal
|
|
* @param {string} ntxid the id of the transaction proposal to reject
|
|
* @emits txProposalsUpdated
|
|
*/
|
|
Wallet.prototype.reject = function(ntxid) {
|
|
var txp = this.txProposals.reject(ntxid, this.getMyCopayerId());
|
|
this.sendReject(ntxid);
|
|
this.store();
|
|
this.emit('txProposalsUpdated');
|
|
};
|
|
|
|
/**
|
|
* @callback signCallback
|
|
* @param {boolean} ret - true if it was successfully signed
|
|
*/
|
|
/**
|
|
* @desc Sign a proposal
|
|
* @param {string} ntxid the id of the transaction proposal to sign
|
|
* @param {signCallback} cb - a callback to be called on successful signing
|
|
* @emits txProposalsUpdated
|
|
*/
|
|
Wallet.prototype.sign = function(ntxid, cb) {
|
|
preconditions.checkState(!_.isUndefined(this.getMyCopayerId()));
|
|
var self = this;
|
|
setTimeout(function() {
|
|
var myId = self.getMyCopayerId();
|
|
var txp = self.txProposals.get(ntxid);
|
|
// if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
|
|
// if (cb) cb(false);
|
|
// }
|
|
//
|
|
|
|
// If this is a payment protocol request,
|
|
// ensure it hasn't been tampered with.
|
|
if (!self.verifyPaymentRequest(ntxid)) {
|
|
if (cb) cb(false);
|
|
return;
|
|
}
|
|
|
|
var keys = self.privateKey.getForPaths(txp.inputChainPaths);
|
|
|
|
var b = txp.builder;
|
|
var before = txp.countSignatures();
|
|
b.sign(keys);
|
|
|
|
var ret = false;
|
|
if (txp.countSignatures() > before) {
|
|
txp.signedBy[myId] = Date.now();
|
|
self.sendTxProposal(ntxid);
|
|
self.store();
|
|
self.emit('txProposalsUpdated');
|
|
ret = true;
|
|
}
|
|
if (cb) return cb(ret);
|
|
}, 10);
|
|
};
|
|
|
|
/**
|
|
* @callback broadcastCallback
|
|
* @param {string} txid - the transaction id on the blockchain
|
|
*/
|
|
/**
|
|
* @desc Broadcasts a transaction to the blockchain
|
|
* @param {string} ntxid - the transaction proposal id
|
|
* @param {broadcastCallback} cb
|
|
*/
|
|
Wallet.prototype.sendTx = function(ntxid, cb) {
|
|
var txp = this.txProposals.get(ntxid);
|
|
|
|
if (txp.merchant) {
|
|
return this.sendPaymentTx(ntxid, cb);
|
|
}
|
|
var tx = txp.builder.build();
|
|
if (!tx.isComplete())
|
|
throw new Error('Tx is not complete. Can not broadcast');
|
|
log.debug('Broadcasting Transaction');
|
|
var scriptSig = tx.ins[0].getScript();
|
|
var size = scriptSig.serialize().length;
|
|
|
|
var txHex = tx.serialize().toString('hex');
|
|
log.debug('Raw transaction: ', txHex);
|
|
|
|
var self = this;
|
|
this.blockchain.broadcast(txHex, function(err, txid) {
|
|
log.debug('BITCOIND txid:', txid);
|
|
if (txid) {
|
|
self.txProposals.get(ntxid).setSent(txid);
|
|
self.sendTxProposal(ntxid);
|
|
self.store();
|
|
return cb(txid);
|
|
} else {
|
|
log.debug('Sent failed. Checking if the TX was sent already');
|
|
self._checkSentTx(ntxid, function(txid) {
|
|
if (txid)
|
|
self.store();
|
|
|
|
return cb(txid);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Create a Payment Protocol transaction
|
|
* @param {Object|string} options - if it's a string, parse it as the uri
|
|
* @param {string} options.uri the url for the transaction
|
|
* @param {Function} cb
|
|
*/
|
|
Wallet.prototype.createPaymentTx = function(options, cb) {
|
|
var self = this;
|
|
|
|
if (_.isString(options)) {
|
|
options = {
|
|
uri: options
|
|
};
|
|
}
|
|
options.uri = options.uri || options.url;
|
|
|
|
if (options.uri.indexOf('bitcoin:') === 0) {
|
|
options.uri = new bitcore.BIP21(options.uri).data.merchant;
|
|
if (!options.uri) {
|
|
return cb(new Error('No URI.'));
|
|
}
|
|
}
|
|
|
|
var req = this.paymentRequests[options.uri];
|
|
if (req) {
|
|
delete this.paymentRequests[options.uri];
|
|
this.receivePaymentRequest(options, req.pr, cb);
|
|
return;
|
|
}
|
|
|
|
return Wallet.request({
|
|
method: 'GET',
|
|
url: options.uri,
|
|
headers: {
|
|
'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE
|
|
},
|
|
responseType: 'arraybuffer'
|
|
})
|
|
.success(function(data, status, headers, config) {
|
|
data = PayPro.PaymentRequest.decode(data);
|
|
var pr = new PayPro();
|
|
pr = pr.makePaymentRequest(data);
|
|
return self.receivePaymentRequest(options, pr, cb);
|
|
})
|
|
.error(function(data, status, headers, config) {
|
|
return cb(new Error('Status: ' + JSON.stringify(status)));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Creates a Payment TxProposal from a uri
|
|
* @param {Object} options
|
|
* @param {string=} options.uri
|
|
* @param {string=} options.url
|
|
* @param {Function} cb
|
|
*/
|
|
Wallet.prototype.fetchPaymentTx = function(options, cb) {
|
|
var self = this;
|
|
|
|
options = options || {};
|
|
if (_.isString(options)) {
|
|
options = {
|
|
uri: options
|
|
};
|
|
}
|
|
options.uri = options.uri || options.url;
|
|
options.fetch = true;
|
|
|
|
var req = this.paymentRequests[options.uri];
|
|
if (req) {
|
|
return cb(null, req.merchantData);
|
|
}
|
|
|
|
return this.createPaymentTx(options, function(err, merchantData, pr) {
|
|
if (err) return cb(err);
|
|
self.paymentRequests[options.uri] = {
|
|
merchantData: merchantData,
|
|
pr: pr
|
|
};
|
|
return cb(null, merchantData);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Analyzes a payment request and generates a transaction proposal for it.
|
|
* @param {Object} options
|
|
* @param {PayProRequest} pr
|
|
* @param {string} pr.payment_details_version
|
|
* @param {string} pr.pki_type
|
|
* @param {Object} pr.data
|
|
* @param {string} pr.serialized_payment_details
|
|
* @param {string} pr.signature
|
|
* @param {string} options.memo
|
|
* @param {string} options.comment
|
|
* @param {Function} cb
|
|
*/
|
|
Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
|
var self = this;
|
|
|
|
var ver = pr.get('payment_details_version');
|
|
var pki_type = pr.get('pki_type');
|
|
var pki_data = pr.get('pki_data');
|
|
var details = pr.get('serialized_payment_details');
|
|
var sig = pr.get('signature');
|
|
|
|
var certs = PayPro.X509Certificates.decode(pki_data);
|
|
certs = certs.certificate;
|
|
|
|
// Fix for older versions of bitcore
|
|
if (!PayPro.RootCerts) {
|
|
PayPro.RootCerts = {
|
|
getTrusted: function() {}
|
|
};
|
|
}
|
|
|
|
var trusted = certs.map(function(cert) {
|
|
var der = cert.toString('hex');
|
|
var pem = PayPro.prototype._DERtoPEM(der, 'CERTIFICATE');
|
|
return PayPro.RootCerts.getTrusted(pem);
|
|
}).filter(Boolean);
|
|
|
|
// Verify Signature
|
|
var verified = pr.verify();
|
|
|
|
if (!verified) {
|
|
return cb(new Error('Server sent a bad signature.'));
|
|
}
|
|
|
|
var ca = trusted[0];
|
|
|
|
details = PayPro.PaymentDetails.decode(details);
|
|
var pd = new PayPro();
|
|
pd = pd.makePaymentDetails(details);
|
|
|
|
var network = pd.get('network');
|
|
var outputs = pd.get('outputs');
|
|
var time = pd.get('time');
|
|
var expires = pd.get('expires');
|
|
var memo = pd.get('memo');
|
|
var payment_url = pd.get('payment_url');
|
|
var merchant_data = pd.get('merchant_data');
|
|
|
|
var merchantData = {
|
|
pr: {
|
|
payment_details_version: ver,
|
|
pki_type: pki_type,
|
|
pki_data: certs,
|
|
pd: {
|
|
network: network,
|
|
outputs: outputs.map(function(output) {
|
|
return {
|
|
amount: output.get('amount'),
|
|
script: {
|
|
offset: output.get('script').offset,
|
|
limit: output.get('script').limit,
|
|
// NOTE: For some reason output.script.buffer
|
|
// is only an ArrayBuffer
|
|
buffer: new Buffer(new Uint8Array(
|
|
output.get('script').buffer)).toString('hex')
|
|
}
|
|
};
|
|
}),
|
|
time: time,
|
|
expires: expires,
|
|
memo: memo || 'This server would like some BTC from you.',
|
|
payment_url: payment_url,
|
|
merchant_data: merchant_data.toString('hex')
|
|
},
|
|
signature: sig.toString('hex'),
|
|
ca: ca,
|
|
untrusted: !ca
|
|
},
|
|
request_url: options.uri,
|
|
total: bignum('0', 10).toString(10),
|
|
// Expose so other copayers can verify signature
|
|
// and identity, not to mention data.
|
|
raw: pr.serialize().toString('hex')
|
|
};
|
|
|
|
return this.getUnspent(function(err, safeUnspent, unspent) {
|
|
if (options.fetch) {
|
|
if (!unspent || !unspent.length) {
|
|
return cb(new Error('No unspent outputs available.'));
|
|
}
|
|
try {
|
|
self.createPaymentTxSync(options, merchantData, safeUnspent);
|
|
} catch (e) {
|
|
var msg = e.message || '';
|
|
if (msg.indexOf('not enough unspent tx outputs to fulfill')) {
|
|
var sat = /(\d+)/.exec(msg)[1];
|
|
e = new Error('No unspent outputs available.');
|
|
e.amount = sat;
|
|
return cb(e);
|
|
}
|
|
}
|
|
return cb(null, merchantData, pr);
|
|
}
|
|
|
|
var ntxid = self.createPaymentTxSync(options, merchantData, safeUnspent);
|
|
if (ntxid) {
|
|
self.sendIndexes();
|
|
self.sendTxProposal(ntxid);
|
|
self.store();
|
|
self.emit('txProposalsUpdated');
|
|
}
|
|
|
|
log.debug('You are currently on this BTC network:', network);
|
|
log.debug('The server sent you a message:', memo);
|
|
|
|
return cb(ntxid, merchantData);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Send a payment transaction to a server, complying with BIP70
|
|
*
|
|
* @TODO: Get this out of here.
|
|
*
|
|
* @param {string} ntxid - the transaction proposal id
|
|
* @param {Object} options
|
|
* @param {string} options.refund_to
|
|
* @param {string} options.memo
|
|
* @param {string} options.comment
|
|
* @param {Function} cb
|
|
*/
|
|
Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) {
|
|
var self = this;
|
|
|
|
if (!cb) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
var txp = this.txProposals.get(ntxid);
|
|
if (!txp) return;
|
|
|
|
var tx = txp.builder.build();
|
|
if (!tx.isComplete()) return;
|
|
log.debug('Sending Transaction');
|
|
|
|
var refund_outputs = [];
|
|
|
|
options.refund_to = options.refund_to || this.publicKeyRing.getPubKeys(0, false, this.getMyCopayerId())[0];
|
|
|
|
if (options.refund_to) {
|
|
var total = txp.merchant.pr.pd.outputs.reduce(function(total, _, i) {
|
|
// XXX reverse endianness to work around bignum bug:
|
|
var txv = tx.outs[i].v;
|
|
var v = new Buffer(8);
|
|
for (var j = 0; j < 8; j++) v[j] = txv[7 - j];
|
|
return total.add(bignum.fromBuffer(v, {
|
|
endian: 'big',
|
|
size: 1
|
|
}));
|
|
}, bignum('0', 10));
|
|
|
|
var rpo = new PayPro();
|
|
rpo = rpo.makeOutput();
|
|
|
|
// XXX Bad - the amount *has* to be a Number in protobufjs
|
|
// Possibly does not matter - server can ignore the amount anyway.
|
|
rpo.set('amount', +total.toString(10));
|
|
|
|
rpo.set('script',
|
|
Buffer.concat([
|
|
new Buffer([
|
|
118, // OP_DUP
|
|
169, // OP_HASH160
|
|
76, // OP_PUSHDATA1
|
|
20, // number of bytes
|
|
]),
|
|
// needs to be ripesha'd
|
|
bitcore.util.sha256ripe160(options.refund_to),
|
|
new Buffer([
|
|
136, // OP_EQUALVERIFY
|
|
172 // OP_CHECKSIG
|
|
])
|
|
])
|
|
);
|
|
|
|
refund_outputs.push(rpo.message);
|
|
}
|
|
|
|
// We send this to the serve after receiving a PaymentRequest
|
|
var pay = new PayPro();
|
|
pay = pay.makePayment();
|
|
var merchant_data = txp.merchant.pr.pd.merchant_data;
|
|
merchant_data = new Buffer(merchant_data, 'hex');
|
|
pay.set('merchant_data', merchant_data);
|
|
pay.set('transactions', [tx.serialize()]);
|
|
pay.set('refund_to', refund_outputs);
|
|
|
|
options.memo = options.memo || options.comment || 'Hi server, I would like to give you some money.';
|
|
|
|
pay.set('memo', options.memo);
|
|
|
|
pay = pay.serialize();
|
|
|
|
log.debug('Sending Payment Message:', pay.toString('hex'));
|
|
|
|
var buf = new ArrayBuffer(pay.length);
|
|
var view = new Uint8Array(buf);
|
|
for (var i = 0; i < pay.length; i++) {
|
|
view[i] = pay[i];
|
|
}
|
|
|
|
return Wallet.request({
|
|
method: 'POST',
|
|
url: txp.merchant.pr.pd.payment_url,
|
|
headers: {
|
|
// BIP-71
|
|
'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE,
|
|
'Content-Type': PayPro.PAYMENT_CONTENT_TYPE
|
|
// XHR does not allow these:
|
|
// 'Content-Length': (pay.byteLength || pay.length) + '',
|
|
// 'Content-Transfer-Encoding': 'binary'
|
|
},
|
|
// Technically how this should be done via XHR (used to
|
|
// be the ArrayBuffer, now you send the View instead).
|
|
data: view,
|
|
responseType: 'arraybuffer'
|
|
})
|
|
.success(function(data, status, headers, config) {
|
|
data = PayPro.PaymentACK.decode(data);
|
|
var ack = new PayPro();
|
|
ack = ack.makePaymentACK(data);
|
|
return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb);
|
|
})
|
|
.error(function(data, status, headers, config) {
|
|
return cb(new Error('Status: ' + JSON.stringify(status)));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Handles a PaymentRequestACK from the server
|
|
*/
|
|
Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) {
|
|
var self = this;
|
|
|
|
var payment = ack.get('payment');
|
|
var memo = ack.get('memo');
|
|
|
|
log.debug('Our payment was acknowledged!');
|
|
log.debug('Message from Merchant: %s', memo);
|
|
|
|
payment = PayPro.Payment.decode(payment);
|
|
var pay = new PayPro();
|
|
payment = pay.makePayment(payment);
|
|
|
|
txp.merchant.ack = {
|
|
memo: memo
|
|
};
|
|
|
|
var tx = payment.message.transactions[0];
|
|
|
|
if (!tx) {
|
|
log.debug('Sending to server was not met with a returned tx.');
|
|
return this._checkSentTx(ntxid, function(txid) {
|
|
self.log('[Wallet.js.1048:txid:%s]', txid);
|
|
if (txid) self.store();
|
|
return cb(txid, txp.merchant);
|
|
});
|
|
}
|
|
|
|
if (tx.buffer) {
|
|
tx.buffer = new Buffer(new Uint8Array(tx.buffer));
|
|
tx.buffer = tx.buffer.slice(tx.offset, tx.limit);
|
|
var ptx = new bitcore.Transaction();
|
|
ptx.parse(tx.buffer);
|
|
tx = ptx;
|
|
}
|
|
|
|
var txid = tx.getHash().toString('hex');
|
|
var txHex = tx.serialize().toString('hex');
|
|
log.debug('Raw transaction: ', txHex);
|
|
log.debug('BITCOIND txid:', txid);
|
|
this.txProposals.get(ntxid).setSent(txid);
|
|
this.sendTxProposal(ntxid);
|
|
this.store();
|
|
|
|
return cb(txid, txp.merchant);
|
|
};
|
|
|
|
/**
|
|
* @desc Create a Payment Transaction Sync (see BIP70)
|
|
* @TODO: Document better
|
|
*/
|
|
Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) {
|
|
var self = this;
|
|
var priv = this.privateKey;
|
|
var pkr = this.publicKeyRing;
|
|
|
|
preconditions.checkState(pkr.isComplete());
|
|
if (options.memo) {
|
|
preconditions.checkArgument(options.memo.length <= 100);
|
|
}
|
|
|
|
var opts = {
|
|
remainderOut: {
|
|
address: this._doGenerateAddress(true).toString()
|
|
}
|
|
};
|
|
|
|
if (_.isUndefined(opts.spendUnconfirmed)) {
|
|
opts.spendUnconfirmed = this.spendUnconfirmed;
|
|
}
|
|
|
|
for (var k in Wallet.builderOpts) {
|
|
opts[k] = Wallet.builderOpts[k];
|
|
}
|
|
|
|
merchantData.total = bignum(merchantData.total, 10);
|
|
|
|
var outs = [];
|
|
merchantData.pr.pd.outputs.forEach(function(output) {
|
|
var amount = output.amount;
|
|
|
|
// big endian
|
|
var v = new Buffer(8);
|
|
v[0] = (amount.high >> 24) & 0xff;
|
|
v[1] = (amount.high >> 16) & 0xff;
|
|
v[2] = (amount.high >> 8) & 0xff;
|
|
v[3] = (amount.high >> 0) & 0xff;
|
|
v[4] = (amount.low >> 24) & 0xff;
|
|
v[5] = (amount.low >> 16) & 0xff;
|
|
v[6] = (amount.low >> 8) & 0xff;
|
|
v[7] = (amount.low >> 0) & 0xff;
|
|
|
|
var script = {
|
|
offset: output.script.offset,
|
|
limit: output.script.limit,
|
|
buffer: new Buffer(output.script.buffer, 'hex')
|
|
};
|
|
var s = script.buffer.slice(script.offset, script.limit);
|
|
var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet';
|
|
var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network);
|
|
|
|
outs.push({
|
|
address: addr[0].toString(),
|
|
amountSatStr: bignum.fromBuffer(v, {
|
|
endian: 'big',
|
|
size: 1
|
|
}).toString(10)
|
|
});
|
|
|
|
merchantData.total = merchantData.total.add(bignum.fromBuffer(v, {
|
|
endian: 'big',
|
|
size: 1
|
|
}));
|
|
});
|
|
|
|
merchantData.total = merchantData.total.toString(10);
|
|
|
|
var b = new Builder(opts)
|
|
.setUnspent(unspent)
|
|
.setOutputs(outs);
|
|
|
|
merchantData.pr.pd.outputs.forEach(function(output, i) {
|
|
var script = {
|
|
offset: output.script.offset,
|
|
limit: output.script.limit,
|
|
buffer: new Buffer(output.script.buffer, 'hex')
|
|
};
|
|
var s = script.buffer.slice(script.offset, script.limit);
|
|
b.tx.outs[i].s = s;
|
|
});
|
|
|
|
var selectedUtxos = b.getSelectedUnspent();
|
|
var inputChainPaths = selectedUtxos.map(function(utxo) {
|
|
return pkr.pathForAddress(utxo.address);
|
|
});
|
|
|
|
b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
|
|
|
|
var keys = priv.getForPaths(inputChainPaths);
|
|
var signed = b.sign(keys);
|
|
|
|
if (options.fetch) return;
|
|
|
|
log.debug('Created transaction: %s', b.tx.getStandardizedObject());
|
|
|
|
var myId = this.getMyCopayerId();
|
|
var now = Date.now();
|
|
|
|
var tx = b.build();
|
|
if (!tx.countInputSignatures(0))
|
|
throw new Error('Could not sign generated tx');
|
|
|
|
var me = {};
|
|
me[myId] = now;
|
|
var meSeen = {};
|
|
if (priv) meSeen[myId] = now;
|
|
|
|
var ntxid = this.txProposals.add(new TxProposal({
|
|
inputChainPaths: inputChainPaths,
|
|
signedBy: me,
|
|
seenBy: meSeen,
|
|
creator: myId,
|
|
createdTs: now,
|
|
builder: b,
|
|
comment: options.memo,
|
|
merchant: merchantData
|
|
}));
|
|
return ntxid;
|
|
};
|
|
|
|
/**
|
|
* @desc Verifies a PaymentRequest sent by another peer
|
|
* This essentially ensures that a copayer hasn't tampered with a
|
|
* PaymentRequest message from a payment server. It verifies the signature
|
|
* based on the cert, and checks to ensure the desired outputs are the same as
|
|
* the ones on the tx proposal.
|
|
* @TODO: Document better
|
|
*/
|
|
Wallet.prototype.verifyPaymentRequest = function(ntxid) {
|
|
if (!ntxid) return false;
|
|
|
|
var txp = _.isObject(ntxid) ? ntxid : this.txProposals.get(ntxid);
|
|
|
|
// If we're not a payment protocol proposal, ignore.
|
|
if (!txp.merchant) return true;
|
|
|
|
// The copayer didn't send us the raw payment request, unverifiable.
|
|
if (!txp.merchant.raw) return false;
|
|
|
|
// var tx = txp.builder.tx;
|
|
var tx = txp.builder.build();
|
|
|
|
var data = new Buffer(txp.merchant.raw, 'hex');
|
|
data = PayPro.PaymentRequest.decode(data);
|
|
var pr = new PayPro();
|
|
pr = pr.makePaymentRequest(data);
|
|
|
|
// Verify the signature so we know this is the real request.
|
|
if (!pr.verify()) {
|
|
// Signature does not match cert. It may have
|
|
// been modified by an untrustworthy person.
|
|
// We should not sign this transaction proposal!
|
|
return false;
|
|
}
|
|
|
|
var details = pr.get('serialized_payment_details');
|
|
details = PayPro.PaymentDetails.decode(details);
|
|
var pd = new PayPro();
|
|
pd = pd.makePaymentDetails(details);
|
|
|
|
var outputs = pd.get('outputs');
|
|
|
|
if (tx.outs.length < outputs.length) {
|
|
// Outputs do not and cannot match.
|
|
return false;
|
|
}
|
|
|
|
// Figure out whether the user is supposed
|
|
// to decide the value of the outputs.
|
|
var undecided = false;
|
|
var total = bignum('0', 10);
|
|
for (var i = 0; i < outputs.length; i++) {
|
|
var output = outputs[i];
|
|
var amount = output.get('amount');
|
|
// big endian
|
|
var v = new Buffer(8);
|
|
v[0] = (amount.high >> 24) & 0xff;
|
|
v[1] = (amount.high >> 16) & 0xff;
|
|
v[2] = (amount.high >> 8) & 0xff;
|
|
v[3] = (amount.high >> 0) & 0xff;
|
|
v[4] = (amount.low >> 24) & 0xff;
|
|
v[5] = (amount.low >> 16) & 0xff;
|
|
v[6] = (amount.low >> 8) & 0xff;
|
|
v[7] = (amount.low >> 0) & 0xff;
|
|
total = total.add(bignum.fromBuffer(v, {
|
|
endian: 'big',
|
|
size: 1
|
|
}));
|
|
}
|
|
if (+total.toString(10) === 0) {
|
|
undecided = true;
|
|
}
|
|
|
|
for (var i = 0; i < outputs.length; i++) {
|
|
var output = outputs[i];
|
|
|
|
var amount = output.get('amount');
|
|
var script = {
|
|
offset: output.get('script').offset,
|
|
limit: output.get('script').limit,
|
|
buffer: new Buffer(new Uint8Array(output.get('script').buffer))
|
|
};
|
|
|
|
// Expected value
|
|
// little endian (keep this LE to compare with tx output value)
|
|
var ev = new Buffer(8);
|
|
ev[0] = (amount.low >> 0) & 0xff;
|
|
ev[1] = (amount.low >> 8) & 0xff;
|
|
ev[2] = (amount.low >> 16) & 0xff;
|
|
ev[3] = (amount.low >> 24) & 0xff;
|
|
ev[4] = (amount.high >> 0) & 0xff;
|
|
ev[5] = (amount.high >> 8) & 0xff;
|
|
ev[6] = (amount.high >> 16) & 0xff;
|
|
ev[7] = (amount.high >> 24) & 0xff;
|
|
|
|
// Expected script
|
|
var es = script.buffer.slice(script.offset, script.limit);
|
|
|
|
// Actual value
|
|
var av = tx.outs[i].v;
|
|
|
|
// Actual script
|
|
var as = tx.outs[i].s;
|
|
|
|
// XXX allow changing of script as long as address is same
|
|
// var as = es;
|
|
|
|
// XXX allow changing of script as long as address is same
|
|
// var network = pd.get('network') === 'main' ? 'livenet' : 'testnet';
|
|
// var es = bitcore.Address.fromScriptPubKey(new bitcore.Script(es), network)[0];
|
|
// var as = bitcore.Address.fromScriptPubKey(new bitcore.Script(tx.outs[i].s), network)[0];
|
|
|
|
if (undecided) {
|
|
av = ev = new Buffer([0]);
|
|
}
|
|
|
|
// Make sure the tx's output script and values match the payment request's.
|
|
if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) {
|
|
// Verifiable outputs do not match outputs of merchant
|
|
// data. We should not sign this transaction proposal!
|
|
return false;
|
|
}
|
|
|
|
// Checking the merchant data itself isn't technically
|
|
// necessary as long as we check the transaction, but
|
|
// we can do it for good measure.
|
|
var ro = txp.merchant.pr.pd.outputs[i];
|
|
|
|
// Actual value
|
|
// little endian (keep this LE to compare with the ev above)
|
|
var av = new Buffer(8);
|
|
av[0] = (ro.amount.low >> 0) & 0xff;
|
|
av[1] = (ro.amount.low >> 8) & 0xff;
|
|
av[2] = (ro.amount.low >> 16) & 0xff;
|
|
av[3] = (ro.amount.low >> 24) & 0xff;
|
|
av[4] = (ro.amount.high >> 0) & 0xff;
|
|
av[5] = (ro.amount.high >> 8) & 0xff;
|
|
av[6] = (ro.amount.high >> 16) & 0xff;
|
|
av[7] = (ro.amount.high >> 24) & 0xff;
|
|
|
|
// Actual script
|
|
var as = new Buffer(ro.script.buffer, 'hex')
|
|
.slice(ro.script.offset, ro.script.limit);
|
|
|
|
if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* @desc Mark that a user has seen a given TxProposal
|
|
* @return {boolean} true if the internal state has changed
|
|
*/
|
|
Wallet.prototype.addSeenToTxProposals = function() {
|
|
var ret = false;
|
|
var myId = this.getMyCopayerId();
|
|
|
|
for (var k in this.txProposals.txps) {
|
|
var txp = this.txProposals.txps[k];
|
|
if (!txp.seenBy[myId]) {
|
|
|
|
txp.seenBy[myId] = Date.now();
|
|
ret = true;
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* @desc Alias for {@link PublicKeyRing#getAddresses}
|
|
* @TODO: remove this method and use getAddressesInfo everywhere
|
|
* @return {Buffer[]}
|
|
*/
|
|
Wallet.prototype.getAddresses = function(opts) {
|
|
return this.publicKeyRing.getAddresses(opts);
|
|
};
|
|
|
|
/**
|
|
* @desc Retrieves all addresses as strings.
|
|
*
|
|
* @param {Object} opts - Same options as {@link PublicKeyRing#getAddresses}
|
|
* @return {string[]}
|
|
*/
|
|
Wallet.prototype.getAddressesStr = function(opts) {
|
|
return this.getAddresses(opts).map(function(a) {
|
|
return a.toString();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Alias for {@link PublicKeyRing#getAddressesInfo}
|
|
*/
|
|
Wallet.prototype.getAddressesInfo = function(opts) {
|
|
return this.publicKeyRing.getAddressesInfo(opts, this.publicKey);
|
|
};
|
|
/**
|
|
* @desc Returns true if a given address was generated by deriving our master public key
|
|
* @return {boolean}
|
|
*/
|
|
Wallet.prototype.addressIsOwn = function(addrStr, opts) {
|
|
var addrList = this.getAddressesStr(opts);
|
|
return _.any(addrList, function(value) { return value === addrStr; });
|
|
};
|
|
|
|
|
|
/**
|
|
* @callback {getBalanceCallback}
|
|
* @param {string=} err - an error, if any
|
|
* @param {number} balance - total number of satoshis for all addresses
|
|
* @param {Object} balanceByAddr - maps string addresses to satoshis
|
|
* @param {number} safeBalance - total number of satoshis in UTXOs that are not part of any TxProposal
|
|
*/
|
|
/**
|
|
* @desc Returns the balances for all addresses in Satoshis
|
|
* @param {getBalanceCallback} cb
|
|
*/
|
|
Wallet.prototype.getBalance = function(cb) {
|
|
var balance = 0;
|
|
var safeBalance = 0;
|
|
var balanceByAddr = {};
|
|
var COIN = coinUtil.COIN;
|
|
|
|
this.getUnspent(function(err, safeUnspent, unspent) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
|
|
for (var i = 0; i < unspent.length; i++) {
|
|
var u = unspent[i];
|
|
var amt = u.amount * COIN;
|
|
balance += amt;
|
|
balanceByAddr[u.address] = (balanceByAddr[u.address] || 0) + amt;
|
|
}
|
|
|
|
// we multiply and divide by BIT to avoid rounding errors when adding
|
|
for (var a in balanceByAddr) {
|
|
balanceByAddr[a] = parseInt(balanceByAddr[a].toFixed(0), 10);
|
|
}
|
|
|
|
balance = parseInt(balance.toFixed(0), 10);
|
|
|
|
for (var i = 0; i < safeUnspent.length; i++) {
|
|
var u = safeUnspent[i];
|
|
var amt = u.amount * COIN;
|
|
safeBalance += amt;
|
|
}
|
|
|
|
safeBalance = parseInt(safeBalance.toFixed(0), 10);
|
|
return cb(null, balance, balanceByAddr, safeBalance);
|
|
});
|
|
};
|
|
|
|
|
|
// See
|
|
// https://github.com/bitpay/copay/issues/1056
|
|
//
|
|
// maxRejectCount should equal requiredCopayers
|
|
// strictly.
|
|
/**
|
|
* @desc Get the number of copayers that need to reject a transaction so it can't be signed
|
|
* @return {number}
|
|
*/
|
|
Wallet.prototype.maxRejectCount = function() {
|
|
return this.totalCopayers - this.requiredCopayers;
|
|
};
|
|
|
|
/**
|
|
* @callback getUnspentCallback
|
|
* @TODO: Document this better
|
|
* @param {string} error
|
|
* @param {Object[]} safeUnspendList
|
|
* @param {Object[]} unspentList
|
|
*/
|
|
/**
|
|
* @desc Get a list of unspent transaction outputs
|
|
* @param {getUnspentCallback} cb
|
|
*/
|
|
Wallet.prototype.getUnspent = function(cb) {
|
|
var self = this;
|
|
this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) {
|
|
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
|
|
var safeUnspendList = [];
|
|
var uu = self.txProposals.getUsedUnspent(self.maxRejectCount());
|
|
|
|
for (var i in unspentList) {
|
|
var u = unspentList[i];
|
|
var name = u.txid + ',' + u.vout;
|
|
if (!uu[name] && (self.spendUnconfirmed || u.confirmations >= 1))
|
|
safeUnspendList.push(u);
|
|
}
|
|
|
|
return cb(null, safeUnspendList, unspentList);
|
|
});
|
|
};
|
|
|
|
Wallet.prototype.removeTxWithSpentInputs = function(cb) {
|
|
var self = this;
|
|
|
|
cb = cb || function () {};
|
|
|
|
var txps = [];
|
|
var maxRejectCount = this.maxRejectCount();
|
|
for (var ntxid in this.txProposals.txps) {
|
|
var txp = this.txProposals.txps[ntxid];
|
|
txp.ntxid = ntxid;
|
|
if (txp.isPending(maxRejectCount)) {
|
|
txps.push(txp);
|
|
}
|
|
}
|
|
|
|
var inputs = [];
|
|
txps.forEach(function (txp) {
|
|
txp.builder.utxos.forEach(function (utxo) {
|
|
inputs.push({ ntxid: txp.ntxid, txid: utxo.txid, vout: utxo.vout });
|
|
});
|
|
});
|
|
if (inputs.length === 0)
|
|
return;
|
|
|
|
|
|
var proposalsChanged = false;
|
|
this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) {
|
|
if (err) return cb(err);
|
|
|
|
unspentList.forEach(function (unspent) {
|
|
inputs.forEach(function (input) {
|
|
input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout);
|
|
});
|
|
});
|
|
|
|
inputs.forEach(function (input) {
|
|
if (!input.unspent) {
|
|
proposalsChanged = true;
|
|
self.txProposals.deleteOne(input.ntxid);
|
|
}
|
|
});
|
|
|
|
if (proposalsChanged) {
|
|
self.emit('txProposalsUpdated');
|
|
self.store();
|
|
}
|
|
|
|
cb(null);
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* @desc Create a transaction proposal
|
|
* @TODO: Document more
|
|
*/
|
|
Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) {
|
|
var self = this;
|
|
|
|
if (_.isFunction(amountSatStr)) {
|
|
var cb = amountSatStr;
|
|
var merchant = toAddress;
|
|
return this.createPaymentTx({
|
|
uri: merchant
|
|
}, cb);
|
|
}
|
|
|
|
if (_.isFunction(comment)) {
|
|
var cb = comment;
|
|
var merchant = toAddress;
|
|
var comment = amountSatStr;
|
|
return this.createPaymentTx({
|
|
uri: merchant,
|
|
memo: comment
|
|
}, cb);
|
|
}
|
|
|
|
if (_.isFunction(opts)) {
|
|
cb = opts;
|
|
opts = {};
|
|
}
|
|
opts = opts || {};
|
|
|
|
if (_.isUndefined(opts.spendUnconfirmed)) {
|
|
opts.spendUnconfirmed = this.spendUnconfirmed;
|
|
}
|
|
|
|
this.getUnspent(function(err, safeUnspent) {
|
|
var ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts);
|
|
if (ntxid) {
|
|
self.sendIndexes();
|
|
self.sendTxProposal(ntxid);
|
|
self.store();
|
|
self.emit('txProposalsUpdated');
|
|
}
|
|
return cb(ntxid);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Create a transaction proposal
|
|
* @TODO: Document more
|
|
*/
|
|
Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos, opts) {
|
|
var pkr = this.publicKeyRing;
|
|
var priv = this.privateKey;
|
|
opts = opts || {};
|
|
|
|
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch');
|
|
preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete');
|
|
preconditions.checkState(priv, 'no private key');
|
|
preconditions.checkArgument(bignum(amountSatStr, 10).cmp(copayConfig.limits.minAmountSatoshi) >= 0, 'invalid amount');
|
|
if (comment) preconditions.checkArgument(comment.length <= 100);
|
|
|
|
if (!opts.remainderOut) {
|
|
opts.remainderOut = {
|
|
address: this._doGenerateAddress(true).toString()
|
|
};
|
|
}
|
|
|
|
for (var k in Wallet.builderOpts) {
|
|
opts[k] = Wallet.builderOpts[k];
|
|
}
|
|
|
|
var b = new Builder(opts)
|
|
.setUnspent(utxos)
|
|
.setOutputs([{
|
|
address: toAddress,
|
|
amountSatStr: amountSatStr,
|
|
}]);
|
|
|
|
var selectedUtxos = b.getSelectedUnspent();
|
|
var inputChainPaths = selectedUtxos.map(function(utxo) {
|
|
return pkr.pathForAddress(utxo.address);
|
|
});
|
|
|
|
b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
|
|
|
|
var keys = priv.getForPaths(inputChainPaths);
|
|
var signed = b.sign(keys);
|
|
var myId = this.getMyCopayerId();
|
|
var now = Date.now();
|
|
|
|
|
|
var tx = b.build();
|
|
if (!tx.countInputSignatures(0))
|
|
throw new Error('Could not sign generated tx');
|
|
|
|
var me = {};
|
|
me[myId] = now;
|
|
|
|
var meSeen = {};
|
|
if (priv) meSeen[myId] = now;
|
|
|
|
var ntxid = this.txProposals.add(new TxProposal({
|
|
inputChainPaths: inputChainPaths,
|
|
signedBy: me,
|
|
seenBy: meSeen,
|
|
creator: myId,
|
|
createdTs: now,
|
|
builder: b,
|
|
comment: comment
|
|
}));
|
|
return ntxid;
|
|
};
|
|
|
|
/**
|
|
* @desc Updates all the indexes for the current publicKeyRing
|
|
*
|
|
* Triggers a wallet {@link Wallet#store} call
|
|
* @param {Function} callback - called when all indexes have been updated. Receives an error, if any, as first argument
|
|
* @emits publicKeyRingUpdated
|
|
*/
|
|
Wallet.prototype.updateIndexes = function(callback) {
|
|
var self = this;
|
|
log.debug('Updating indexes...');
|
|
|
|
var tasks = this.publicKeyRing.indexes.map(function(index) {
|
|
return function(callback) {
|
|
self.updateIndex(index, callback);
|
|
};
|
|
});
|
|
|
|
async.parallel(tasks, function(err) {
|
|
if (err) callback(err);
|
|
log.debug('Indexes updated');
|
|
self.emit('publicKeyRingUpdated');
|
|
self.store();
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Updates the lastly used index
|
|
* @param {Object} index - an index, as used by {@link PublicKeyRing}
|
|
* @param {Function} callback - called with no arguments when done updating
|
|
*/
|
|
Wallet.prototype.updateIndex = function(index, callback) {
|
|
var self = this;
|
|
var SCANN_WINDOW = 20;
|
|
self.indexDiscovery(index.changeIndex, true, index.copayerIndex, SCANN_WINDOW, function(err, changeIndex) {
|
|
if (err) return callback(err);
|
|
if (changeIndex != -1)
|
|
index.changeIndex = changeIndex + 1;
|
|
|
|
self.indexDiscovery(index.receiveIndex, false, index.copayerIndex, SCANN_WINDOW, function(err, receiveIndex) {
|
|
if (err) return callback(err);
|
|
if (receiveIndex != -1)
|
|
index.receiveIndex = receiveIndex + 1;
|
|
callback();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @desc Derive addresses using the given parameters
|
|
*
|
|
* @param {number} index - the index to start with
|
|
* @param {number} amount - number of addresses to derive
|
|
* @param {boolean} isChange - derive change addresses or receive addresses
|
|
* @param {number} copayerIndex - the index of the copayer for whom to derive addresses
|
|
* @return {string[]} the result of calling {@link PublicKeyRing#getAddress}
|
|
*/
|
|
Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerIndex) {
|
|
preconditions.checkArgument(amount);
|
|
preconditions.shouldBeDefined(copayerIndex);
|
|
|
|
var ret = new Array(amount);
|
|
for (var i = 0; i < amount; i++) {
|
|
ret[i] = this.publicKeyRing.getAddress(index + i, isChange, copayerIndex).toString();
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* @callback {indexDiscoveryCallback}
|
|
* @param {?} err
|
|
* @param {number} lastActivityIndex
|
|
*/
|
|
/**
|
|
* @desc Scans the block chain for the last index with activity for a copayer
|
|
*
|
|
* This function scans the publicKeyRing branch starting at index @start and reports the index with last activity,
|
|
* using a scan window of @gap. The argument @change defines the branch to scan: internal or external.
|
|
* Returns -1 if no activity is found in range.
|
|
* @param {number} start - the number for which to start scanning
|
|
* @param {boolean} change - whether to search for in the change branch or the receive branch
|
|
* @param {number} copayerIndex - the index of the copayer
|
|
* @param {number} gap - the maximum number of addresses to scan after the last active address
|
|
* @param {indexDiscoveryCallback} cb - callback
|
|
* @return {number} -1 if there's no activity in the range provided
|
|
*/
|
|
Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) {
|
|
preconditions.shouldBeDefined(copayerIndex);
|
|
preconditions.checkArgument(gap);
|
|
var scanIndex = start;
|
|
var lastActive = -1;
|
|
var hasActivity = false;
|
|
|
|
var self = this;
|
|
async.doWhilst(
|
|
function _do(next) {
|
|
// Optimize window to minimize the derivations.
|
|
var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1;
|
|
var addresses = self.deriveAddresses(scanIndex, scanWindow, change, copayerIndex);
|
|
self.blockchain.getActivity(addresses, function(err, actives) {
|
|
if (err) throw err;
|
|
|
|
// Check for new activities in the newlly scanned addresses
|
|
var recentActive = actives.reduce(function(r, e, i) {
|
|
return e ? scanIndex + i : r;
|
|
}, lastActive);
|
|
hasActivity = lastActive != recentActive;
|
|
lastActive = recentActive;
|
|
scanIndex += scanWindow;
|
|
next();
|
|
});
|
|
},
|
|
function _while() {
|
|
return hasActivity;
|
|
},
|
|
function _finally(err) {
|
|
if (err) return cb(err);
|
|
cb(null, lastActive);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @desc Closes the wallet and disconnects all services
|
|
*/
|
|
Wallet.prototype.close = function() {
|
|
log.debug('## CLOSING');
|
|
this.lock.release();
|
|
this.network.cleanUp();
|
|
this.blockchain.destroy();
|
|
};
|
|
|
|
/**
|
|
* @desc Returns the name of the network ('livenet' or 'testnet')
|
|
* @return {string}
|
|
*/
|
|
Wallet.prototype.getNetwork = function() {
|
|
return this.network;
|
|
};
|
|
|
|
/**
|
|
* @desc Throws an error if an address already exists in the address book
|
|
* @private
|
|
*/
|
|
Wallet.prototype._checkAddressBook = function(key) {
|
|
if (this.addressBook[key] && this.addressBook[key].copayerId != -1) {
|
|
throw new Error('This address already exists in your Address Book: ' + address);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @desc Add an entry to the address book
|
|
*
|
|
* @param {string} key - the address to be added
|
|
* @param {string} label - a name for the address
|
|
*/
|
|
Wallet.prototype.setAddressBook = function(key, label) {
|
|
this._checkAddressBook(key);
|
|
var copayerId = this.getMyCopayerId();
|
|
var ts = Date.now();
|
|
var payload = {
|
|
address: key,
|
|
label: label,
|
|
copayerId: copayerId,
|
|
createdTs: ts
|
|
};
|
|
var newEntry = {
|
|
hidden: false,
|
|
createdTs: ts,
|
|
copayerId: copayerId,
|
|
label: label,
|
|
signature: this.signJson(payload)
|
|
};
|
|
this.addressBook[key] = newEntry;
|
|
this.sendAddressBook();
|
|
this.store();
|
|
};
|
|
|
|
/**
|
|
* @desc Verifies that an addressbook entry is correctly signed by a copayer
|
|
*
|
|
* @param {Object} rcvEntry - the entry in the address book
|
|
* @param {string} senderId - the pubkey of a copayer
|
|
* @param {string} key - the base58 encoded address
|
|
* @return {boolean} true if the signature matches
|
|
*/
|
|
Wallet.prototype.verifyAddressbookEntry = function(rcvEntry, senderId, key) {
|
|
if (!key) throw new Error('Keys are required');
|
|
var signature = rcvEntry.signature;
|
|
var payload = {
|
|
address: key,
|
|
label: rcvEntry.label,
|
|
copayerId: rcvEntry.copayerId,
|
|
createdTs: rcvEntry.createdTs
|
|
};
|
|
return this.verifySignedJson(senderId, payload, signature);
|
|
};
|
|
|
|
/**
|
|
* @desc Hides or unhides an address book entry
|
|
* @param {string} key - the address in the addressbook
|
|
*/
|
|
Wallet.prototype.toggleAddressBookEntry = function(key) {
|
|
if (!key) throw new Error('Key is required');
|
|
this.addressBook[key].hidden = !this.addressBook[key].hidden;
|
|
this.store();
|
|
};
|
|
|
|
/**
|
|
* @desc Returns true if there are more than one cosigners
|
|
* @return {boolean}
|
|
*/
|
|
Wallet.prototype.isShared = function() {
|
|
return this.totalCopayers > 1;
|
|
};
|
|
|
|
/**
|
|
* @desc Returns true if the keyring is complete and all users have backed up the wallet
|
|
* @return {boolean}
|
|
*/
|
|
Wallet.prototype.isReady = function() {
|
|
var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup();
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* @desc Mark that our backup is ready and send a sync to other users.
|
|
*
|
|
* Also backs up the wallet
|
|
*/
|
|
Wallet.prototype.setBackupReady = function() {
|
|
this.publicKeyRing.setBackupReady();
|
|
this.sendPublicKeyRing();
|
|
this.store();
|
|
};
|
|
|
|
/**
|
|
* @desc Sign a JSON
|
|
*
|
|
* @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order
|
|
* @param {Object} payload - the payload to verify
|
|
* @return {string} base64 encoded string
|
|
*/
|
|
Wallet.prototype.signJson = function(payload) {
|
|
var key = new bitcore.Key();
|
|
key.private = new Buffer(this.getMyCopayerIdPriv(), 'hex');
|
|
key.regenerateSync();
|
|
var sign = bitcore.Message.sign(JSON.stringify(payload), key);
|
|
return sign.toString('hex');
|
|
}
|
|
|
|
/**
|
|
* @desc Verify that a JSON object is correctly signed
|
|
*
|
|
* @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order
|
|
*
|
|
* @param {string} senderId - a sender's public key, hex encoded
|
|
* @param {Object} payload - the object to verify
|
|
* @param {string} signature - a sender's public key, hex encoded
|
|
* @return {boolean}
|
|
*/
|
|
Wallet.prototype.verifySignedJson = function(senderId, payload, signature) {
|
|
var pubkey = new Buffer(senderId, 'hex');
|
|
var sign = new Buffer(signature, 'hex');
|
|
var v = bitcore.Message.verifyWithPubKey(pubkey, JSON.stringify(payload), sign);
|
|
return v;
|
|
}
|
|
|
|
// NOTE: Angular $http module does not send ArrayBuffers correctly, so we're
|
|
// not going to use it. We'll have to write our own. Otherwise, we could
|
|
// hex-encoded our messages and decode them on the other side, but that
|
|
// deviates from BIP-70.
|
|
|
|
// if (typeof angular !== 'undefined') {
|
|
// var $http = angular.bootstrap().get('$http');
|
|
// }
|
|
|
|
/**
|
|
* @desc Create a HTTP request
|
|
* @TODO: This shouldn't be a wallet responsibility
|
|
*/
|
|
Wallet.request = function(options, callback) {
|
|
if (_.isString(options)) {
|
|
options = {
|
|
uri: options
|
|
};
|
|
}
|
|
|
|
options.method = options.method || 'GET';
|
|
options.headers = options.headers || {};
|
|
|
|
var ret = {
|
|
success: function(cb) {
|
|
this._success = cb;
|
|
return this;
|
|
},
|
|
error: function(cb) {
|
|
this._error = cb;
|
|
return this;
|
|
},
|
|
_success: function() {;
|
|
},
|
|
_error: function(_, err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
var method = (options.method || 'GET').toUpperCase();
|
|
var uri = options.uri || options.url;
|
|
var req = options;
|
|
|
|
req.headers = req.headers || {};
|
|
req.body = req.body || req.data || {};
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open(method, uri, true);
|
|
|
|
Object.keys(req.headers).forEach(function(key) {
|
|
var val = req.headers[key];
|
|
if (key === 'Content-Length') return;
|
|
if (key === 'Content-Transfer-Encoding') return;
|
|
xhr.setRequestHeader(key, val);
|
|
});
|
|
|
|
if (req.responseType) {
|
|
xhr.responseType = req.responseType;
|
|
}
|
|
|
|
xhr.onload = function(event) {
|
|
var response = xhr.response;
|
|
var buf = new Uint8Array(response);
|
|
var headers = {};
|
|
(xhr.getAllResponseHeaders() || '').replace(
|
|
/(?:\r?\n|^)([^:\r\n]+): *([^\r\n]+)/g,
|
|
function($0, $1, $2) {
|
|
headers[$1.toLowerCase()] = $2;
|
|
}
|
|
);
|
|
return ret._success(buf, xhr.status, headers, options);
|
|
};
|
|
|
|
xhr.onerror = function(event) {
|
|
return ret._error(null, new Error(event.message), null, options);
|
|
};
|
|
|
|
if (req.body) {
|
|
xhr.send(req.body);
|
|
} else {
|
|
xhr.send(null);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
module.exports = Wallet;
|