darkwallet/darkwallet

View on GitHub
src/js/frontend/providers/history.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * @fileOverview HistoryProvider angular provider
 */
'use strict';

define(['./module', 'util/btc', 'darkwallet', 'dwutil/multisig', 'util/bip47', "bitcoinjs-lib"],
function (providers, BtcUtils, DarkWallet, MultisigFund, PaymentCodes, Bitcoin) {
  providers.factory('$history', ['$rootScope', '$wallet', '$location', '_Filter', function($rootScope, $wallet, $location, _) {

  /**
   * History provider class
   */ 
  function HistoryProvider($scope, $wallet) {
      this.pocket = {index: undefined, name: _('All Pockets'), mpk: undefined, addresses: $wallet.allAddresses, isAll: true, type: 'init', incoming: 0, outgoing: 0};
      this.txFilter = 'last';
      this.addrFilter = 'unused';
      this.$wallet = $wallet;
      this.$scope = $scope;
      this.rows = [];
  }

  /**
   * Subscribe for changes on history provider scope.
   */ 
  HistoryProvider.prototype.watch = function(name, cb) {
      this.$scope.$watch(name, cb);
  };

  /**
   * Balance
   */ 
  HistoryProvider.prototype.calculateBalance = function(pocket) {
      var identity = DarkWallet.getIdentity();
      return identity.wallet.getBalance(pocket.index, pocket.type);
  };

  /**
   * Pocket change
   */ 
  HistoryProvider.prototype.isCurrentPocket = function(pocketId) {
      if (this.pocket.isAll) {
          return true;
      // The test here needs to be == since pocket.index can be a string "1"
      } else if (this.pocket.index == pocketId) {
          return true;
      }
  };

  HistoryProvider.prototype.removePocket = function(type, pocketId) {
    var self = this;
    var identity = DarkWallet.getIdentity();
    var pocket = identity.wallet.pockets.getPocket(pocketId, type);
    if (pocket) {
        var removed = pocket.destroy();
        removed.forEach(function(walletAddress) {
            self.$wallet.removeAddress(walletAddress);
        });
        // go to 'all' if this is the current pocket
        if (this.pocket.index === pocketId && this.pocket.type === type) {
            this.selectAll();
        }
        return pocket;
    }
  };

  HistoryProvider.prototype.onBalanceUpdate = function() {
      this.pocket.balance = this.calculateBalance(this.pocket);
      return this.chooseRows();
  };

  HistoryProvider.prototype.getCurrentPocket = function() {
      return this.pocket;
  };

  HistoryProvider.prototype.setCurrentPocket = function(type, idx, force) {
      if (type === undefined) {
          if (idx === undefined) {
              type = 'all';
          } else {
              type = 'hd';
          }
      }

      // If type is the same or 
      if (type === this.pocket.type && idx === this.pocket.lastIndex && !force) {
          return false;
      }

      var identity = DarkWallet.getIdentity();
      switch(type) {
          case 'all':
              this.selectAll();
              break;
          case 'hd':
              var keys = Object.keys(identity.wallet.pockets.pockets.hd);
              if (this.selectGenericPocket('hd', keys.indexOf(''+idx))) {
                  this.selectedPocket = 'hd:' + idx;
                  this.pocket.lastIndex = idx;
                  var pcode = identity.wallet.pockets.getPocket(this.pocket.index, 'hd').store.pcode;
                  if (pcode) {
                    this.pocket.pCode = PaymentCodes.formatAddress(Bitcoin.HDNode.fromBase58(pcode));
                  }
                  this.$scope.selectedPocket = 'hd:' + idx;
              }
              break;
          case 'readonly':
              this.selectGenericPocket(type, idx);
              break;
          case 'multisig':
              this.selectFund(idx);
              break;
      }
      return true;
  };

  // History Listing
  HistoryProvider.prototype.selectFund = function(fundIndex) {
      var identity = DarkWallet.getIdentity();
      var fund = identity.wallet.multisig.funds[fundIndex];

      if (!fund) {
          $location.path('/wallet');
          return this.selectAll();
      }

      // Need to find the original index for this fund
      var keys = Object.keys(identity.wallet.pockets.pockets.multisig);
      var rowIndex = keys.indexOf(fund.address);

      if (this.selectGenericPocket('multisig', rowIndex)) {
          // some custom data...
          this.pocket.lastIndex = fundIndex;
          this.pocket.fund = new MultisigFund(fund);
          this.pocket.tasks = this.pocket.fund.tasks;
          this.pocket.isFund = true;
          this.selectedPocket = 'multisig:' + fundIndex;
          this.$scope.selectedPocket = 'multisig:' + fundIndex;
      }
  };

  HistoryProvider.prototype.selectAll = function() {
      var identity = DarkWallet.getIdentity();
      this.pocket.name = _('All Pockets');
      this.pocket.index = undefined;
      this.pocket.mpk = undefined;
      this.pocket.type = 'all';

      var mainAddress = identity.wallet.getAddress([0]);
      this.pocket.stealth = mainAddress.stealth;
      this.pocket.mainAddress = mainAddress.stealth;
      this.pocket.mainHash = identity.contacts.generateContactHash(this.pocket.mainAddress);

      this.pocket.fund = null;
      this.pocket.addresses = this.$wallet.allAddresses;
      this.pocket.lastIndex = undefined;
      this.pocket.isAll = true;
      this.pocket.isFund = false;
      this.pocket.readOnly = false;

      this.pocket.balance = identity.wallet.getBalance();

      this.pocket.tasks = [];
      this.selectedPocket = 'pocket:all';
      this.$scope.selectedPocket = 'pocket:all';
      return this.chooseRows();
  };

  HistoryProvider.prototype.selectGenericPocket = function(type, rowIndex) {
      if (rowIndex === -1) {
          this.selectAll();
          $location.path('/wallet');
          return false;
      }
      var identity = DarkWallet.getIdentity();

      var pockets = identity.wallet.pockets.getPockets(type);

      // ensure order is the same as the sidebar
      var keys = Object.keys(pockets);
      var pocketId = keys[rowIndex];
      if (pocketId === undefined) {
          this.selectAll();
          $location.path('/wallet');
          return false;
      }
      var pocket = pockets[pocketId];

      // set some type information
      this.pocket.isAll = false;
      this.pocket.isFund = false;
      // warning: we need to save pocketId as int for hd pockets so it can be
      // autodetected
      this.pocket.index = (type === 'hd') ? parseInt(pocketId) : pocketId;
      this.pocket.type = type;
      this.pocket.lastIndex = rowIndex;
      this.pocket.name = pocket.name;
      this.pocket.fund = null;

      // warning: refresh addresses needs to come after setting pocket.isAll
      this.refreshAddresses();

      this.pocket.mixing = pocket.store.mixing;
      this.pocket.mixingOptions = pocket.store.mixingOptions;
      this.pocket.tasks = [];
      this.pocket.readOnly = pocket.readOnly;

      this.pocket.balance = this.calculateBalance(this.pocket);

      // Get the main address for the pocket
      var walletAddress = pocket.getMainAddress();

      // Set some contact fields
      this.pocket.mpk = walletAddress.mpk || walletAddress.address;
      this.pocket.stealth = walletAddress.stealth;
      this.pocket.mainAddress = walletAddress.stealth || walletAddress.address;
      this.pocket.mainHash = identity.contacts.generateContactHash(this.pocket.mainAddress);

      this.selectedPocket = type+':' + rowIndex;
      this.chooseRows();
      this.$scope.selectedPocket = type+':' + rowIndex;
      return true;
  };

  HistoryProvider.prototype.refreshAddresses = function() {
      var identity = DarkWallet.getIdentity();
      if (this.pocket.isAll) {
          this.pocket.addresses = this.$wallet.allAddresses;
      } else {
          var pocket = identity.wallet.pockets.getPocket(this.pocket.index, this.pocket.type);
          this.pocket.addresses = pocket.getWalletAddresses();
      }
  };

  // Filters
  HistoryProvider.prototype.fillRowContact = function(contacts, row) {
      if (!row.contact) {
          var contact = contacts.findByAddress(row.address);
          if (contact) {
              row.contact = contact;
          }
      }
  };
 
  // Filter the rows we want to show
  HistoryProvider.prototype.chooseRows = function() {
      this.pocket.incoming = 0;
      this.pocket.outgoing = 0;
      var identity =  DarkWallet.getIdentity();
      var self = this;
      var history = identity.history.history;
      var rows = history.filter(this.pocketFilter, this);
      rows = rows.sort(function(a, b) {
         if (!a.height) {
            return -10000000;
         }
         if (!b.height) {
            return 10000000;
         }
         return b.height - a.height;
      });
      var shownRows = [];
      rows = rows.filter(function(row) { return self.historyFilter(row, shownRows); } );
      if (!rows.length) {
          this.rows = [];
          return [];
      }
      if (this.txFilter === 'weekly') {
          this.rows = this.calculateWeekly(rows);
      }
      else if (this.txFilter === 'monthly') {
          this.rows = this.calculateMonthly(rows);
      } else {
          this.rows = this.calculateHistory(rows);
      }
      return this.rows;
  };

  HistoryProvider.prototype.calculateMonthly = function(rows) {
      var self = this;
      var now = new Date();
      var d = now.getDate(); //get the current day
      var monthStart = new Date(now.valueOf() - ((d===1?0:d-1)*86400000)); //rewind to start day
      var monthEnd;
      var getLabel = function(dateStart, dateEnd) {
         return dateEnd.toLocaleDateString(_(), {month: 'long', year: 'numeric'});
      };
      var month = {index: 0, incoming: 0, outgoing: 0, transactions: 0, label: getLabel(monthStart, monthStart)};
      var result = [month];

      var monthIndex = 0;
      var blockDiff = DarkWallet.service.wallet.blockDiff;
      rows.forEach(function(row) {
           if (row.height) {
               var timestamp = BtcUtils.heightToTimestamp(row.height, blockDiff);
               while (timestamp < monthStart) {
                   monthEnd = new Date(monthStart.valueOf()-86400000);
                   monthStart = new Date(monthStart.valueOf()-(monthEnd.getDate()*86400000));
                   monthIndex -= 1;
                   var label = getLabel(monthStart, monthEnd);
                   month = {index: monthIndex, incoming: 0, outgoing: 0, transactions: 0, label: label};
                   result.push(month);
               }
           }
           month.transactions += 1;
           var impact = self.getRowImpact(row);
           if (impact>0) {
               month.incoming += impact;
           } else {
               month.outgoing -= impact;
           }
      });

      return result;
  };

  HistoryProvider.prototype.calculateWeekly = function(rows) {
      var self = this;
      var now = new Date();
      var startDay = 1; //0=sunday, 1=monday etc.
      var d = now.getDay(); //get the current day
      var weekStart = new Date(now.valueOf() - (d<=0 ? 7-startDay:d-startDay)*86400000); //rewind to start day
      weekStart -= now.getHours()*3600000;
      weekStart -= now.getMinutes()*60000;

      var getLabel = function(dateStart, dateEnd) {
         var start = dateStart.toLocaleDateString(_());
         var end = dateEnd.toLocaleDateString(_());
         //return monthNames[weekStart.getMonth()]+"-"+(Math.floor(weekStart.getDate()/7)+1);
         return start + "-" + end;
      };
      var week = {index: 0, incoming: 0, outgoing: 0, transactions: 0};
      var result = [week];

      var weekIndex = 0;
      var blockDiff = DarkWallet.service.wallet.blockDiff;

      rows.forEach(function(row) {
           if (row.height) {
               var timestamp = BtcUtils.heightToTimestamp(row.height, blockDiff);
               while (timestamp < weekStart) {
                   var weekEnd = new Date(weekStart-86400000);
                   weekStart = new Date(weekStart-(7*86400000));
                   weekIndex -= 1;
                   var label = getLabel(weekStart, weekEnd);
                   week = {index: weekIndex, incoming: 0, outgoing: 0, transactions: 0, label: label};
                   result.push(week);
               }
           }
           week.transactions += 1;
           var impact = self.getRowImpact(row);
           if (impact>0) {
               week.incoming += impact;
           } else {
               week.outgoing -= impact;
           }
      });

      return result;
  };

  HistoryProvider.prototype.getRowImpact = function(row) {
      if (this.pocket.index === undefined) {
          return row.total;
      } else {
          return row.impact[this.pocket.index].total;
      }
  };

  HistoryProvider.prototype.calculateHistory = function(rows) {
      var identity =  DarkWallet.getIdentity();

      // Now calculate balances
      var prevRow = rows[0];
      prevRow.confirmed = this.pocket.balance.confirmed;
      prevRow.unconfirmed = this.pocket.balance.unconfirmed;
      prevRow.current = this.pocket.balance.current;
      prevRow.partial = this.getRowImpact(prevRow);

      var contacts = identity.contacts;
      this.fillRowContact(contacts, prevRow);
      var idx = 1;
      while(idx<rows.length) {
          var row = rows[idx];
          this.fillRowContact(contacts, row);
          
          var value = prevRow.partial;

          row.partial = this.getRowImpact(row);

          row.current = prevRow.current;

          if (prevRow.height || prevRow.inMine) {
              row.confirmed = prevRow.confirmed-value;
              row.unconfirmed = prevRow.unconfirmed;
              if (!prevRow.height) {
                 // outgoing
                 row.unconfirmed -= value;
                 if (value < 0) {
                     row.current -= value;
                 }
              } else {
                 row.current -= value;
              }
          } else {
              row.confirmed = prevRow.confirmed;
              row.unconfirmed = prevRow.unconfirmed-value;
          }
          prevRow = row;
          idx++;
      }
      return rows;
  };


  HistoryProvider.prototype.pocketFilter = function(row) {
      // Making sure shownRows is reset before historyFilter stage is reached.
      if (this.pocket.isAll) {
          if (!row.height) {
              var rowImpact = row.myOutValue - row.myInValue;
              if (rowImpact < 0) {
                  this.pocket.outgoing += -rowImpact;
              } else {
                  this.pocket.incoming += rowImpact;
              } 
          }
          // only add pocket transactions for now
          return ((typeof row.inPocket === 'number') || (typeof row.outPocket === 'number'));
      }
      else {
          var keys = Object.keys(row.impact);
          var impacted = (keys.indexOf(''+this.pocket.index) > -1);
          if (!row.height && impacted && row.impact[this.pocket.index].total > 0) {
              this.pocket.incoming += row.impact[this.pocket.index].total;
          } else if (impacted && !row.height) {
              this.pocket.outgoing -= row.impact[this.pocket.index].total;
          }
          return impacted;
      }
  };

  // Set the history filter
  HistoryProvider.prototype.setAddressFilter = function(name) {
      this.addrFilter = name;
      return name;
  };

  HistoryProvider.prototype.addressFilter = function(row) {
      // filter out pocket addresses with no inputs so the user never see them but can receive funds.
      if (row.index.length === 1 && row.label === 'pocket' && !row.nOutputs) { return false; }
      if (row.type === 'pocket') { return false; }
      switch(this.addrFilter) {
          case 'all':
              return true;
          case 'unused':
              return !row.nOutputs && row.label !== 'change';
          case 'top':
              return row.balance>0;
          case 'labelled':
              return ['unused', 'change'].indexOf(row.label) === -1;
          default:
              break;
      }

  };

  // Set the history filter
  HistoryProvider.prototype.setHistoryFilter = function(name) {
      this.txFilter = name;
      return this.chooseRows();
  };

  // Clear the contact in any rows that have it linked
  HistoryProvider.prototype.clearRowContacts = function(contact) {
      var identity = DarkWallet.getIdentity();
      identity.history.history.forEach(function(row) {
          if (contact === row.contact) {
              row.contact = undefined;
          }
      });
  };

  HistoryProvider.prototype.historyFilter = function(row, shownRows) {
      if (!row.height) {
          shownRows.push(row.hash);
          return true;
      }
      switch(this.txFilter) {
          case 'last':
          case 'weekly':
          case 'monthly':
              return true;
          case 'last10':
          default:
              if (shownRows.indexOf(row.hash) !== -1) {
                  return true;
              } else if (shownRows.length < 10) {
                  shownRows.push(row.hash);
                  return true;
              }
      }
      return false;
  };

      return new HistoryProvider($rootScope.$new(), $wallet);
  }]);
});