Browse Source

Merge pull request #4 from matiu/feat/addresses

Feat/addresses
activeAddress
Ivan Socolsky 10 years ago
parent
commit
f4f7af09d9
  1. 34
      lib/model/addressable.js
  2. 16
      lib/model/copayer.js
  3. 116
      lib/model/hdpath.js
  4. 36
      lib/model/wallet.js
  5. 27
      lib/server.js
  6. 28
      test/copayer.js
  7. 71
      test/hdpath.js
  8. 52
      test/integration.js
  9. 91
      test/wallet.js

34
lib/model/addressable.js

@ -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;

16
lib/model/copayer.js

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

116
lib/model/hdpath.js

@ -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;

36
lib/model/wallet.js

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

27
lib/server.js

@ -75,7 +75,7 @@ CopayServer.prototype.createWallet = function (opts, cb) {
name: opts.name, name: opts.name,
m: opts.m, m: opts.m,
n: opts.n, n: opts.n,
network: network, network: opts.network || 'livenet',
pubKey: pubKey, pubKey: pubKey,
}); });
@ -132,30 +132,28 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
return cb('Bad request'); 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'); if (wallet.copayers.length == wallet.n) return cb('Wallet full');
var copayer = new Copayer({ var copayer = new Copayer({
id: opts.id, id: opts.id,
name: opts.name, name: opts.name,
xPubKey: opts.xPubKey, xPubKey: opts.xPubKey,
xPubKeySignature: opts.xPubKeySignature, xPubKeySignature: opts.xPubKeySignature,
copayerIndex: wallet.copayers.length,
}); });
wallet.addCopayer(copayer); wallet.addCopayer(copayer);
self.storage.storeWallet(wallet, function (err) { self.storage.storeWallet(wallet, function (err) {
if (err) return cb(err); return cb(err);
return cb();
}); });
}); });
}); });
}; };
CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
throw 'not implemented';
};
/** /**
* *
* TODO: How this is going to be authenticated? * TODO: How this is going to be authenticated?
@ -168,6 +166,7 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
*/ */
CopayServer.prototype.createAddress = function (opts, cb) { CopayServer.prototype.createAddress = function (opts, cb) {
var self = this; var self = this;
var isChange = opts.isChange || false;
Utils.checkRequired(opts, ['walletId', 'isChange']); Utils.checkRequired(opts, ['walletId', 'isChange']);
@ -175,11 +174,15 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
self.getWallet({ id: opts.walletId }, function (err, wallet) { self.getWallet({ id: opts.walletId }, function (err, wallet) {
if (err) return cb(err); if (err) return cb(err);
var index = wallet.addressIndex++; var copayer = wallet.copayers[0]; // TODO: Assign copayer from authentication.
var path = copayer.getNewAddressPath(isChange);
self.storage.storeWallet(wallet, function(err) { self.storage.storeWallet(wallet, function(err) {
if (err) return cb(err); if (err) return cb(err);
var address = self._doCreateAddress(wallet.publicKeyRing, index, opts.isChange); var address = wallet.createAddress(path);
self.storage.storeAddress(opts.walletId, address, function(err) { self.storage.storeAddress(opts.walletId, address, function(err) {
if (err) return cb(err); if (err) return cb(err);
@ -267,7 +270,6 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
dictionary[input].locked = true; dictionary[input].locked = true;
} }
}); });
return cb(null, utxos); return cb(null, utxos);
}); });
}); });
@ -460,7 +462,6 @@ CopayServer.prototype.getPendingTxs = function (opts, cb) {
if (err) return cb(err); if (err) return cb(err);
var pending = _.filter(txps, { status: 'pending' }); var pending = _.filter(txps, { status: 'pending' });
return cb(null, pending); return cb(null, pending);
}); });
}; };

28
test/copayer.js

@ -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

@ -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);
});
});
});

52
test/integration.js

@ -69,6 +69,7 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
if (err) return cb(err); if (err) return cb(err);
async.each(_.range(1, n + 1), function(i, cb) { async.each(_.range(1, n + 1), function(i, cb) {
var copayerOpts = { var copayerOpts = {
walletId: id, walletId: id,
id: '' + i, id: '' + i,
@ -76,11 +77,13 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
xPubKey: someXPubKeys[i - 1], xPubKey: someXPubKeys[i - 1],
xPubKeySignature: someXPubKeysSignatures[i - 1], xPubKeySignature: someXPubKeysSignatures[i - 1],
}; };
server.joinWallet(copayerOpts, function(err) { server.joinWallet(copayerOpts, function(err) {
return cb(err); return cb(err);
}); });
}, function(err) { }, function(err) {
if (err) return cb(err); if (err) return cb(err);
server.getWallet({ server.getWallet({
id: id, id: id,
includeCopayers: true 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) { helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) {
server.getWallet({ server.getWallet({
id: '123' id: '123'
@ -471,6 +487,12 @@ describe('Copay server', function() {
should.not.exist(err); should.not.exist(err);
wallet.status.should.equal('complete'); wallet.status.should.equal('complete');
wallet.publicKeyRing.length.should.equal(3); 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(); done();
}); });
}); });
@ -478,7 +500,6 @@ describe('Copay server', function() {
}); });
describe('#verifyMessageSignature', function() { describe('#verifyMessageSignature', function() {
beforeEach(function() { beforeEach(function() {
server = new CopayServer({ server = new CopayServer({
@ -525,11 +546,7 @@ describe('Copay server', function() {
}); });
}); });
it('should create address', function(done) { it('should create main address', function(done) {
server._doCreateAddress = sinon.stub().returns(new Address({
address: 'addr1',
path: 'path1',
}));
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
server.createAddress({ server.createAddress({
walletId: '123', walletId: '123',
@ -537,12 +554,29 @@ describe('Copay server', function() {
}, function(err, address) { }, function(err, address) {
should.not.exist(err); should.not.exist(err);
address.should.exist; address.should.exist;
address.address.should.equal('addr1'); address.address.should.equal('3BPfHzwq5j72TBYtYv3Uggk3vyHFHX3QpA');
address.path.should.equal('path1'); address.path.should.equal('m/0/0/1');
done(); 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() { describe('#createTx', function() {

91
test/wallet.js

@ -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
};
Loading…
Cancel
Save