Merge pull request #4 from matiu/feat/addresses

Feat/addresses
This commit is contained in:
Ivan Socolsky 2015-02-02 17:45:54 -03:00
commit f4f7af09d9
9 changed files with 460 additions and 47 deletions

34
lib/model/addressable.js Normal file
View File

@ -0,0 +1,34 @@
var _ = require('lodash');
var HDPath = require('./hdpath');
function Addressable (opts) {
this.receiveAddressIndex = 0;
this.changeAddressIndex = 0;
this.copayerIndex = ( opts && _.isNumber(opts.copayerIndex)) ? opts.copayerIndex : HDPath.SHARED_INDEX;
};
Addressable.prototype.fromObj = function (obj) {
this.receiveAddressIndex = obj.receiveAddressIndex;
this.changeAddressIndex = obj.changeAddressIndex;
this.copayerIndex = obj.copayerIndex;
};
Addressable.prototype.addAddress = function (isChange) {
if (isChange) {
this.changeAddressIndex++;
} else {
this.receiveAddressIndex++;
}
};
Addressable.prototype.getCurrentAddressPath = function (isChange) {
return HDPath.Branch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex);
};
Addressable.prototype.getNewAddressPath = function (isChange) {
this.addAddress(isChange);
return this.getCurrentAddressPath(isChange);
};
module.exports = Addressable;

View File

