Fix Conflicts:

views/unsupported.html
This commit is contained in:
Gustavo Maximiliano Cortez 2014-08-06 18:51:04 -03:00
commit 0c2141c380
53 changed files with 1997 additions and 1311 deletions

2
.gitignore vendored
View File

@ -8,6 +8,7 @@ lib-cov
*.pid
*.gz
*.swp
*.sig
tags
pids
logs
@ -48,6 +49,7 @@ browser-extensions/firefox/firefox-addon
browser-extensions/firefox/data
browser-extensions/firefox/copay.xpi
version.js
!js/controllers/version.js
android/package
android/*.apk

View File

@ -1,5 +1,6 @@
// core
module.exports.PublicKeyRing = require('./js/models/core/PublicKeyRing');
module.exports.TxProposal = require('./js/models/core/TxProposal');
module.exports.TxProposals = require('./js/models/core/TxProposals');
module.exports.PrivateKey = require('./js/models/core/PrivateKey');
module.exports.Passphrase = require('./js/models/core/Passphrase');

View File

@ -1006,7 +1006,7 @@ input.ng-invalid-match, input.ng-invalid-match:focus {
.text-primary {color: #1ABC9C;}
.text-secondary {color: #3498DB;}
.text-white {color: #fff;}
.text-warning {color: #CA5649;}
.footer-setup a.text-gray:hover {color: #fff;}
a.text-gray:hover {color: #2C3E50;}
@ -1014,6 +1014,7 @@ a.text-black:hover {color: #213140;}
a.text-primary:hover {color: #50E3C2;}
a.text-secondary:hover {color: #4A90E2;}
a.text-white:hover {color: #ccc;}
a.text-warning:hover {color: #FD7262;}
.box-setup-copayers {
background: #2C3E50;

30
img/step-1.svg Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="159px" height="16px" viewBox="0 0 159 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>Group@1x</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0.5" dy="0.5" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0663185009 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetInner1" result="shadowBlurInner1"></feGaussianBlur>
<feComposite in="shadowBlurInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0" in="shadowInnerInner1" type="matrix" result="shadowMatrixInner1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
<feMergeNode in="shadowMatrixInner1"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="new-2" sketch:type="MSArtboardGroup" transform="translate(-735.000000, -171.000000)">
<g id="Group" sketch:type="MSLayerGroup" transform="translate(736.000000, 171.000000)">
<path d="M71.4408381,10.856186 C72.6947093,13.3192708 75.2877935,15.0106827 78.2832665,15.0106827 C81.2787395,15.0106827 83.8718236,13.3192708 85.1256949,10.856186 L85.1256979,10.8561853 L142.032872,10.856186 C143.286744,13.3192708 145.879828,15.0106827 148.875301,15.0106827 C153.097385,15.0106827 156.52006,11.6504269 156.52006,7.50534137 C156.52006,3.36025579 153.097385,0 148.875301,0 C145.412202,0 142.486933,2.26072789 141.54712,5.36095812 L85.611447,5.36095812 C84.6716339,2.26072789 81.7463651,0 78.2832665,0 C74.8201679,0 71.894899,2.26072789 70.955086,5.36095812 L14.9264673,5.36095812 C13.9866542,2.26072789 11.0613854,0 7.59828677,0 C3.3762027,0 -0.046472702,3.36025579 -0.046472702,7.50534137 C-0.046472702,11.6504269 3.3762027,15.0106827 7.59828677,15.0106827 C10.5937598,15.0106827 13.1868439,13.3192708 14.4407152,10.856186 L71.4408351,10.8561853 Z" id="Oval-14" fill="#2C3E50" filter="url(#filter-1)" sketch:type="MSShapeGroup"></path>
<circle id="Oval-12" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="7.5" cy="7.5" r="4.5"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

32
img/step-2.svg Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="159px" height="18px" viewBox="0 0 159 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0.5" dy="0.5" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0663185009 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetInner1" result="shadowBlurInner1"></feGaussianBlur>
<feComposite in="shadowBlurInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0" in="shadowInnerInner1" type="matrix" result="shadowMatrixInner1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
<feMergeNode in="shadowMatrixInner1"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="new-3" sketch:type="MSArtboardGroup" transform="translate(-732.000000, -204.000000)">
<g id="Group" sketch:type="MSLayerGroup" transform="translate(733.000000, 204.724993)">
<path d="M71.376385,10.4695498 C72.6302562,12.9326346 75.2233404,14.6240465 78.2188133,14.6240465 C81.2142863,14.6240465 83.8073705,12.9326346 85.0612417,10.4695498 L85.0612448,10.4695491 L141.968419,10.4695498 C143.222291,12.9326346 145.815375,14.6240465 148.810848,14.6240465 C153.032932,14.6240465 156.455607,11.2637907 156.455607,7.11870514 C156.455607,2.97361957 153.032932,-0.386636224 148.810848,-0.386636224 C145.347749,-0.386636224 142.42248,1.87409167 141.482667,4.9743219 L85.5469939,4.9743219 C84.6071808,1.87409167 81.6819119,-0.386636224 78.2188133,-0.386636224 C74.7557148,-0.386636224 71.8304459,1.87409167 70.8906328,4.9743219 L14.8620142,4.9743219 C13.9222011,1.87409167 10.9969322,-0.386636224 7.53383365,-0.386636224 C3.31174957,-0.386636224 -0.110925827,2.97361957 -0.110925827,7.11870514 C-0.110925827,11.2637907 3.31174957,14.6240465 7.53383365,14.6240465 C10.5293066,14.6240465 13.1223908,12.9326346 14.376262,10.4695498 L71.3763819,10.4695491 Z" id="Oval-14" fill="#2C3E50" filter="url(#filter-1)" sketch:type="MSShapeGroup"></path>
<rect id="Rectangle-81" fill="#7A8C9E" sketch:type="MSShapeGroup" x="9.82695588" y="6.5" width="64" height="2"></rect>
<circle id="Oval-12" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="7.63363776" cy="7.31145467" r="4.5"></circle>
<circle id="Oval-15" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="78.0715917" cy="7.31145467" r="4.5"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

34
img/step-3.svg Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="159px" height="18px" viewBox="0 0 159 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0.5" dy="0.5" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0663185009 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetInner1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetInner1" result="shadowBlurInner1"></feGaussianBlur>
<feComposite in="shadowBlurInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0" in="shadowInnerInner1" type="matrix" result="shadowMatrixInner1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
<feMergeNode in="shadowMatrixInner1"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="new-3" sketch:type="MSArtboardGroup" transform="translate(-726.000000, -145.173121)">
<g id="Group" sketch:type="MSLayerGroup" transform="translate(727.000000, 146.000000)">
<path d="M71.4408381,10.856186 C72.6947093,13.3192708 75.2877935,15.0106827 78.2832665,15.0106827 C81.2787395,15.0106827 83.8718236,13.3192708 85.1256949,10.856186 L85.1256979,10.8561853 L142.032872,10.856186 C143.286744,13.3192708 145.879828,15.0106827 148.875301,15.0106827 C153.097385,15.0106827 156.52006,11.6504269 156.52006,7.50534137 C156.52006,3.36025579 153.097385,0 148.875301,0 C145.412202,0 142.486933,2.26072789 141.54712,5.36095812 L85.611447,5.36095812 C84.6716339,2.26072789 81.7463651,0 78.2832665,0 C74.8201679,0 71.894899,2.26072789 70.955086,5.36095812 L14.9264673,5.36095812 C13.9866542,2.26072789 11.0613854,0 7.59828677,0 C3.3762027,0 -0.046472702,3.36025579 -0.046472702,7.50534137 C-0.046472702,11.6504269 3.3762027,15.0106827 7.59828677,15.0106827 C10.5937598,15.0106827 13.1868439,13.3192708 14.4407152,10.856186 L71.4408351,10.8561853 Z" id="Oval-21" fill="#2C3E50" filter="url(#filter-1)" sketch:type="MSShapeGroup"></path>
<rect id="Rectangle-83" fill="#7A8C9E" sketch:type="MSShapeGroup" x="81" y="7" width="64" height="2"></rect>
<circle id="Oval-22" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="149.5" cy="7.5" r="4.5"></circle>
<rect id="Rectangle-84" fill="#7A8C9E" sketch:type="MSShapeGroup" x="10" y="7" width="64" height="2"></rect>
<circle id="Oval-19" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="7.5" cy="7.5" r="4.5"></circle>
<circle id="Oval-20" fill="#7A8C9E" sketch:type="MSShapeGroup" cx="78.5" cy="7.5" r="4.5"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -107,6 +107,7 @@
<script src="js/controllers/import.js"></script>
<script src="js/controllers/settings.js"></script>
<script src="js/controllers/uriPayment.js"></script>
<script src="js/controllers/version.js"></script>
<!-- PLACEHOLDER: CORDOVA SRIPT -->
<script src="js/mobile.js"></script>

View File

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.controllers').controller('CopayersController',
function($scope, $rootScope, $location, backupService) {
function($scope, $rootScope, $location, backupService, walletFactory, controllerUtils) {
$scope.backup = function() {
var w = $rootScope.wallet;
@ -18,4 +18,12 @@ angular.module('copayApp.controllers').controller('CopayersController',
$location.path('/addresses');
};
$scope.deleteWallet = function() {
var w = $rootScope.wallet;
w.disconnect();
walletFactory.delete(w.id, function() {
controllerUtils.logout();
});
};
});

View File

@ -9,5 +9,5 @@ angular.module('copayApp.controllers').controller('HomeController',
if ($rootScope.pendingPayment) {
notification.info('Login Required', 'Please open wallet to complete payment');
}
$scope.hasWallets = walletFactory.getWallets().length > 0 ? true : false;
$scope.hasWallets = (walletFactory.getWallets() && walletFactory.getWallets().length > 0) ? true : false;
});

View File

@ -11,7 +11,7 @@ angular.module('copayApp.controllers').controller('OpenController',
};
$scope.loading = false;
$scope.wallets = walletFactory.getWallets().sort(cmp);
$scope.selectedWalletId = $scope.wallets.length ? $scope.wallets[0].id : null;
$scope.selectedWalletId = walletFactory.storage.getLastOpened() || ($scope.wallets[0] && $scope.wallets[0].id);
$scope.openPassword = '';
$scope.open = function(form) {

View File

@ -50,7 +50,7 @@ angular.module('copayApp.controllers').controller('SendController',
var w = $rootScope.wallet;
w.createTx(address, amount, commentText, function(ntxid) {
if (w.totalCopayers > 1) {
if (w.isShared()) {
$scope.loading = false;
var message = 'The transaction proposal has been created';
notification.success('Success!', message);

View File

@ -76,6 +76,8 @@ angular.module('copayApp.controllers').controller('SettingsController',
unitToSatoshi: $scope.selectedUnit.value,
}));
window.location.reload();
// Go home reloading the application
var hashIndex = window.location.href.indexOf('#!/');
window.location = window.location.href.substr(0, hashIndex);
};
});

View File

@ -3,8 +3,6 @@
angular.module('copayApp.controllers').controller('SidebarController',
function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) {
$scope.version = copay.version;
$scope.networkName = config.networkName;
$scope.menu = [{
'title': 'Addresses',
'icon': 'fi-address-book',
@ -62,23 +60,6 @@ angular.module('copayApp.controllers').controller('SidebarController',
return new Array(num);
}
$http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) {
var toInt = function(s) {
return parseInt(s);
};
var latestVersion = data[0].name.replace('v', '').split('.').map(toInt);
var currentVersion = copay.version.split('.').map(toInt);
var title = 'Copay ' + data[0].name + ' available.';
var content;
if (currentVersion[0] < latestVersion[0]) {
content = 'It\'s important that you update your wallet at https://copay.io';
notification.version(title, content, true);
} else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) {
var content = 'Please update your wallet at https://copay.io';
notification.version(title, content, false);
}
});
// Init socket handlers (with no wallet yet)
controllerUtils.setSocketHandlers();

26
js/controllers/version.js Normal file
View File

@ -0,0 +1,26 @@
'use strict';
angular.module('copayApp.controllers').controller('VersionController',
function($scope, $rootScope, $http, notification) {
$scope.version = copay.version;
$scope.networkName = config.networkName;
$http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) {
var toInt = function(s) {
return parseInt(s);
};
var latestVersion = data[0].name.replace('v', '').split('.').map(toInt);
var currentVersion = copay.version.split('.').map(toInt);
var title = 'Copay ' + data[0].name + ' available.';
var content;
if (currentVersion[0] < latestVersion[0]) {
content = 'It\'s important that you update your wallet at https://copay.io';
notification.version(title, content, true);
} else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) {
var content = 'Please update your wallet at https://copay.io';
notification.version(title, content, false);
}
});
});

View File

@ -2,6 +2,7 @@
var imports = require('soop').imports();
var bitcore = require('bitcore');
var coinUtil = bitcore.util;
var preconditions = require('preconditions').singleton();
var http;
@ -37,33 +38,6 @@ function _asyncForEach(array, fn, callback) {
}
};
function removeRepeatedElements(ar) {
var ya = false,
v = "",
aux = [].concat(ar),
r = Array();
for (var i in aux) { //
v = aux[i];
ya = false;
for (var a in aux) {
if (v == aux[a]) {
if (ya == false) {
ya = true;
} else {
aux[a] = "";
}
}
}
}
for (var a in aux) {
if (aux[a] != "") {
r.push(aux[a]);
}
}
return r;
}
Insight.prototype._getOptions = function(method, path, data) {
return {
host: this.host,
@ -78,6 +52,25 @@ Insight.prototype._getOptions = function(method, path, data) {
};
};
// This is vulneable to txid maneability
// TODO: if ret = false,
// check output address from similar transactions.
//
Insight.prototype.checkSentTx = function(tx, cb) {
var hash = coinUtil.formatHashFull(tx.getHash());
var options = this._getOptions('GET', '/api/tx/' + hash);
this._request(options, function(err, res) {
if (err) return cb(err);
var ret = false;
if (res && res.txid === hash) {
ret = hash;
}
return cb(err, ret);
});
};
Insight.prototype.getTransactions = function(addresses, cb) {
preconditions.shouldBeArray(addresses);
preconditions.shouldBeFunction(cb);
@ -101,8 +94,11 @@ Insight.prototype.getTransactions = function(addresses, cb) {
callback();
});
}, function() {
var clean_txids = removeRepeatedElements(txids);
_asyncForEach(clean_txids, function(txid, callback2) {
var uniqueTxids = {};
for (var k in txids) {
uniqueTxids[txids[k]] = 1;
}
_asyncForEach(Object.keys(uniqueTxids), function(txid, callback2) {
var options = self._getOptions('GET', '/api/tx/' + txid);
self._request(options, function(err, res) {
txs.push(res);
@ -164,8 +160,8 @@ Insight.prototype.checkActivity = function(addresses, cb) {
var getOutputs = function(t) {
return flatArray(
t.vout.map(function(vout) {
return vout.scriptPubKey.addresses;
})
return vout.scriptPubKey.addresses;
})
);
};

View File

@ -33,12 +33,12 @@ HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) {
return BIP45_PUBLIC_PREFIX + '/' + sub;
};
HDPath.indicesForPath = function(path) {
HDPath.indexesForPath = function(path) {
preconditions.shouldBeString(path);
var s = path.split('/');
return {
isChange: s[3] === '1',
index: parseInt(s[4]),
addressIndex: parseInt(s[4]),
copayerIndex: parseInt(s[2])
};
};

View File

@ -23,14 +23,13 @@ function PublicKeyRing(opts) {
this.copayersHK = opts.copayersHK || [];
this.indexes = opts.indexes ? HDParams.fromList(opts.indexes)
: HDParams.init(this.totalCopayers);
this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) : HDParams.init(this.totalCopayers);
this.publicKeysCache = opts.publicKeysCache || {};
this.nicknameFor = opts.nicknameFor || {};
this.copayerIds = [];
this.copayersBackup = opts.copayersBackup || [];
this.addressToPath = {};
this.publicKeysCache = opts.publicKeysCache || {};
this.nicknameFor = opts.nicknameFor || {};
this.copayerIds = [];
this.copayersBackup = opts.copayersBackup || [];
this.addressToPath = {};
}
PublicKeyRing.fromObj = function(data) {
@ -100,14 +99,6 @@ PublicKeyRing.prototype._checkKeys = function() {
throw new Error('dont have required keys yet');
};
PublicKeyRing.prototype._newExtendedPublicKey = function() {
return new PrivateKey({
networkName: this.network.name
})
.deriveBIP45Branch()
.extendedPublicKeyString();
};
PublicKeyRing.prototype._updateBip = function(index) {
var hk = this.copayersHK[index].derive(HDPath.IdBranch);
this.copayerIds[index] = hk.eckey.public.toString('hex');
@ -126,6 +117,8 @@ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) {
};
PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) {
preconditions.checkArgument(newEpk);
if (this.isComplete())
throw new Error('PKR already has all required key:' + this.totalCopayers);
@ -134,10 +127,6 @@ PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) {
throw new Error('PKR already has that key');
});
if (!newEpk) {
newEpk = this._newExtendedPublicKey();
}
var i = this.copayersHK.length;
var bip = new HK(newEpk);
this.copayersHK.push(bip);
@ -192,7 +181,9 @@ PublicKeyRing.prototype.getAddress = function(index, isChange, id) {
// Overloaded to receive a PubkeyString or a consigner index
PublicKeyRing.prototype.getHDParams = function(id) {
var copayerIndex = this.getCosigner(id);
var index = this.indexes.filter(function(i) { return i.copayerIndex == copayerIndex });
var index = this.indexes.filter(function(i) {
return i.copayerIndex == copayerIndex
});
if (index.length != 1) throw new Error('no index for copayerIndex');
return index[0];
@ -231,9 +222,11 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) {
if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX;
if (typeof pubKey == 'number') return pubKey;
var sorted = this.copayersHK.map(function(h, i){
var sorted = this.copayersHK.map(function(h, i) {
return h.eckey.public.toString('hex');
}).sort(function(h1, h2){ return h1.localeCompare(h2); });
}).sort(function(h1, h2) {
return h1.localeCompare(h2);
});
var index = sorted.indexOf(pubKey);
if (index == -1) throw new Error('no public key in ring');
@ -255,41 +248,87 @@ PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) {
PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) {
opts = opts || {};
var isOwned = index.copayerIndex == HDPath.SHARED_INDEX
|| index.copayerIndex == copayerIndex;
var isOwned = index.copayerIndex == HDPath.SHARED_INDEX || index.copayerIndex == copayerIndex;
var ret = [];
if (!opts.excludeChange) {
for (var i = 0; i < index.changeIndex; i++) {
var a = this.getAddress(i, true, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: true,
owned: isOwned
});
}
var ret = [];
if (!opts.excludeChange) {
for (var i = 0; i < index.changeIndex; i++) {
var a = this.getAddress(i, true, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: true,
owned: isOwned
});
}
}
if (!opts.excludeMain) {
for (var i = 0; i < index.receiveIndex; i++) {
var a = this.getAddress(i, false, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: false,
owned: isOwned
});
}
if (!opts.excludeMain) {
for (var i = 0; i < index.receiveIndex; i++) {
var a = this.getAddress(i, false, index.copayerIndex);
ret.unshift({
address: a,
addressStr: a.toString(),
isChange: false,
owned: isOwned
});
}
}
return ret;
return ret;
};
PublicKeyRing.prototype.getForPath = function(path) {
var p = HDPath.indexesForPath(path);
var pubKeys = this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex);
return pubKeys;
};
PublicKeyRing.prototype.getForPaths = function(paths) {
preconditions.checkArgument(paths);
return paths.map(this.getForPath.bind(this));
};
PublicKeyRing.prototype.forPaths = function(paths) {
return {
pubKeys: paths.map(this.getForPath.bind(this)),
copayerIds: this.copayerIds,
}
};
// returns pubkey -> copayerId.
PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) {
preconditions.checkArgument(pubkeys);
preconditions.checkArgument(paths);
var inKeyMap = {}, ret = {};
for(var i in pubkeys ){
inKeyMap[pubkeys[i]] = 1;
};
var keys = this.getForPaths(paths);
for(var i in keys ){
for(var copayerIndex in keys[i] ){
var kHex = keys[i][copayerIndex].toString('hex');
if (inKeyMap[kHex]) {
ret[kHex] =this.copayerIds[copayerIndex];
delete inKeyMap[kHex];
}
}
}
for(var i in inKeyMap)
throw new Error('Pubkey not identified')
return ret;
};
// TODO this could be cached
PublicKeyRing.prototype._addScriptMap = function(map, path) {
var p = HDPath.indicesForPath(path);
var script = this.getRedeemScript(p.index, p.isChange, p.copayerIndex);
var p = HDPath.indexesForPath(path);
var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex);
map[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex');
};

View File

@ -0,0 +1,321 @@
'use strict';
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
var VERSION = 1;
var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment'];
function TxProposal(opts) {
preconditions.checkArgument(opts);
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
preconditions.checkArgument(opts.builder, 'no builder');
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
this.inputChainPaths = opts.inputChainPaths;
this.version = opts.version;
this.builder = opts.builder;
this.createdTs = opts.createdTs;
this.createdTs = opts.createdTs;
this._inputSignatures = [];
// CopayerIds
this.creator = opts.creator;
this.signedBy = opts.signedBy || {};
this.seenBy = opts.seenBy || {};
this.rejectedBy = opts.rejectedBy || {};
this.sentTs = opts.sentTs || null;
this.sentTxid = opts.sentTxid || null;
this.comment = opts.comment || null;
this.readonly = opts.readonly || null;
this._sync();
}
TxProposal.prototype._check = function() {
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
throw new Error('Invalid tx proposal');
}
var tx = this.builder.build();
if (!tx.ins.length)
throw new Error('Invalid tx proposal: no ins');
for (var i in tx.ins) {
var scriptSig = tx.ins[i].s;
if (!scriptSig || !scriptSig.length) {
throw new Error('Invalid tx proposal: no signatures');
}
}
for (var i = 0; i < tx.ins.length; i++) {
var hashType = tx.getHashType(i);
if (hashType && hashType !== Transaction.SIGHASH_ALL)
throw new Error('Invalid tx proposal: bad signatures');
}
};
TxProposal.prototype._updateSignedBy = function() {
this._inputSignatures = [];
var tx = this.builder.build();
for (var i in tx.ins) {
var scriptSig = new Script(tx.ins[i].s);
var signatureCount = scriptSig.countSignatures();
var info = TxProposal._infoFromRedeemScript(scriptSig);
var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL);
var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash);
if (signatureIndexes.length !== signatureCount)
throw new Error('Invalid signature');
this._inputSignatures[i] = signatureIndexes.map(function(i) {
var r = info.keys[i].toString('hex');
return r;
});
};
};
TxProposal.prototype._sync = function() {
this._check();
this._updateSignedBy();
return this;
}
TxProposal.prototype.getId = function() {
preconditions.checkState(this.builder);
return this.builder.build().getNormalizedHash().toString('hex');
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
o.builderObj = this.builder.toObj();
return o;
};
TxProposal._trim = function(o) {
var ret = {};
CORE_FIELDS.forEach(function(k) {
ret[k] = o[k];
});
return ret;
};
TxProposal.fromObj = function(o, forceOpts) {
preconditions.checkArgument(o.builderObj);
delete o['builder'];
try {
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
o.builder = TransactionBuilder.fromObj(o.builderObj);
} catch (e) {
// backwards (V0) compatatibility fix.
if (!o.version) {
o.builder = new BuilderMockV0(o.builderObj);
o.readonly = 1;
};
}
return new TxProposal(o);
};
TxProposal.fromUntrustedObj = function(o, forceOpts) {
return TxProposal.fromObj(TxProposal._trim(o), forceOpts);
};
TxProposal.prototype.toObjTrim = function() {
return TxProposal._trim(this.toObj());
};
TxProposal._formatKeys = function(keys) {
var ret = [];
for (var i in keys) {
if (!Buffer.isBuffer(keys[i]))
throw new Error('keys must be buffers');
var k = new Key();
k.public = keys[i];
ret.push(k);
};
return ret;
};
TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) {
preconditions.checkArgument(Buffer.isBuffer(txSigHash));
preconditions.checkArgument(inKeys);
preconditions.checkState(Buffer.isBuffer(inKeys[0]));
if (scriptSig.chunks[0] !== 0)
throw new Error('Invalid scriptSig');
var keys = TxProposal._formatKeys(inKeys);
var ret = [];
for (var i = 1; i <= scriptSig.countSignatures(); i++) {
var chunk = scriptSig.chunks[i];
var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1));
for (var j in keys) {
var k = keys[j];
if (k.verifySignatureSync(txSigHash, sigRaw)) {
ret.push(parseInt(j));
break;
}
}
}
return ret;
};
TxProposal._infoFromRedeemScript = function(s) {
var redeemScript = new Script(s.chunks[s.chunks.length - 1]);
if (!redeemScript)
throw new Error('Bad scriptSig (no redeemscript)');
var pubkeys = redeemScript.capture();
if (!pubkeys || !pubkeys.length)
throw new Error('Bad scriptSig (no pubkeys)');
return {
keys: pubkeys,
script: redeemScript,
};
};
TxProposal.prototype.mergeBuilder = function(incoming) {
var b0 = this.builder;
var b1 = incoming.builder;
var before = JSON.stringify(b0.toObj());
b0.merge(b1);
var after = JSON.stringify(b0.toObj());
return after !== before;
};
TxProposal.prototype.setSeen = function(copayerId) {
if (!this.seenBy[copayerId])
this.seenBy[copayerId] = Date.now();
};
TxProposal.prototype.setRejected = function(copayerId) {
if (this.signedBy[copayerId])
throw new Error('Can not reject a signed TX');
if (!this.rejectedBy[copayerId])
this.rejectedBy[copayerId] = Date.now();
};
TxProposal.prototype.setSent = function(sentTxid) {
this.sentTxid = sentTxid;
this.sentTs = Date.now();
};
TxProposal.prototype._allSignatures = function() {
var ret = {};
for (var i in this._inputSignatures)
for (var j in this._inputSignatures[i])
ret[this._inputSignatures[i][j]] = true;
return ret;
};
TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
var newCopayer = {},
oldCopayers = {},
newSignedBy = {},
readOnlyPeers = {},
isNew = 1;
for (var k in this.signedBy) {
oldCopayers[k] = 1;
isNew = 0;
};
if (isNew == 0) {
if (!this.creator || !this.createdTs)
throw new Error('Existing TX has no creator');
if (!this.signedBy[this.creator])
throw new Error('Existing TX is not signed by creator');
if (Object.keys(this.signedBy).length === 0)
throw new Error('Existing TX has no signatures');
}
var iSig = this._inputSignatures[0];
for (var i in iSig) {
var copayerId = keyMap[iSig[i]];
if (!copayerId)
throw new Error('Found unknown signature')
if (oldCopayers[copayerId]) {
//Already have it. Do nothing
} else {
newCopayer[copayerId] = Date.now();
delete oldCopayers[i];
}
}
// Seems unncessary to check this:
// if (!newCopayer[senderId] && !readOnlyPeers[senderId])
// throw new Error('TX must have a (new) senders signature')
if (Object.keys(newCopayer).length > 1)
throw new Error('New TX must have only 1 new signature');
// Handler creator / createdTs.
// from senderId, and must be signed by senderId
if (isNew) {
this.creator = Object.keys(newCopayer)[0];
this.seenBy[this.creator] = this.createdTs = Date.now();
}
//Ended. Update this.
for (var i in newCopayer) {
this.signedBy[i] = newCopayer[i];
}
// signedBy has preference over rejectedBy
for (var i in this.signedBy) {
delete this.rejectedBy[i];
}
return Object.keys(newCopayer);
};
// merge will not merge any metadata.
TxProposal.prototype.merge = function(incoming) {
var hasChanged = this.mergeBuilder(incoming);
this._sync();
return hasChanged;
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();
var ret = 0;
for (var i in tx.ins) {
ret += tx.countInputSignatures(i);
}
return ret;
};
module.exports = TxProposal;

View File

@ -1,174 +1,16 @@
'use strict';
var imports = require('soop').imports();
var BuilderMockV0 = require('./BuilderMockV0');;
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var TxProposal = require('./TxProposal');;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
function TxProposal(opts) {
this.creator = opts.creator;
this.createdTs = opts.createdTs;
this.seenBy = opts.seenBy || {};
this.signedBy = opts.signedBy || {};
this.rejectedBy = opts.rejectedBy || {};
this.builder = opts.builder;
this.sentTs = opts.sentTs || null;
this.sentTxid = opts.sentTxid || null;
this.inputChainPaths = opts.inputChainPaths || [];
this.comment = opts.comment || null;
}
TxProposal.prototype.getID = function() {
return this.builder.build().getNormalizedHash().toString('hex');
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
o.builderObj = this.builder.toObj();
return o;
};
TxProposal.prototype.setSent = function(sentTxid) {
this.sentTxid = sentTxid;
this.sentTs = Date.now();
};
TxProposal.fromObj = function(o, forceOpts) {
var t = new TxProposal(o);
try {
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
t.builder = TransactionBuilder.fromObj(o.builderObj);
} catch (e) {
if (!o.version) {
t.builder = new BuilderMockV0(o.builderObj);
t.readonly = 1;
};
}
return t;
};
TxProposal.prototype.isValid = function() {
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
return false;
}
var tx = this.builder.build();
for (var i = 0; i < tx.ins.length; i++) {
var hashType = tx.getHashType(i);
if (hashType && hashType !== Transaction.SIGHASH_ALL) {
return false;
}
}
return true;
};
TxProposal.getSentTs = function() {
return this.sentTs;
};
TxProposal.prototype.merge = function(other, author) {
var ret = {};
ret.events = this.mergeMetadata(other, author);
ret.hasChanged = this.mergeBuilder(other);
return ret;
};
TxProposal.prototype.mergeBuilder = function(other) {
var b0 = this.builder;
var b1 = other.builder;
// TODO: improve this comparison
var before = JSON.stringify(b0.toObj());
b0.merge(b1);
var after = JSON.stringify(b0.toObj());
return after !== before;
};
TxProposal.prototype.mergeMetadata = function(v1, author) {
var events = [];
var v0 = this;
var ntxid = this.getID();
Object.keys(v1.seenBy).forEach(function(k) {
if (!v0.seenBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative seenBy change by ' + author);
v0.seenBy[k] = v1.seenBy[k];
events.push({
type: 'seen',
cId: k,
txId: ntxid
});
}
});
Object.keys(v1.signedBy).forEach(function(k) {
if (!v0.signedBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative signedBy change by ' + author);
v0.signedBy[k] = v1.signedBy[k];
events.push({
type: 'signed',
cId: k,
txId: ntxid
});
}
});
Object.keys(v1.rejectedBy).forEach(function(k) {
if (!v0.rejectedBy[k]) {
// TODO: uncomment below and change protocol to make this work
//if (k != author) throw new Error('Non authoritative rejectedBy change by ' + author);
v0.rejectedBy[k] = v1.rejectedBy[k];
events.push({
type: 'rejected',
cId: k,
txId: ntxid
});
}
});
if (!v0.sentTxid && v1.sentTxid) {
v0.sentTs = v1.sentTs;
v0.sentTxid = v1.sentTxid;
events.push({
type: 'broadcast',
txId: ntxid
});
}
return events;
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();
var ret = 0;
for (var i in tx.ins) {
ret += tx.countInputSignatures(i);
}
return ret;
};
module.exports = require('soop')(TxProposal);
function TxProposals(opts) {
opts = opts || {};
@ -178,6 +20,7 @@ function TxProposals(opts) {
this.txps = {};
}
// fromObj => from a trusted source
TxProposals.fromObj = function(o, forceOpts) {
var ret = new TxProposals({
networkName: o.networkName,
@ -187,7 +30,7 @@ TxProposals.fromObj = function(o, forceOpts) {
o.txps.forEach(function(o2) {
var t = TxProposal.fromObj(o2, forceOpts);
if (t.builder) {
var id = t.getID();
var id = t.getId();
ret.txps[id] = t;
}
});
@ -198,14 +41,9 @@ TxProposals.prototype.getNtxids = function() {
return Object.keys(this.txps);
};
TxProposals.prototype.toObj = function(onlyThisNtxid) {
if (onlyThisNtxid) throw new Error();
TxProposals.prototype.toObj = function() {
var ret = [];
for (var id in this.txps) {
if (onlyThisNtxid && id != onlyThisNtxid)
continue;
var t = this.txps[id];
if (!t.sent)
ret.push(t.toObj());
@ -217,50 +55,53 @@ TxProposals.prototype.toObj = function(onlyThisNtxid) {
};
};
TxProposals.prototype.merge = function(inTxp, author) {
var myTxps = this.txps;
var ntxid = inTxp.getID();
var ret = {};
ret.events = [];
ret.events.hasChanged = false;
TxProposals.prototype.merge = function(inObj, builderOpts) {
var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts);
incomingTx._sync();
var myTxps = this.txps;
var ntxid = incomingTx.getId();
var ret = {
ntxid: ntxid
};
if (myTxps[ntxid]) {
var v0 = myTxps[ntxid];
var v1 = inTxp;
ret = v0.merge(v1, author);
// Merge an existing txProposal
ret.hasChanged = myTxps[ntxid].merge(incomingTx);
} else {
this.txps[ntxid] = inTxp;
ret.hasChanged = true;
ret.events.push({
type: 'new',
cid: inTxp.creator,
tx: ntxid
});
// Create a new one
ret.new = ret.hasChanged = 1;
this.txps[ntxid] = incomingTx;
}
ret.txp = this.txps[ntxid];
return ret;
};
TxProposals.prototype.add = function(data) {
preconditions.checkArgument(data.inputChainPaths);
preconditions.checkArgument(data.signedBy);
preconditions.checkArgument(data.creator);
preconditions.checkArgument(data.createdTs);
preconditions.checkArgument(data.builder);
var txp = new TxProposal(data);
var ntxid = txp.getID();
// Add a LOCALLY CREATED (trusted) tx proposal
TxProposals.prototype.add = function(txp) {
txp._sync();
var ntxid = txp.getId();
this.txps[ntxid] = txp;
return ntxid;
};
TxProposals.prototype.setSent = function(ntxid, txid) {
//sent TxProposals are local an not broadcasted.
this.txps[ntxid].setSent(txid);
TxProposals.prototype.get = function(ntxid) {
var ret = this.txps[ntxid];
if (!ret)
throw new Error('Unknown TXP: '+ntxid);
return ret;
};
TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
var txp = this.txps[ntxid];
var txp = this.get(ntxid);
var i = JSON.parse(JSON.stringify(txp));
i.builder = txp.builder;
i.ntxid = ntxid;
@ -296,6 +137,17 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
return i;
};
TxProposals.prototype.reject = function(ntxid, copayerId) {
var txp = this.get(ntxid);
txp.setRejected(copayerId);
};
TxProposals.prototype.seen = function(ntxid, copayerId) {
var txp = this.get(ntxid);
txp.setSeen(copayerId);
};
//returns the unspent txid-vout used in PENDING Txs
TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
var ret = {};
@ -312,5 +164,4 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
return ret;
};
TxProposals.TxProposal = TxProposal;
module.exports = require('soop')(TxProposals);
module.exports = TxProposals;

View File

@ -17,6 +17,7 @@ var Address = bitcore.Address;
var HDParams = require('./HDParams');
var PublicKeyRing = require('./PublicKeyRing');
var TxProposal = require('./TxProposal');
var TxProposals = require('./TxProposals');
var PrivateKey = require('./PrivateKey');
var copayConfig = require('../../../config');
@ -36,7 +37,7 @@ function Wallet(opts) {
});
if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName)
throw new Error('Network forced to ' + copayConfig.networkName +
' and tried to create a Wallet with network ' + this.getNetworkName());
' and tried to create a Wallet with network ' + this.getNetworkName());
this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet');
@ -58,11 +59,11 @@ function Wallet(opts) {
}
Wallet.builderOpts = {
lockTime: null,
signhash: bitcore.Transaction.SIGNHASH_ALL,
fee: null,
feeSat: null,
Wallet.builderOpts = {
lockTime: null,
signhash: bitcore.Transaction.SIGNHASH_ALL,
fee: null,
feeSat: null,
};
Wallet.parent = EventEmitter;
@ -129,39 +130,158 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
};
Wallet.prototype._handleTxProposal = function(senderId, data) {
this.log('RECV TXPROPOSAL: ', data);
var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts);
var valid = inTxp.isValid();
if (!valid) {
var corruptEvent = {
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: inTxp.creator
cId: senderId,
};
this.emit('txProposalEvent', corruptEvent);
return;
}
var mergeInfo = this.txProposals.merge(inTxp, senderId);
var added = this.addSeenToTxProposals();
if (added) {
this.log('### BROADCASTING txProposals with my seenBy updated.');
this.sendTxProposal(inTxp.getID());
}
this.emit('txProposalsUpdated');
this.store();
for (var i = 0; i < mergeInfo.events.length; i++) {
this.emit('txProposalEvent', mergeInfo.events[i]);
}
if (ev)
this.emit('txProposalEvent', ev);
};
/* OTDO
events.push({
type: 'signed',
cId: k,
txId: ntxid
});
*/
Wallet.prototype._getKeyMap = function(txp) {
preconditions.checkArgument(txp);
var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[0], txp.inputChainPaths);
var inSig = JSON.stringify(txp._inputSignatures[0].sort());
if (JSON.stringify(Object.keys(keyMap).sort()) !== inSig) {
throw new Error('inputSignatures dont match know copayers pubkeys');
}
var keyMapStr = JSON.stringify(keyMap);
// All inputs must be signed with the same copayers
for (var i in txp._inputSignatures) {
if (!i) continue;
var inSigX = JSON.stringify(txp._inputSignatures[i].sort());
if (inSigX !== inSig)
throw new Error('found inputs with different signatures:');
}
return keyMap;
};
Wallet.prototype._checkSentTx = function(ntxid, cb) {
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
this.blockchain.checkSentTx(tx, function(err, txid) {
var ret = false;
if (txid) {
txp.setSent(txid);
ret = txid;
}
return cb(ret);
});
};
Wallet.prototype._handleTxProposal = function(senderId, data) {
var self = this;
this.log('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) {
this.log('Corrupt TX proposal received from:', senderId, e);
}
if (m) {
if (m.hasChanged) {
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);
};
Wallet.prototype._handleReject = function(senderId, data, isInbound) {
preconditions.checkState(data.ntxid);
this.log('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,
});
};
Wallet.prototype._handleSeen = function(senderId, data, isInbound) {
preconditions.checkState(data.ntxid);
this.log('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,
});
};
Wallet.prototype._handleAddressBook = function(senderId, data, isInbound) {
preconditions.checkState(data.addressBook);
this.log('RECV ADDRESSBOOK:', data);
var rcv = data.addressBook;
var hasChange;
@ -193,24 +313,30 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) {
// This handler is repeaded on WalletFactory (#join). TODO
case 'walletId':
this.sendWalletReady(senderId);
break;
break;
case 'walletReady':
this.sendPublicKeyRing(senderId);
this.sendAddressBook(senderId);
this.sendAllTxProposals(senderId); // send old txps
break;
this.sendAddressBook(senderId);
this.sendAllTxProposals(senderId); // send old txps
break;
case 'publicKeyRing':
this._handlePublicKeyRing(senderId, data, isInbound);
break;
break;
case 'reject':
this._handleReject(senderId, data, isInbound);
break;
case 'seen':
this._handleSeen(senderId, data, isInbound);
break;
case 'txProposal':
this._handleTxProposal(senderId, data, isInbound);
break;
break;
case 'indexes':
this._handleIndexes(senderId, data, isInbound);
break;
break;
case 'addressbook':
this._handleAddressBook(senderId, data, isInbound);
break;
break;
}
};
@ -384,6 +510,7 @@ Wallet.prototype.toObj = function() {
return walletObj;
};
// fromObj => from a trusted source
Wallet.fromObj = function(o, storage, network, blockchain) {
var opts = JSON.parse(JSON.stringify(o.opts));
opts.addressBook = o.addressBook;
@ -418,11 +545,31 @@ Wallet.prototype.sendAllTxProposals = function(recipients) {
Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
preconditions.checkArgument(ntxid);
preconditions.checkState(this.txProposals.txps[ntxid]);
this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals);
this.send(recipients, {
type: 'txProposal',
txProposal: this.txProposals.txps[ntxid].toObj(),
txProposal: this.txProposals.get(ntxid).toObjTrim(),
walletId: this.id,
});
};
Wallet.prototype.sendSeen = function(ntxid) {
preconditions.checkArgument(ntxid);
this.log('### SENDING seen: ' + ntxid + ' TO: All');
this.send(null, {
type: 'seen',
ntxid: ntxid,
walletId: this.id,
});
};
Wallet.prototype.sendReject = function(ntxid) {
preconditions.checkArgument(ntxid);
this.log('### SENDING reject: ' + ntxid + ' TO: All');
this.send(null, {
type: 'reject',
ntxid: ntxid,
walletId: this.id,
});
};
@ -517,30 +664,22 @@ Wallet.prototype.getTxProposals = function() {
Wallet.prototype.reject = function(ntxid) {
var myId = this.getMyCopayerId();
var txp = this.txProposals.txps[ntxid];
if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
throw new Error('Invalid transaction to reject: ' + ntxid);
}
txp.rejectedBy[myId] = Date.now();
this.sendTxProposal(ntxid);
var txp = this.txProposals.reject(ntxid, this.getMyCopayerId());
this.sendReject(ntxid);
this.store();
this.emit('txProposalsUpdated');
};
Wallet.prototype.sign = function(ntxid, cb) {
preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined');
var self = this;
setTimeout(function() {
var myId = self.getMyCopayerId();
var txp = self.txProposals.txps[ntxid];
if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
if (cb) cb(false);
}
var txp = self.txProposals.get(ntxid);
// if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
// if (cb) cb(false);
// }
//
var keys = self.privateKey.getForPaths(txp.inputChainPaths);
var b = txp.builder;
@ -559,14 +698,13 @@ Wallet.prototype.sign = function(ntxid, cb) {
}, 10);
};
Wallet.prototype.sendTx = function(ntxid, cb) {
var txp = this.txProposals.txps[ntxid];
if (!txp) return;
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
if (!tx.isComplete()) return;
if (!tx.isComplete())
throw new Error('Tx is not complete. Can not broadcast');
this.log('Broadcasting Transaction');
var scriptSig = tx.ins[0].getScript();
var size = scriptSig.serialize().length;
@ -577,28 +715,23 @@ Wallet.prototype.sendTx = function(ntxid, cb) {
this.blockchain.sendRawTransaction(txHex, function(txid) {
self.log('BITCOIND txid:', txid);
if (txid) {
self.txProposals.setSent(ntxid, txid);
self.txProposals.get(ntxid).setSent(txid);
self.sendTxProposal(ntxid);
self.store();
return cb(txid);
} else {
self.log('Sent failed. Checking is the TX was sent already');
self._checkSentTx(ntxid, function(txid) {
console.log('[Wallet.js.730:txid:]', txid); //TODO
if (txid)
self.store();
return cb(txid);
});
}
return cb(txid);
});
};
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;
};
// TODO: remove this method and use getAddressesInfo everywhere
Wallet.prototype.getAddresses = function(opts) {
@ -719,8 +852,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
var priv = this.privateKey;
opts = opts || {};
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName());
preconditions.checkState(pkr.isComplete());
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch');
preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete');
preconditions.checkState(priv, 'no private key');
if (comment) preconditions.checkArgument(comment.length <= 100);
if (!opts.remainderOut) {
@ -729,16 +863,16 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
};
}
for (var k in Wallet.builderOpts){
for (var k in Wallet.builderOpts) {
opts[k] = Wallet.builderOpts[k];
}
var b = new Builder(opts)
.setUnspent(utxos)
.setOutputs([{
address: toAddress,
amountSatStr: amountSatStr,
}]);
.setUnspent(utxos)
.setOutputs([{
address: toAddress,
amountSatStr: amountSatStr,
}]);
var selectedUtxos = b.getSelectedUnspent();
var inputChainPaths = selectedUtxos.map(function(utxo) {
@ -747,22 +881,23 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
if (priv) {
var keys = priv.getForPaths(inputChainPaths);
var signed = b.sign(keys);
}
var keys = priv.getForPaths(inputChainPaths);
var signed = b.sign(keys);
var myId = this.getMyCopayerId();
var now = Date.now();
var me = {};
var tx = b.build();
if (priv && tx.countInputSignatures(0)) me[myId] = now;
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 data = {
var ntxid = this.txProposals.add(new TxProposal({
inputChainPaths: inputChainPaths,
signedBy: me,
seenBy: meSeen,
@ -770,9 +905,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
createdTs: now,
builder: b,
comment: comment
};
var ntxid = this.txProposals.add(data);
}));
return ntxid;
};
@ -831,29 +964,29 @@ Wallet.prototype.indexDiscovery = function(start, change, cosigner, gap, cb) {
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, cosigner);
self.blockchain.checkActivity(addresses, function(err, actives) {
if (err) throw err;
// Optimize window to minimize the derivations.
var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1;
var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner);
self.blockchain.checkActivity(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 _finnaly(err) {
if (err) return cb(err);
cb(null, lastActive);
}
// 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 _finnaly(err) {
if (err) return cb(err);
cb(null, lastActive);
}
);
}
@ -913,6 +1046,10 @@ Wallet.prototype.toggleAddressBookEntry = function(key) {
this.store();
};
Wallet.prototype.isShared = function() {
return this.totalCopayers > 1;
}
Wallet.prototype.isReady = function() {
var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup();
return ret;

View File

@ -102,10 +102,7 @@ WalletFactory.prototype.read = function(walletId) {
WalletFactory.prototype.create = function(opts) {
opts = opts || {};
this.log('### CREATING NEW WALLET.' +
(opts.id ? ' USING ID: ' + opts.id : ' NEW ID') +
(opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')
);
this.log('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey'));
opts.privateKey = opts.privateKey || new PrivateKey({
networkName: this.networkName
@ -121,7 +118,8 @@ WalletFactory.prototype.create = function(opts) {
});
opts.publicKeyRing.addCopayer(
opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(),
opts.nickname);
opts.nickname
);
this.log('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({
@ -143,6 +141,7 @@ WalletFactory.prototype.create = function(opts) {
opts.version = opts.version || this.version;
var w = new Wallet(opts);
w.store();
this.storage.setLastOpened(w.id);
return w;
};
@ -156,9 +155,9 @@ WalletFactory.prototype._checkVersion = function(inVersion) {
//We only check for major version differences
if (thisV0 < inV0) {
throw new Error('Major difference in software versions' +
'. Received:' + inVersion +
'. Current version:' + this.version +
'. Aborting.');
'. Received:' + inVersion +
'. Current version:' + this.version +
'. Aborting.');
}
};
@ -179,6 +178,8 @@ WalletFactory.prototype.open = function(walletId, opts) {
if (w) {
w.store();
}
this.storage.setLastOpened(walletId);
return w;
};
@ -194,6 +195,7 @@ WalletFactory.prototype.delete = function(walletId, cb) {
var s = this.storage;
this.log('## DELETING WALLET ID:' + walletId); //TODO
s.deleteWallet(walletId);
s.setLastOpened(undefined);
return cb();
};

View File

@ -172,6 +172,13 @@ Storage.prototype.deleteWallet = function(walletId) {
}
};
Storage.prototype.setLastOpened = function(walletId) {
this.setGlobal('lastOpened', walletId);
}
Storage.prototype.getLastOpened = function() {
return this.getGlobal('lastOpened');
}
//obj contains keys to be set
Storage.prototype.setFromObj = function(walletId, obj) {

View File

@ -277,17 +277,28 @@ angular.module('copayApp.services')
i.outs = outs;
i.fee = i.builder.feeSat * satToUnit;
i.missingSignatures = tx.countInputMissingSignatures(0);
i.actionList = getActionList(i.peerActions);
txs.push(i);
}
});
$rootScope.txs = txs; //.some(function(i) {return i.isPending; } );
$rootScope.txs = txs;
if ($rootScope.pendingTxCount < pendingForUs) {
$rootScope.txAlertCount = pendingForUs;
}
$rootScope.pendingTxCount = pendingForUs;
};
function getActionList(actions) {
var peers = Object.keys(actions).map(function(i) {
return {cId: i, actions: actions[i] }
});
return peers.sort(function(a, b) {
return !!b.actions.create - !!a.actions.create;
});
}
$rootScope.$watch('insightError', function(status) {
if (status) {
if (status === -1) {

View File

@ -1,6 +1,5 @@
'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore');
function FakeBlockchain(opts) {
@ -47,4 +46,4 @@ FakeBlockchain.prototype.sendRawTransaction = function(rawtx, cb) {
return cb(txid);
};
module.exports = require('soop')(FakeBlockchain);
module.exports = FakeBlockchain;

51
test/mocks/FakeBuilder.js Normal file
View File

@ -0,0 +1,51 @@
'use scrict';
var bitcore = bitcore || require('bitcore');
var Script = bitcore.Script;
var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae','hex');
function Tx() {
this.ins = [{s: VALID_SCRIPTSIG_BUF }];
};
Tx.prototype.getHashType = function() {
return 1;
};
Tx.prototype.getNormalizedHash = function() {
return '123456';
};
Tx.prototype.hashForSignature = function() {
return new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex');
};
function FakeBuilder() {
this.test = 1;
this.tx = new Tx();
this.signhash = 1;
this.inputMap = [{ address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6',
scriptPubKey: new Script(new Buffer('a914dc0623476aefb049066b09b0147a022e6eb8429187', 'hex')),
scriptType: 4,
i: 0 }];
this.vanilla = {
scriptSig: [VALID_SCRIPTSIG_BUF],
}
}
FakeBuilder.prototype.merge = function() {
};
FakeBuilder.prototype.build = function() {
return this.tx;
};
FakeBuilder.prototype.toObj = function() {
return this;
};
FakeBuilder.VALID_SCRIPTSIG_BUF = VALID_SCRIPTSIG_BUF;
module.exports = FakeBuilder;

View File

@ -19,6 +19,13 @@ FakeStorage.prototype.getGlobal = function(id) {
return this.storage[id];
};
FakeStorage.prototype.setLastOpened = function(val) {
this.storage['lastOpened'] = val;
};
FakeStorage.prototype.getLastOpened = function() {
return this.storage['lastOpened'];
};
FakeStorage.prototype.removeGlobal = function(id) {
delete this.storage[id];

View File

@ -46,6 +46,10 @@ FakeWallet.prototype.getAddressesInfo = function() {
return ret;
};
FakeWallet.prototype.isShared = function() {
return this.totalCopayers > 1;
}
FakeWallet.prototype.isReady = function() {
return true;
}

View File

@ -70,9 +70,9 @@ describe('HDPath model', function() {
].forEach(function(datum) {
var path = datum[0];
var result = datum[1];
it('should get the correct indices for path ' + path, function() {
var i = HDPath.indicesForPath(path);
i.index.should.equal(result.index);
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

@ -13,11 +13,20 @@ try {
} catch (e) {
var copay = require('../copay'); //node
}
var PrivateKey = copay.PrivateKey;
var PublicKeyRing = copay.PublicKeyRing;
var aMasterPubKey = 'tprv8ZgxMBicQKsPdSVTiWXEqCCzqRaRr9EAQdn5UVMpT9UHX67Dh1FmzEMbavPumpAicsUm2XvC6NTdcWB89yN5DUWx5HQ7z3KByUg7Ht74VRZ';
var getNewEpk = function() {
return new PrivateKey({
networkName: 'livenet',
})
.deriveBIP45Branch()
.extendedPublicKeyString();
}
var createW = function(networkName) {
var config = {
networkName: networkName || 'livenet',
@ -29,8 +38,8 @@ var createW = function(networkName) {
var copayers = [];
for (var i = 0; i < 5; i++) {
w.isComplete().should.equal(false);
w.remainingCopayers().should.equal(5-i);
var newEpk = w.addCopayer();
w.remainingCopayers().should.equal(5 - i);
var newEpk = w.addCopayer(getNewEpk());
copayers.push(newEpk);
}
w.isComplete().should.equal(true);
@ -43,6 +52,14 @@ var createW = function(networkName) {
};
};
var cachedW;
var getCachedW = function() {
if (!cachedW) {
cachedW = createW();
}
return cachedW;
};
describe('PublicKeyRing model', function() {
it('should create an instance (livenet)', function() {
@ -78,7 +95,7 @@ describe('PublicKeyRing model', function() {
});
it('should add and check when adding shared pub keys', function() {
var k = createW();
var k = getCachedW();
var w = k.w;
var copayers = k.copayers;
@ -92,7 +109,7 @@ describe('PublicKeyRing model', function() {
});
it('should be able to to store and read', function() {
var k = createW();
var k = getCachedW();
var w = k.w;
var copayers = k.copayers;
var changeN = 2;
@ -124,10 +141,10 @@ describe('PublicKeyRing model', function() {
it('should generate some p2sh addresses', function() {
var k = createW();
var k = getCachedW();
var w = k.w;
[true, false].forEach(function(isChange){
[true, false].forEach(function(isChange) {
for (var i = 0; i < 2; i++) {
var a = w.generateAddress(isChange, k.pub);
a.isValid().should.equal(true);
@ -148,7 +165,7 @@ describe('PublicKeyRing model', function() {
var a = w.getAddresses();
a.length.should.equal(1);
[true, false].forEach(function(isChange){
[true, false].forEach(function(isChange) {
for (var i = 0; i < 2; i++) {
w.generateAddress(isChange, k.pub);
}
@ -185,18 +202,12 @@ describe('PublicKeyRing model', function() {
});
it('should set backup ready', function() {
var w = createW().w;
var w = getCachedW().w;
w.isBackupReady().should.equal(false);
w.setBackupReady();
w.isBackupReady().should.equal(true);
});
it('should set backup ready', function() {
var w = createW().w;
w.isBackupReady().should.equal(false);
w.setBackupReady();
w.isBackupReady().should.equal(true);
});
it('should check for other backups', function() {
var w = createW().w;
@ -213,7 +224,7 @@ describe('PublicKeyRing model', function() {
});
it('should merge backup', function() {
var w = createW().w;
var w = getCachedW().w;
w.copayersBackup = ["a", "b"];
var hasChanged = w.mergeBackups(["b", "c"]);
@ -313,11 +324,10 @@ describe('PublicKeyRing model', function() {
var w0 = new PublicKeyRing({
networkName: 'livenet',
});
w0.addCopayer();
w0.addCopayer();
w0.addCopayer();
w0.addCopayer();
w0.addCopayer();
for (var i = 0; i < 5; i++)
w0.addCopayer(getNewEpk());
(function() {
w0.merge(w);
}).should.throw();
@ -327,7 +337,7 @@ describe('PublicKeyRing model', function() {
var wx = new PublicKeyRing({
networkName: 'livenet',
});
wx.addCopayer();
wx.addCopayer(getNewEpk());
(function() {
w.merge(wx);
}).should.throw();
@ -343,7 +353,7 @@ describe('PublicKeyRing model', function() {
var copayers = [];
for (var i = 0; i < 2; i++) {
w.isComplete().should.equal(false);
w.addCopayer();
w.addCopayer(getNewEpk());
}
var w2 = new PublicKeyRing({
@ -354,7 +364,7 @@ describe('PublicKeyRing model', function() {
var copayers = [];
for (var i = 0; i < 3; i++) {
w2.isComplete().should.equal(false);
w2.addCopayer();
w2.addCopayer(getNewEpk());
}
w2.merge(w).should.equal(true);
w2.isComplete().should.equal(true);
@ -379,7 +389,7 @@ describe('PublicKeyRing model', function() {
networkName: 'livenet',
id: w.id,
});
w2.addCopayer();
w2.addCopayer(getNewEpk());
w.merge(w2).should.equal(true);
}
w.isComplete().should.equal(true);
@ -393,7 +403,7 @@ describe('PublicKeyRing model', function() {
var w = new PublicKeyRing(config);
should.exist(w);
for (var i = 0; i < 3; i++) {
w.addCopayer();
w.addCopayer(getNewEpk());
};
w._setNicknameForIndex(0, 'pepe0');
w._setNicknameForIndex(1, 'pepe1');
@ -409,7 +419,7 @@ describe('PublicKeyRing model', function() {
networkName: 'livenet',
id: w.id,
});
w2.addCopayer();
w2.addCopayer(getNewEpk());
w2._setNicknameForIndex(0, 'juan' + i);
w.merge(w2).should.equal(true);
}
@ -430,7 +440,7 @@ describe('PublicKeyRing model', function() {
var w = new PublicKeyRing(config);
should.exist(w);
for (var i = 0; i < 3; i++) {
w.addCopayer(null, 'tito' + i);
w.addCopayer(getNewEpk(), 'tito' + i);
};
w.nicknameForIndex(0).should.equal('tito0');
w.nicknameForIndex(1).should.equal('tito1');
@ -468,7 +478,7 @@ describe('PublicKeyRing model', function() {
});
it('#getRedeemScriptMap check tests', function() {
var k = createW();
var k = getCachedW();
var w = k.w;
var amount = 2;
@ -489,4 +499,27 @@ describe('PublicKeyRing model', function() {
});
});
it('#getForPath should return 5 pubkeys', function() {
var w = getCachedW().w;
var pubkeys = w.getForPath('m/45\'/2147483647/1/0');
pubkeys.length.should.equal(5);
});
it('#getForPaths should return 2 arrays of 5 pubkey ', function() {
var w = getCachedW().w;
var pubkeys = w.getForPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']);
pubkeys.length.should.equal(2);
pubkeys[0].length.should.equal(5);
pubkeys[1].length.should.equal(5);
});
it('#forPaths should return copayers and pubkeys ', function() {
var w = getCachedW().w;
var ret = w.forPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']);
ret.copayerIds.length.should.equal(5);
ret.pubKeys.length.should.equal(2);
ret.pubKeys[0].length.should.equal(5);
ret.pubKeys[1].length.should.equal(5);
});
});

431
test/test.TxProposal.js Normal file
View File

@ -0,0 +1,431 @@
'use strict';
var chai = chai || require('chai');
var should = chai.should();
var bitcore = bitcore || require('bitcore');
var Transaction = bitcore.Transaction;
var buffertools = bitcore.buffertools;
var WalletKey = bitcore.WalletKey;
var Key = bitcore.Key;
var bignum = bitcore.Bignum;
var Script = bitcore.Script;
var TransactionBuilder = bitcore.TransactionBuilder;
var util = bitcore.util;
var networks = bitcore.networks;
var sinon = require('sinon');
try {
var copay = require('copay'); //browser
} catch (e) {
var copay = require('../copay'); //node
}
var FakeBuilder = require('./mocks/FakeBuilder');
var TxProposal = copay.TxProposal;
var dummyProposal = new TxProposal({
creator: 1,
createdTs: 1,
builder: new FakeBuilder(),
inputChainPaths: ['m/1'],
});
var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"];
describe('TxProposal', function() {
describe('new', function() {
it('should fail to create an instance with wrong arguments', function() {
(function() {
var txp = new TxProposal();
}).should.throw('Illegal Argument');
(function() {
var txp = new TxProposal({
creator: 1
});
}).should.throw('no inputChainPaths');
});
it('should create an instance', function() {
var txp = new TxProposal({
creator: 1,
createdTs: 1,
builder: new FakeBuilder(),
inputChainPaths: 'm/1',
});
should.exist(txp);
txp.creator.should.equal(1);
should.exist(txp.builder);
txp.inputChainPaths.should.equal('m/1');
});
});
describe('#getId', function() {
it('should return id', function() {
var b = new FakeBuilder();
var spy = sinon.spy(b.tx, 'getNormalizedHash');
var txp = new TxProposal({
creator: 1,
createdTs: 1,
builder: b,
inputChainPaths: 'm/1',
});
txp.getId().should.equal('123456');;
sinon.assert.callCount(spy, 1);
});
});
describe('#toObj', function() {
it('should return an object and remove builder', function() {
var b = new FakeBuilder();
var txp = new TxProposal({
creator: 1,
createdTs: 1,
builder: b,
inputChainPaths: 'm/1',
});
var o = txp.toObj();
should.exist(o);
o.creator.should.equal(1);
should.not.exist(o.builder);
should.exist(o.builderObj);
});
it('toObjTrim', function() {
var b = new FakeBuilder();
var txp = new TxProposal({
creator: 1,
createdTs: 1,
builder: b,
inputChainPaths: 'm/1',
comment: 'hola',
});
var o = txp.toObjTrim();
should.exist(o);
should.not.exist(o.creator);
should.not.exist(o.builder);
should.exist(o.comment);
should.exist(o.builderObj);
});
});
describe('#fromObj', function() {
it.skip('should create from Object', function() {
var b = new FakeBuilder();
var txp = TxProposal.fromObj({
creator: 1,
createdTs: 1,
builderObj: b.toObj(),
inputChainPaths: ['m/1'],
});
should.exist(txp);
});
it('should fail to create from wrong object', function() {
var b = new FakeBuilder();
(function() {
var txp = TxProposal.fromObj({
creator: 1,
createdTs: 1,
builderObj: b.toObj(),
inputChainPaths: ['m/1'],
});
}).should.throw('Invalid');
});
});
describe('#setSent', function() {
it('should set txid and timestamp', function() {
var now = Date.now();
var txp = dummyProposal;
txp.setSent('3a42');
txp.sentTs.should.gte(now);
txp.sentTxid.should.equal('3a42');
});
});
describe('Signature verification', function() {
var validScriptSig = new bitcore.Script(FakeBuilder.VALID_SCRIPTSIG_BUF);
var pubkeys = [
'03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d',
'0380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127',
'0392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed03',
'03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3',
'03e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e4'
].map(function(hex) {
return new Buffer(hex, 'hex');
});
var keyBuf = someKeys.map(function(hex) {
return new Buffer(hex, 'hex');
});
it('#_formatKeys', function() {
(function() {
TxProposal._formatKeys(someKeys);
}).should.throw('buffers');
var res = TxProposal._formatKeys(keyBuf);
});
it('#_verifyScriptSig arg checks', function() {
(function() {
TxProposal._verifySignatures(
keyBuf,
new bitcore.Script(new Buffer('112233', 'hex')),
new Buffer('1a', 'hex'));
}).should.throw('script');
});
it('#_verifyScriptSig, no signatures', function() {
var ret = TxProposal._verifySignatures(keyBuf, validScriptSig, new Buffer(32));
ret.length.should.equal(0);
});
it('#_verifyScriptSig, two signatures', function() {
// Data taken from bitcore's TransactionBuilder test
var txp = dummyProposal;
var tx = dummyProposal.builder.build();
var ret = TxProposal._verifySignatures(pubkeys, validScriptSig, tx.hashForSignature());
ret.should.deep.equal([0, 3]);
});
it('#_infoFromRedeemScript', function() {
var info = TxProposal._infoFromRedeemScript(validScriptSig);
var keys = info.keys;
keys.length.should.equal(5);
for (var i in keys) {
keys[i].toString('hex').should.equal(pubkeys[i].toString('hex'));
}
Buffer.isBuffer(info.script.getBuffer()).should.equal(true);
});
it('#_updateSignedBy', function() {
var txp = dummyProposal;
txp._inputSignatures.should.deep.equal([
['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']
]);
});
describe('#_check', function() {
var txp = dummyProposal;
var backup = txp.builder.tx.ins;
it('OK', function() {
txp._check();
});
it('FAIL ins', function() {
txp.builder.tx.ins = [];
(function() {
txp._check();
}).should.throw('no ins');
txp.builder.tx.ins = backup;
});
it('FAIL signhash SINGLE', function() {
sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_SINGLE);
(function() {
txp._check();
}).should.throw('signatures');
txp.builder.tx.getHashType.restore();
});
it('FAIL signhash NONE', function() {
sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_NONE);
(function() {
txp._check();
}).should.throw('signatures');
txp.builder.tx.getHashType.restore();
});
it('FAIL signhash ANYONECANPAY', function() {
sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_ANYONECANPAY);
(function() {
txp._check();
}).should.throw('signatures');
txp.builder.tx.getHashType.restore();
});
it('FAIL no signatures', function() {
var backup = txp.builder.tx.ins[0].s;
txp.builder.tx.ins[0].s = undefined;
(function() {
txp._check();
}).should.throw('no signatures');
txp.builder.tx.ins[0].s = backup;
});
});
describe('#merge', function() {
var txp = dummyProposal;
var backup = txp.builder.tx.ins;
it('with self', function() {
var hasChanged = txp.merge(txp);
hasChanged.should.equal(false);
});
it('with less signatures', function() {
var backup = txp.builder.vanilla.scriptSig[0];
txp.builder.merge = function() {
// 2 signatures.
this.vanilla.scriptSig = ['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae'];
this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex');
};
var hasChanged = txp.merge(txp);
hasChanged.should.equal(true);
txp.builder.vanilla.scriptSig = [backup];
txp.builder.tx.ins[0].s = new Buffer(backup, 'hex');
});
it('with more signatures', function() {
txp.builder.merge = function() {
// 3 signatures.
this.vanilla.scriptSig = ['00483045022100f75bd3eb92d8c9be9a94d848bbd1985fc0eaf4c47fb470a0b222881802a1f03802204eb239ae3082779b1ec4f2e69baa0362494071e707e1696c14ad23c8f2e184e20148304502201981482db0f369ce943293b6fec06a0347918663c766a79d4cbd0457801768d1022100aedf8d7c51d55a9ddbdcc0067ed6b648b77ce9660447bbcf4e2c209698efa0a30148304502203f0ddad47757f8705cb40e7c706590d2e2028a7027ffdb26dd208fd6155e0d28022100ccd206f9b969ab7f88ee4c5c6cee48c800a62dda024c5a8de7eb8612b833a0c0014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae'];
this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex');
};
var hasChanged = txp.merge(txp);
hasChanged.should.equal(true);
});
});
describe('#setCopayers', function() {
it("should fails if Tx has no creator", function() {
var txp = dummyProposal;
txp.signedBy = {
'hugo': 1
};
delete txp['creator'];
(function() {
txp.setCopayers('juan', {
pk1: 'pepe'
})
}).should.throw('no creator');
});
it("should fails if Tx is not signed by creator", function() {
var txp = dummyProposal;
txp.creator = 'creator';
txp.signedBy = {
'hugo': 1
};
txp._inputSignatures = [
['pkX']
];
(function() {
txp.setCopayers('juan', {
pk1: 'pepe'
})
}).should.throw('creator');
});
it("should fails if Tx has unmapped signatures", function() {
var txp = dummyProposal;
txp.creator = 'creator';
txp.signedBy = {
creator: 1
};
txp._inputSignatures = [
['pk0', 'pkX']
];
(function() {
txp.setCopayers('juan', {
pk1: 'pepe'
})
}).should.throw('unknown sig');
});
// This was disabled. Unnecessary to check this.
it.skip("should be signed by sender", function() {
var txp = dummyProposal;
var ts = Date.now();
txp._inputSignatures = [
['pk1', 'pk0']
];
txp.signedBy = {
'creator': Date.now()
};
(function() {
txp.setCopayers('juan', {
pk0: 'creator',
pk1: 'pepe',
pk2: 'john'
})
}).should.throw('senders sig');
});
it("should set signedBy (trivial case)", function() {
var txp = dummyProposal;
var ts = Date.now();
txp._inputSignatures = [
['pk1', 'pk0']
];
txp.signedBy = {
'creator': Date.now()
};
txp.setCopayers('pepe', {
pk0: 'creator',
pk1: 'pepe',
pk2: 'john'
})
Object.keys(txp.signedBy).length.should.equal(2);
txp.signedBy['pepe'].should.gte(ts);
txp.signedBy['creator'].should.gte(ts);
});
it("should assign creator", function() {
var txp = dummyProposal;
var ts = Date.now();
txp._inputSignatures = [
['pk0']
];
txp.signedBy = {};
delete txp['creator'];
delete txp['creatorTs'];
txp.setCopayers('creator', {
pk0: 'creator',
pk1: 'pepe',
pk2: 'john'
})
Object.keys(txp.signedBy).length.should.equal(1);
txp.creator.should.equal('creator');
txp.createdTs.should.gte(ts);
txp.seenBy['creator'].should.equal(txp.createdTs);
})
it("New tx should have only 1 signature", function() {
var txp = dummyProposal;
var ts = Date.now();
txp.signedBy = {};
delete txp['creator'];
delete txp['creatorTs'];
txp._inputSignatures = [
['pk0', 'pk1']
];
(function() {
txp.setCopayers(
'creator', {
pk0: 'creator',
pk1: 'pepe',
pk2: 'john'
}, {
'creator2': 1
}
);
}).should.throw('only 1');
})
it("if signed, should not change ts", function() {
var txp = dummyProposal;
var ts = Date.now();
txp._inputSignatures = [
['pk0', 'pk1']
];
txp.creator = 'creator';
txp.signedBy = {
'creator': 1
};
txp.setCopayers('pepe', {
pk0: 'creator',
pk1: 'pepe',
pk2: 'john'
})
Object.keys(txp.signedBy).length.should.equal(2);
txp.creator.should.equal('creator');
txp.signedBy['creator'].should.equal(1);
txp.signedBy['pepe'].should.gte(ts);
})
});
});
});

View File

@ -12,717 +12,105 @@ var Script = bitcore.Script;
var TransactionBuilder = bitcore.TransactionBuilder;
var util = bitcore.util;
var networks = bitcore.networks;
var sinon = require('sinon');
try {
var copay = require('copay'); //browser
} catch (e) {
var copay = require('../copay'); //node
}
var fakeStorage = copay.FakeStorage;
var PrivateKey = copay.PrivateKey || require('../js/models/PrivateKey');
var TxProposals = copay.TxProposals || require('../js/models/TxProposal');
var is_browser = (typeof process == 'undefined' || typeof process.versions === 'undefined')
var PublicKeyRing = is_browser ? copay.PublicKeyRing :
require('soop').load('../js/models/core/PublicKeyRing', {
Storage: fakeStorage
var FakeBuilder = require('./mocks/FakeBuilder');
var TxProposal = copay.TxProposal;
var TxProposals = copay.TxProposals;
var dummyProposal = new TxProposal({
creator: 1,
createdTs: 1,
builder: new FakeBuilder(),
inputChainPaths: ['m/1'],
});
var config = {
networkName: 'testnet',
};
var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"];
var unspentTest = [{
"address": "dummy",
"scriptPubKey": "dummy",
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1",
"vout": 1,
"amount": 10,
"confirmations": 7
}];
var createPKR = function(bip32s) {
var w = new PublicKeyRing(config);
should.exist(w);
for (var i = 0; i < 5; i++) {
if (bip32s && i < bip32s.length) {
var b = bip32s[i];
w.addCopayer(b.deriveBIP45Branch().extendedPublicKeyString());
} else {
w.addCopayer();
}
}
var pubkey = bip32s[0].publicHex;
w.generateAddress(false, pubkey);
w.generateAddress(false, pubkey);
w.generateAddress(false, pubkey);
w.generateAddress(true, pubkey);
w.generateAddress(true, pubkey);
w.generateAddress(true, pubkey);
return w;
};
var vopts = {
verifyP2SH: true,
dontVerifyStrictEnc: true
};
describe('TxProposals model', function() {
var isChange = false;
var addressIndex = 0;
it('verify TXs', function(done) {
var priv = new PrivateKey(config);
var priv2 = new PrivateKey(config);
var priv3 = new PrivateKey(config);
var pub = priv.publicHex;
var ts = Date.now();
var pkr = createPKR([priv, priv2, priv3]);
var opts = {
remainderOut: {
address: pkr.generateAddress(true, pub).toString()
}
};
var w = new TxProposals({
networkName: config.networkName,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var b = w.txps[ntxid].builder;
var tx = b.build();
tx.isComplete().should.equal(false);
var ringIndex = pkr.getHDParams(pub);
b.sign(priv2.getAll(ringIndex.getReceiveIndex(), ringIndex.getChangeIndex(), ringIndex.copayerIndex));
b.sign(priv3.getAll(ringIndex.getReceiveIndex(), ringIndex.getChangeIndex(), ringIndex.copayerIndex));
tx = b.build();
tx.isComplete().should.equal(true);
var s = new Script(new bitcore.Buffer(unspentTest[0].scriptPubKey, 'hex'));
tx.verifyInput(0, s, {
verifyP2SH: true,
dontVerifyStrictEnc: true
}, function(err, results) {
should.not.exist(err);
results.should.equal(true);
done();
describe('TxProposals', function() {
describe('constructor', function() {
it('should create an instance', function() {
var txps = new TxProposals();
should.exist(txps);
txps.network.name.should.equal('testnet');
});
});
it('should create an instance', function() {
var w = new TxProposals({
networkName: config.networkName
});
should.exist(w);
w.network.name.should.equal(config.networkName);
});
var createTx = function(toAddress, amountSatStr, utxos, opts, priv, pkr) {
opts = opts || {};
var pub = priv.publicHex;
if (!pkr.isComplete()) {
throw new Error('publicKeyRing is not complete');
}
if (!opts.remainderOut) {
opts.remainderOut = {
address: pkr.generateAddress(true, pub).toString()
};
};
var b = new TransactionBuilder(opts)
.setUnspent(utxos)
.setOutputs([{
address: toAddress,
amountSatStr: amountSatStr,
}]);
var selectedUtxos = b.getSelectedUnspent();
var inputChainPaths = selectedUtxos.map(function(utxo) {
return pkr.pathForAddress(utxo.address);
});
var selectedUtxos = b.getSelectedUnspent();
var inputChainPaths = selectedUtxos.map(function(utxo) {
return pkr.pathForAddress(utxo.address);
});
b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
var signRet;
if (priv) {
var pkeys = priv.getForPaths(inputChainPaths);
b.sign(pkeys);
}
var me = {};
if (priv) me[priv.getId()] = Date.now();
var tx = b.build();
return {
inputChainPaths: inputChainPaths,
creator: priv.getId(),
createdTs: new Date(),
signedBy: priv && tx.countInputSignatures(0) ? me : {},
seenBy: priv ? me : {},
builder: b,
};
};
it('#getUsedUnspend', function() {
var priv = new PrivateKey(config);
var pub = priv.publicHex;
var w = new TxProposals({
networkName: config.networkName,
});
var start = new Date().getTime();
var pkr = createPKR([priv]);
var ts = Date.now();
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest, {},
priv,
pkr
));
var uu = w.getUsedUnspent();
var uuk = Object.keys(uu);
uuk.length.should.equal(1);
uuk[0].split(',')[0].should.equal(unspentTest[0].txid);
});
it('#merge with self', function() {
var priv = new PrivateKey(config);
var pub = priv.publicHex;
var w = new TxProposals({
networkName: config.networkName,
});
var start = new Date().getTime();
var pkr = createPKR([priv]);
var ts = Date.now();
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest, {},
priv,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
var x = priv.getId();
(w.txps[ntxid].signedBy[priv.getId()] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var info = w.merge(w.txps[ntxid], pkr.getCopayerId(0));
info.events.length.should.equal(0);
Object.keys(w.txps).length.should.equal(1);
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
});
it('#merge, merge signatures case 1', function() {
var priv2 = new PrivateKey(config);
var priv = new PrivateKey(config);
var pub = priv.publicHex;
var ts = Date.now();
var pkr = createPKR([priv]);
var opts = {
remainderOut: {
address: pkr.generateAddress(true, pub).toString()
}
};
var w = new TxProposals({
networkName: config.networkName,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv2,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputSignatures(0).should.equal(0);
tx.countInputMissingSignatures(0).should.equal(1);
Object.keys(w.txps[ntxid].signedBy).length.should.equal(0);
Object.keys(w.txps[ntxid].seenBy).length.should.equal(1);
var w2 = new TxProposals({
networkName: config.networkName,
publicKeyRing: w.publicKeyRing,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w2.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var tx = w2.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true, 'asdsd');
(w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(0));
info.events.length.should.equal(2);
info.events[0].type.should.equal('seen');
info.events[1].type.should.equal('signed');
Object.keys(w.txps).length.should.equal(1);
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
});
var _dumpChunks = function(scriptSig, label) {
console.log('## DUMP: ' + label + ' ##');
for (var i = 0; i < scriptSig.chunks.length; i++) {
console.log('\tCHUNK ', i, scriptSig.chunks[i]);
}
};
it('#merge, merge signatures case 2', function() {
var o1 = {
extendedPrivateKeyString: 'tprv8ZgxMBicQKsPdSF1avR6mXyDj5Uv1XY2UyUHSDpAXQ5TvPN7prGeDppjy4562rBB9gMMAhRfFdJrNDpQ4t69kkqHNEEen3PX1zBJqSehJDH',
networkName: 'testnet',
privateKeyCache: {}
};
var o2 = {
extendedPrivateKeyString: 'tprv8ZgxMBicQKsPdVeB5RzuxS9JQcACueZYgUaM5eWzaEBkHjW5Pg6Mqez1APSqoUP1jUdbT8WVG7ZJYTXvUL7XtPzFYBXjmdKuwSor1dcNQ8j',
networkName: 'testnet',
privateKeyCache: {}
};
var o3 = {
extendedPrivateKeyString: 'tprv8ZgxMBicQKsPeHWNrPVZtQVgcCtXBr5TACNbDQ56rwqNJce9MEc64US6DJKxpWsrebEomxxWZFDtkvkZGkzA43uLvdF4XHiWqoNaL6Dq2Gd',
networkName: 'testnet',
privateKeyCache: {}
};
var priv = PrivateKey.fromObj(o1);
var priv2 = PrivateKey.fromObj(o2);
var priv3 = PrivateKey.fromObj(o3);
var pub = priv.publicHex;
var ts = Date.now();
var pkr = createPKR([priv, priv2]);
var opts = {
remainderOut: {
address: '2MxK2m7cPtEwjZBB8Ksq7ppjkgJyFPJGemr'
}
};
var addressToSign = pkr.generateAddress(false, pub);
unspentTest[0].address = addressToSign.toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
var tx, txb;
var w = new TxProposals({
networkName: config.networkName,
});
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv3,
pkr
));
var ntxid = Object.keys(w.txps)[0];
txb = w.txps[ntxid].builder;
tx = txb.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(1);
Object.keys(w.txps[ntxid].signedBy).length.should.equal(0);
Object.keys(w.txps[ntxid].seenBy).length.should.equal(1);
var w2 = new TxProposals({
networkName: config.networkName,
});
w2.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv,
pkr
));
var ntxid = Object.keys(w2.txps)[0];
txb = w2.txps[ntxid].builder;
tx = txb.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(0));
info.events.length.should.equal(2);
info.events[0].type.should.equal('seen');
info.events[1].type.should.equal('signed');
tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var w3 = new TxProposals({
networkName: config.networkName,
publicKeyRing: pkr,
});
w3.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv2,
pkr
));
tx = w3.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w3.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true);
(w3.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true);
var info = w.merge(w3.txps[ntxid], pkr.getCopayerId(1));
Object.keys(w.txps).length.should.equal(1);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true);
tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(1);
});
it('#merge, merge signatures case 3', function() {
var priv = new PrivateKey(config);
var priv2 = new PrivateKey(config);
var priv3 = new PrivateKey(config);
var pub = priv.publicHex;
var ts = Date.now();
var pkr = createPKR([priv, priv2, priv3]);
var opts = {
remainderOut: {
address: pkr.generateAddress(true, pub).toString()
}
};
var w = new TxProposals({
networkName: config.networkName,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var w2 = new TxProposals({
networkName: config.networkName,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w2.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv2,
pkr
));
var tx = w2.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w2.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true);
(w2.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true);
var w3 = new TxProposals({
networkName: config.networkName,
});
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w3.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest,
opts,
priv3,
pkr
));
var tx = w3.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w3.txps[ntxid].signedBy[priv3.id] - ts > 0).should.equal(true);
(w3.txps[ntxid].seenBy[priv3.id] - ts > 0).should.equal(true);
var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(1));
Object.keys(w.txps).length.should.equal(1);
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(1);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true);
var info = w.merge(w3.txps[ntxid], pkr.getCopayerId(2));
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(true);
tx.countInputMissingSignatures(0).should.equal(0);
Object.keys(w.txps).length.should.equal(1);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv3.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true);
(w.txps[ntxid].signedBy[priv3.id] - ts > 0).should.equal(true);
});
it('#fromObj stored (hardcoded) data', function() {
var txp = TxProposals.TxProposal.fromObj(txpv1);
txp.getID().should.equal('5cae6e225335acd2725856c71ef1ca61c42f118967102c5d0ed6710343e4a19f');
var tx = txp.builder.build();
tx.countInputSignatures(0).should.equal(2);
tx.countInputMissingSignatures(0).should.equal(0);
});
it('#toObj #fromObj roundtrip', function() {
var priv = new PrivateKey(config);
var pub = priv.publicHex;
var pkr = createPKR([priv]);
var w = new TxProposals({
walletId: 'qwerty',
networkName: config.networkName,
});
var ts = Date.now();
unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString();
unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub);
w.add(createTx(
'15q6HKjWHAksHcH91JW23BJEuzZgFwydBt',
'123456789',
unspentTest, {},
priv,
pkr
));
var ntxid = Object.keys(w.txps)[0];
var tx = w.txps[ntxid].builder.build();
tx.isComplete().should.equal(false);
tx.countInputMissingSignatures(0).should.equal(2);
(w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
var o = w.toObj();
should.exist(o);
o.txps.length.should.equal(1);
should.exist(o.txps[0]);
should.exist(o.txps[0].signedBy);
should.exist(o.txps[0].seenBy);
should.exist(o.txps[0].builderObj);
should.exist(o.txps[0].signedBy[priv.id]);
var o2 = JSON.parse(JSON.stringify(o));
var w2 = TxProposals.fromObj(o2);
w2.walletId.should.equal(w.walletId);
var tx2 = w2.txps[ntxid].builder.build();
tx2.isComplete().should.equal(false);
tx2.countInputMissingSignatures(0).should.equal(2);
(w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true);
(w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true);
should.exist(w2.txps[ntxid].builder);
should.exist(w2.txps[ntxid].builder.valueInSat);
w2.merge(w.txps[ntxid], pkr.getCopayerId(0));
Object.keys(w2.txps).length.should.equal(1);
});
describe('TxProposal model', function() {
var createMockTxp = function(raw) {
var tx = new Transaction();
tx.parse(new Buffer(raw, 'hex'));
var txb = new TransactionBuilder();
var txp = new TxProposals.TxProposal({
builder: txb
describe('#fromObj', function() {
it('should create an instance from an Object', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
walletId: '123a12',
txps: [],
});
txb.build = function() {
return tx;
};
return txp;
};
it('should validate for no signatures yet in tx', function() {
// taken from https://gist.github.com/gavinandresen/3966071
var raw = '010000000189632848f99722915727c5c75da8db2dbf194342a0429828f66ff88fab2af7d60000000000ffffffff0140420f000000000017a914f815b036d9bbbce5e9f2a00abd1bf3dc91e955108700000000';
var txp = createMockTxp(raw);
txp.isValid().should.equal(true);
should.exist(txps);
txps.network.name.should.equal('livenet');
});
it('should validate for no signatures yet in copay generated tx', function() {
// taken from copay incomplete tx proposal
var raw = '0100000001e205297fd05e4504d72761dc7a16e5cc9f4ab89877f28aee97c1cc66b3f07d690100000000ffffffff01706f9800000000001976a91473707e88f79c9c616b44bc766a25efcb9f49346688ac00000000';
var txp = createMockTxp(raw);
txp.isValid().should.equal(true);
});
it('should validate for a SIGHASH_NONE tx in builder', function() {
var raw = '010000000145c3bf51ced6cefaea8c6578a645316270dbf8600f46969d31136e1e06829598000000007000483045022100877c715e0f3bd6377086c96d4757b2c983682a1934d9e3f894941f4f1e18d4710220272ed81758d7a391ee4c15a29246f3fe75efbddeaf1118e4c0d3bb14f57cdba601255121022f58491a833933a9bea80d8e820e66bee91bd8c71bfa972fe70482360b48129951aeffffffff01706f9800000000001976a91408328947f0caf8728729d740cbecdfe3c2327db588ac00000000';
var txp = createMockTxp(raw);
txp.isValid().should.equal(true);
});
it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() {
var raw = '0100000001eaf08f93f895127fbf000128ac74f6e8c7f003854e5ee1f02a5fd820cb689beb00000000fdfe00004730440220778f3174393e9ee6b0bfa876b4150db6f12a4da9715044ead5e345c2781ceee002203aab31f1e1d3dcf77ca780d9af798139719891917c9a09123dba54483ef462bc02493046022100dd93b64b30580029605dbba09d7fa34194d9ff38fda0c4fa187c52bf7f79ae98022100dd7b056762087b9aa8ccfde328d7067fa1753b78c0ee25577122569ff9de1d57024c695221039f847c24f09d7299c10bba4e41b24dc78e47bbb05fd7c1d209c994899d6881062103d363476e634fc5cdc11e9330c05a141c1e0c7f8b616817bdb83e7579bbf870942103fb2072953ceab87c6da450ac661685a881ddb661002d2ec1d60bfd33e3ec807d53aeffffffff01d06bf5050000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000';
var txp = createMockTxp(raw);
txp.isValid().should.equal(false);
});
it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() {
var raw = '0100000002d903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407501000000fdfe0000493046022100b232e994fdca7fd61fcf8ffe4a7f746ff8f8baf2667ac80841de0250f521c402022100862c0783ca7eafcbd2786b9444ed6e83ae941dcc2248bea4db12b7815d15de050247304402200189fe0cde9d1dd192553f4dddb6764df3eb643f9f71be8aa015f41f2d4fd11f02205513b8ca985c3b5b936f814c7eba92e2e2985c83927ca06c41081d264c0be7a7024c695221026fa1a3ed0c820c1053c8ba101f3c96f85c55624a902a82cf6b2896ed5f9b3d1521035a3383c13dd346a5784adfe3ec3026ab31d519fdfae2740497b10bdfb994e6442103c7477a6668d5bc250fe727e358d951b9e05f1d7c02059bf59ecbb335f1eeec7953aeffffffffd903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407500000000fdfd0000483045022100bdb9d14569af66d84af63416d77296ace24a96f1720d30e74bc6e316a4b3727502206ed54d532467393488889d72edbb667d075de491a89e8e496fee8791b943fa37024730440220379c30c884a21a949d8ec32d6934ffa9faf86add4d839de0f5fbd2b90f8ef1e802204048df2ec0035ce5e4bf01e9d70fd93a45a41ce2630100d692cd908cdaa61fc0024c69522102203938ef947327edce2cf2997c55b433be3d3ffcf3284c10d6fcdf4b01c6221f21033b60c3363a226ce9b850af655c6e1470d9a0936d7f56ea4a07ab84005f91cd1b210385755bc813fe7f92577b93bf689bf0d9b2118e6bbb7fee5d3d16976f4f7271af53aeffffffff01c02d9a3b0000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000';
var txp = createMockTxp(raw);
txp.isValid().should.equal(false);
it('should fail create an instance from an Object with errors', function() {
(function() {var txps = TxProposals.fromObj({
networkName:'livenet',
walletId: '123a12',
txps: [ { a: 1 }],
}) }).should.throw('Illegal');
});
});
describe('#getNtxids', function() {
it('should return keys', function() {
var txps = new TxProposals();
txps.txps = {a:1, b:2};
txps.getNtxids().should.deep.equal(['a','b']);
});
});
describe('#toObj', function() {
it('should an object', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
walletId: '123a12',
txps: [],
});
var o = txps.toObj();
o.walletId.should.equal('123a12');
o.networkName.should.equal('livenet');
});
it('should export txps', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
walletId: '123a12',
txps: [],
});
txps.txps = {
'hola' : dummyProposal,
'chau' : dummyProposal,
};
var o = txps.toObj();
o.txps.length.should.equal(2);
});
it('should filter sent txp', function() {
var txps = TxProposals.fromObj({
networkName:'livenet',
walletId: '123a12',
txps: [],
});
var d = JSON.parse(JSON.stringify(dummyProposal));
d.sent=1;
txps.txps = {
'hola' : dummyProposal,
'chau' : d,
};
var o = txps.toObj();
o.txps.length.should.equal(1);
});
});
describe.skip('#merge', function() {
it('should merge', function() {
var txps = new TxProposals();
var d = dummyProposal;
txps.merge(d.toObj(),{});
});
});
});
var txpv1 = {
"creator": "0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0",
"createdTs": 1406310417996,
"seenBy": {
"0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0": 1406310417996,
"02ba1599c64da4d80e25985be46c50e944b65f02e2b48c930528ce763d6710158f": 1406310418162
},
"signedBy": {
"0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0": 1406310417996,
"02ba1599c64da4d80e25985be46c50e944b65f02e2b48c930528ce763d6710158f": 1406310645549
},
"rejectedBy": {},
"sentTs": 1406310645873,
"sentTxid": "87296c50e8601437d63d556afb27c3b8e3819214be0a9d756d401a8286c0ec43",
"inputChainPaths": ["m/45'/0/1/1"],
"comment": "test 6",
"builderObj": {
"version": 1,
"outs": [{
"address": "mph66bnLvcn9KUSMrpikUBUZZkN2C1Z5tg",
"amountSatStr": 100
}],
"utxos": [{
"address": "2NEodmgBa4SH3VwE2asgW34vMYe8VThBZNo",
"txid": "8f8deda12dad6248e655054632a27f6891ebb37e8d2b3dd1bff87e71fd451ac7",
"vout": 1,
"ts": 1406312717,
"scriptPubKey": "a914ec7bce12d0e82a7d2b5431f6d89ca70af317f5a187",
"amount": 0.009798,
"confirmations": 0,
"confirmationsFromCache": false
}],
"opts": {
"spendUnconfirmed": true,
"remainderOut": {
"address": "2N74XAozMH3JB3XgeBkRvRw1J8TtfLTtvny"
}
},
"scriptSig": ["00483045022100f167ad33b8bef4c65af8d19c1a849d1770cc8d1e35bffebe6b5459dcbe655c7802207b37370b308ba668fe19f8e8bc462c9fbdc6c67f79900670758d228d83ea96da014730440220038ad3f4cc7b0738b593454ec189913ae4b442bc83da153d68d9a0077bd1b09102202b5728a08f302e97de61ea37280b48ccdd575f0d235c22f5e0ecac6a4ab0f46401475221024739614847d5233a46913482c17c6860194ad78abb3bf47de46223047d8a0b5821024c6dc65a52c5eaaa080b96888091544f8ab8712caa7e0b69ea4b45f6f059557452ae"],
"hashToScriptMap": {
"2NEodmgBa4SH3VwE2asgW34vMYe8VThBZNo": "5221024739614847d5233a46913482c17c6860194ad78abb3bf47de46223047d8a0b5821024c6dc65a52c5eaaa080b96888091544f8ab8712caa7e0b69ea4b45f6f059557452ae"
}
}
};

View File

@ -10,7 +10,7 @@ try {
}
var copayConfig = require('../config');
var Wallet = require('../js/models/core/Wallet');
var Structure = copay.Structure;
var PrivateKey = copay.PrivateKey;
var Storage = require('./mocks/FakeStorage');
var Network = require('./mocks/FakeNetwork');
var Blockchain = require('./mocks/FakeBlockchain');
@ -19,22 +19,30 @@ var TransactionBuilder = bitcore.TransactionBuilder;
var Transaction = bitcore.Transaction;
var Address = bitcore.Address;
var config = {
requiredCopayers: 3,
totalCopayers: 5,
spendUnconfirmed: true,
reconnectDelay: 100,
networkName: 'testnet',
};
var getNewEpk = function() {
return new PrivateKey({
networkName: config.networkName,
})
.deriveBIP45Branch()
.extendedPublicKeyString();
}
var addCopayers = function(w) {
for (var i = 0; i < 4; i++) {
w.publicKeyRing.addCopayer();
w.publicKeyRing.addCopayer(getNewEpk());
}
};
describe('Wallet model', function() {
var config = {
requiredCopayers: 3,
totalCopayers: 5,
spendUnconfirmed: true,
reconnectDelay: 100,
networkName: 'testnet',
};
it('should fail to create an instance', function() {
(function() {
new Wallet(config)
@ -47,12 +55,11 @@ describe('Wallet model', function() {
});
var createW = function(netKey, N, conf) {
var createW = function(N, conf) {
var c = JSON.parse(JSON.stringify(conf || config));
if (!N) N = c.totalCopayers;
if (netKey) c.netKey = netKey;
var mainPrivateKey = new copay.PrivateKey({
networkName: config.networkName
});
@ -148,8 +155,7 @@ describe('Wallet model', function() {
var createW2 = function(privateKeys, N, conf) {
if (!N) N = 3;
var netKey = 'T0FbU2JLby0=';
var w = createW(netKey, N, conf);
var w = createW(N, conf);
should.exist(w);
var pkr = w.publicKeyRing;
@ -157,9 +163,9 @@ describe('Wallet model', function() {
for (var i = 0; i < N - 1; i++) {
if (privateKeys) {
var k = privateKeys[i];
pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : null);
pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk());
} else {
pkr.addCopayer();
pkr.addCopayer(getNewEpk());
}
}
@ -212,12 +218,12 @@ describe('Wallet model', function() {
var t = w.txProposals;
var txp = t.txps[ntxid];
Object.keys(txp._inputSignatures).length.should.equal(1);
var tx = txp.builder.build();
should.exist(tx);
chai.expect(txp.comment).to.be.null;
tx.isComplete().should.equal(false);
Object.keys(txp.seenBy).length.should.equal(1);
Object.keys(txp.signedBy).length.should.equal(1);
});
it('#create with comment', function() {
@ -363,6 +369,14 @@ describe('Wallet model', function() {
}, w.reconnectDelay * callCount * (callCount + 1) / 2);
});
it('#isSingleUser', function() {
var w = createW();
w.isShared().should.equal(true);
w.totalCopayers = 1;
w.isShared().should.equal(false);
});
it('#isReady', function() {
var w = createW();
w.publicKeyRing.isComplete().should.equal(false);
@ -426,19 +440,7 @@ describe('Wallet model', function() {
var w = createW();
var txp = {
'txProposal': {
creator: '02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96',
createdTs: '2014-07-24T23:54:26.682Z',
seenBy: {
'02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96': 1406246066682
},
signedBy: {
'02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96': 1406246066682
},
rejectedBy: {},
sentTs: null,
sentTxid: null,
inputChainPaths: ['m/45\'/2/0/0'],
comment: null,
inputChainPaths: ['m/1'],
builderObj: {
version: 1,
outs: [{
@ -466,9 +468,13 @@ describe('Wallet model', function() {
}
};
var stub = sinon.stub(w.publicKeyRing,'copayersForPubkeys').returns(
{'027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d509':'pepe'}
);
w._handleTxProposal('senderID', txp, true);
Object.keys(w.txProposals.txps).length.should.equal(1);
w.getTxProposals().length.should.equal(1);
//stub.restore();
});
var newId = '00bacacafe';
@ -494,7 +500,8 @@ describe('Wallet model', function() {
var w = createW();
var r = w.getRegisteredCopayerIds();
r.length.should.equal(1);
w.publicKeyRing.addCopayer();
w.publicKeyRing.addCopayer(getNewEpk());
r = w.getRegisteredCopayerIds();
r.length.should.equal(2);
r[0].should.not.equal(r[1]);
@ -504,7 +511,7 @@ describe('Wallet model', function() {
var w = createW();
var r = w.getRegisteredPeerIds();
r.length.should.equal(1);
w.publicKeyRing.addCopayer();
w.publicKeyRing.addCopayer(getNewEpk());
r = w.getRegisteredPeerIds();
r.length.should.equal(2);
r[0].should.not.equal(r[1]);
@ -634,10 +641,11 @@ describe('Wallet model', function() {
});
});
it('should create & sign transaction from received funds', function(done) {
this.timeout(10000);
var w = cachedCreateW2();
var pk = w.privateKey;
w.privateKey = null;
var k2 = new PrivateKey({
networkName: config.networkName
});
var w = createW2([k2]);
var utxo = createUTXO(w);
w.blockchain.fixUnspent(utxo);
w.createTx(toAddress, amountSatStr, null, function(ntxid) {
@ -646,24 +654,36 @@ describe('Wallet model', function() {
w.getTxProposals()[0].rejectedByUs.should.equal(false);
done();
});
w.privateKey = pk;
w.privateKey = k2;
w.sign(ntxid, function(success) {
success.should.equal(true);
});
});
});
it('should create & reject transaction', function(done) {
it('should fail to reject a signed transaction', function() {
var w = cachedCreateW2();
w.privateKey = null;
var utxo = createUTXO(w);
w.blockchain.fixUnspent(utxo);
w.createTx(toAddress, amountSatStr, null, function(ntxid) {
w.on('txProposalsUpdated', function() {
w.getTxProposals()[0].signedByUs.should.equal(false);
w.getTxProposals()[0].rejectedByUs.should.equal(true);
done();
});
(function() {
w.reject(ntxid);
}).should.throw('reject a signed');
});
});
it('should create & reject transaction', function(done) {
var w = cachedCreateW2();
var oldK = w.privateKey;
var utxo = createUTXO(w);
w.blockchain.fixUnspent(utxo);
w.createTx(toAddress, amountSatStr, null, function(ntxid) {
var s = sinon.stub(w, 'getMyCopayerId').returns('213');
Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0);
w.reject(ntxid);
Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1);
w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1);
s.restore();
done();
});
});
it('should create & sign & send a transaction', function(done) {
@ -1013,32 +1033,91 @@ describe('Wallet model', function() {
copayConfig.forceNetwork = backup;
});
});
describe('_getKeymap', function() {
var w = cachedCreateW();
describe('validate txProposals', function() {
var a1 = 'n1pKARYYUnZwxBuGj3y7WqVDu6VLN7n971';
var a2 = 'mtxYYJXZJmQc2iJRHQ4RZkfxU5K7TE2qMJ';
var utxos = [{
address: a1,
txid: '2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1',
vout: 1,
scriptPubKey: Address.getScriptPubKeyFor(a1).serialize().toString('hex'),
amount: 0.5,
confirmations: 200
}, {
address: a2,
txid: '88c4520ffd97ea565578afe0b40919120be704b36561c71ba4e450e83cb3c9fd',
vout: 1,
scriptPubKey: Address.getScriptPubKeyFor(a2).serialize().toString('hex'),
amount: 0.5001,
confirmations: 200
}];
var destAddress = 'myuAQcCc1REUgXGsCTiYhZvPPc3XxZ36G1';
var outs = [{
address: destAddress,
amount: 1.0
}];
it('should set keymap', function() {
var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
return {
'123': 'juan'
};
});
var txp = {
_inputSignatures: [
['123']
],
inputChainPaths: ['/m/1'],
};
var map = w._getKeyMap(txp);
Object.keys(map).length.should.equal(1);
map['123'].should.equal('juan');
stub.restore();
});
it('should throw if unmatched sigs', function() {
var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
return {
'123': 'juan'
};
});
var txp = {
_inputSignatures: [
['234']
],
inputChainPaths: ['/m/1'],
};
(function() {
w._getKeyMap(txp);
}).should.throw('dont match know copayers');
stub.restore();
});
it('should set keymap with multiple signatures', function() {
var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
return {
'123': 'juan',
'234': 'pepe',
};
});
var txp = {
_inputSignatures: [
['234', '123']
],
inputChainPaths: ['/m/1'],
};
var map = w._getKeyMap(txp);
Object.keys(map).length.should.equal(2);
map['123'].should.equal('juan');
map['234'].should.equal('pepe');
stub.restore();
});
it('should throw is one inputs has missing sigs', function() {
var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
return {
'123': 'juan',
'234': 'pepe',
};
});
var txp = {
_inputSignatures: [
['234', '123'],
['234']
],
inputChainPaths: ['/m/1'],
};
(function() {
w._getKeyMap(txp);
}).should.throw('different sig');
stub.restore();
});
});
describe('_handleTxProposal', function() {
var testValidate = function(response, result, done) {
var testValidate = function(signhash, result, done) {
var w = cachedCreateW();
var spy = sinon.spy();
w.on('txProposalEvent', spy);
@ -1046,47 +1125,149 @@ describe('Wallet model', function() {
e.type.should.equal(result);
done();
});
var opts = {};
opts.signhash = signhash;
var txb = new TransactionBuilder(opts)
.setUnspent(utxos)
.setOutputs(outs)
.sign(['cVBtNonMyTydnS3NnZyipbduXo9KZfF1aUZ3uQHcvJB6UARZbiWG',
'cRVF68hhZp1PUQCdjr2k6aVYb2cn6uabbySDPBizAJ3PXF7vDXTL'
]);
// txp.prototype.getId = function() {return 'aa'};
var txp = {
'txProposal': {
'builderObj': txb.toObj()
}
dummy: 1
};
w._handleTxProposal('senderID', txp, true);
var txp = {
'txProposal': txp
};
var merge = sinon.stub(w.txProposals, 'merge', function() {
if (response == 0) throw new Error();
return {
newCopayer: ['juan'],
ntxid: 1,
new: response == 1
};
});
w._handleTxProposal('senderID', txp);
spy.callCount.should.equal(1);
merge.restore();
};
it('should validate for undefined', function(done) {
it('should handle corrupt', function(done) {
var result = 'corrupt';
testValidate(0, result, done);
});
it('should handle new', function(done) {
var result = 'new';
var signhash;
testValidate(signhash, result, done);
testValidate(1, result, done);
});
it('should validate for SIGHASH_ALL', function(done) {
var result = 'new';
var signhash = Transaction.SIGHASH_ALL;
testValidate(signhash, result, done);
it('should handle signed', function(done) {
var result = 'signed';
testValidate(2, result, done);
});
it('should not validate for different SIGHASH_NONE', function(done) {
var result = 'corrupt';
var signhash = Transaction.SIGHASH_NONE;
testValidate(signhash, result, done);
});
describe('_handleReject', function() {
it('should fails if unknown tx', function() {
var w = cachedCreateW();
(function() {
w._handleReject(1, {
ntxid: 1
}, 1);
}).should.throw('Unknown TXP');
});
it('should not validate for different SIGHASH_SINGLE', function(done) {
var result = 'corrupt';
var signhash = Transaction.SIGHASH_SINGLE;
testValidate(signhash, result, done);
it('should fail to reject a signed tx', function() {
var w = cachedCreateW();
w.txProposals.txps['qwerty'] = {
signedBy: {
john: 1
}
};
(function() {
w._handleReject('john', {
ntxid: 'qwerty'
}, 1);
}).should.throw('already signed');
});
it('should not validate for different SIGHASH_ANYONECANPAY', function(done) {
var result = 'corrupt';
var signhash = Transaction.SIGHASH_ANYONECANPAY;
testValidate(signhash, result, done);
it('should reject a tx', function() {
var w = cachedCreateW();
function txp() {
this.ok = 0;
this.signedBy = {};
};
txp.prototype.setRejected = function() {
this.ok = 1;
};
txp.prototype.toObj = function() {};
var spy1 = sinon.spy(w, 'store');
var spy2 = sinon.spy(w, 'emit');
w.txProposals.txps['qwerty'] = new txp();
w.txProposals.txps['qwerty'].ok.should.equal(0);
w._handleReject('john', {
ntxid: 'qwerty'
}, 1);
w.txProposals.txps['qwerty'].ok.should.equal(1);
spy1.calledOnce.should.equal(true);
spy2.callCount.should.equal(2);
spy2.firstCall.args.should.deep.equal(['txProposalsUpdated']);
spy2.secondCall.args.should.deep.equal(['txProposalEvent', {
type: 'rejected',
cId: 'john',
txId: 'qwerty',
}]);
});
});
describe('_handleSeen', function() {
it('should fails if unknown tx', function() {
var w = cachedCreateW();
(function() {
w._handleReject(1, {
ntxid: 1
}, 1);
}).should.throw('Unknown TXP');
});
it('should set seen a tx', function() {
var w = cachedCreateW();
function txp() {
this.ok = 0;
this.signedBy = {};
};
txp.prototype.setSeen = function() {
this.ok = 1;
};
txp.prototype.toObj = function() {};
var spy1 = sinon.spy(w, 'store');
var spy2 = sinon.spy(w, 'emit');
w.txProposals.txps['qwerty'] = new txp();
w.txProposals.txps['qwerty'].ok.should.equal(0);
w._handleSeen('john', {
ntxid: 'qwerty'
}, 1);
w.txProposals.txps['qwerty'].ok.should.equal(1);
spy1.calledOnce.should.equal(true);
spy2.callCount.should.equal(2);
spy2.firstCall.args.should.deep.equal(['txProposalsUpdated']);
spy2.secondCall.args.should.deep.equal(['txProposalEvent', {
type: 'seen',
cId: 'john',
txId: 'qwerty',
}]);
});
});
it('getNetwork', function() {
var w = cachedCreateW();
var n = w.getNetwork();
n.maxPeers.should.equal(5);
should.exist(n.networkNonce);
});
it('#disconnect', function() {
var w = cachedCreateW();
var spy1 = sinon.spy(w.network, 'disconnect');
w.disconnect();
spy1.callCount.should.equal(1);
});
});

View File

@ -317,6 +317,20 @@ describe('WalletFactory model', function() {
});
});
it('should clean lastOpened on delete wallet', function(done) {
var wf = new WalletFactory(config, '0.0.1');
var w = wf.create({
name: 'test wallet'
});
wf.storage.setLastOpened(w.id);
wf.delete(w.id, function() {
var last = wf.storage.getLastOpened();
should.equal(last, undefined);
done();
});
});
it('should return false if wallet does not exist', function() {
var opts = {
'requiredCopayers': 2,
@ -343,6 +357,23 @@ describe('WalletFactory model', function() {
wf.read.calledWith(walletId).should.be.true;
});
it('should save lastOpened on create/open a wallet', function() {
var opts = {
'requiredCopayers': 2,
'totalCopayers': 3
};
var wf = new WalletFactory(config, '0.0.1');
var w = wf.create(opts);
var last = wf.storage.getLastOpened();
should.equal(last, w.id);
wf.storage.setLastOpened('other_id');
var wo = wf.open(w.id, opts);
last = wf.storage.getLastOpened();
should.equal(last, w.id);
});
it('should return error if network are differents', function() {
var opts = {
'requiredCopayers': 2,

View File

@ -82,9 +82,9 @@ describe('Insight model', function() {
sinon
.stub(http, 'request')
.returns(req)
.yields(request);
.stub(http, 'request')
.returns(req)
.yields(request);
i.getUnspent(['2MuD5LnZSViZZYwZbpVsagwrH8WWvCztdmV', '2NBSLoMvsHsf2Uv3LA17zV4beH6Gze6RovA'], function(e, ret) {
should.not.exist(e);
@ -113,9 +113,9 @@ describe('Insight model', function() {
req.end = function() {};
sinon
.stub(http, 'request')
.returns(req)
.yields(request);
.stub(http, 'request')
.returns(req)
.yields(request);
i.sendRawTransaction(rawtx, function(a) {
should.exist(a);
@ -200,5 +200,33 @@ describe('Insight model', function() {
});
});
describe("#checkSentTx", function() {
it('should return true if Tx is found', function(done) {
var w = new Insight();
w._request = sinon.stub().yields(null, {
txid: "414142",
});
var tx = function() {};
tx.prototype.getHash = function(){return new Buffer('BAA')};
w.checkSentTx(new tx(), function(err, ret) {
should.not.exist(err);
ret.should.equal('414142');
done();
});
});
it('should return false if Tx is not found', function(done) {
var w = new Insight();
w._request = sinon.stub().yields(null, {
txid: "414142",
});
var tx = function() {};
tx.prototype.getHash = function(){return new Buffer('ABC')};
w.checkSentTx(new tx(), function(err, ret) {
should.not.exist(err);
ret.should.equal(false);
done();
});
});
});
});

View File

@ -5,6 +5,15 @@ var should = chai.should();
var PrivateKey = require('../js/models/core/PrivateKey');
var PublicKeyRing = require('../js/models/core/PublicKeyRing');
var getNewEpk = function() {
return new PrivateKey({
networkName: 'livenet',
})
.deriveBIP45Branch()
.extendedPublicKeyString();
}
describe('Performance tests', function() {
describe('PrivateKey', function() {
it('should optimize BIP32 private key gen time with cache', function() {
@ -43,7 +52,7 @@ describe('Performance tests', function() {
requiredCopayers: M
});
for (var i = 0; i < N; i++) {
pkr1.addCopayer(); // add new random ext public key
pkr1.addCopayer(getNewEpk()); // add new random ext public key
}
var generateN = 5;
var generated = [];

View File

@ -148,6 +148,18 @@ describe('Storage/LocalEncrypted model', function() {
s.getName(1).should.equal('hola');
});
});
describe('#getLastOpened #setLastOpened', function() {
it('should get/set names', function() {
var s = new LocalEncrypted({
localStorage: localMock,
password: 'password'
});
s.setLastOpened('hey');
s.getLastOpened().should.equal('hey');
});
});
describe('#getWallets', function() {
it('should retreive wallets from storage', function() {
var s = new LocalEncrypted({

View File

@ -214,7 +214,7 @@ describe("Unit: Controllers", function() {
});
describe("Unit: Sidebar Controller", function() {
describe("Unit: Version Controller", function() {
var scope, $httpBackendOut;
var GH = 'https://api.github.com/repos/bitpay/copay/tags';
beforeEach(inject(function($controller, $injector) {
@ -235,7 +235,7 @@ describe("Unit: Controllers", function() {
beforeEach(inject(function($controller, $rootScope) {
rootScope = $rootScope;
scope = $rootScope.$new();
headerCtrl = $controller('SidebarController', {
headerCtrl = $controller('VersionController', {
$scope: scope,
});
}));
@ -273,8 +273,24 @@ describe("Unit: Controllers", function() {
scope.$apply();
});
it('should return an array of n undefined elements', function() {
it('should return networkName', function() {
$httpBackend.flush(); // need flush
var networkName = scope.networkName;
expect(networkName).equal('livenet');
});
});
describe("Unit: Sidebar Controller", function() {
var rootScope;
beforeEach(inject(function($controller, $rootScope) {
rootScope = $rootScope;
scope = $rootScope.$new();
headerCtrl = $controller('SidebarController', {
$scope: scope,
});
}));
it('should return an array of n undefined elements', function() {
var n = 5;
var array = scope.getNumber(n);
expect(array.length).equal(n);

View File

@ -3,6 +3,7 @@
<div class="row" ng-if='$root.wallet && !$root.wallet.isReady() && !loading'>
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup oh">
@ -73,8 +74,12 @@
</div>
<div class="text-right">
<a class="text-warning" ng-really-click="deleteWallet()"
ng-really-message="Are you sure to delete this wallet from this
computer?">Delete wallet</a>
<span class="text-gray">|</span>
<a class="text-primary m20r" ng-click="downloadBackup()"
ng-show="!$root.wallet.publicKeyRing.isComplete()">Download seed backup</a>
ng-show="!$root.wallet.publicKeyRing.isComplete()">Download seed backup</a>
<button class="button primary m0"
ng-click="backup()"
ng-show="!$root.wallet.publicKeyRing.isBackupReady()"

View File

@ -2,6 +2,7 @@
<div class="row">
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="button-setup" ng-show="hasWallets">

View File

@ -6,6 +6,7 @@
<div class="row" ng-init="choosefile=0; pastetext=0" ng-show="!loading">
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup">

View File

@ -25,7 +25,7 @@
src="./img/satoshi.gif"
alt="{{copayer}}">
<span ng-show="copayer.index == 0">you</span>
<span ng-show="copayer.index == 0">Me</span>
<span ng-show="copayer.index > 0">{{copayer.nick}}</span>
<span class="btn-copy" clip-copy="copayer.peerId"></span>
</li>

View File

@ -4,9 +4,7 @@
<a href="#!/addresses" class="db">
<img src="img/logo-negative-beta.svg" alt="" width="80">
</a>
<small>v{{version}}</small>
<small ng-if="$root.wallet.getNetworkName()=='livenet'">LIVENET</small>
<small ng-if="$root.wallet.getNetworkName()=='testnet'">TESTNET</small>
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="line-sidebar-b"></div>
<div class="founds size-12 text-center box-founds p10t">

View File

@ -4,11 +4,7 @@
<a href="#!/addresses" class="db">
<img src="img/logo-negative-beta.svg" alt="" width="100">
</a>
<div>
<small>v{{version}}</small>
<small ng-if="$root.wallet.getNetworkName()=='livenet'">LIVENET</small>
<small ng-if="$root.wallet.getNetworkName()=='testnet'">TESTNET</small>
</div>
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="line-sidebar"></div>
<div>
@ -64,7 +60,7 @@
</li>
</ul>
<div ng-include="'views/includes/peer-list.html'"></div>
<div ng-show="$root.wallet.isShared()" ng-include="'views/includes/peer-list.html'"></div>
</div>

View File

@ -0,0 +1,5 @@
<div ng-controller="VersionController">
<small>v{{version}}</small>
<small ng-if="networkName=='testnet'">[ {{networkName}} ]</small>
</div>

View File

@ -18,7 +18,7 @@
class="ellipsis"
tooltip="ID: {{copayer.peerId}}"
tooltip-placement="bottom">
<small class="text-gray" ng-show="copayer.index == 0"><i class="fi-check m5r"></i>you</small>
<small class="text-gray" ng-show="copayer.index == 0"><i class="fi-check m5r"></i>Me</small>
<small class="text-gray" ng-show="copayer.index > 0"><i class="fi-check m5r"></i>{{copayer.nick}}</small>
</div>

View File

@ -6,6 +6,7 @@
<div class="row" ng-show="!loading">
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup">
@ -19,13 +20,17 @@
ng-show="joinForm.connectionId.$pristine" class="has-tip
text-gray" tooltip="Paste wallet secret here" >Required</small>
</label>
<input id="connectionId" type="text" class="small-9 columns" placeholder="Paste wallet secret here" name="connectionId" ng-model="connectionId" wallet-secret required style="width:85%;">
<div class="small-2 columns" style="padding:0px;width:15%;" ng-hide="showScanner">
<a class="postfix button primary" ng-click="openScanner()"><i class="fi-camera">&nbsp;</i></a>
</div>
<div class="small-2 columns" style="padding:0px;width:15%;" ng-show="showScanner">
<a class="postfix button warning" ng-click="cancelScanner()"><i class="fi-x">&nbsp;</i></a>
<div class="row collapse">
<div class="large-10 columns">
<input id="connectionId" type="text" class="small-9 columns" placeholder="Paste wallet secret here" name="connectionId" ng-model="connectionId" wallet-secret required>
</div>
<div class="small-2 columns" ng-hide="showScanner">
<a class="postfix button primary" ng-click="openScanner()"><i class="fi-camera">&nbsp;</i></a>
</div>
<div class="small-2 columns" ng-show="showScanner">
<a class="postfix button warning" ng-click="cancelScanner()"><i class="fi-x">&nbsp;</i></a>
</div>
</div>
<div id="scanner" class="row" ng-if="showScanner">
@ -46,7 +51,6 @@
</div>
</div>
<div style="clear: both;"></div>
<label for="joinPassword"> User info </label>
<input id="joinPassword" type="text" class="form-control" placeholder="Your name (optional)" name="nickname" ng-model="nickname">
<input type="password" class="form-control"

View File

@ -10,7 +10,7 @@
{{address.balance || 0|noFractionNumber}} {{$root.unitName}}
</p>
<button class="m15t button secondary" open-external address="{{address.address}}">
<i class="fi-link">&nbsp;</i> Open in external aplication
<i class="fi-link">&nbsp;</i> Open in external application
</button>
</div>
</div>

View File

@ -6,6 +6,7 @@
<div class="row" ng-show="!loading">
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup">

View File

@ -77,7 +77,7 @@
</div>
</div>
<div class="row" ng-show="wallet.totalCopayers > 1">
<div class="row" ng-show="wallet.isShared()">
<div class="large-12 columns">
<div class="row collapse">
<label for="comment">Note
@ -116,7 +116,7 @@
Including fee of {{defaultFee|noFractionNumber}} {{$root.unitName}}
</small>
</p>
<div ng-show="wallet.totalCopayers > 1">
<div ng-show="wallet.isShared()">
<h6>Note</h6>
<p ng-class="{'hidden': !commentText}">{{commentText}}</p>
</div>

View File

@ -2,6 +2,7 @@
<div class="row">
<div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup">

View File

@ -8,6 +8,7 @@
<div class="row">
<div class="large-4 columns logo-setup text-center">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div>
<div class="large-8 columns line-dashed-setup-v">
<div class="box-setup oh">
@ -16,7 +17,7 @@
<input type="text" placeholder="Family vacation funds" class="form-control" ng-model="walletName">
</label>
<div class="row" ng-show="isSetupWalletPage">
<div>
<div ng-if="totalCopayers > 1">
<label for="Name">Your name</label>
<input id="Name" type="text" placeholder="Name" class="form-control" ng-model="myNickname">
</div>

View File

@ -1,7 +1,7 @@
<div class="transactions" data-ng-controller="TransactionsController">
<div ng-show='$root.wallet.isReady()'>
<h1 ng-show="wallet.totalCopayers > 1"> Transaction proposals <small>({{txs.length}})</small></h1>
<div class="large-12" ng-show="wallet.totalCopayers > 1">
<h1 ng-show="wallet.isShared()"> Transaction proposals <small>({{txs.length}})</small></h1>
<div class="large-12" ng-show="wallet.isShared()">
<ul class="inline-list">
<li> <a class="text-gray size-12" ng-click="show(true)" ng-disabled="loading || onlyPending" loading="Updating" ng-class="{'active' : onlyPending}"> [ Pending ] </a> </li>
<li> <a class="text-gray size-12" ng-click="show()" ng-disabled="loading || !onlyPending" loading="Updating" ng-class="{'active' : !onlyPending}"> [ All ] </a> </li>
@ -40,37 +40,37 @@
</div>
<div class="last-transactions-content">
<div class="box-copayer" ng-repeat="(cId, actions) in tx.peerActions">
<a href="#!/transactions" class="has-tip" tooltip-popup-delay="1000" tooltip="{{cId === $root.wallet.getMyCopayerId() ? 'You' : $root.wallet.publicKeyRing.nicknameForCopayer(cId)}}">
<img class="copayer-ico br100" src="./img/satoshi.gif" alt="{{cId}}">
<div class="box-copayer" ng-repeat="c in tx.actionList">
<a href="#!/transactions" class="has-tip">
<img class="copayer-ico br100" src="./img/satoshi.gif" alt="{{c.cId}}">
</a>
<div class="box-status">
<a ng-if="actions.create" tooltip-popup-delay="1000" tooltip="Created {{ts | amTimeAgo}}">
<a ng-if="c.actions.create" tooltip-popup-delay="1000" tooltip="Created {{c.actions.create | amTimeAgo}}">
<i class="fi-crown icon-status icon-active"></i>
</a>
<a ng-if="!actions.create"><i class="fi-crown icon-status"></i></a>
<a ng-if="!c.actions.create"><i class="fi-crown icon-status"></i></a>
<a ng-if="actions.seen" tooltip-popup-delay="1000" tooltip="Seen {{ts | amTimeAgo}}">
<a ng-if="c.actions.seen" tooltip-popup-delay="1000" tooltip="Seen {{c.actions.seen | amTimeAgo}}">
<i class="fi-eye icon-status icon-active"></i>
</a>
<a ng-if="!actions.seen"><i class="fi-eye icon-status"></i></a>
<a ng-if="!c.actions.seen"><i class="fi-eye icon-status"></i></a>
<a ng-if="actions.rejected" tooltip-popup-delay="1000" tooltip="Rejected {{ts | amTimeAgo}}">
<a ng-if="c.actions.rejected" tooltip-popup-delay="1000" tooltip="Rejected {{c.actions.rejected | amTimeAgo}}">
<i class="fi-x icon-status icon-active-x"></i>
</a>
<a ng-if="actions.sign" tooltip-popup-delay="1000" tooltip="Signed {{ts | amTimeAgo}}">
<a ng-if="c.actions.sign" tooltip-popup-delay="1000" tooltip="Signed {{c.actions.sign | amTimeAgo}}">
<i class="fi-check icon-status icon-active-check"></i>
</a>
<a ng-if="!actions.sign && !actions.rejected" href="#!/transactions" class="icon-status">
<a ng-if="!c.actions.sign && !c.actions.rejected" href="#!/transactions" class="icon-status">
<i class="fi-loop icon-rotate"></i>
</a>
</div>
<div class="text-center">
<p class="size-12 text-gray ellipsis">
{{$root.wallet.publicKeyRing.nicknameForCopayer(cId)}}
{{c.cId === $root.wallet.getMyCopayerId() ? 'Me' : $root.wallet.publicKeyRing.nicknameForCopayer(c.cId)}}
</p>
</div>
</div>
@ -135,9 +135,9 @@
<pagination ng-show="!onlyPending && txs.length > txpItemsPerPage" total-items="txs.length" items-per-page="txpItemsPerPage" page="txpCurrentPage" on-select-page="show()" class="pagination-small primary"></pagination>
</div>
<h1 ng-class="{'line-dashed': wallet.totalCopayers > 1}">
<h1 ng-class="{'line-dashed': wallet.isShared()}">
Last transactions
<small ng-hide="wallet.totalCopayers > 1 || !loading">
<small ng-hide="wallet.isShared() || !loading">
<i class="fi-bitcoin-circle icon-rotate spinner"></i>
</small>
</h1>

View File

@ -1,6 +1,7 @@
<div class="wide-page">
<div class="text-center">
<img src="img/logo-negative-beta.svg" alt="Copay">
<div class="text-white" ng-include="'views/includes/version.html'"></div>
</div>
<h1 class="text-center text-white">Browser unsupported</h1>
<h3 class="text-center text-white">