diff --git a/src/js/controllers/confirm.js b/src/js/controllers/confirm.js index c9ce5943a..9623206f2 100644 --- a/src/js/controllers/confirm.js +++ b/src/js/controllers/confirm.js @@ -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; diff --git a/src/js/controllers/modals/txpDetails.js b/src/js/controllers/modals/txpDetails.js index bfab29089..3767b6e93 100644 --- a/src/js/controllers/modals/txpDetails.js +++ b/src/js/controllers/modals/txpDetails.js @@ -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 = []; diff --git a/src/js/controllers/tx-details.js b/src/js/controllers/tx-details.js index 5af36a0e0..6d76fe84c 100644 --- a/src/js/controllers/tx-details.js +++ b/src/js/controllers/tx-details.js @@ -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(); }); diff --git a/src/js/controllers/walletDetails.js b/src/js/controllers/walletDetails.js index 24cf0afbc..259ca2ff3 100644 --- a/src/js/controllers/walletDetails.js +++ b/src/js/controllers/walletDetails.js @@ -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; @@ -154,9 +154,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun }); }; - $timeout(function() { + feeService.getLowAmount($scope.wallet, function(err, lowAmount){ walletService.getTxHistory($scope.wallet, { progressFn: progressFn, + lowAmount: lowAmount, }, function(err, txHistory) { $scope.updatingTxHistory = false; if (err) { diff --git a/src/js/services/feeService.js b/src/js/services/feeService.js index c88b867ca..a3e554f38 100644 --- a/src/js/services/feeService.js +++ b/src/js/services/feeService.js @@ -4,6 +4,7 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou var root = {}; var CACHE_TIME_TS = 60; // 1 min + var LOW_AMOUNT_RATIO = 0.15; //Ratio low amount warning (econ fee/amount) // Constant fee options to translate root.feeOpts = { @@ -43,7 +44,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 +56,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 +71,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 +82,52 @@ angular.module('copayApp.services').factory('feeService', function($log, $timeou }); }; + + // 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; + } + }; + + + function getEstimatedSize(wallet) { + // 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 = 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.getLowAmount = function(wallet, cb) { + root.getFeeLevels(function(err, levels) { + if (err) return cb(err); + + var lowLevelRate = (lodash.find(levels[wallet.network], { + level: 'economy', + }).feePerKB / 1000).toFixed(0); + + var size = getEstimatedSize(wallet); + + var minFee = size * lowLevelRate; + + return cb(null, parseInt(minFee / (LOW_AMOUNT_RATIO))); + }); + }; + return root; }); diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 8c5a93662..5f1118a1d 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -90,6 +90,8 @@ angular.module('copayApp.services') wallet.m = wallet.credentials.m; wallet.n = wallet.credentials.n; + wallet.lowAmount = + root.updateWalletSettings(wallet); root.wallet[walletId] = wallet; diff --git a/src/js/services/txFormatService.js b/src/js/services/txFormatService.js index 7524bb548..fee503a18 100644 --- a/src/js/services/txFormatService.js +++ b/src/js/services/txFormatService.js @@ -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; }; diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index a9d45f3ff..7f53f1ce8 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -413,7 +413,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 +510,15 @@ 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() { // @@ -567,9 +575,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 +610,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim }); }; - + root.getTxHistory = function(wallet, opts, cb) { opts = opts || {}; diff --git a/src/sass/views/confirm.scss b/src/sass/views/confirm.scss index 8adc61672..9ceee92c4 100644 --- a/src/sass/views/confirm.scss +++ b/src/sass/views/confirm.scss @@ -5,6 +5,10 @@ float: none; .fee-rate { display: inline-block; + .warn { + color: red; + } + } } .icon-amazon { diff --git a/src/sass/views/includes/txp-details.scss b/src/sass/views/includes/txp-details.scss index c68841a4c..7303c1f82 100644 --- a/src/sass/views/includes/txp-details.scss +++ b/src/sass/views/includes/txp-details.scss @@ -98,7 +98,7 @@ &.low-fees { display: flex; font-size: 14px; - color: #aaa; + color: #777; align-items: center; i { padding-right: 20px; diff --git a/www/views/confirm.html b/www/views/confirm.html index cff04ebf3..0048cb951 100644 --- a/www/views/confirm.html +++ b/www/views/confirm.html @@ -81,8 +81,15 @@ {{'Fee:' | translate}} {{tx.feeLevelName | translate}} {{tx.txp[wallet.id].feeStr || '...'}} - {{tx.txp[wallet.id].alternativeFeeStr || '...'}} - {{tx.txp[wallet.id].feeRatePerStr}} of the transaction + {{tx.txp[wallet.id].alternativeFeeStr || '...'}}  + · +   + {{tx.txp[wallet.id].feeRatePerStr}} of the sending amount + + + + diff --git a/www/views/includes/walletHistory.html b/www/views/includes/walletHistory.html index bdd372e2f..81124ab7d 100644 --- a/www/views/includes/walletHistory.html +++ b/www/views/includes/walletHistory.html @@ -26,6 +26,11 @@ Low fees +
+ + Amount too low to spend +
+
diff --git a/www/views/modals/txp-details.html b/www/views/modals/txp-details.html index a093ba441..c5649201a 100644 --- a/www/views/modals/txp-details.html +++ b/www/views/modals/txp-details.html @@ -19,7 +19,7 @@ Sending
-
{{displayAmount}} {{displayUnit}}
+
{{tx.amountValueStr}} {{tx.amountUnitStr}}
{{tx.alternativeAmountStr}}
diff --git a/www/views/tx-details.html b/www/views/tx-details.html index 5af45c3aa..cd13d7535 100644 --- a/www/views/tx-details.html +++ b/www/views/tx-details.html @@ -24,7 +24,7 @@ Receiving
-
{{displayAmount}} {{displayUnit}}
+
{{btx.amountValueStr}} {{btx.amountUnitStr}}
{{btx.alternativeAmountStr}} @@ -88,6 +88,11 @@ This transaction could take a long time to confirm or could be dropped due to the low fees set by the sender
+
+ + This transaction amount is too small given current Bitcoin network fees. Spending these funds will incur in fees comparable to the amount itself. +
+
Confirmations