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 *.pid
*.gz *.gz
*.swp *.swp
*.sig
tags tags
pids pids
logs logs
@ -48,6 +49,7 @@ browser-extensions/firefox/firefox-addon
browser-extensions/firefox/data browser-extensions/firefox/data
browser-extensions/firefox/copay.xpi browser-extensions/firefox/copay.xpi
version.js version.js
!js/controllers/version.js
android/package android/package
android/*.apk android/*.apk

View File

@ -1,5 +1,6 @@
// core // core
module.exports.PublicKeyRing = require('./js/models/core/PublicKeyRing'); 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.TxProposals = require('./js/models/core/TxProposals');
module.exports.PrivateKey = require('./js/models/core/PrivateKey'); module.exports.PrivateKey = require('./js/models/core/PrivateKey');
module.exports.Passphrase = require('./js/models/core/Passphrase'); 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-primary {color: #1ABC9C;}
.text-secondary {color: #3498DB;} .text-secondary {color: #3498DB;}
.text-white {color: #fff;} .text-white {color: #fff;}
.text-warning {color: #CA5649;}
.footer-setup a.text-gray:hover {color: #fff;} .footer-setup a.text-gray:hover {color: #fff;}
a.text-gray:hover {color: #2C3E50;} a.text-gray:hover {color: #2C3E50;}
@ -1014,6 +1014,7 @@ a.text-black:hover {color: #213140;}
a.text-primary:hover {color: #50E3C2;} a.text-primary:hover {color: #50E3C2;}
a.text-secondary:hover {color: #4A90E2;} a.text-secondary:hover {color: #4A90E2;}
a.text-white:hover {color: #ccc;} a.text-white:hover {color: #ccc;}
a.text-warning:hover {color: #FD7262;}
.box-setup-copayers { .box-setup-copayers {
background: #2C3E50; 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/import.js"></script>
<script src="js/controllers/settings.js"></script> <script src="js/controllers/settings.js"></script>
<script src="js/controllers/uriPayment.js"></script> <script src="js/controllers/uriPayment.js"></script>
<script src="js/controllers/version.js"></script>
<!-- PLACEHOLDER: CORDOVA SRIPT --> <!-- PLACEHOLDER: CORDOVA SRIPT -->
<script src="js/mobile.js"></script> <script src="js/mobile.js"></script>

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('CopayersController', angular.module('copayApp.controllers').controller('CopayersController',
function($scope, $rootScope, $location, backupService) { function($scope, $rootScope, $location, backupService, walletFactory, controllerUtils) {
$scope.backup = function() { $scope.backup = function() {
var w = $rootScope.wallet; var w = $rootScope.wallet;
@ -18,4 +18,12 @@ angular.module('copayApp.controllers').controller('CopayersController',
$location.path('/addresses'); $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) { if ($rootScope.pendingPayment) {
notification.info('Login Required', 'Please open wallet to complete payment'); 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.loading = false;
$scope.wallets = walletFactory.getWallets().sort(cmp); $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.openPassword = '';
$scope.open = function(form) { $scope.open = function(form) {

View File

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

View File

@ -76,6 +76,8 @@ angular.module('copayApp.controllers').controller('SettingsController',
unitToSatoshi: $scope.selectedUnit.value, 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', angular.module('copayApp.controllers').controller('SidebarController',
function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) { function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) {
$scope.version = copay.version;
$scope.networkName = config.networkName;
$scope.menu = [{ $scope.menu = [{
'title': 'Addresses', 'title': 'Addresses',
'icon': 'fi-address-book', 'icon': 'fi-address-book',
@ -62,23 +60,6 @@ angular.module('copayApp.controllers').controller('SidebarController',
return new Array(num); 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) // Init socket handlers (with no wallet yet)
controllerUtils.setSocketHandlers(); 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 imports = require('soop').imports();
var bitcore = require('bitcore'); var bitcore = require('bitcore');
var coinUtil = bitcore.util;
var preconditions = require('preconditions').singleton(); var preconditions = require('preconditions').singleton();
var http; 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) { Insight.prototype._getOptions = function(method, path, data) {
return { return {
host: this.host, 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) { Insight.prototype.getTransactions = function(addresses, cb) {
preconditions.shouldBeArray(addresses); preconditions.shouldBeArray(addresses);
preconditions.shouldBeFunction(cb); preconditions.shouldBeFunction(cb);
@ -101,8 +94,11 @@ Insight.prototype.getTransactions = function(addresses, cb) {
callback(); callback();
}); });
}, function() { }, function() {
var clean_txids = removeRepeatedElements(txids); var uniqueTxids = {};
_asyncForEach(clean_txids, function(txid, callback2) { for (var k in txids) {
uniqueTxids[txids[k]] = 1;
}
_asyncForEach(Object.keys(uniqueTxids), function(txid, callback2) {
var options = self._getOptions('GET', '/api/tx/' + txid); var options = self._getOptions('GET', '/api/tx/' + txid);
self._request(options, function(err, res) { self._request(options, function(err, res) {
txs.push(res); txs.push(res);
@ -164,8 +160,8 @@ Insight.prototype.checkActivity = function(addresses, cb) {
var getOutputs = function(t) { var getOutputs = function(t) {
return flatArray( return flatArray(
t.vout.map(function(vout) { 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; return BIP45_PUBLIC_PREFIX + '/' + sub;
}; };
HDPath.indicesForPath = function(path) { HDPath.indexesForPath = function(path) {
preconditions.shouldBeString(path); preconditions.shouldBeString(path);
var s = path.split('/'); var s = path.split('/');
return { return {
isChange: s[3] === '1', isChange: s[3] === '1',
index: parseInt(s[4]), addressIndex: parseInt(s[4]),
copayerIndex: parseInt(s[2]) copayerIndex: parseInt(s[2])
}; };
}; };

View File

@ -23,14 +23,13 @@ function PublicKeyRing(opts) {
this.copayersHK = opts.copayersHK || []; this.copayersHK = opts.copayersHK || [];
this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) : HDParams.init(this.totalCopayers);
: HDParams.init(this.totalCopayers);
this.publicKeysCache = opts.publicKeysCache || {}; this.publicKeysCache = opts.publicKeysCache || {};
this.nicknameFor = opts.nicknameFor || {}; this.nicknameFor = opts.nicknameFor || {};
this.copayerIds = []; this.copayerIds = [];
this.copayersBackup = opts.copayersBackup || []; this.copayersBackup = opts.copayersBackup || [];
this.addressToPath = {}; this.addressToPath = {};
} }
PublicKeyRing.fromObj = function(data) { PublicKeyRing.fromObj = function(data) {
@ -100,14 +99,6 @@ PublicKeyRing.prototype._checkKeys = function() {
throw new Error('dont have required keys yet'); 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) { PublicKeyRing.prototype._updateBip = function(index) {
var hk = this.copayersHK[index].derive(HDPath.IdBranch); var hk = this.copayersHK[index].derive(HDPath.IdBranch);
this.copayerIds[index] = hk.eckey.public.toString('hex'); this.copayerIds[index] = hk.eckey.public.toString('hex');
@ -126,6 +117,8 @@ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) {
}; };
PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) {
preconditions.checkArgument(newEpk);
if (this.isComplete()) if (this.isComplete())
throw new Error('PKR already has all required key:' + this.totalCopayers); 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'); throw new Error('PKR already has that key');
}); });
if (!newEpk) {
newEpk = this._newExtendedPublicKey();
}
var i = this.copayersHK.length; var i = this.copayersHK.length;
var bip = new HK(newEpk); var bip = new HK(newEpk);
this.copayersHK.push(bip); this.copayersHK.push(bip);
@ -192,7 +181,9 @@ PublicKeyRing.prototype.getAddress = function(index, isChange, id) {
// Overloaded to receive a PubkeyString or a consigner index // Overloaded to receive a PubkeyString or a consigner index
PublicKeyRing.prototype.getHDParams = function(id) { PublicKeyRing.prototype.getHDParams = function(id) {
var copayerIndex = this.getCosigner(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'); if (index.length != 1) throw new Error('no index for copayerIndex');
return index[0]; return index[0];
@ -231,9 +222,11 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) {
if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX; if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX;
if (typeof pubKey == 'number') return pubKey; 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'); 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); var index = sorted.indexOf(pubKey);
if (index == -1) throw new Error('no public key in ring'); 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) { PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) {
opts = opts || {}; opts = opts || {};
var isOwned = index.copayerIndex == HDPath.SHARED_INDEX var isOwned = index.copayerIndex == HDPath.SHARED_INDEX || index.copayerIndex == copayerIndex;
|| index.copayerIndex == copayerIndex;
var ret = []; var ret = [];
if (!opts.excludeChange) { if (!opts.excludeChange) {
for (var i = 0; i < index.changeIndex; i++) { for (var i = 0; i < index.changeIndex; i++) {
var a = this.getAddress(i, true, index.copayerIndex); var a = this.getAddress(i, true, index.copayerIndex);
ret.unshift({ ret.unshift({
address: a, address: a,
addressStr: a.toString(), addressStr: a.toString(),
isChange: true, isChange: true,
owned: isOwned owned: isOwned
}); });
}
} }
}
if (!opts.excludeMain) { if (!opts.excludeMain) {
for (var i = 0; i < index.receiveIndex; i++) { for (var i = 0; i < index.receiveIndex; i++) {
var a = this.getAddress(i, false, index.copayerIndex); var a = this.getAddress(i, false, index.copayerIndex);
ret.unshift({ ret.unshift({
address: a, address: a,
addressStr: a.toString(), addressStr: a.toString(),
isChange: false, isChange: false,
owned: isOwned 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 // TODO this could be cached
PublicKeyRing.prototype._addScriptMap = function(map, path) { PublicKeyRing.prototype._addScriptMap = function(map, path) {
var p = HDPath.indicesForPath(path); var p = HDPath.indexesForPath(path);
var script = this.getRedeemScript(p.index, p.isChange, p.copayerIndex); var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex);
map[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex'); 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'; 'use strict';
var BuilderMockV0 = require('./BuilderMockV0');;
var imports = require('soop').imports();
var bitcore = require('bitcore'); var bitcore = require('bitcore');
var util = bitcore.util; var util = bitcore.util;
var Transaction = bitcore.Transaction; var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');; var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder; var TxProposal = require('./TxProposal');;
var Script = bitcore.Script; var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools; var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance(); 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) { function TxProposals(opts) {
opts = opts || {}; opts = opts || {};
@ -178,6 +20,7 @@ function TxProposals(opts) {
this.txps = {}; this.txps = {};
} }
// fromObj => from a trusted source
TxProposals.fromObj = function(o, forceOpts) { TxProposals.fromObj = function(o, forceOpts) {
var ret = new TxProposals({ var ret = new TxProposals({
networkName: o.networkName, networkName: o.networkName,
@ -187,7 +30,7 @@ TxProposals.fromObj = function(o, forceOpts) {
o.txps.forEach(function(o2) { o.txps.forEach(function(o2) {
var t = TxProposal.fromObj(o2, forceOpts); var t = TxProposal.fromObj(o2, forceOpts);
if (t.builder) { if (t.builder) {
var id = t.getID(); var id = t.getId();
ret.txps[id] = t; ret.txps[id] = t;
} }
}); });
@ -198,14 +41,9 @@ TxProposals.prototype.getNtxids = function() {
return Object.keys(this.txps); return Object.keys(this.txps);
}; };
TxProposals.prototype.toObj = function(onlyThisNtxid) { TxProposals.prototype.toObj = function() {
if (onlyThisNtxid) throw new Error();
var ret = []; var ret = [];
for (var id in this.txps) { for (var id in this.txps) {
if (onlyThisNtxid && id != onlyThisNtxid)
continue;
var t = this.txps[id]; var t = this.txps[id];
if (!t.sent) if (!t.sent)
ret.push(t.toObj()); 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(); TxProposals.prototype.merge = function(inObj, builderOpts) {
var ret = {}; var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts);
ret.events = []; incomingTx._sync();
ret.events.hasChanged = false;
var myTxps = this.txps;
var ntxid = incomingTx.getId();
var ret = {
ntxid: ntxid
};
if (myTxps[ntxid]) { if (myTxps[ntxid]) {
var v0 = myTxps[ntxid];
var v1 = inTxp; // Merge an existing txProposal
ret = v0.merge(v1, author); ret.hasChanged = myTxps[ntxid].merge(incomingTx);
} else { } else {
this.txps[ntxid] = inTxp; // Create a new one
ret.hasChanged = true; ret.new = ret.hasChanged = 1;
ret.events.push({ this.txps[ntxid] = incomingTx;
type: 'new',
cid: inTxp.creator,
tx: ntxid
});
} }
ret.txp = this.txps[ntxid];
return ret; return ret;
}; };
TxProposals.prototype.add = function(data) { // Add a LOCALLY CREATED (trusted) tx proposal
preconditions.checkArgument(data.inputChainPaths); TxProposals.prototype.add = function(txp) {
preconditions.checkArgument(data.signedBy); txp._sync();
preconditions.checkArgument(data.creator); var ntxid = txp.getId();
preconditions.checkArgument(data.createdTs);
preconditions.checkArgument(data.builder);
var txp = new TxProposal(data);
var ntxid = txp.getID();
this.txps[ntxid] = txp; this.txps[ntxid] = txp;
return ntxid; return ntxid;
}; };
TxProposals.prototype.setSent = function(ntxid, txid) {
//sent TxProposals are local an not broadcasted. TxProposals.prototype.get = function(ntxid) {
this.txps[ntxid].setSent(txid); var ret = this.txps[ntxid];
if (!ret)
throw new Error('Unknown TXP: '+ntxid);
return ret;
}; };
TxProposals.prototype.getTxProposal = function(ntxid, copayers) { TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
var txp = this.txps[ntxid]; var txp = this.get(ntxid);
var i = JSON.parse(JSON.stringify(txp)); var i = JSON.parse(JSON.stringify(txp));
i.builder = txp.builder; i.builder = txp.builder;
i.ntxid = ntxid; i.ntxid = ntxid;
@ -296,6 +137,17 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
return i; 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 //returns the unspent txid-vout used in PENDING Txs
TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
var ret = {}; var ret = {};
@ -312,5 +164,4 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
return ret; return ret;
}; };
TxProposals.TxProposal = TxProposal; module.exports = TxProposals;
module.exports = require('soop')(TxProposals);

View File

@ -17,6 +17,7 @@ var Address = bitcore.Address;
var HDParams = require('./HDParams'); var HDParams = require('./HDParams');
var PublicKeyRing = require('./PublicKeyRing'); var PublicKeyRing = require('./PublicKeyRing');
var TxProposal = require('./TxProposal');
var TxProposals = require('./TxProposals'); var TxProposals = require('./TxProposals');
var PrivateKey = require('./PrivateKey'); var PrivateKey = require('./PrivateKey');
var copayConfig = require('../../../config'); var copayConfig = require('../../../config');
@ -36,7 +37,7 @@ function Wallet(opts) {
}); });
if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName) if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName)
throw new Error('Network forced to ' + 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'); this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet');
@ -58,11 +59,11 @@ function Wallet(opts) {
} }
Wallet.builderOpts = { Wallet.builderOpts = {
lockTime: null, lockTime: null,
signhash: bitcore.Transaction.SIGNHASH_ALL, signhash: bitcore.Transaction.SIGNHASH_ALL,
fee: null, fee: null,
feeSat: null, feeSat: null,
}; };
Wallet.parent = EventEmitter; Wallet.parent = EventEmitter;
@ -129,39 +130,158 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
}; };
Wallet.prototype._processProposalEvents = function(senderId, m) {
Wallet.prototype._handleTxProposal = function(senderId, data) { var ev;
this.log('RECV TXPROPOSAL: ', data); if (m) {
var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts); if (m.new) {
ev = {
type: 'new',
cid: senderId
var valid = inTxp.isValid(); }
if (!valid) { } else if (m.newCopayer) {
var corruptEvent = { ev = {
type: 'signed',
cid: m.newCopayer
};
}
} else {
ev = {
type: 'corrupt', 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'); if (ev)
this.store(); this.emit('txProposalEvent', ev);
for (var i = 0; i < mergeInfo.events.length; i++) {
this.emit('txProposalEvent', mergeInfo.events[i]);
}
}; };
/* 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) { Wallet.prototype._handleAddressBook = function(senderId, data, isInbound) {
preconditions.checkState(data.addressBook);
this.log('RECV ADDRESSBOOK:', data); this.log('RECV ADDRESSBOOK:', data);
var rcv = data.addressBook; var rcv = data.addressBook;
var hasChange; var hasChange;
@ -193,24 +313,30 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) {
// This handler is repeaded on WalletFactory (#join). TODO // This handler is repeaded on WalletFactory (#join). TODO
case 'walletId': case 'walletId':
this.sendWalletReady(senderId); this.sendWalletReady(senderId);
break; break;
case 'walletReady': case 'walletReady':
this.sendPublicKeyRing(senderId); this.sendPublicKeyRing(senderId);
this.sendAddressBook(senderId); this.sendAddressBook(senderId);
this.sendAllTxProposals(senderId); // send old txps this.sendAllTxProposals(senderId); // send old txps
break; break;
case 'publicKeyRing': case 'publicKeyRing':
this._handlePublicKeyRing(senderId, data, isInbound); 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': case 'txProposal':
this._handleTxProposal(senderId, data, isInbound); this._handleTxProposal(senderId, data, isInbound);
break; break;
case 'indexes': case 'indexes':
this._handleIndexes(senderId, data, isInbound); this._handleIndexes(senderId, data, isInbound);
break; break;
case 'addressbook': case 'addressbook':
this._handleAddressBook(senderId, data, isInbound); this._handleAddressBook(senderId, data, isInbound);
break; break;
} }
}; };
@ -384,6 +510,7 @@ Wallet.prototype.toObj = function() {
return walletObj; return walletObj;
}; };
// fromObj => from a trusted source
Wallet.fromObj = function(o, storage, network, blockchain) { Wallet.fromObj = function(o, storage, network, blockchain) {
var opts = JSON.parse(JSON.stringify(o.opts)); var opts = JSON.parse(JSON.stringify(o.opts));
opts.addressBook = o.addressBook; opts.addressBook = o.addressBook;
@ -418,11 +545,31 @@ Wallet.prototype.sendAllTxProposals = function(recipients) {
Wallet.prototype.sendTxProposal = function(ntxid, recipients) { Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
preconditions.checkArgument(ntxid); preconditions.checkArgument(ntxid);
preconditions.checkState(this.txProposals.txps[ntxid]);
this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals);
this.send(recipients, { this.send(recipients, {
type: 'txProposal', 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, walletId: this.id,
}); });
}; };
@ -517,30 +664,22 @@ Wallet.prototype.getTxProposals = function() {
Wallet.prototype.reject = function(ntxid) { Wallet.prototype.reject = function(ntxid) {
var myId = this.getMyCopayerId(); var txp = this.txProposals.reject(ntxid, this.getMyCopayerId());
var txp = this.txProposals.txps[ntxid]; this.sendReject(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);
this.store(); this.store();
this.emit('txProposalsUpdated'); this.emit('txProposalsUpdated');
}; };
Wallet.prototype.sign = function(ntxid, cb) { Wallet.prototype.sign = function(ntxid, cb) {
preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined'); preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined');
var self = this; var self = this;
setTimeout(function() { setTimeout(function() {
var myId = self.getMyCopayerId(); var myId = self.getMyCopayerId();
var txp = self.txProposals.txps[ntxid]; var txp = self.txProposals.get(ntxid);
if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { // if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) {
if (cb) cb(false); // if (cb) cb(false);
} // }
//
var keys = self.privateKey.getForPaths(txp.inputChainPaths); var keys = self.privateKey.getForPaths(txp.inputChainPaths);
var b = txp.builder; var b = txp.builder;
@ -559,14 +698,13 @@ Wallet.prototype.sign = function(ntxid, cb) {
}, 10); }, 10);
}; };
Wallet.prototype.sendTx = function(ntxid, cb) { Wallet.prototype.sendTx = function(ntxid, cb) {
var txp = this.txProposals.txps[ntxid]; var txp = this.txProposals.get(ntxid);
if (!txp) return;
var tx = txp.builder.build(); 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'); this.log('Broadcasting Transaction');
var scriptSig = tx.ins[0].getScript(); var scriptSig = tx.ins[0].getScript();
var size = scriptSig.serialize().length; var size = scriptSig.serialize().length;
@ -577,28 +715,23 @@ Wallet.prototype.sendTx = function(ntxid, cb) {
this.blockchain.sendRawTransaction(txHex, function(txid) { this.blockchain.sendRawTransaction(txHex, function(txid) {
self.log('BITCOIND txid:', txid); self.log('BITCOIND txid:', txid);
if (txid) { if (txid) {
self.txProposals.setSent(ntxid, txid); self.txProposals.get(ntxid).setSent(txid);
self.sendTxProposal(ntxid); self.sendTxProposal(ntxid);
self.store(); 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 // TODO: remove this method and use getAddressesInfo everywhere
Wallet.prototype.getAddresses = function(opts) { Wallet.prototype.getAddresses = function(opts) {
@ -719,8 +852,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
var priv = this.privateKey; var priv = this.privateKey;
opts = opts || {}; opts = opts || {};
preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName()); preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch');
preconditions.checkState(pkr.isComplete()); preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete');
preconditions.checkState(priv, 'no private key');
if (comment) preconditions.checkArgument(comment.length <= 100); if (comment) preconditions.checkArgument(comment.length <= 100);
if (!opts.remainderOut) { 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]; opts[k] = Wallet.builderOpts[k];
} }
var b = new Builder(opts) var b = new Builder(opts)
.setUnspent(utxos) .setUnspent(utxos)
.setOutputs([{ .setOutputs([{
address: toAddress, address: toAddress,
amountSatStr: amountSatStr, amountSatStr: amountSatStr,
}]); }]);
var selectedUtxos = b.getSelectedUnspent(); var selectedUtxos = b.getSelectedUnspent();
var inputChainPaths = selectedUtxos.map(function(utxo) { var inputChainPaths = selectedUtxos.map(function(utxo) {
@ -747,22 +881,23 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths));
if (priv) { var keys = priv.getForPaths(inputChainPaths);
var keys = priv.getForPaths(inputChainPaths); var signed = b.sign(keys);
var signed = b.sign(keys);
}
var myId = this.getMyCopayerId(); var myId = this.getMyCopayerId();
var now = Date.now(); var now = Date.now();
var me = {};
var tx = b.build(); 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 = {}; var meSeen = {};
if (priv) meSeen[myId] = now; if (priv) meSeen[myId] = now;
var data = { var ntxid = this.txProposals.add(new TxProposal({
inputChainPaths: inputChainPaths, inputChainPaths: inputChainPaths,
signedBy: me, signedBy: me,
seenBy: meSeen, seenBy: meSeen,
@ -770,9 +905,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
createdTs: now, createdTs: now,
builder: b, builder: b,
comment: comment comment: comment
}; }));
var ntxid = this.txProposals.add(data);
return ntxid; return ntxid;
}; };
@ -831,29 +964,29 @@ Wallet.prototype.indexDiscovery = function(start, change, cosigner, gap, cb) {
var self = this; var self = this;
async.doWhilst( async.doWhilst(
function _do(next) { function _do(next) {
// Optimize window to minimize the derivations. // Optimize window to minimize the derivations.
var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1;
var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner); var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner);
self.blockchain.checkActivity(addresses, function(err, actives) { self.blockchain.checkActivity(addresses, function(err, actives) {
if (err) throw err; if (err) throw err;
// Check for new activities in the newlly scanned addresses // Check for new activities in the newlly scanned addresses
var recentActive = actives.reduce(function(r, e, i) { var recentActive = actives.reduce(function(r, e, i) {
return e ? scanIndex + i : r; return e ? scanIndex + i : r;
}, lastActive); }, lastActive);
hasActivity = lastActive != recentActive; hasActivity = lastActive != recentActive;
lastActive = recentActive; lastActive = recentActive;
scanIndex += scanWindow; scanIndex += scanWindow;
next(); next();
}); });
}, },
function _while() { function _while() {
return hasActivity; return hasActivity;
}, },
function _finnaly(err) { function _finnaly(err) {
if (err) return cb(err); if (err) return cb(err);
cb(null, lastActive); cb(null, lastActive);
} }
); );
} }
@ -913,6 +1046,10 @@ Wallet.prototype.toggleAddressBookEntry = function(key) {
this.store(); this.store();
}; };
Wallet.prototype.isShared = function() {
return this.totalCopayers > 1;
}
Wallet.prototype.isReady = function() { Wallet.prototype.isReady = function() {
var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup(); var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup();
return ret; return ret;

View File

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

View File

@ -277,17 +277,28 @@ angular.module('copayApp.services')
i.outs = outs; i.outs = outs;
i.fee = i.builder.feeSat * satToUnit; i.fee = i.builder.feeSat * satToUnit;
i.missingSignatures = tx.countInputMissingSignatures(0); i.missingSignatures = tx.countInputMissingSignatures(0);
i.actionList = getActionList(i.peerActions);
txs.push(i); txs.push(i);
} }
}); });
$rootScope.txs = txs; //.some(function(i) {return i.isPending; } ); $rootScope.txs = txs;
if ($rootScope.pendingTxCount < pendingForUs) { if ($rootScope.pendingTxCount < pendingForUs) {
$rootScope.txAlertCount = pendingForUs; $rootScope.txAlertCount = pendingForUs;
} }
$rootScope.pendingTxCount = 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) { $rootScope.$watch('insightError', function(status) {
if (status) { if (status) {
if (status === -1) { if (status === -1) {

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
var imports = require('soop').imports();
var bitcore = require('bitcore'); var bitcore = require('bitcore');
function FakeBlockchain(opts) { function FakeBlockchain(opts) {
@ -47,4 +46,4 @@ FakeBlockchain.prototype.sendRawTransaction = function(rawtx, cb) {
return cb(txid); 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]; 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) { FakeStorage.prototype.removeGlobal = function(id) {
delete this.storage[id]; delete this.storage[id];

View File

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

View File

@ -70,9 +70,9 @@ describe('HDPath model', function() {
].forEach(function(datum) { ].forEach(function(datum) {
var path = datum[0]; var path = datum[0];
var result = datum[1]; var result = datum[1];
it('should get the correct indices for path ' + path, function() { it('should get the correct indexes for path ' + path, function() {
var i = HDPath.indicesForPath(path); var i = HDPath.indexesForPath(path);
i.index.should.equal(result.index); i.addressIndex.should.equal(result.index);
i.isChange.should.equal(result.isChange); i.isChange.should.equal(result.isChange);
}); });
}); });

View File

@ -13,11 +13,20 @@ try {
} catch (e) { } catch (e) {
var copay = require('../copay'); //node var copay = require('../copay'); //node
} }
var PrivateKey = copay.PrivateKey;
var PublicKeyRing = copay.PublicKeyRing; var PublicKeyRing = copay.PublicKeyRing;
var aMasterPubKey = 'tprv8ZgxMBicQKsPdSVTiWXEqCCzqRaRr9EAQdn5UVMpT9UHX67Dh1FmzEMbavPumpAicsUm2XvC6NTdcWB89yN5DUWx5HQ7z3KByUg7Ht74VRZ'; var aMasterPubKey = 'tprv8ZgxMBicQKsPdSVTiWXEqCCzqRaRr9EAQdn5UVMpT9UHX67Dh1FmzEMbavPumpAicsUm2XvC6NTdcWB89yN5DUWx5HQ7z3KByUg7Ht74VRZ';
var getNewEpk = function() {
return new PrivateKey({
networkName: 'livenet',
})
.deriveBIP45Branch()
.extendedPublicKeyString();
}
var createW = function(networkName) { var createW = function(networkName) {
var config = { var config = {
networkName: networkName || 'livenet', networkName: networkName || 'livenet',
@ -29,8 +38,8 @@ var createW = function(networkName) {
var copayers = []; var copayers = [];
for (var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
w.isComplete().should.equal(false); w.isComplete().should.equal(false);
w.remainingCopayers().should.equal(5-i); w.remainingCopayers().should.equal(5 - i);
var newEpk = w.addCopayer(); var newEpk = w.addCopayer(getNewEpk());
copayers.push(newEpk); copayers.push(newEpk);
} }
w.isComplete().should.equal(true); 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() { describe('PublicKeyRing model', function() {
it('should create an instance (livenet)', 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() { it('should add and check when adding shared pub keys', function() {
var k = createW(); var k = getCachedW();
var w = k.w; var w = k.w;
var copayers = k.copayers; var copayers = k.copayers;
@ -92,7 +109,7 @@ describe('PublicKeyRing model', function() {
}); });
it('should be able to to store and read', function() { it('should be able to to store and read', function() {
var k = createW(); var k = getCachedW();
var w = k.w; var w = k.w;
var copayers = k.copayers; var copayers = k.copayers;
var changeN = 2; var changeN = 2;
@ -124,10 +141,10 @@ describe('PublicKeyRing model', function() {
it('should generate some p2sh addresses', function() { it('should generate some p2sh addresses', function() {
var k = createW(); var k = getCachedW();
var w = k.w; var w = k.w;
[true, false].forEach(function(isChange){ [true, false].forEach(function(isChange) {
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
var a = w.generateAddress(isChange, k.pub); var a = w.generateAddress(isChange, k.pub);
a.isValid().should.equal(true); a.isValid().should.equal(true);
@ -148,7 +165,7 @@ describe('PublicKeyRing model', function() {
var a = w.getAddresses(); var a = w.getAddresses();
a.length.should.equal(1); a.length.should.equal(1);
[true, false].forEach(function(isChange){ [true, false].forEach(function(isChange) {
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
w.generateAddress(isChange, k.pub); w.generateAddress(isChange, k.pub);
} }
@ -185,18 +202,12 @@ describe('PublicKeyRing model', function() {
}); });
it('should set backup ready', function() { it('should set backup ready', function() {
var w = createW().w; var w = getCachedW().w;
w.isBackupReady().should.equal(false); w.isBackupReady().should.equal(false);
w.setBackupReady(); w.setBackupReady();
w.isBackupReady().should.equal(true); 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() { it('should check for other backups', function() {
var w = createW().w; var w = createW().w;
@ -213,7 +224,7 @@ describe('PublicKeyRing model', function() {
}); });
it('should merge backup', function() { it('should merge backup', function() {
var w = createW().w; var w = getCachedW().w;
w.copayersBackup = ["a", "b"]; w.copayersBackup = ["a", "b"];
var hasChanged = w.mergeBackups(["b", "c"]); var hasChanged = w.mergeBackups(["b", "c"]);
@ -313,11 +324,10 @@ describe('PublicKeyRing model', function() {
var w0 = new PublicKeyRing({ var w0 = new PublicKeyRing({
networkName: 'livenet', networkName: 'livenet',
}); });
w0.addCopayer();
w0.addCopayer(); for (var i = 0; i < 5; i++)
w0.addCopayer(); w0.addCopayer(getNewEpk());
w0.addCopayer();
w0.addCopayer();
(function() { (function() {
w0.merge(w); w0.merge(w);
}).should.throw(); }).should.throw();
@ -327,7 +337,7 @@ describe('PublicKeyRing model', function() {
var wx = new PublicKeyRing({ var wx = new PublicKeyRing({
networkName: 'livenet', networkName: 'livenet',
}); });
wx.addCopayer(); wx.addCopayer(getNewEpk());
(function() { (function() {
w.merge(wx); w.merge(wx);
}).should.throw(); }).should.throw();
@ -343,7 +353,7 @@ describe('PublicKeyRing model', function() {
var copayers = []; var copayers = [];
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
w.isComplete().should.equal(false); w.isComplete().should.equal(false);
w.addCopayer(); w.addCopayer(getNewEpk());
} }
var w2 = new PublicKeyRing({ var w2 = new PublicKeyRing({
@ -354,7 +364,7 @@ describe('PublicKeyRing model', function() {
var copayers = []; var copayers = [];
for (var i = 0; i < 3; i++) { for (var i = 0; i < 3; i++) {
w2.isComplete().should.equal(false); w2.isComplete().should.equal(false);
w2.addCopayer(); w2.addCopayer(getNewEpk());
} }
w2.merge(w).should.equal(true); w2.merge(w).should.equal(true);
w2.isComplete().should.equal(true); w2.isComplete().should.equal(true);
@ -379,7 +389,7 @@ describe('PublicKeyRing model', function() {
networkName: 'livenet', networkName: 'livenet',
id: w.id, id: w.id,
}); });
w2.addCopayer(); w2.addCopayer(getNewEpk());
w.merge(w2).should.equal(true); w.merge(w2).should.equal(true);
} }
w.isComplete().should.equal(true); w.isComplete().should.equal(true);
@ -393,7 +403,7 @@ describe('PublicKeyRing model', function() {
var w = new PublicKeyRing(config); var w = new PublicKeyRing(config);
should.exist(w); should.exist(w);
for (var i = 0; i < 3; i++) { for (var i = 0; i < 3; i++) {
w.addCopayer(); w.addCopayer(getNewEpk());
}; };
w._setNicknameForIndex(0, 'pepe0'); w._setNicknameForIndex(0, 'pepe0');
w._setNicknameForIndex(1, 'pepe1'); w._setNicknameForIndex(1, 'pepe1');
@ -409,7 +419,7 @@ describe('PublicKeyRing model', function() {
networkName: 'livenet', networkName: 'livenet',
id: w.id, id: w.id,
}); });
w2.addCopayer(); w2.addCopayer(getNewEpk());
w2._setNicknameForIndex(0, 'juan' + i); w2._setNicknameForIndex(0, 'juan' + i);
w.merge(w2).should.equal(true); w.merge(w2).should.equal(true);
} }
@ -430,7 +440,7 @@ describe('PublicKeyRing model', function() {
var w = new PublicKeyRing(config); var w = new PublicKeyRing(config);
should.exist(w); should.exist(w);
for (var i = 0; i < 3; i++) { 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(0).should.equal('tito0');
w.nicknameForIndex(1).should.equal('tito1'); w.nicknameForIndex(1).should.equal('tito1');
@ -468,7 +478,7 @@ describe('PublicKeyRing model', function() {
}); });
it('#getRedeemScriptMap check tests', function() { it('#getRedeemScriptMap check tests', function() {
var k = createW(); var k = getCachedW();
var w = k.w; var w = k.w;
var amount = 2; 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 TransactionBuilder = bitcore.TransactionBuilder;
var util = bitcore.util; var util = bitcore.util;
var networks = bitcore.networks; var networks = bitcore.networks;
var sinon = require('sinon');
try { try {
var copay = require('copay'); //browser var copay = require('copay'); //browser
} catch (e) { } catch (e) {
var copay = require('../copay'); //node var copay = require('../copay'); //node
} }
var fakeStorage = copay.FakeStorage;
var PrivateKey = copay.PrivateKey || require('../js/models/PrivateKey'); var FakeBuilder = require('./mocks/FakeBuilder');
var TxProposals = copay.TxProposals || require('../js/models/TxProposal'); var TxProposal = copay.TxProposal;
var is_browser = (typeof process == 'undefined' || typeof process.versions === 'undefined') var TxProposals = copay.TxProposals;
var PublicKeyRing = is_browser ? copay.PublicKeyRing :
require('soop').load('../js/models/core/PublicKeyRing', { var dummyProposal = new TxProposal({
Storage: fakeStorage creator: 1,
createdTs: 1,
builder: new FakeBuilder(),
inputChainPaths: ['m/1'],
}); });
var config = { var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"];
networkName: 'testnet',
};
var unspentTest = [{ describe('TxProposals', function() {
"address": "dummy", describe('constructor', function() {
"scriptPubKey": "dummy", it('should create an instance', function() {
"txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", var txps = new TxProposals();
"vout": 1, should.exist(txps);
"amount": 10, txps.network.name.should.equal('testnet');
"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('#fromObj', function() {
it('should create an instance from an Object', function() {
it('should create an instance', function() { var txps = TxProposals.fromObj({
var w = new TxProposals({ networkName:'livenet',
networkName: config.networkName walletId: '123a12',
}); txps: [],
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
}); });
txb.build = function() { should.exist(txps);
return tx; txps.network.name.should.equal('livenet');
};
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);
}); });
it('should validate for no signatures yet in copay generated tx', function() { it('should fail create an instance from an Object with errors', function() {
// taken from copay incomplete tx proposal (function() {var txps = TxProposals.fromObj({
var raw = '0100000001e205297fd05e4504d72761dc7a16e5cc9f4ab89877f28aee97c1cc66b3f07d690100000000ffffffff01706f9800000000001976a91473707e88f79c9c616b44bc766a25efcb9f49346688ac00000000'; networkName:'livenet',
var txp = createMockTxp(raw); walletId: '123a12',
txp.isValid().should.equal(true); txps: [ { a: 1 }],
}); }) }).should.throw('Illegal');
it('should validate for a SIGHASH_NONE tx in builder', function() { });
var raw = '010000000145c3bf51ced6cefaea8c6578a645316270dbf8600f46969d31136e1e06829598000000007000483045022100877c715e0f3bd6377086c96d4757b2c983682a1934d9e3f894941f4f1e18d4710220272ed81758d7a391ee4c15a29246f3fe75efbddeaf1118e4c0d3bb14f57cdba601255121022f58491a833933a9bea80d8e820e66bee91bd8c71bfa972fe70482360b48129951aeffffffff01706f9800000000001976a91408328947f0caf8728729d740cbecdfe3c2327db588ac00000000'; });
var txp = createMockTxp(raw); describe('#getNtxids', function() {
txp.isValid().should.equal(true); it('should return keys', function() {
}); var txps = new TxProposals();
it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() { txps.txps = {a:1, b:2};
var raw = '0100000001eaf08f93f895127fbf000128ac74f6e8c7f003854e5ee1f02a5fd820cb689beb00000000fdfe00004730440220778f3174393e9ee6b0bfa876b4150db6f12a4da9715044ead5e345c2781ceee002203aab31f1e1d3dcf77ca780d9af798139719891917c9a09123dba54483ef462bc02493046022100dd93b64b30580029605dbba09d7fa34194d9ff38fda0c4fa187c52bf7f79ae98022100dd7b056762087b9aa8ccfde328d7067fa1753b78c0ee25577122569ff9de1d57024c695221039f847c24f09d7299c10bba4e41b24dc78e47bbb05fd7c1d209c994899d6881062103d363476e634fc5cdc11e9330c05a141c1e0c7f8b616817bdb83e7579bbf870942103fb2072953ceab87c6da450ac661685a881ddb661002d2ec1d60bfd33e3ec807d53aeffffffff01d06bf5050000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000'; txps.getNtxids().should.deep.equal(['a','b']);
var txp = createMockTxp(raw); });
txp.isValid().should.equal(false); });
}); describe('#toObj', function() {
it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() { it('should an object', function() {
var raw = '0100000002d903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407501000000fdfe0000493046022100b232e994fdca7fd61fcf8ffe4a7f746ff8f8baf2667ac80841de0250f521c402022100862c0783ca7eafcbd2786b9444ed6e83ae941dcc2248bea4db12b7815d15de050247304402200189fe0cde9d1dd192553f4dddb6764df3eb643f9f71be8aa015f41f2d4fd11f02205513b8ca985c3b5b936f814c7eba92e2e2985c83927ca06c41081d264c0be7a7024c695221026fa1a3ed0c820c1053c8ba101f3c96f85c55624a902a82cf6b2896ed5f9b3d1521035a3383c13dd346a5784adfe3ec3026ab31d519fdfae2740497b10bdfb994e6442103c7477a6668d5bc250fe727e358d951b9e05f1d7c02059bf59ecbb335f1eeec7953aeffffffffd903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407500000000fdfd0000483045022100bdb9d14569af66d84af63416d77296ace24a96f1720d30e74bc6e316a4b3727502206ed54d532467393488889d72edbb667d075de491a89e8e496fee8791b943fa37024730440220379c30c884a21a949d8ec32d6934ffa9faf86add4d839de0f5fbd2b90f8ef1e802204048df2ec0035ce5e4bf01e9d70fd93a45a41ce2630100d692cd908cdaa61fc0024c69522102203938ef947327edce2cf2997c55b433be3d3ffcf3284c10d6fcdf4b01c6221f21033b60c3363a226ce9b850af655c6e1470d9a0936d7f56ea4a07ab84005f91cd1b210385755bc813fe7f92577b93bf689bf0d9b2118e6bbb7fee5d3d16976f4f7271af53aeffffffff01c02d9a3b0000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000'; var txps = TxProposals.fromObj({
var txp = createMockTxp(raw); networkName:'livenet',
txp.isValid().should.equal(false); 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 copayConfig = require('../config');
var Wallet = require('../js/models/core/Wallet'); var Wallet = require('../js/models/core/Wallet');
var Structure = copay.Structure; var PrivateKey = copay.PrivateKey;
var Storage = require('./mocks/FakeStorage'); var Storage = require('./mocks/FakeStorage');
var Network = require('./mocks/FakeNetwork'); var Network = require('./mocks/FakeNetwork');
var Blockchain = require('./mocks/FakeBlockchain'); var Blockchain = require('./mocks/FakeBlockchain');
@ -19,22 +19,30 @@ var TransactionBuilder = bitcore.TransactionBuilder;
var Transaction = bitcore.Transaction; var Transaction = bitcore.Transaction;
var Address = bitcore.Address; 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) { var addCopayers = function(w) {
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
w.publicKeyRing.addCopayer(); w.publicKeyRing.addCopayer(getNewEpk());
} }
}; };
describe('Wallet model', function() { describe('Wallet model', function() {
var config = {
requiredCopayers: 3,
totalCopayers: 5,
spendUnconfirmed: true,
reconnectDelay: 100,
networkName: 'testnet',
};
it('should fail to create an instance', function() { it('should fail to create an instance', function() {
(function() { (function() {
new Wallet(config) 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)); var c = JSON.parse(JSON.stringify(conf || config));
if (!N) N = c.totalCopayers; if (!N) N = c.totalCopayers;
if (netKey) c.netKey = netKey;
var mainPrivateKey = new copay.PrivateKey({ var mainPrivateKey = new copay.PrivateKey({
networkName: config.networkName networkName: config.networkName
}); });
@ -148,8 +155,7 @@ describe('Wallet model', function() {
var createW2 = function(privateKeys, N, conf) { var createW2 = function(privateKeys, N, conf) {
if (!N) N = 3; if (!N) N = 3;
var netKey = 'T0FbU2JLby0='; var w = createW(N, conf);
var w = createW(netKey, N, conf);
should.exist(w); should.exist(w);
var pkr = w.publicKeyRing; var pkr = w.publicKeyRing;
@ -157,9 +163,9 @@ describe('Wallet model', function() {
for (var i = 0; i < N - 1; i++) { for (var i = 0; i < N - 1; i++) {
if (privateKeys) { if (privateKeys) {
var k = privateKeys[i]; var k = privateKeys[i];
pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : null); pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk());
} else { } else {
pkr.addCopayer(); pkr.addCopayer(getNewEpk());
} }
} }
@ -212,12 +218,12 @@ describe('Wallet model', function() {
var t = w.txProposals; var t = w.txProposals;
var txp = t.txps[ntxid]; var txp = t.txps[ntxid];
Object.keys(txp._inputSignatures).length.should.equal(1);
var tx = txp.builder.build(); var tx = txp.builder.build();
should.exist(tx); should.exist(tx);
chai.expect(txp.comment).to.be.null; chai.expect(txp.comment).to.be.null;
tx.isComplete().should.equal(false); tx.isComplete().should.equal(false);
Object.keys(txp.seenBy).length.should.equal(1); Object.keys(txp.seenBy).length.should.equal(1);
Object.keys(txp.signedBy).length.should.equal(1);
}); });
it('#create with comment', function() { it('#create with comment', function() {
@ -363,6 +369,14 @@ describe('Wallet model', function() {
}, w.reconnectDelay * callCount * (callCount + 1) / 2); }, 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() { it('#isReady', function() {
var w = createW(); var w = createW();
w.publicKeyRing.isComplete().should.equal(false); w.publicKeyRing.isComplete().should.equal(false);
@ -426,19 +440,7 @@ describe('Wallet model', function() {
var w = createW(); var w = createW();
var txp = { var txp = {
'txProposal': { 'txProposal': {
creator: '02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96', inputChainPaths: ['m/1'],
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,
builderObj: { builderObj: {
version: 1, version: 1,
outs: [{ outs: [{
@ -466,9 +468,13 @@ describe('Wallet model', function() {
} }
}; };
var stub = sinon.stub(w.publicKeyRing,'copayersForPubkeys').returns(
{'027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d509':'pepe'}
);
w._handleTxProposal('senderID', txp, true); w._handleTxProposal('senderID', txp, true);
Object.keys(w.txProposals.txps).length.should.equal(1); Object.keys(w.txProposals.txps).length.should.equal(1);
w.getTxProposals().length.should.equal(1); w.getTxProposals().length.should.equal(1);
//stub.restore();
}); });
var newId = '00bacacafe'; var newId = '00bacacafe';
@ -494,7 +500,8 @@ describe('Wallet model', function() {
var w = createW(); var w = createW();
var r = w.getRegisteredCopayerIds(); var r = w.getRegisteredCopayerIds();
r.length.should.equal(1); r.length.should.equal(1);
w.publicKeyRing.addCopayer(); w.publicKeyRing.addCopayer(getNewEpk());
r = w.getRegisteredCopayerIds(); r = w.getRegisteredCopayerIds();
r.length.should.equal(2); r.length.should.equal(2);
r[0].should.not.equal(r[1]); r[0].should.not.equal(r[1]);
@ -504,7 +511,7 @@ describe('Wallet model', function() {
var w = createW(); var w = createW();
var r = w.getRegisteredPeerIds(); var r = w.getRegisteredPeerIds();
r.length.should.equal(1); r.length.should.equal(1);
w.publicKeyRing.addCopayer(); w.publicKeyRing.addCopayer(getNewEpk());
r = w.getRegisteredPeerIds(); r = w.getRegisteredPeerIds();
r.length.should.equal(2); r.length.should.equal(2);
r[0].should.not.equal(r[1]); 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) { it('should create & sign transaction from received funds', function(done) {
this.timeout(10000); var k2 = new PrivateKey({
var w = cachedCreateW2(); networkName: config.networkName
var pk = w.privateKey; });
w.privateKey = null;
var w = createW2([k2]);
var utxo = createUTXO(w); var utxo = createUTXO(w);
w.blockchain.fixUnspent(utxo); w.blockchain.fixUnspent(utxo);
w.createTx(toAddress, amountSatStr, null, function(ntxid) { w.createTx(toAddress, amountSatStr, null, function(ntxid) {
@ -646,24 +654,36 @@ describe('Wallet model', function() {
w.getTxProposals()[0].rejectedByUs.should.equal(false); w.getTxProposals()[0].rejectedByUs.should.equal(false);
done(); done();
}); });
w.privateKey = pk; w.privateKey = k2;
w.sign(ntxid, function(success) { w.sign(ntxid, function(success) {
success.should.equal(true); success.should.equal(true);
}); });
}); });
}); });
it('should create & reject transaction', function(done) { it('should fail to reject a signed transaction', function() {
var w = cachedCreateW2(); var w = cachedCreateW2();
w.privateKey = null;
var utxo = createUTXO(w); var utxo = createUTXO(w);
w.blockchain.fixUnspent(utxo); w.blockchain.fixUnspent(utxo);
w.createTx(toAddress, amountSatStr, null, function(ntxid) { w.createTx(toAddress, amountSatStr, null, function(ntxid) {
w.on('txProposalsUpdated', function() { (function() {
w.getTxProposals()[0].signedByUs.should.equal(false); w.reject(ntxid);
w.getTxProposals()[0].rejectedByUs.should.equal(true); }).should.throw('reject a signed');
done(); });
}); });
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); 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) { it('should create & sign & send a transaction', function(done) {
@ -1013,32 +1033,91 @@ describe('Wallet model', function() {
copayConfig.forceNetwork = backup; copayConfig.forceNetwork = backup;
}); });
}); });
describe('_getKeymap', function() {
var w = cachedCreateW();
describe('validate txProposals', function() { it('should set keymap', function() {
var a1 = 'n1pKARYYUnZwxBuGj3y7WqVDu6VLN7n971'; var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
var a2 = 'mtxYYJXZJmQc2iJRHQ4RZkfxU5K7TE2qMJ'; return {
var utxos = [{ '123': 'juan'
address: a1, };
txid: '2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1', });
vout: 1, var txp = {
scriptPubKey: Address.getScriptPubKeyFor(a1).serialize().toString('hex'), _inputSignatures: [
amount: 0.5, ['123']
confirmations: 200 ],
}, { inputChainPaths: ['/m/1'],
address: a2, };
txid: '88c4520ffd97ea565578afe0b40919120be704b36561c71ba4e450e83cb3c9fd', var map = w._getKeyMap(txp);
vout: 1, Object.keys(map).length.should.equal(1);
scriptPubKey: Address.getScriptPubKeyFor(a2).serialize().toString('hex'), map['123'].should.equal('juan');
amount: 0.5001, stub.restore();
confirmations: 200 });
}];
var destAddress = 'myuAQcCc1REUgXGsCTiYhZvPPc3XxZ36G1'; it('should throw if unmatched sigs', function() {
var outs = [{ var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() {
address: destAddress, return {
amount: 1.0 '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 w = cachedCreateW();
var spy = sinon.spy(); var spy = sinon.spy();
w.on('txProposalEvent', spy); w.on('txProposalEvent', spy);
@ -1046,47 +1125,149 @@ describe('Wallet model', function() {
e.type.should.equal(result); e.type.should.equal(result);
done(); done();
}); });
var opts = {}; // txp.prototype.getId = function() {return 'aa'};
opts.signhash = signhash;
var txb = new TransactionBuilder(opts)
.setUnspent(utxos)
.setOutputs(outs)
.sign(['cVBtNonMyTydnS3NnZyipbduXo9KZfF1aUZ3uQHcvJB6UARZbiWG',
'cRVF68hhZp1PUQCdjr2k6aVYb2cn6uabbySDPBizAJ3PXF7vDXTL'
]);
var txp = { var txp = {
'txProposal': { dummy: 1
'builderObj': txb.toObj()
}
}; };
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); 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 result = 'new';
var signhash; testValidate(1, result, done);
testValidate(signhash, result, done);
}); });
it('should validate for SIGHASH_ALL', function(done) { it('should handle signed', function(done) {
var result = 'new'; var result = 'signed';
var signhash = Transaction.SIGHASH_ALL; testValidate(2, result, done);
testValidate(signhash, 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) { it('should fail to reject a signed tx', function() {
var result = 'corrupt'; var w = cachedCreateW();
var signhash = Transaction.SIGHASH_SINGLE; w.txProposals.txps['qwerty'] = {
testValidate(signhash, result, done); signedBy: {
john: 1
}
};
(function() {
w._handleReject('john', {
ntxid: 'qwerty'
}, 1);
}).should.throw('already signed');
}); });
it('should not validate for different SIGHASH_ANYONECANPAY', function(done) { it('should reject a tx', function() {
var result = 'corrupt'; var w = cachedCreateW();
var signhash = Transaction.SIGHASH_ANYONECANPAY;
testValidate(signhash, result, done); 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() { it('should return false if wallet does not exist', function() {
var opts = { var opts = {
'requiredCopayers': 2, 'requiredCopayers': 2,
@ -343,6 +357,23 @@ describe('WalletFactory model', function() {
wf.read.calledWith(walletId).should.be.true; 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() { it('should return error if network are differents', function() {
var opts = { var opts = {
'requiredCopayers': 2, 'requiredCopayers': 2,

View File

@ -82,9 +82,9 @@ describe('Insight model', function() {
sinon sinon
.stub(http, 'request') .stub(http, 'request')
.returns(req) .returns(req)
.yields(request); .yields(request);
i.getUnspent(['2MuD5LnZSViZZYwZbpVsagwrH8WWvCztdmV', '2NBSLoMvsHsf2Uv3LA17zV4beH6Gze6RovA'], function(e, ret) { i.getUnspent(['2MuD5LnZSViZZYwZbpVsagwrH8WWvCztdmV', '2NBSLoMvsHsf2Uv3LA17zV4beH6Gze6RovA'], function(e, ret) {
should.not.exist(e); should.not.exist(e);
@ -113,9 +113,9 @@ describe('Insight model', function() {
req.end = function() {}; req.end = function() {};
sinon sinon
.stub(http, 'request') .stub(http, 'request')
.returns(req) .returns(req)
.yields(request); .yields(request);
i.sendRawTransaction(rawtx, function(a) { i.sendRawTransaction(rawtx, function(a) {
should.exist(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 PrivateKey = require('../js/models/core/PrivateKey');
var PublicKeyRing = require('../js/models/core/PublicKeyRing'); var PublicKeyRing = require('../js/models/core/PublicKeyRing');
var getNewEpk = function() {
return new PrivateKey({
networkName: 'livenet',
})
.deriveBIP45Branch()
.extendedPublicKeyString();
}
describe('Performance tests', function() { describe('Performance tests', function() {
describe('PrivateKey', function() { describe('PrivateKey', function() {
it('should optimize BIP32 private key gen time with cache', function() { it('should optimize BIP32 private key gen time with cache', function() {
@ -43,7 +52,7 @@ describe('Performance tests', function() {
requiredCopayers: M requiredCopayers: M
}); });
for (var i = 0; i < N; i++) { 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 generateN = 5;
var generated = []; var generated = [];

View File

@ -148,6 +148,18 @@ describe('Storage/LocalEncrypted model', function() {
s.getName(1).should.equal('hola'); 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() { describe('#getWallets', function() {
it('should retreive wallets from storage', function() { it('should retreive wallets from storage', function() {
var s = new LocalEncrypted({ 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 scope, $httpBackendOut;
var GH = 'https://api.github.com/repos/bitpay/copay/tags'; var GH = 'https://api.github.com/repos/bitpay/copay/tags';
beforeEach(inject(function($controller, $injector) { beforeEach(inject(function($controller, $injector) {
@ -235,7 +235,7 @@ describe("Unit: Controllers", function() {
beforeEach(inject(function($controller, $rootScope) { beforeEach(inject(function($controller, $rootScope) {
rootScope = $rootScope; rootScope = $rootScope;
scope = $rootScope.$new(); scope = $rootScope.$new();
headerCtrl = $controller('SidebarController', { headerCtrl = $controller('VersionController', {
$scope: scope, $scope: scope,
}); });
})); }));
@ -273,8 +273,24 @@ describe("Unit: Controllers", function() {
scope.$apply(); scope.$apply();
}); });
it('should return an array of n undefined elements', function() { it('should return networkName', function() {
$httpBackend.flush(); // need flush $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 n = 5;
var array = scope.getNumber(n); var array = scope.getNumber(n);
expect(array.length).equal(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="row" ng-if='$root.wallet && !$root.wallet.isReady() && !loading'>
<div class="large-4 columns logo-setup"> <div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay"> <img src="img/logo-negative-beta.svg" alt="Copay">
<div ng-include="'views/includes/version.html'"></div>
</div> </div>
<div class="large-8 columns line-dashed-setup-v"> <div class="large-8 columns line-dashed-setup-v">
<div class="box-setup oh"> <div class="box-setup oh">
@ -73,8 +74,12 @@
</div> </div>
<div class="text-right"> <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()" <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" <button class="button primary m0"
ng-click="backup()" ng-click="backup()"
ng-show="!$root.wallet.publicKeyRing.isBackupReady()" ng-show="!$root.wallet.publicKeyRing.isBackupReady()"

View File

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

View File

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

View File

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

View File

@ -4,11 +4,7 @@
<a href="#!/addresses" class="db"> <a href="#!/addresses" class="db">
<img src="img/logo-negative-beta.svg" alt="" width="100"> <img src="img/logo-negative-beta.svg" alt="" width="100">
</a> </a>
<div> <div ng-include="'views/includes/version.html'"></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> </div>
<div class="line-sidebar"></div> <div class="line-sidebar"></div>
<div> <div>
@ -64,7 +60,7 @@
</li> </li>
</ul> </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> </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" class="ellipsis"
tooltip="ID: {{copayer.peerId}}" tooltip="ID: {{copayer.peerId}}"
tooltip-placement="bottom"> 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> <small class="text-gray" ng-show="copayer.index > 0"><i class="fi-check m5r"></i>{{copayer.nick}}</small>
</div> </div>

View File

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

View File

@ -10,7 +10,7 @@
{{address.balance || 0|noFractionNumber}} {{$root.unitName}} {{address.balance || 0|noFractionNumber}} {{$root.unitName}}
</p> </p>
<button class="m15t button secondary" open-external address="{{address.address}}"> <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> </button>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<div class="transactions" data-ng-controller="TransactionsController"> <div class="transactions" data-ng-controller="TransactionsController">
<div ng-show='$root.wallet.isReady()'> <div ng-show='$root.wallet.isReady()'>
<h1 ng-show="wallet.totalCopayers > 1"> Transaction proposals <small>({{txs.length}})</small></h1> <h1 ng-show="wallet.isShared()"> Transaction proposals <small>({{txs.length}})</small></h1>
<div class="large-12" ng-show="wallet.totalCopayers > 1"> <div class="large-12" ng-show="wallet.isShared()">
<ul class="inline-list"> <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(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> <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>
<div class="last-transactions-content"> <div class="last-transactions-content">
<div class="box-copayer" ng-repeat="(cId, actions) in tx.peerActions"> <div class="box-copayer" ng-repeat="c in tx.actionList">
<a href="#!/transactions" class="has-tip" tooltip-popup-delay="1000" tooltip="{{cId === $root.wallet.getMyCopayerId() ? 'You' : $root.wallet.publicKeyRing.nicknameForCopayer(cId)}}"> <a href="#!/transactions" class="has-tip">
<img class="copayer-ico br100" src="./img/satoshi.gif" alt="{{cId}}"> <img class="copayer-ico br100" src="./img/satoshi.gif" alt="{{c.cId}}">
</a> </a>
<div class="box-status"> <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> <i class="fi-crown icon-status icon-active"></i>
</a> </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> <i class="fi-eye icon-status icon-active"></i>
</a> </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> <i class="fi-x icon-status icon-active-x"></i>
</a> </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> <i class="fi-check icon-status icon-active-check"></i>
</a> </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> <i class="fi-loop icon-rotate"></i>
</a> </a>
</div> </div>
<div class="text-center"> <div class="text-center">
<p class="size-12 text-gray ellipsis"> <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> </p>
</div> </div>
</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> <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> </div>
<h1 ng-class="{'line-dashed': wallet.totalCopayers > 1}"> <h1 ng-class="{'line-dashed': wallet.isShared()}">
Last transactions Last transactions
<small ng-hide="wallet.totalCopayers > 1 || !loading"> <small ng-hide="wallet.isShared() || !loading">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</small> </small>
</h1> </h1>

View File

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