@ -1,26 +1,33 @@
'use strict';
var _ = require('lodash');
var util = require('util');
var Bitcore = require('bitcore');
var HDPublicKey = Bitcore.HDPublicKey;
var Addressable = require('./Addressable');
var VERSION = '1.0.0';
var MESSAGE_SIGNING_PATH = "m/1/0";
function Copayer(opts) {
opts = opts || {};
opts.copayerIndex = opts.copayerIndex || 0;
Copayer.super_.apply(this, [opts]);
this.version = VERSION;
this.createdOn = Math.floor(Date.now() / 1000);
this.id = opts.id;
this.name = opts.name;
this.xPubKey = opts.xPubKey;
this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently
if (opts.xPubKey) {
this.signingPubKey = this.getSigningPubKey();
}
this.createdOn = Math.floor(Date.now() / 1000);
this.id = opts.id;
this.name = opts.name;
this.xPubKey = opts.xPubKey;
this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently
this.signingPubKey = opts.signingPubKey || this.getSigningPubKey();
};
util.inherits(Copayer, Addressable);
Copayer.prototype.getSigningPubKey = function () {
if (!this.xPubKey) return null;
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
@ -29,7 +36,6 @@ Copayer.prototype.getSigningPubKey = function () {
Copayer.fromObj = function (obj) {
var x = new Copayer();
x.version = obj.version;
x.createdOn = obj.createdOn;
x.id = obj.id;
x.name = obj.name;
@ -37,8 +43,8 @@ Copayer.fromObj = function (obj) {
x.xPubKeySignature = obj.xPubKeySignature;
x.signingPubKey = obj.signingPubKey;
Wallet.super_.prototype.fromObj.apply(this, [obj]);
return x;
};
module.exports = Copayer;

116
lib/model/hdpath.js Normal file
View File

@ -0,0 +1,116 @@
'use strict';
// 90.2% typed (by google's closure-compiler account)
var preconditions = require('preconditions').singleton();
var _ = require('lodash');
/**
* @namespace
* @desc
* HDPath contains helper functions to handle BIP32 branches as
* Copay uses them.
* Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki
* <pre>
* m / purpose' / copayerIndex / change:boolean / addressIndex
* </pre>
*/
var HDPath = {};
/**
* @desc Copay's BIP45 purpose code
* @const
* @type number
*/
HDPath.PURPOSE = 45;
/**
* @desc Maximum number for non-hardened values (BIP32)
* @const
* @type number
*/
HDPath.MAX_NON_HARDENED = 0x80000000 - 1;
/**
* @desc Shared Index: used for creating addresses for no particular purpose
* @const
* @type number
*/
HDPath.SHARED_INDEX = HDPath.MAX_NON_HARDENED - 0;
/**
* @desc ???
* @const
* @type number
*/
HDPath.ID_INDEX = HDPath.MAX_NON_HARDENED - 1;
/**
* @desc BIP45 prefix for COPAY
* @const
* @type string
*/
HDPath.BIP45_PUBLIC_PREFIX = 'm/' + HDPath.PURPOSE + '\'';
/**
* @desc Retrieve a string to be used with bitcore representing a Copay branch
* @param {number} addressIndex - the last value of the HD derivation
* @param {boolean} isChange - whether this is a change address or a receive
* @param {number} copayerIndex - the index of the copayer in the pubkeyring
* @return {string} - the path for the HD derivation
*/
HDPath.Branch = function(addressIndex, isChange, copayerIndex) {
preconditions.checkArgument(_.isNumber(addressIndex));
preconditions.checkArgument(_.isBoolean(isChange));
var ret = 'm/' +
(typeof copayerIndex !== 'undefined' ? copayerIndex : HDPath.SHARED_INDEX) + '/' +
(isChange ? 1 : 0) + '/' +
addressIndex;
return ret;
};
/**
* @desc ???
* @param {number} addressIndex - the last value of the HD derivation
* @param {boolean} isChange - whether this is a change address or a receive
* @param {number} copayerIndex - the index of the copayer in the pubkeyring
* @return {string} - the path for the HD derivation
*/
HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) {
preconditions.checkArgument(_.isNumber(addressIndex));
preconditions.checkArgument(_.isBoolean(isChange));
var sub = HDPath.Branch(addressIndex, isChange, copayerIndex);
sub = sub.substring(2);
return HDPath.BIP45_PUBLIC_PREFIX + '/' + sub;
};
/**
* @desc
* Decompose a string and retrieve its arguments as if it where a Copay address.
* @param {string} path - the HD path
* @returns {Object} an object with three keys: addressIndex, isChange, and
* copayerIndex
*/
HDPath.indexesForPath = function(path) {
preconditions.checkArgument(_.isString(path));
var s = path.split('/');
return {
isChange: s[3] === '1',
addressIndex: parseInt(s[4], 10),
copayerIndex: parseInt(s[2], 10)
};
};
/**
* @desc The ID for a shared branch
*/
HDPath.IdFullBranch = HDPath.FullBranch(0, false, HDPath.ID_INDEX);
/**
* @desc Partial ID for a shared branch
*/
HDPath.IdBranch = HDPath.Branch(0, false, HDPath.ID_INDEX);
module.exports = HDPath;

View File

@ -1,11 +1,19 @@
'use strict';
var _ = require('lodash');
var util = require('util');
var Bitcore = require('bitcore');
var BitcoreAddress = Bitcore.Address;
var Address = require('./address');
var Copayer = require('./copayer');
var Addressable = require('./Addressable');
var VERSION = '1.0.0';
function Wallet(opts) {
Wallet.super_.apply(this, arguments);
opts = opts || {};
this.version = VERSION;
@ -19,6 +27,7 @@ function Wallet(opts) {
this.addressIndex = 0;
this.copayers = [];
this.pubKey = opts.pubKey;
this.isTestnet = false;
};
/* For compressed keys, m*73 + n*34 <= 496 */
@ -36,6 +45,7 @@ Wallet.COPAYER_PAIR_LIMITS = {
11: 1,
12: 1,
};
util.inherits(Wallet, Addressable);
/**
* Get the maximum allowed number of required copayers.
@ -54,7 +64,6 @@ Wallet.verifyCopayerLimits = function (m, n) {
Wallet.fromObj = function (obj) {
var x = new Wallet();
x.version = obj.version;
x.createdOn = obj.createdOn;
x.id = obj.id;
x.name = obj.name;
@ -62,12 +71,13 @@ Wallet.fromObj = function (obj) {
x.n = obj.n;
x.status = obj.status;
x.publicKeyRing = obj.publicKeyRing;
x.addressIndex = obj.addressIndex;
x.copayers = _.map(obj.copayers, function (copayer) {
return new Copayer(copayer);
});
x.pubKey = obj.pubKey;
x.isTestnet = obj.isTestnet;
Wallet.super_.prototype.fromObj.apply(this, [obj]);
return x;
};
@ -84,4 +94,26 @@ Wallet.prototype.getCopayer = function (copayerId) {
return _.find(this.copayers, { id: copayerId });
};
Wallet.prototype._getBitcoreNetwork = function () {
return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet;
};
Wallet.prototype.createAddress = function (path) {
var publicKeys = _.map(this.copayers, function(copayer) {
var xpub = new Bitcore.HDPublicKey(copayer.xPubKey);
return xpub.derive(path).publicKey;
});
var bitcoreAddress = BitcoreAddress.createMultisig(publicKeys, this.m, this._getBitcoreNetwork());
return new Address({
address: bitcoreAddress.toString(),
path: path,
});
};
module.exports = Wallet;

View File

@ -75,10 +75,10 @@ CopayServer.prototype.createWallet = function (opts, cb) {
name: opts.name,
m: opts.m,
n: opts.n,
network: network,
network: opts.network || 'livenet',
pubKey: pubKey,
});
self.storage.storeWallet(wallet, cb);
});
};
@ -87,7 +87,7 @@ CopayServer.prototype.createWallet = function (opts, cb) {
* Retrieves a wallet from storage.
* @param {Object} opts
* @param {string} opts.id - The wallet id.
* @returns {Object} wallet
* @returns {Object} wallet
*/
CopayServer.prototype.getWallet = function (opts, cb) {
var self = this;
@ -106,8 +106,8 @@ CopayServer.prototype.createWallet = function (opts, cb) {
* @param signature
* @param pubKey
*/
CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
return SignUtils.verify( text, signature, pubKey);
CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
return SignUtils.verify(text, signature, pubKey);
};
/**
@ -132,30 +132,28 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
return cb('Bad request');
}
if (_.find(wallet.copayers, { xPubKey: opts.xPubKey })) return cb('Copayer already in wallet');
if (_.find(wallet.copayers, {
xPubKey: opts.xPubKey
})) return cb('Copayer already in wallet');
if (wallet.copayers.length == wallet.n) return cb('Wallet full');
var copayer = new Copayer({
id: opts.id,
name: opts.name,
xPubKey: opts.xPubKey,
xPubKeySignature: opts.xPubKeySignature,
copayerIndex: wallet.copayers.length,
});
wallet.addCopayer(copayer);
self.storage.storeWallet(wallet, function (err) {
if (err) return cb(err);
return cb();
return cb(err);
});
});
});
};
CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
throw 'not implemented';
};
/**
*
* TODO: How this is going to be authenticated?
@ -164,10 +162,11 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @param {truthy} opts.isChange - Indicates whether this is a regular address or a change address.
* @returns {Address} address
* @returns {Address} address
*/
CopayServer.prototype.createAddress = function (opts, cb) {
var self = this;
var isChange = opts.isChange || false;
Utils.checkRequired(opts, ['walletId', 'isChange']);
@ -175,12 +174,16 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
self.getWallet({ id: opts.walletId }, function (err, wallet) {
if (err) return cb(err);
var index = wallet.addressIndex++;
self.storage.storeWallet(wallet, function (err) {
var copayer = wallet.copayers[0]; // TODO: Assign copayer from authentication.
var path = copayer.getNewAddressPath(isChange);
self.storage.storeWallet(wallet, function(err) {
if (err) return cb(err);
var address = self._doCreateAddress(wallet.publicKeyRing, index, opts.isChange);
self.storage.storeAddress(opts.walletId, address, function (err) {
var address = wallet.createAddress(path);
self.storage.storeAddress(opts.walletId, address, function(err) {
if (err) return cb(err);
return cb(null, address);
@ -267,7 +270,6 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
dictionary[input].locked = true;
}
});
return cb(null, utxos);
});
});
@ -279,7 +281,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
* Creates a new transaction proposal.
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @returns {Object} balance - Total amount & locked amount.
* @returns {Object} balance - Total amount & locked amount.
*/
CopayServer.prototype.getBalance = function (opts, cb) {
var self = this;
@ -298,7 +300,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
};
CopayServer.prototype._createRawTx = function (txp) {
CopayServer.prototype._createRawTx = function(txp) {
var rawTx = new Bitcore.Transaction()
.from(tx.inputs)
.to(txp.toAddress, txp.amount)
@ -307,7 +309,7 @@ CopayServer.prototype._createRawTx = function (txp) {
return rawTx;
};
CopayServer.prototype._selectUtxos = function (txp, utxos) {
CopayServer.prototype._selectUtxos = function(txp, utxos) {
var i = 0;
var total = 0;
var selected = [];
@ -332,7 +334,7 @@ CopayServer.prototype._selectUtxos = function (txp, utxos) {
* @param {string} opts.toAddress - Destination address.
* @param {number} opts.amount - Amount to transfer in satoshi.
* @param {string} opts.message - A message to attach to this transaction.
* @returns {TxProposal} Transaction proposal.
* @returns {TxProposal} Transaction proposal.
*/
CopayServer.prototype.createTx = function (opts, cb) {
var self = this;
@ -413,7 +415,7 @@ CopayServer.prototype.signTx = function (opts, cb) {
});
});
});
};
};
/**
* Reject a transaction proposal.
@ -449,7 +451,7 @@ CopayServer.prototype.rejectTx = function (opts, cb) {
* Retrieves all pending transaction proposals.
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @returns {TxProposal[]} Transaction proposal.
* @returns {TxProposal[]} Transaction proposal.
*/
CopayServer.prototype.getPendingTxs = function (opts, cb) {
var self = this;
@ -460,7 +462,6 @@ CopayServer.prototype.getPendingTxs = function (opts, cb) {
if (err) return cb(err);
var pending = _.filter(txps, { status: 'pending' });
return cb(null, pending);
});
};

28
test/copayer.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var Copayer = require('../lib/model/copayer');
describe('Copayer', function() {
describe('#getCurrentAddressPath', function() {
it('return a valid BIP32 path for defaut copayer Index', function() {
var c = new Copayer();
c.getCurrentAddressPath(false).should.equal('m/0/0/0');
c.getCurrentAddressPath(true).should.equal('m/0/1/0');
});
it('return a valid BIP32 path for given index', function() {
var c = new Copayer({
copayerIndex: 4
});
c.getCurrentAddressPath(false).should.equal('m/4/0/0');
c.getCurrentAddressPath(true).should.equal('m/4/1/0');
});
});
});

71
test/hdpath.js Normal file
View File

@ -0,0 +1,71 @@
'use strict';
var HDPath = require('../lib/model/hdpath');
describe('HDPath model', function() {
it('should have the correct constants', function() {
HDPath.MAX_NON_HARDENED.should.equal(Math.pow(2, 31) - 1);
HDPath.SHARED_INDEX.should.equal(HDPath.MAX_NON_HARDENED);
HDPath.ID_INDEX.should.equal(HDPath.SHARED_INDEX - 1);
HDPath.IdFullBranch.should.equal('m/45\'/2147483646/0/0');
});
it('should get the correct branches', function() {
// shared branch (no cosigner index specified)
HDPath.FullBranch(0, false).should.equal('m/45\'/2147483647/0/0');
// copayer 0, address 0, external address (receiving)
HDPath.FullBranch(0, false, 0).should.equal('m/45\'/0/0/0');
// copayer 0, address 10, external address (receiving)
HDPath.FullBranch(0, false, 10).should.equal('m/45\'/10/0/0');
// copayer 0, address 0, internal address (change)
HDPath.FullBranch(0, true, 0).should.equal('m/45\'/0/1/0');
// copayer 0, address 10, internal address (change)
HDPath.FullBranch(10, true, 0).should.equal('m/45\'/0/1/10');
// copayer 7, address 10, internal address (change)
HDPath.FullBranch(10, true, 7).should.equal('m/45\'/7/1/10');
});
[
['m/45\'/0/0/0', {
index: 0,
isChange: false
}],
['m/45\'/0/0/1', {
index: 1,
isChange: false
}],
['m/45\'/0/0/2', {
index: 2,
isChange: false
}],
['m/45\'/0/1/0', {
index: 0,
isChange: true
}],
['m/45\'/0/1/1', {
index: 1,
isChange: true
}],
['m/45\'/0/1/2', {
index: 2,
isChange: true
}],
['m/45\'/0/0/900', {
index: 900,
isChange: false
}],
].forEach(function(datum) {
var path = datum[0];
var result = datum[1];
it('should get the correct indexes for path ' + path, function() {
var i = HDPath.indexesForPath(path);
i.addressIndex.should.equal(result.index);
i.isChange.should.equal(result.isChange);
});
});
});

View File

@ -69,6 +69,7 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
if (err) return cb(err);
async.each(_.range(1, n + 1), function(i, cb) {
var copayerOpts = {
walletId: id,
id: '' + i,
@ -76,11 +77,13 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
xPubKey: someXPubKeys[i - 1],
xPubKeySignature: someXPubKeysSignatures[i - 1],
};
server.joinWallet(copayerOpts, function(err) {
return cb(err);
});
}, function(err) {
if (err) return cb(err);
server.getWallet({
id: id,
includeCopayers: true
@ -461,9 +464,22 @@ describe('Copay server', function() {
});
});
it('should set index in 1-1 wallet creation.', function(done) {
helpers.createAndJoinWallet('123', 1, 1, function(err, wallet) {
wallet.receiveAddressIndex.should.equal(0);
wallet.changeAddressIndex.should.equal(0);
wallet.copayerIndex.should.equal(0x80000000 - 1);
var copayer = wallet.copayers[0];
copayer.receiveAddressIndex.should.equal(0);
copayer.changeAddressIndex.should.equal(0);
copayer.copayerIndex.should.equal(0);
done();
});
});
it('should set pkr and status = complete on last copayer joining', function(done) {
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) {
server.getWallet({
id: '123'
@ -471,6 +487,12 @@ describe('Copay server', function() {
should.not.exist(err);
wallet.status.should.equal('complete');
wallet.publicKeyRing.length.should.equal(3);
_.each([0,1,2], function(i) {
var copayer = wallet.copayers[i];
copayer.receiveAddressIndex.should.equal(0);
copayer.changeAddressIndex.should.equal(0);
copayer.copayerIndex.should.equal(i);
});
done();
});
});
@ -478,7 +500,6 @@ describe('Copay server', function() {
});
describe('#verifyMessageSignature', function() {
beforeEach(function() {
server = new CopayServer({
@ -525,11 +546,7 @@ describe('Copay server', function() {
});
});
it('should create address', function(done) {
server._doCreateAddress = sinon.stub().returns(new Address({
address: 'addr1',
path: 'path1',
}));
it('should create main address', function(done) {
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
server.createAddress({
walletId: '123',
@ -537,12 +554,29 @@ describe('Copay server', function() {
}, function(err, address) {
should.not.exist(err);
address.should.exist;
address.address.should.equal('addr1');
address.path.should.equal('path1');
address.address.should.equal('3BPfHzwq5j72TBYtYv3Uggk3vyHFHX3QpA');
address.path.should.equal('m/0/0/1');
done();
});
});
});
it('should create change address', function(done) {
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
server.createAddress({
walletId: '123',
isChange: true,
}, function(err, address) {
should.not.exist(err);
address.should.exist;
address.address.should.equal('39Dzj5mBJWvzH7bDfmYzXDvTbZS5HdQ4a4');
address.path.should.equal('m/0/1/1');
done();
});
});
});
});
describe('#createTx', function() {

91
test/wallet.js Normal file
View File

@ -0,0 +1,91 @@
'use strict';
var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var Wallet = require('../lib/model/wallet');
describe('Wallet', function() {
describe('#fromObj', function() {
it('read a wallet', function() {
var w = Wallet.fromObj(testWallet);
w.status.should.equal('complete');
});
});
describe('#createAddress', function() {
it('create an address', function() {
var w = Wallet.fromObj(testWallet);
var a = w.createAddress('m/1/1');
a.address.should.equal('32HG4C9tWMhWoDoTHFvjmbV5sUJMjWs4vL');
a.path.should.equal('m/1/1');
a.createdOn.should.be.above(1);
});
});
describe('#getCurrentAddressPath', function() {
it('return a valid BIP32 path for defaut wallet Index', function() {
var w = new Wallet();
w.getCurrentAddressPath(false).should.equal('m/2147483647/0/0');
w.getCurrentAddressPath(true).should.equal('m/2147483647/1/0');
});
});
});
var testWallet = {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 2147483647,
createdOn: 1422904188,
id: '123',
name: '123 wallet',
m: 2,
n: 3,
status: 'complete',
publicKeyRing: ['xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9',
'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o',
'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd'
],
copayers: [{
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 0,
createdOn: 1422904189,
id: '1',
name: 'copayer 1',
xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9',
xPubKeySignature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46',
version: '1.0.0',
signingPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc'
}, {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 1,
createdOn: 1422904189,
id: '2',
name: 'copayer 2',
xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o',
xPubKeySignature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62',
version: '1.0.0',
signingPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60'
}, {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 2,
createdOn: 1422904189,
id: '3',
name: 'copayer 3',
xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd',
xPubKeySignature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c',
version: '1.0.0',
signingPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a'
}],
version: '1.0.0',
pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}',
isTestnet: false
};