Merge pull request #6284 from matiu/feat/fee-warns

Feat/fee warns
This commit is contained in:
Gustavo Maximiliano Cortez 2017-06-23 09:14:00 -03:00 committed by GitHub
commit 784921f478
16 changed files with 224 additions and 41 deletions

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('addressesController', function($scope, $log, $stateParams, $state, $timeout, $ionicHistory, $ionicScrollDelegate, configService, popupService, gettextCatalog, ongoingProcess, lodash, profileService, walletService, bwcError, platformInfo, appConfigService) {
angular.module('copayApp.controllers').controller('addressesController', function($scope, $log, $stateParams, $state, $timeout, $ionicHistory, $ionicScrollDelegate, configService, popupService, gettextCatalog, ongoingProcess, lodash, profileService, walletService, bwcError, platformInfo, appConfigService, txFormatService, feeService) {
var UNUSED_ADDRESS_LIMIT = 5;
var BALANCE_ADDRESS_LIMIT = 5;
var config = configService.getSync().wallet.settings;
@ -55,7 +55,7 @@ angular.module('copayApp.controllers').controller('addressesController', functio
$scope.latestWithBalance = lodash.slice(withBalance, 0, BALANCE_ADDRESS_LIMIT);
lodash.each(withBalance, function(a) {
a.balanceStr = (a.amount * satToUnit).toFixed(unitDecimals) + ' ' + unitName;
a.balanceStr = txFormatService.formatAmount(a.amount);
});
$scope.viewAll = {
@ -72,6 +72,31 @@ angular.module('copayApp.controllers').controller('addressesController', functio
});
});
});
feeService.getFeeLevels(function(err, levels){
walletService.getLowUtxos($scope.wallet, levels, function(err, resp) {
if (err) return;
if (resp.allUtxos && resp.allUtxos.length) {
var allSum = lodash.sum(resp.allUtxos || 0, 'satoshis');
var per = (resp.minFee / allSum) * 100;
$scope.lowWarning = resp.warning;
$scope.lowUtxosNb = resp.lowUtxos.length;
$scope.allUtxosNb = resp.allUtxos.length;
$scope.lowUtxosSum = txFormatService.formatAmountStr(lodash.sum(resp.lowUtxos || 0, 'satoshis'));
$scope.allUtxosSum = txFormatService.formatAmountStr(allSum);
$scope.minFee = txFormatService.formatAmountStr(resp.minFee || 0);
$scope.minFeePer = per.toFixed(2) + '%';
}
});
});
};
function processPaths(list) {

View File

@ -4,6 +4,7 @@ angular.module('copayApp.controllers').controller('confirmController', function(
var countDown = null;
var CONFIRM_LIMIT_USD = 20;
var FEE_TOO_HIGH_LIMIT_PER = 15;
var tx = {};
@ -286,7 +287,10 @@ angular.module('copayApp.controllers').controller('confirmController', function(
txFormatService.formatAlternativeStr(txp.fee, function(v) {
txp.alternativeFeeStr = v;
});
txp.feeRatePerStr = (txp.fee / (txp.amount + txp.fee) * 100).toFixed(2) + '%';
var per = (txp.fee / (txp.amount + txp.fee) * 100);
txp.feeRatePerStr = per.toFixed(2) + '%';
txp.feeToHigh = per > FEE_TOO_HIGH_LIMIT_PER;
tx.txp[wallet.id] = txp;

View File

@ -14,8 +14,6 @@ angular.module('copayApp.controllers').controller('txpDetailsController', functi
$scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal();
$scope.color = $scope.wallet.color;
$scope.data = {};
$scope.displayAmount = getDisplayAmount($scope.tx.amountStr);
$scope.displayUnit = getDisplayUnit($scope.tx.amountStr);
displayFeeValues();
initActionList();
checkPaypro();
@ -43,14 +41,6 @@ angular.module('copayApp.controllers').controller('txpDetailsController', functi
$scope.buttonText += gettextCatalog.getString('to accept');
};
function getDisplayAmount(amountStr) {
return amountStr.split(' ')[0];
};
function getDisplayUnit(amountStr) {
return amountStr.split(' ')[1];
};
function initActionList() {
$scope.actionList = [];

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('txDetailsController', function($rootScope, $log, $ionicHistory, $scope, $timeout, walletService, lodash, gettextCatalog, profileService, externalLinkService, popupService, ongoingProcess, txFormatService, txConfirmNotification) {
angular.module('copayApp.controllers').controller('txDetailsController', function($rootScope, $log, $ionicHistory, $scope, $timeout, walletService, lodash, gettextCatalog, profileService, externalLinkService, popupService, ongoingProcess, txFormatService, txConfirmNotification, feeService) {
var txId;
var listeners = [];
@ -40,14 +40,6 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
});
});
function getDisplayAmount(amountStr) {
return amountStr.split(' ')[0];
}
function getDisplayUnit(amountStr) {
return amountStr.split(' ')[1];
}
function updateMemo() {
walletService.getTxNote($scope.wallet, $scope.btx.txid, function(err, note) {
if (err) {
@ -122,12 +114,14 @@ angular.module('copayApp.controllers').controller('txDetailsController', functio
if ($scope.btx.action == 'moved') $scope.title = gettextCatalog.getString('Moved Funds');
}
$scope.displayAmount = getDisplayAmount($scope.btx.amountStr);
$scope.displayUnit = getDisplayUnit($scope.btx.amountStr);
updateMemo();
initActionList();
getFiatRate();
feeService.getLowAmount($scope.wallet, function(err, amount) {
$scope.btx.lowAmount = tx.amount< amount;
});
$timeout(function() {
$scope.$apply();
});

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService) {
angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService, feeService) {
var HISTORY_SHOW_LIMIT = 10;
var currentTxHistoryPage = 0;
@ -47,6 +47,19 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.txps = lodash.sortBy(txps, 'createdOn').reverse();
};
var analyzeUtxosDone;
var analyzeUtxos = function() {
if (analyzeUtxosDone) return;
feeService.getFeeLevels(function(err, levels){
walletService.getLowUtxos($scope.wallet, levels, function(err, resp){
analyzeUtxosDone = true;
$scope.lowUtxosWarning = resp.warning;
});
});
};
var updateStatus = function(force) {
$scope.updatingStatus = true;
$scope.updateStatusError = null;
@ -72,6 +85,8 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.$apply();
});
analyzeUtxos();
});
};
@ -154,9 +169,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
});
};
$timeout(function() {
feeService.getFeeLevels(function(err, levels){
walletService.getTxHistory($scope.wallet, {
progressFn: progressFn,
feeLevels: levels,
}, function(err, txHistory) {
$scope.updatingTxHistory = false;
if (err) {

View File

@ -43,7 +43,7 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou
var feeRate = feeLevelRate.feePerKB;
if (!fromCache) $log.debug('Dynamic fee: ' + feeLevel + '/' + network +' ' + (feeLevelRate.feePerKB / 1000).toFixed() + ' SAT/B');
if (!fromCache) $log.debug('Dynamic fee: ' + feeLevel + '/' + network + ' ' + (feeLevelRate.feePerKB / 1000).toFixed() + ' SAT/B');
return cb(null, feeRate);
});
@ -55,8 +55,8 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou
root.getFeeLevels = function(cb) {
if (cache.updateTs > Date.now() - CACHE_TIME_TS * 1000 ) {
$timeout( function() {
if (cache.updateTs > Date.now() - CACHE_TIME_TS * 1000) {
$timeout(function() {
return cb(null, cache.data, true);
}, 1);
}
@ -70,7 +70,7 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou
return cb(gettextCatalog.getString('Could not get dynamic fee'));
}
cache.updateTs =Date.now();
cache.updateTs = Date.now();
cache.data = {
'livenet': levelsLivenet,
'testnet': levelsTestnet
@ -81,5 +81,6 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou
});
};
return root;
});

View File

@ -93,6 +93,11 @@ angular.module('copayApp.services').factory('txFormatService', function($filter,
tx.alternativeAmountStr = root.formatAlternativeStr(tx.amount);
tx.feeStr = root.formatAmountStr(tx.fee || tx.fees);
if (tx.amountStr) {
tx.amountValueStr = tx.amountStr.split(' ')[0];
tx.amountUnitStr = tx.amountStr.split(' ')[1];
}
return tx;
};

View File

@ -1,7 +1,12 @@
'use strict';
angular.module('copayApp.services').factory('walletService', function($log, $timeout, lodash, trezor, ledger, intelTEE, storageService, configService, rateService, uxLanguage, $filter, gettextCatalog, bwcError, $ionicPopup, fingerprintService, ongoingProcess, gettext, $rootScope, txFormatService, $ionicModal, $state, bwcService, bitcore, popupService) {
// `wallet` is a decorated version of client.
// Ratio low amount warning (fee/amount) in incoming TX
var LOW_AMOUNT_RATIO = 0.15;
// Ratio of "many utxos" warning in total balance (fee/amount)
var TOTAL_LOW_WARNING_RATIO = .3;
var root = {};
@ -401,6 +406,11 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
var progressFn = opts.progressFn || function() {};
var foundLimitTx = false;
if (opts.feeLevels) {
opts.lowAmount = root.getLowAmount(wallet, opts.feeLevels);
}
var fixTxsUnit = function(txs) {
if (!txs || !txs[0] || !txs[0].amountStr) return;
@ -413,7 +423,6 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
$log.debug('Fixing Tx Cache Unit to:' + name)
lodash.each(txs, function(tx) {
tx.amountStr = txFormatService.formatAmount(tx.amount) + name;
tx.feeStr = txFormatService.formatAmount(tx.fees) + name;
});
@ -511,6 +520,16 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
}
function updateLowAmount(txs) {
if (!opts.lowAmount) return;
lodash.each(txs, function(tx) {
tx.lowAmount = tx.amount < opts.lowAmount;
});
};
updateLowAmount(txs);
updateNotes(function() {
// <HACK>
@ -567,9 +586,9 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
root.getTx = function(wallet, txid, cb) {
function finish(list){
function finish(list) {
var tx = lodash.find(list, {
txid: txid
txid: txid
});
if (!tx) return cb('Could not get transaction');
@ -602,7 +621,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
};
root.getTxHistory = function(wallet, opts, cb) {
opts = opts || {};
@ -873,6 +892,79 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
};
// These 2 functions were taken from
// https://github.com/bitpay/bitcore-wallet-service/blob/master/lib/model/txproposal.js#L243
function getEstimatedSizeForSingleInput(wallet) {
switch (wallet.credentials.addressType) {
case 'P2PKH':
return 147;
default:
case 'P2SH':
return wallet.m * 72 + wallet.n * 36 + 44;
}
};
root.getEstimatedTxSize = function(wallet, nbOutputs) {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.02;
var overhead = 4 + 4 + 9 + 9;
var inputSize = getEstimatedSizeForSingleInput(wallet);
var outputSize = 34;
var nbInputs = 1; //Assume 1 input
var nbOutputs = nbOutputs || 2; // Assume 2 outputs
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
return parseInt((size * (1 + safetyMargin)).toFixed(0));
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getMinFee = function(wallet, feeLevels, nbOutputs) {
var lowLevelRate = (lodash.find(feeLevels[wallet.network], {
level: 'normal',
}).feePerKB / 1000).toFixed(0);
var size = root.getEstimatedTxSize(wallet, nbOutputs);
return size * lowLevelRate;
};
// Approx utxo amount, from which the uxto is economically redeemable
root.getLowAmount = function(wallet, feeLevels, nbOutputs) {
var minFee = root.getMinFee(wallet,feeLevels, nbOutputs);
return parseInt( minFee / LOW_AMOUNT_RATIO);
};
root.getLowUtxos = function(wallet, levels, cb) {
wallet.getUtxos({}, function(err, resp) {
if (err || !resp || !resp.length) return cb();
var lowAmountN = root.getLowAmount(wallet, levels, resp.length + 1);
var total = lodash.sum(resp, 'satoshis');
var lowAmount1 = root.getLowAmount(wallet, levels);
var lowUtxos = lodash.filter(resp, function(x) {
return x.satoshis < lowAmount1;
});
var totalLow = lodash.sum(lowUtxos, 'satoshis');
return cb(err, {
allUtxos: resp || [],
lowUtxos: lowUtxos || [],
warning: lowAmountN / total > TOTAL_LOW_WARNING_RATIO,
minFee: root.getMinFee(wallet, levels, resp.length),
});
});
};
root.getAddress = function(wallet, forceNew, cb) {
storageService.getLastAddress(wallet.id, function(err, addr) {
if (err) return cb(err);

View File

@ -5,6 +5,10 @@
float: none;
.fee-rate {
display: inline-block;
.warn {
color: red;
}
}
}
.icon-amazon {

View File

@ -98,7 +98,7 @@
&.low-fees {
display: flex;
font-size: 14px;
color: #aaa;
color: #777;
align-items: center;
i {
padding-right: 20px;

View File

@ -35,7 +35,7 @@
<i class="icon ion-ios-arrow-thin-right"></i>
</div>
<div class="item item-divider item-icon-right" ng-click="newAddress()">
<div class="item item-divider item-icon-right" ng-click="newAddress()">
<span translate>Unused Addresses</span>
<i class="icon ion-ios-plus-empty"></i>
</div>
@ -70,6 +70,34 @@
<div class="addr-balance">{{w.balanceStr}}</div>
</div>
</div>
<div ng-if="allUtxosNb">
<div class="item item-divider" translate>
Wallet Inputs
</div>
<div class="item" >
<span translate> Total wallet inputs </span>
<div class="addr-path">
{{allUtxosNb}} [{{allUtxosSum}}]
</div>
</div>
<div class="item" >
<span translate> Low amount inputs </span>
<div class="addr-path">
{{lowUtxosNb}} [{{ lowUtxosSum }}]
</div>
</div>
<div class="item" >
<span translate> Approximate fee to move all wallet's balance (with normal priority) </span>
<div class="addr-path">
{{minFeePer}} [{{minFee}}]
</div>
</div>
</div>
</div>
</div>
</ion-content>

View File

@ -81,8 +81,15 @@
<span class="label">{{'Fee:' | translate}} {{tx.feeLevelName | translate}}</span>
<span class="m10l">{{tx.txp[wallet.id].feeStr || '...'}}</span>
<span class="item-note m10l">
<span>{{tx.txp[wallet.id].alternativeFeeStr || '...'}}&nbsp;<span class="fee-rate" ng-if="tx.txp[wallet.id].feeRatePerStr" translate>- {{tx.txp[wallet.id].feeRatePerStr}} of the transaction</span></span>
<span>{{tx.txp[wallet.id].alternativeFeeStr || '...'}}&nbsp;
<span class="fee-rate" ng-if="tx.txp[wallet.id].feeRatePerStr"> &middot;
<i class="ion-alert-circled warn" ng-show="tx.txp[wallet.id].feeToHigh"></i> &nbsp;
<span class="fee-rate" ng-class="{'warn':tx.txp[wallet.id].feeToHigh}" translate> {{tx.txp[wallet.id].feeRatePerStr}} of the sending amount </span>
</span>
</span>
</span>
<i class="icon bp-arrow-right"></i>
</div>
<a class="item item-icon-right" ng-if="wallet" ng-click="showDescriptionPopup(tx)">

View File

@ -26,6 +26,11 @@
<i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span class="comment" translate>Low fees</span>
</div>
<div class="low-fees" ng-if="btx.lowAmount">
<i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span class="comment" translate>Amount too low to spend</span>
</div>
</div>
<div ng-show="btx.action == 'sent'" class="ellipsis">

View File

@ -19,7 +19,7 @@
<span translate>Sending</span>
</div>
<div class="amount-label">
<div class="amount">{{displayAmount}} <span class="unit">{{displayUnit}}</span></div>
<div class="amount">{{tx.amountValueStr}} <span class="unit">{{tx.amountUnitStr}}</span></div>
<div class="alternative" ng-show="tx.alternativeAmountStr">{{tx.alternativeAmountStr}}</div>
</div>

View File

@ -24,7 +24,7 @@
<span ng-if="btx.action == 'received'" translate>Receiving</span>
</div>
<div class="amount-label">
<div class="amount">{{displayAmount}} <span class="unit">{{displayUnit}}</span></div>
<div class="amount">{{btx.amountValueStr}} <span class="unit">{{btx.amountUnitStr}}</span></div>
<div class="alternative" ng-click="showRate = !showRate">
<span ng-if="!showRate">{{btx.alternativeAmountStr}}</span>
<span ng-if="showRate">
@ -88,6 +88,12 @@
<i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span translate>This transaction could take a long time to confirm or could be dropped due to the low fees set by the sender</span>
</div>
<div class="item low-fees" ng-if="btx.lowAmount">
<i class="icon"><img src="img/icon-warning.png" width="20px"></i>
<span translate>
This transaction amount is too small compared to current Bitcoin network fees. Spending these funds will need a Bitcoin network fee cost comparable to the funds itself. </span>
</div>
<div class="item single-line">
<span class="label" translate>Confirmations</span>
<span class="item-note">

View File

@ -158,6 +158,12 @@
Wallet not backed up
</a>
<a class="wallet-not-backed-up-warning" ng-if="lowUtxosWarning" ui-sref="tabs.settings.addresses({walletId:wallet.id,from: 'tabs.wallet'})" translate>
Spending this balance will need significant Bitcoin network fees
</a>
<div class="p60b" ng-if="wallet && wallet.isComplete() && !walletNotRegistered" style="padding-top: 1rem;">
<div class="oh pr m20t" ng-show="wallet.incorrectDerivation">
<div class="text-center text-warning">