예제 #1
0
 public static DbIterator<RewardRecipientAssignment> getAccountsWithRewardRecipient(
     Long recipientId) {
   return rewardRecipientAssignmentTable.getManyBy(
       getAccountsWithRewardRecipientClause(recipientId, Nxt.getBlockchain().getHeight() + 1),
       0,
       -1);
 }
예제 #2
0
  public long getEffectiveBalanceNXT() {

    Block lastBlock = Nxt.getBlockchain().getLastBlock();
    if (lastBlock.getHeight() >= Constants.TRANSPARENT_FORGING_BLOCK_6
        && (getPublicKey() == null || lastBlock.getHeight() - keyHeight <= 1440)) {
      return 0; // cfb: Accounts with the public key revealed less than 1440 blocks ago are not
      // allowed to generate blocks
    }
    if (lastBlock.getHeight() < Constants.TRANSPARENT_FORGING_BLOCK_3
        && this.creationHeight < Constants.TRANSPARENT_FORGING_BLOCK_2) {
      if (this.creationHeight == 0) {
        return getBalanceNQT() / Constants.ONE_NXT;
      }
      if (lastBlock.getHeight() - this.creationHeight < 1440) {
        return 0;
      }
      long receivedInlastBlock = 0;
      for (Transaction transaction : lastBlock.getTransactions()) {
        if (id == transaction.getRecipientId()) {
          receivedInlastBlock += transaction.getAmountNQT();
        }
      }
      return (getBalanceNQT() - receivedInlastBlock) / Constants.ONE_NXT;
    }
    if (lastBlock.getHeight() < currentLeasingHeightFrom) {
      return (getGuaranteedBalanceNQT(1440) + getLessorsGuaranteedBalanceNQT()) / Constants.ONE_NXT;
    }
    return getLessorsGuaranteedBalanceNQT() / Constants.ONE_NXT;
  }
예제 #3
0
 void leaseEffectiveBalance(long lesseeId, short period) {
   Account lessee = Account.getAccount(lesseeId);
   if (lessee != null && lessee.getPublicKey() != null) {
     int height = Nxt.getBlockchain().getHeight();
     if (currentLeasingHeightFrom == Integer.MAX_VALUE) {
       currentLeasingHeightFrom = height + 1440;
       currentLeasingHeightTo = currentLeasingHeightFrom + period;
       currentLesseeId = lesseeId;
       nextLeasingHeightFrom = Integer.MAX_VALUE;
       accountTable.insert(this);
       leaseListeners.notify(
           new AccountLease(
               this.getId(), lesseeId, currentLeasingHeightFrom, currentLeasingHeightTo),
           Event.LEASE_SCHEDULED);
     } else {
       nextLeasingHeightFrom = height + 1440;
       if (nextLeasingHeightFrom < currentLeasingHeightTo) {
         nextLeasingHeightFrom = currentLeasingHeightTo;
       }
       nextLeasingHeightTo = nextLeasingHeightFrom + period;
       nextLesseeId = lesseeId;
       accountTable.insert(this);
       leaseListeners.notify(
           new AccountLease(this.getId(), lesseeId, nextLeasingHeightFrom, nextLeasingHeightTo),
           Event.LEASE_SCHEDULED);
     }
   }
 }
예제 #4
0
 private void save(Connection con) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "MERGE INTO account (id, creation_height, public_key, "
               + "key_height, balance, unconfirmed_balance, forged_balance, name, description, "
               + "current_leasing_height_from, current_leasing_height_to, current_lessee_id, "
               + "next_leasing_height_from, next_leasing_height_to, next_lessee_id, "
               + "height, latest) "
               + "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) {
     int i = 0;
     pstmt.setLong(++i, this.getId());
     pstmt.setInt(++i, this.getCreationHeight());
     DbUtils.setBytes(pstmt, ++i, this.getPublicKey());
     pstmt.setInt(++i, this.getKeyHeight());
     pstmt.setLong(++i, this.getBalanceNQT());
     pstmt.setLong(++i, this.getUnconfirmedBalanceNQT());
     pstmt.setLong(++i, this.getForgedBalanceNQT());
     DbUtils.setString(pstmt, ++i, this.getName());
     DbUtils.setString(pstmt, ++i, this.getDescription());
     DbUtils.setIntZeroToNull(pstmt, ++i, this.getCurrentLeasingHeightFrom());
     DbUtils.setIntZeroToNull(pstmt, ++i, this.getCurrentLeasingHeightTo());
     DbUtils.setLongZeroToNull(pstmt, ++i, this.getCurrentLesseeId());
     DbUtils.setIntZeroToNull(pstmt, ++i, this.getNextLeasingHeightFrom());
     DbUtils.setIntZeroToNull(pstmt, ++i, this.getNextLeasingHeightTo());
     DbUtils.setLongZeroToNull(pstmt, ++i, this.getNextLesseeId());
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #5
0
 private void addToGuaranteedBalanceNQT(long amountNQT) {
   if (amountNQT <= 0) {
     return;
   }
   int blockchainHeight = Nxt.getBlockchain().getHeight();
   try (Connection con = Db.getConnection();
       PreparedStatement pstmtSelect =
           con.prepareStatement(
               "SELECT additions FROM account_guaranteed_balance "
                   + "WHERE account_id = ? and height = ?");
       PreparedStatement pstmtUpdate =
           con.prepareStatement(
               "MERGE INTO account_guaranteed_balance (account_id, "
                   + " additions, height) KEY (account_id, height) VALUES(?, ?, ?)")) {
     pstmtSelect.setLong(1, this.id);
     pstmtSelect.setInt(2, blockchainHeight);
     try (ResultSet rs = pstmtSelect.executeQuery()) {
       long additions = amountNQT;
       if (rs.next()) {
         additions = Convert.safeAdd(additions, rs.getLong("additions"));
       }
       pstmtUpdate.setLong(1, this.id);
       pstmtUpdate.setLong(2, additions);
       pstmtUpdate.setInt(3, blockchainHeight);
       pstmtUpdate.executeUpdate();
     }
   } catch (SQLException e) {
     throw new RuntimeException(e.toString(), e);
   }
 }
예제 #6
0
 public long getGuaranteedBalanceNQT(final int numberOfConfirmations, final int currentHeight) {
   if (numberOfConfirmations >= Nxt.getBlockchain().getHeight()) {
     return 0;
   }
   if (numberOfConfirmations > 2880 || numberOfConfirmations < 0) {
     throw new IllegalArgumentException(
         "Number of required confirmations must be between 0 and " + 2880);
   }
   int height = currentHeight - numberOfConfirmations;
   try (Connection con = Db.getConnection();
       PreparedStatement pstmt =
           con.prepareStatement(
               "SELECT SUM (additions) AS additions "
                   + "FROM account_guaranteed_balance WHERE account_id = ? AND height > ? AND height <= ?")) {
     pstmt.setLong(1, this.id);
     pstmt.setInt(2, height);
     pstmt.setInt(3, currentHeight);
     try (ResultSet rs = pstmt.executeQuery()) {
       if (!rs.next()) {
         return balanceNQT;
       }
       return Math.max(Convert.safeSubtract(balanceNQT, rs.getLong("additions")), 0);
     }
   } catch (SQLException e) {
     throw new RuntimeException(e.toString(), e);
   }
 }
예제 #7
0
 private Account(long id) {
   if (id != Crypto.rsDecode(Crypto.rsEncode(id))) {
     Logger.logMessage("CRITICAL ERROR: Reed-Solomon encoding fails for " + id);
   }
   this.id = id;
   this.dbKey = accountDbKeyFactory.newKey(this.id);
   this.creationHeight = Nxt.getBlockchain().getHeight();
   currentLeasingHeightFrom = Integer.MAX_VALUE;
 }
예제 #8
0
 static {
   Nxt.getBlockchainProcessor()
       .addListener(
           new Listener<Block>() {
             @Override
             public void notify(Block block) {
               int height = block.getHeight();
               try (DbIterator<Account> leasingAccounts = getLeasingAccounts()) {
                 while (leasingAccounts.hasNext()) {
                   Account account = leasingAccounts.next();
                   if (height == account.currentLeasingHeightFrom) {
                     leaseListeners.notify(
                         new AccountLease(
                             account.getId(),
                             account.currentLesseeId,
                             height,
                             account.currentLeasingHeightTo),
                         Event.LEASE_STARTED);
                   } else if (height == account.currentLeasingHeightTo) {
                     leaseListeners.notify(
                         new AccountLease(
                             account.getId(),
                             account.currentLesseeId,
                             account.currentLeasingHeightFrom,
                             height),
                         Event.LEASE_ENDED);
                     if (account.nextLeasingHeightFrom == Integer.MAX_VALUE) {
                       account.currentLeasingHeightFrom = Integer.MAX_VALUE;
                       account.currentLesseeId = 0;
                       accountTable.insert(account);
                     } else {
                       account.currentLeasingHeightFrom = account.nextLeasingHeightFrom;
                       account.currentLeasingHeightTo = account.nextLeasingHeightTo;
                       account.currentLesseeId = account.nextLesseeId;
                       account.nextLeasingHeightFrom = Integer.MAX_VALUE;
                       account.nextLesseeId = 0;
                       accountTable.insert(account);
                       if (height == account.currentLeasingHeightFrom) {
                         leaseListeners.notify(
                             new AccountLease(
                                 account.getId(),
                                 account.currentLesseeId,
                                 height,
                                 account.currentLeasingHeightTo),
                             Event.LEASE_STARTED);
                       }
                     }
                   }
                 }
               }
             }
           },
           BlockchainProcessor.Event.AFTER_BLOCK_APPLY);
 }
예제 #9
0
파일: Vote.java 프로젝트: andriybobyr/lnk
 private void save(Connection con) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "INSERT INTO vote (id, poll_id, voter_id, "
               + "vote_bytes, height) VALUES (?, ?, ?, ?, ?)")) {
     int i = 0;
     pstmt.setLong(++i, this.id);
     pstmt.setLong(++i, this.pollId);
     pstmt.setLong(++i, this.voterId);
     pstmt.setBytes(++i, this.voteBytes);
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #10
0
 private void save(Connection con) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "MERGE INTO reward_recip_assign "
               + "(account_id, prev_recip_id, recip_id, from_height, height, latest) KEY (account_id, height) VALUES (?, ?, ?, ?, ?, TRUE)")) {
     int i = 0;
     pstmt.setLong(++i, this.accountId);
     pstmt.setLong(++i, this.prevRecipientId);
     pstmt.setLong(++i, this.recipientId);
     pstmt.setInt(++i, this.fromHeight);
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #11
0
 /** Process nxt.ledgerAccounts */
 static {
   List<String> ledgerAccounts = Nxt.getStringListProperty("nxt.ledgerAccounts");
   ledgerEnabled = !ledgerAccounts.isEmpty();
   trackAllAccounts = ledgerAccounts.contains("*");
   if (ledgerEnabled) {
     if (trackAllAccounts) {
       Logger.logInfoMessage("Account ledger is tracking all accounts");
     } else {
       for (String account : ledgerAccounts) {
         try {
           trackAccounts.add(Convert.parseAccountId(account));
           Logger.logInfoMessage("Account ledger is tracking account " + account);
         } catch (RuntimeException e) {
           Logger.logErrorMessage("Account " + account + " is not valid; ignored");
         }
       }
     }
   } else {
     Logger.logInfoMessage("Account ledger is not enabled");
   }
   int temp = Nxt.getIntProperty("nxt.ledgerLogUnconfirmed", 1);
   logUnconfirmed = (temp >= 0 && temp <= 2 ? temp : 1);
 }
예제 #12
0
 private void save(Connection con) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "MERGE INTO account_asset "
               + "(account_id, asset_id, quantity, unconfirmed_quantity, height, latest) "
               + "KEY (account_id, asset_id, height) VALUES (?, ?, ?, ?, ?, TRUE)")) {
     int i = 0;
     pstmt.setLong(++i, this.accountId);
     pstmt.setLong(++i, this.assetId);
     pstmt.setLong(++i, this.quantityQNT);
     pstmt.setLong(++i, this.unconfirmedQuantityQNT);
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #13
0
 @Override
 void validateAttachment(Transaction transaction) throws NxtException.ValidationException {
   Attachment.MonetarySystemPublishExchangeOffer attachment =
       (Attachment.MonetarySystemPublishExchangeOffer) transaction.getAttachment();
   if (attachment.getBuyRateNQT() <= 0
       || attachment.getSellRateNQT() <= 0
       || attachment.getBuyRateNQT() > attachment.getSellRateNQT()) {
     throw new NxtException.NotValidException(
         String.format(
             "Invalid exchange offer, buy rate %d and sell rate %d has to be larger than 0, buy rate cannot be larger than sell rate",
             attachment.getBuyRateNQT(), attachment.getSellRateNQT()));
   }
   if (attachment.getTotalBuyLimit() < 0
       || attachment.getTotalSellLimit() < 0
       || attachment.getInitialBuySupply() < 0
       || attachment.getInitialSellSupply() < 0
       || attachment.getExpirationHeight() < 0) {
     throw new NxtException.NotValidException(
         "Invalid exchange offer, units and height cannot be negative: "
             + attachment.getJSONObject());
   }
   if (attachment.getTotalBuyLimit() < attachment.getInitialBuySupply()
       || attachment.getTotalSellLimit() < attachment.getInitialSellSupply()) {
     throw new NxtException.NotValidException(
         "Initial supplies must not exceed total limits");
   }
   if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK) {
     if (attachment.getTotalBuyLimit() == 0 && attachment.getTotalSellLimit() == 0) {
       throw new NxtException.NotCurrentlyValidException(
           "Total buy and sell limits cannot be both 0");
     }
     if (attachment.getInitialBuySupply() == 0 && attachment.getInitialSellSupply() == 0) {
       throw new NxtException.NotCurrentlyValidException(
           "Initial buy and sell supply cannot be both 0");
     }
   }
   if (attachment.getExpirationHeight()
       <= attachment.getFinishValidationHeight(transaction)) {
     throw new NxtException.NotCurrentlyValidException(
         "Expiration height must be after transaction execution height");
   }
   Currency currency = Currency.getCurrency(attachment.getCurrencyId());
   CurrencyType.validate(currency, transaction);
   if (!currency.isActive()) {
     throw new NxtException.NotCurrentlyValidException(
         "Currency not currently active: " + attachment.getJSONObject());
   }
 }
예제 #14
0
 public static void setRewardRecipientAssignment(Long id, Long recipient) {
   int currentHeight = Nxt.getBlockchain().getLastBlock().getHeight();
   RewardRecipientAssignment assignment = getRewardRecipientAssignment(id);
   if (assignment == null) {
     assignment =
         new RewardRecipientAssignment(
             id,
             id,
             recipient,
             (int) (currentHeight + Constants.BURST_REWARD_RECIPIENT_ASSIGNMENT_WAIT_TIME));
   } else {
     assignment.setRecipient(
         recipient, (int) (currentHeight + Constants.BURST_REWARD_RECIPIENT_ASSIGNMENT_WAIT_TIME));
   }
   rewardRecipientAssignmentTable.insert(assignment);
 }
예제 #15
0
 private void save(Connection con) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "INSERT INTO asset (id, account_id, name, "
               + "description, quantity, decimals, height) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
     int i = 0;
     pstmt.setLong(++i, this.getId());
     pstmt.setLong(++i, this.getAccountId());
     pstmt.setString(++i, this.getName());
     pstmt.setString(++i, this.getDescription());
     pstmt.setLong(++i, this.getQuantityQNT());
     pstmt.setByte(++i, this.getDecimals());
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #16
0
 @Override
 void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {
   Attachment.MonetarySystemReserveIncrease attachment =
       (Attachment.MonetarySystemReserveIncrease) transaction.getAttachment();
   long reserveSupply;
   Currency currency = Currency.getCurrency(attachment.getCurrencyId());
   if (currency != null) {
     reserveSupply = currency.getReserveSupply();
   } else { // currency must have been deleted, get reserve supply from the original issuance
            // transaction
     Transaction currencyIssuance =
         Nxt.getBlockchain().getTransaction(attachment.getCurrencyId());
     Attachment.MonetarySystemCurrencyIssuance currencyIssuanceAttachment =
         (Attachment.MonetarySystemCurrencyIssuance) currencyIssuance.getAttachment();
     reserveSupply = currencyIssuanceAttachment.getReserveSupply();
   }
   senderAccount.addToUnconfirmedBalanceNQT(
       Math.multiplyExact(reserveSupply, attachment.getAmountPerUnitNQT()));
 }
 static {
   Nxt.getBlockchainProcessor()
       .addListener(
           block -> {
             if (block.getHeight() <= Constants.MONETARY_SYSTEM_BLOCK) {
               return;
             }
             List<CurrencyBuyOffer> expired = new ArrayList<>();
             try (DbIterator<CurrencyBuyOffer> offers =
                 CurrencyBuyOffer.getOffers(
                     new DbClause.IntClause("expiration_height", block.getHeight()), 0, -1)) {
               for (CurrencyBuyOffer offer : offers) {
                 expired.add(offer);
               }
             }
             expired.forEach(CurrencyExchangeOffer::removeOffer);
           },
           BlockchainProcessor.Event.AFTER_BLOCK_APPLY);
 }
 CurrencyExchangeOffer(
     long id,
     long currencyId,
     long accountId,
     long rateNQT,
     long limit,
     long supply,
     int expirationHeight,
     int transactionHeight,
     short transactionIndex) {
   this.id = id;
   this.currencyId = currencyId;
   this.accountId = accountId;
   this.rateNQT = rateNQT;
   this.limit = limit;
   this.supply = supply;
   this.expirationHeight = expirationHeight;
   this.creationHeight = Nxt.getBlockchain().getHeight();
   this.transactionIndex = transactionIndex;
   this.transactionHeight = transactionHeight;
 }
 void save(Connection con, String table) throws SQLException {
   try (PreparedStatement pstmt =
       con.prepareStatement(
           "MERGE INTO "
               + table
               + " (id, currency_id, account_id, "
               + "rate, unit_limit, supply, expiration_height, creation_height, transaction_index, transaction_height, height, latest) "
               + "KEY (id, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) {
     int i = 0;
     pstmt.setLong(++i, this.id);
     pstmt.setLong(++i, this.currencyId);
     pstmt.setLong(++i, this.accountId);
     pstmt.setLong(++i, this.rateNQT);
     pstmt.setLong(++i, this.limit);
     pstmt.setLong(++i, this.supply);
     pstmt.setInt(++i, this.expirationHeight);
     pstmt.setInt(++i, this.creationHeight);
     pstmt.setShort(++i, this.transactionIndex);
     pstmt.setInt(++i, this.transactionHeight);
     pstmt.setInt(++i, Nxt.getBlockchain().getHeight());
     pstmt.executeUpdate();
   }
 }
예제 #20
0
  @Override
  public DbIterator<TransactionImpl> getTransactions(
      Account account,
      int numberOfConfirmations,
      byte type,
      byte subtype,
      int blockTimestamp,
      boolean withMessage,
      boolean phasedOnly,
      boolean nonPhasedOnly,
      int from,
      int to) {
    if (phasedOnly && nonPhasedOnly) {
      throw new IllegalArgumentException(
          "At least one of phasedOnly or nonPhasedOnly must be false");
    }
    int height =
        numberOfConfirmations > 0 ? getHeight() - numberOfConfirmations : Integer.MAX_VALUE;
    if (height < 0) {
      throw new IllegalArgumentException(
          "Number of confirmations required "
              + numberOfConfirmations
              + " exceeds current blockchain height "
              + getHeight());
    }
    Connection con = null;
    try {
      StringBuilder buf = new StringBuilder();
      buf.append("SELECT * FROM transaction WHERE recipient_id = ? AND sender_id <> ? ");
      if (blockTimestamp > 0) {
        buf.append("AND block_timestamp >= ? ");
      }
      if (type >= 0) {
        buf.append("AND type = ? ");
        if (subtype >= 0) {
          buf.append("AND subtype = ? ");
        }
      }
      if (height < Integer.MAX_VALUE) {
        buf.append("AND height <= ? ");
      }
      if (withMessage) {
        buf.append("AND (has_message = TRUE OR has_encrypted_message = TRUE ");
        buf.append(
            "OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) ");
      }
      if (phasedOnly) {
        buf.append("AND phased = TRUE ");
      } else if (nonPhasedOnly) {
        buf.append("AND phased = FALSE ");
      }

      buf.append("UNION ALL SELECT * FROM transaction WHERE sender_id = ? ");
      if (blockTimestamp > 0) {
        buf.append("AND block_timestamp >= ? ");
      }
      if (type >= 0) {
        buf.append("AND type = ? ");
        if (subtype >= 0) {
          buf.append("AND subtype = ? ");
        }
      }
      if (height < Integer.MAX_VALUE) {
        buf.append("AND height <= ? ");
      }
      if (withMessage) {
        buf.append(
            "AND (has_message = TRUE OR has_encrypted_message = TRUE OR has_encrypttoself_message = TRUE ");
        buf.append(
            "OR ((has_prunable_message = TRUE OR has_prunable_encrypted_message = TRUE) AND timestamp > ?)) ");
      }
      if (phasedOnly) {
        buf.append("AND phased = TRUE ");
      } else if (nonPhasedOnly) {
        buf.append("AND phased = FALSE ");
      }

      buf.append("ORDER BY block_timestamp DESC, transaction_index DESC");
      buf.append(DbUtils.limitsClause(from, to));
      con = Db.db.getConnection();
      PreparedStatement pstmt;
      int i = 0;
      pstmt = con.prepareStatement(buf.toString());
      pstmt.setLong(++i, account.getId());
      pstmt.setLong(++i, account.getId());
      if (blockTimestamp > 0) {
        pstmt.setInt(++i, blockTimestamp);
      }
      if (type >= 0) {
        pstmt.setByte(++i, type);
        if (subtype >= 0) {
          pstmt.setByte(++i, subtype);
        }
      }
      if (height < Integer.MAX_VALUE) {
        pstmt.setInt(++i, height);
      }
      int prunableExpiration =
          Constants.INCLUDE_EXPIRED_PRUNABLE
              ? 0
              : Nxt.getEpochTime() - Constants.MAX_PRUNABLE_LIFETIME;
      if (withMessage) {
        pstmt.setInt(++i, prunableExpiration);
      }
      pstmt.setLong(++i, account.getId());
      if (blockTimestamp > 0) {
        pstmt.setInt(++i, blockTimestamp);
      }
      if (type >= 0) {
        pstmt.setByte(++i, type);
        if (subtype >= 0) {
          pstmt.setByte(++i, subtype);
        }
      }
      if (height < Integer.MAX_VALUE) {
        pstmt.setInt(++i, height);
      }
      if (withMessage) {
        pstmt.setInt(++i, prunableExpiration);
      }
      DbUtils.setLimits(++i, pstmt, from, to);
      return getTransactions(con, pstmt);
    } catch (SQLException e) {
      DbUtils.close(con);
      throw new RuntimeException(e.toString(), e);
    }
  }
예제 #21
0
  @Override
  public Transaction parseTransaction(byte[] bytes) throws NxtException.ValidationException {

    try {
      boolean useNQT = Nxt.getBlockchain().getLastBlock().getHeight() >= Constants.NQT_BLOCK;
      ByteBuffer buffer = ByteBuffer.wrap(bytes);
      buffer.order(ByteOrder.LITTLE_ENDIAN);

      byte type = buffer.get();
      byte subtype = buffer.get();
      int timestamp = buffer.getInt();
      short deadline = buffer.getShort();
      byte[] senderPublicKey = new byte[32];
      buffer.get(senderPublicKey);
      Long recipientId = buffer.getLong();
      long amountNQT;
      long feeNQT;
      String referencedTransactionFullHash = null;
      Long referencedTransactionId = null;
      if (!useNQT) {
        amountNQT = ((long) buffer.getInt()) * Constants.ONE_NXT;
        feeNQT = ((long) buffer.getInt()) * Constants.ONE_NXT;
        referencedTransactionId = Convert.zeroToNull(buffer.getLong());
      } else {
        amountNQT = buffer.getLong();
        feeNQT = buffer.getLong();
        byte[] referencedTransactionFullHashBytes = new byte[32];
        buffer.get(referencedTransactionFullHashBytes);
        if (Convert.emptyToNull(referencedTransactionFullHashBytes) != null) {
          referencedTransactionFullHash = Convert.toHexString(referencedTransactionFullHashBytes);
          referencedTransactionId = Convert.fullHashToId(referencedTransactionFullHash);
        }
      }
      byte[] signature = new byte[64];
      buffer.get(signature);
      signature = Convert.emptyToNull(signature);

      TransactionType transactionType = TransactionType.findTransactionType(type, subtype);
      TransactionImpl transaction;
      if (!useNQT) {
        transaction =
            new TransactionImpl(
                transactionType,
                timestamp,
                deadline,
                senderPublicKey,
                recipientId,
                amountNQT,
                feeNQT,
                referencedTransactionId,
                signature);
      } else {
        transaction =
            new TransactionImpl(
                transactionType,
                timestamp,
                deadline,
                senderPublicKey,
                recipientId,
                amountNQT,
                feeNQT,
                referencedTransactionFullHash,
                signature);
      }
      transactionType.loadAttachment(transaction, buffer);

      return transaction;

    } catch (RuntimeException e) {
      throw new NxtException.ValidationException(e.toString(), e);
    }
  }
예제 #22
0
 @Override
 boolean isBlockDuplicate(
     Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) {
   return Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK
       && isDuplicate(CURRENCY_ISSUANCE, getName(), duplicates, true);
 }
예제 #23
0
 public DbIterator<Account> getLessors() {
   return accountTable.getManyBy(getLessorsClause(Nxt.getBlockchain().getHeight()), 0, -1);
 }
예제 #24
0
 public long getGuaranteedBalanceNQT(final int numberOfConfirmations) {
   return getGuaranteedBalanceNQT(numberOfConfirmations, Nxt.getBlockchain().getHeight());
 }
예제 #25
0
 public final boolean isFinished() {
   return finishHeight <= Nxt.getBlockchain().getHeight();
 }
예제 #26
0
/** Maintain a ledger of changes to selected accounts */
public class AccountLedger {

  /** Account ledger is enabled */
  private static final boolean ledgerEnabled;

  /** Track all accounts */
  private static final boolean trackAllAccounts;

  /** Accounts to track */
  private static final SortedSet<Long> trackAccounts = new TreeSet<>();

  /** Unconfirmed logging */
  private static final int logUnconfirmed;

  /** Number of blocks to keep when trimming */
  public static final int trimKeep = Nxt.getIntProperty("nxt.ledgerTrimKeep", 30000);

  /** Blockchain */
  private static final Blockchain blockchain = Nxt.getBlockchain();

  /** Blockchain processor */
  private static final BlockchainProcessor blockchainProcessor = Nxt.getBlockchainProcessor();

  /** Pending ledger entries */
  private static final List<LedgerEntry> pendingEntries = new ArrayList<>();

  /** Process nxt.ledgerAccounts */
  static {
    List<String> ledgerAccounts = Nxt.getStringListProperty("nxt.ledgerAccounts");
    ledgerEnabled = !ledgerAccounts.isEmpty();
    trackAllAccounts = ledgerAccounts.contains("*");
    if (ledgerEnabled) {
      if (trackAllAccounts) {
        Logger.logInfoMessage("Account ledger is tracking all accounts");
      } else {
        for (String account : ledgerAccounts) {
          try {
            trackAccounts.add(Convert.parseAccountId(account));
            Logger.logInfoMessage("Account ledger is tracking account " + account);
          } catch (RuntimeException e) {
            Logger.logErrorMessage("Account " + account + " is not valid; ignored");
          }
        }
      }
    } else {
      Logger.logInfoMessage("Account ledger is not enabled");
    }
    int temp = Nxt.getIntProperty("nxt.ledgerLogUnconfirmed", 1);
    logUnconfirmed = (temp >= 0 && temp <= 2 ? temp : 1);
  }

  /** Account ledger table */
  private static class AccountLedgerTable extends DerivedDbTable {

    /** Create the account ledger table */
    public AccountLedgerTable() {
      super("account_ledger");
    }

    /**
     * Insert an entry into the table
     *
     * @param ledgerEntry Ledger entry
     */
    public void insert(LedgerEntry ledgerEntry) {
      try (Connection con = db.getConnection()) {
        ledgerEntry.save(con);
      } catch (SQLException e) {
        throw new RuntimeException(e.toString(), e);
      }
    }

    /**
     * Trim the account ledger table
     *
     * @param height Trim height
     */
    @Override
    public void trim(int height) {
      if (trimKeep <= 0) return;
      try (Connection con = db.getConnection();
          PreparedStatement pstmt =
              con.prepareStatement("DELETE FROM account_ledger WHERE height <= ?")) {
        int trimHeight = Math.max(blockchain.getHeight() - trimKeep, 0);
        pstmt.setInt(1, trimHeight);
        pstmt.executeUpdate();
      } catch (SQLException e) {
        throw new RuntimeException(e.toString(), e);
      }
    }
  }

  private static final AccountLedgerTable accountLedgerTable = new AccountLedgerTable();

  /**
   * Initialization
   *
   * <p>We don't do anything but we need to be called from Nxt.init() in order to register our table
   */
  static void init() {}

  /** Account ledger listener events */
  public enum Event {
    ADD_ENTRY
  }

  /** Account ledger listeners */
  private static final Listeners<LedgerEntry, Event> listeners = new Listeners<>();

  /**
   * Add a listener
   *
   * @param listener Listener
   * @param eventType Event to listen for
   * @return True if the listener was added
   */
  public static boolean addListener(Listener<LedgerEntry> listener, Event eventType) {
    return listeners.addListener(listener, eventType);
  }

  /**
   * Remove a listener
   *
   * @param listener Listener
   * @param eventType Event to listen for
   * @return True if the listener was removed
   */
  public static boolean removeListener(Listener<LedgerEntry> listener, Event eventType) {
    return listeners.removeListener(listener, eventType);
  }

  static boolean mustLogEntry(long accountId, boolean isUnconfirmed) {
    //
    // Must be tracking this account
    //
    if (!ledgerEnabled || (!trackAllAccounts && !trackAccounts.contains(accountId))) {
      return false;
    }
    // confirmed changes only occur while processing block, and unconfirmed changes are
    // only logged while processing block
    if (!blockchainProcessor.isProcessingBlock()) {
      return false;
    }
    //
    // Log unconfirmed changes only when processing a block and logUnconfirmed does not equal 0
    // Log confirmed changes unless logUnconfirmed equals 2
    //
    if (isUnconfirmed && logUnconfirmed == 0) {
      return false;
    }
    if (!isUnconfirmed && logUnconfirmed == 2) {
      return false;
    }
    if (trimKeep > 0 && blockchain.getHeight() <= Constants.LAST_KNOWN_BLOCK - trimKeep) {
      return false;
    }
    //
    // Don't log account changes if we are scanning the blockchain and the current height
    // is less than the minimum account_ledger trim height
    //
    if (blockchainProcessor.isScanning()
        && trimKeep > 0
        && blockchain.getHeight() <= blockchainProcessor.getInitialScanHeight() - trimKeep) {
      return false;
    }
    return true;
  }

  /**
   * Log an event in the account_ledger table
   *
   * @param ledgerEntry Ledger entry
   */
  static void logEntry(LedgerEntry ledgerEntry) {
    //
    // Must be in a database transaction
    //
    if (!Db.db.isInTransaction()) {
      throw new IllegalStateException("Not in transaction");
    }
    //
    // Combine multiple ledger entries
    //
    int index = pendingEntries.indexOf(ledgerEntry);
    if (index >= 0) {
      LedgerEntry existingEntry = pendingEntries.remove(index);
      ledgerEntry.updateChange(existingEntry.getChange());
      long adjustedBalance = existingEntry.getBalance() - existingEntry.getChange();
      for (; index < pendingEntries.size(); index++) {
        existingEntry = pendingEntries.get(index);
        if (existingEntry.getAccountId() == ledgerEntry.getAccountId()
            && existingEntry.getHolding() == ledgerEntry.getHolding()
            && ((existingEntry.getHoldingId() == null && ledgerEntry.getHoldingId() == null)
                || (existingEntry.getHoldingId() != null
                    && existingEntry.getHoldingId().equals(ledgerEntry.getHoldingId())))) {
          adjustedBalance += existingEntry.getChange();
          existingEntry.setBalance(adjustedBalance);
        }
      }
    }
    pendingEntries.add(ledgerEntry);
  }

  /** Commit pending ledger entries */
  static void commitEntries() {
    for (LedgerEntry ledgerEntry : pendingEntries) {
      accountLedgerTable.insert(ledgerEntry);
      listeners.notify(ledgerEntry, Event.ADD_ENTRY);
    }
    pendingEntries.clear();
  }

  /** Clear pending ledger entries */
  static void clearEntries() {
    pendingEntries.clear();
  }

  /**
   * Return a single entry identified by the ledger entry identifier
   *
   * @param ledgerId Ledger entry identifier
   * @return Ledger entry or null if entry not found
   */
  public static LedgerEntry getEntry(long ledgerId) {
    if (!ledgerEnabled) return null;
    LedgerEntry entry;
    try (Connection con = Db.db.getConnection();
        PreparedStatement stmt =
            con.prepareStatement("SELECT * FROM account_ledger WHERE db_id = ?")) {
      stmt.setLong(1, ledgerId);
      try (ResultSet rs = stmt.executeQuery()) {
        if (rs.next()) entry = new LedgerEntry(rs);
        else entry = null;
      }
    } catch (SQLException e) {
      throw new RuntimeException(e.toString(), e);
    }
    return entry;
  }

  /**
   * Return the ledger entries sorted in descending insert order
   *
   * @param accountId Account identifier or zero if no account identifier
   * @param event Ledger event or null
   * @param eventId Ledger event identifier or zero if no event identifier
   * @param holding Ledger holding or null
   * @param holdingId Ledger holding identifier or zero if no holding identifier
   * @param firstIndex First matching entry index, inclusive
   * @param lastIndex Last matching entry index, inclusive
   * @return List of ledger entries
   */
  public static List<LedgerEntry> getEntries(
      long accountId,
      LedgerEvent event,
      long eventId,
      LedgerHolding holding,
      long holdingId,
      int firstIndex,
      int lastIndex) {
    if (!ledgerEnabled) {
      return Collections.emptyList();
    }
    List<LedgerEntry> entryList = new ArrayList<>();
    //
    // Build the SELECT statement to search the entries
    StringBuilder sb = new StringBuilder(128);
    sb.append("SELECT * FROM account_ledger ");
    if (accountId != 0 || event != null || holding != null) {
      sb.append("WHERE ");
    }
    if (accountId != 0) {
      sb.append("account_id = ? ");
    }
    if (event != null) {
      if (accountId != 0) {
        sb.append("AND ");
      }
      sb.append("event_type = ? ");
      if (eventId != 0) sb.append("AND event_id = ? ");
    }
    if (holding != null) {
      if (accountId != 0 || event != null) {
        sb.append("AND ");
      }
      sb.append("holding_type = ? ");
      if (holdingId != 0) sb.append("AND holding_id = ? ");
    }
    sb.append("ORDER BY db_id DESC ");
    sb.append(DbUtils.limitsClause(firstIndex, lastIndex));
    //
    // Get the ledger entries
    //
    blockchain.readLock();
    try (Connection con = Db.db.getConnection();
        PreparedStatement pstmt = con.prepareStatement(sb.toString())) {
      int i = 0;
      if (accountId != 0) {
        pstmt.setLong(++i, accountId);
      }
      if (event != null) {
        pstmt.setByte(++i, (byte) event.getCode());
        if (eventId != 0) {
          pstmt.setLong(++i, eventId);
        }
      }
      if (holding != null) {
        pstmt.setByte(++i, (byte) holding.getCode());
        if (holdingId != 0) {
          pstmt.setLong(++i, holdingId);
        }
      }
      DbUtils.setLimits(++i, pstmt, firstIndex, lastIndex);
      try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
          entryList.add(new LedgerEntry(rs));
        }
      }
    } catch (SQLException e) {
      throw new RuntimeException(e.toString(), e);
    } finally {
      blockchain.readUnlock();
    }
    return entryList;
  }

  /**
   * Ledger events
   *
   * <p>There must be a ledger event defined for each transaction (type,subtype) pair. When adding a
   * new event, do not change the existing code assignments since these codes are stored in the
   * event_type field of the account_ledger table.
   */
  public enum LedgerEvent {
    // Block and Transaction
    BLOCK_GENERATED(1, false),
    REJECT_PHASED_TRANSACTION(2, true),
    TRANSACTION_FEE(50, true),
    // TYPE_PAYMENT
    ORDINARY_PAYMENT(3, true),
    // TYPE_MESSAGING
    ACCOUNT_INFO(4, true),
    ALIAS_ASSIGNMENT(5, true),
    ALIAS_BUY(6, true),
    ALIAS_DELETE(7, true),
    ALIAS_SELL(8, true),
    ARBITRARY_MESSAGE(9, true),
    HUB_ANNOUNCEMENT(10, true),
    PHASING_VOTE_CASTING(11, true),
    POLL_CREATION(12, true),
    VOTE_CASTING(13, true),
    ACCOUNT_PROPERTY(56, true),
    ACCOUNT_PROPERTY_DELETE(57, true),
    // TYPE_COLORED_COINS
    ASSET_ASK_ORDER_CANCELLATION(14, true),
    ASSET_ASK_ORDER_PLACEMENT(15, true),
    ASSET_BID_ORDER_CANCELLATION(16, true),
    ASSET_BID_ORDER_PLACEMENT(17, true),
    ASSET_DIVIDEND_PAYMENT(18, true),
    ASSET_ISSUANCE(19, true),
    ASSET_TRADE(20, true),
    ASSET_TRANSFER(21, true),
    ASSET_DELETE(49, true),
    // TYPE_DIGITAL_GOODS
    DIGITAL_GOODS_DELISTED(22, true),
    DIGITAL_GOODS_DELISTING(23, true),
    DIGITAL_GOODS_DELIVERY(24, true),
    DIGITAL_GOODS_FEEDBACK(25, true),
    DIGITAL_GOODS_LISTING(26, true),
    DIGITAL_GOODS_PRICE_CHANGE(27, true),
    DIGITAL_GOODS_PURCHASE(28, true),
    DIGITAL_GOODS_PURCHASE_EXPIRED(29, true),
    DIGITAL_GOODS_QUANTITY_CHANGE(30, true),
    DIGITAL_GOODS_REFUND(31, true),
    // TYPE_ACCOUNT_CONTROL
    ACCOUNT_CONTROL_EFFECTIVE_BALANCE_LEASING(32, true),
    ACCOUNT_CONTROL_PHASING_ONLY(55, true),
    // TYPE_CURRENCY
    CURRENCY_DELETION(33, true),
    CURRENCY_DISTRIBUTION(34, true),
    CURRENCY_EXCHANGE(35, true),
    CURRENCY_EXCHANGE_BUY(36, true),
    CURRENCY_EXCHANGE_SELL(37, true),
    CURRENCY_ISSUANCE(38, true),
    CURRENCY_MINTING(39, true),
    CURRENCY_OFFER_EXPIRED(40, true),
    CURRENCY_OFFER_REPLACED(41, true),
    CURRENCY_PUBLISH_EXCHANGE_OFFER(42, true),
    CURRENCY_RESERVE_CLAIM(43, true),
    CURRENCY_RESERVE_INCREASE(44, true),
    CURRENCY_TRANSFER(45, true),
    CURRENCY_UNDO_CROWDFUNDING(46, true),
    // TYPE_DATA
    TAGGED_DATA_UPLOAD(47, true),
    TAGGED_DATA_EXTEND(48, true),
    // TYPE_SHUFFLING
    SHUFFLING_REGISTRATION(51, true),
    SHUFFLING_PROCESSING(52, true),
    SHUFFLING_CANCELLATION(53, true),
    SHUFFLING_DISTRIBUTION(54, true);

    /** Event code mapping */
    private static final Map<Integer, LedgerEvent> eventMap = new HashMap<>();

    static {
      for (LedgerEvent event : values()) {
        if (eventMap.put(event.code, event) != null) {
          throw new RuntimeException("LedgerEvent code " + event.code + " reused");
        }
      }
    }

    /** Event code */
    private final int code;

    /** Event identifier is a transaction */
    private final boolean isTransaction;

    /**
     * Create the ledger event
     *
     * @param code Event code
     * @param isTransaction Event identifier is a transaction
     */
    LedgerEvent(int code, boolean isTransaction) {
      this.code = code;
      this.isTransaction = isTransaction;
    }

    /**
     * Check if the event identifier is a transaction
     *
     * @return TRUE if the event identifier is a transaction
     */
    public boolean isTransaction() {
      return isTransaction;
    }

    /**
     * Return the event code
     *
     * @return Event code
     */
    public int getCode() {
      return code;
    }

    /**
     * Get the event from the event code
     *
     * @param code Event code
     * @return Event
     */
    public static LedgerEvent fromCode(int code) {
      LedgerEvent event = eventMap.get(code);
      if (event == null) {
        throw new IllegalArgumentException("LedgerEvent code " + code + " is unknown");
      }
      return event;
    }
  }

  /**
   * Ledger holdings
   *
   * <p>When adding a new holding, do not change the existing code assignments since they are stored
   * in the holding_type field of the account_ledger table.
   */
  public enum LedgerHolding {
    UNCONFIRMED_NXT_BALANCE(1, true),
    NXT_BALANCE(2, false),
    UNCONFIRMED_ASSET_BALANCE(3, true),
    ASSET_BALANCE(4, false),
    UNCONFIRMED_CURRENCY_BALANCE(5, true),
    CURRENCY_BALANCE(6, false);

    /** Holding code mapping */
    private static final Map<Integer, LedgerHolding> holdingMap = new HashMap<>();

    static {
      for (LedgerHolding holding : values()) {
        if (holdingMap.put(holding.code, holding) != null) {
          throw new RuntimeException("LedgerHolding code " + holding.code + " reused");
        }
      }
    }

    /** Holding code */
    private final int code;

    /** Unconfirmed holding */
    private final boolean isUnconfirmed;

    /**
     * Create the holding event
     *
     * @param code Holding code
     * @param isUnconfirmed TRUE if the holding is unconfirmed
     */
    LedgerHolding(int code, boolean isUnconfirmed) {
      this.code = code;
      this.isUnconfirmed = isUnconfirmed;
    }

    /**
     * Check if the holding is unconfirmed
     *
     * @return TRUE if the holding is unconfirmed
     */
    public boolean isUnconfirmed() {
      return this.isUnconfirmed;
    }

    /**
     * Return the holding code
     *
     * @return Holding code
     */
    public int getCode() {
      return code;
    }

    /**
     * Get the holding from the holding code
     *
     * @param code Holding code
     * @return Holding
     */
    public static LedgerHolding fromCode(int code) {
      LedgerHolding holding = holdingMap.get(code);
      if (holding == null) {
        throw new IllegalArgumentException("LedgerHolding code " + code + " is unknown");
      }
      return holding;
    }
  }

  /** Ledger entry */
  public static class LedgerEntry {

    /** Ledger identifier */
    private long ledgerId = -1;

    /** Ledger event */
    private final LedgerEvent event;

    /** Associated event identifier */
    private final long eventId;

    /** Account identifier */
    private final long accountId;

    /** Holding */
    private final LedgerHolding holding;

    /** Holding identifier */
    private final Long holdingId;

    /** Change in balance */
    private long change;

    /** New balance */
    private long balance;

    /** Block identifier */
    private final long blockId;

    /** Blockchain height */
    private final int height;

    /** Block timestamp */
    private final int timestamp;

    /**
     * Create a ledger entry
     *
     * @param event Event
     * @param eventId Event identifier
     * @param accountId Account identifier
     * @param holding Holding or null
     * @param holdingId Holding identifier or null
     * @param change Change in balance
     * @param balance New balance
     */
    public LedgerEntry(
        LedgerEvent event,
        long eventId,
        long accountId,
        LedgerHolding holding,
        Long holdingId,
        long change,
        long balance) {
      this.event = event;
      this.eventId = eventId;
      this.accountId = accountId;
      this.holding = holding;
      this.holdingId = holdingId;
      this.change = change;
      this.balance = balance;
      Block block = blockchain.getLastBlock();
      this.blockId = block.getId();
      this.height = block.getHeight();
      this.timestamp = block.getTimestamp();
    }

    /**
     * Create a ledger entry
     *
     * @param event Event
     * @param eventId Event identifier
     * @param accountId Account identifier
     * @param change Change in balance
     * @param balance New balance
     */
    public LedgerEntry(LedgerEvent event, long eventId, long accountId, long change, long balance) {
      this(event, eventId, accountId, null, null, change, balance);
    }

    /**
     * Create a ledger entry from a database entry
     *
     * @param rs Result set
     * @throws SQLException Database error occurred
     */
    private LedgerEntry(ResultSet rs) throws SQLException {
      ledgerId = rs.getLong("db_id");
      event = LedgerEvent.fromCode(rs.getByte("event_type"));
      eventId = rs.getLong("event_id");
      accountId = rs.getLong("account_id");
      int holdingType = rs.getByte("holding_type");
      if (holdingType >= 0) {
        holding = LedgerHolding.fromCode(holdingType);
      } else {
        holding = null;
      }
      long id = rs.getLong("holding_id");
      if (rs.wasNull()) {
        holdingId = null;
      } else {
        holdingId = id;
      }
      change = rs.getLong("change");
      balance = rs.getLong("balance");
      blockId = rs.getLong("block_id");
      height = rs.getInt("height");
      timestamp = rs.getInt("timestamp");
    }

    /**
     * Return the ledger identifier
     *
     * @return Ledger identifier or -1 if not set
     */
    public long getLedgerId() {
      return ledgerId;
    }

    /**
     * Return the ledger event
     *
     * @return Ledger event
     */
    public LedgerEvent getEvent() {
      return event;
    }

    /**
     * Return the associated event identifier
     *
     * @return Event identifier
     */
    public long getEventId() {
      return eventId;
    }

    /**
     * Return the account identifier
     *
     * @return Account identifier
     */
    public long getAccountId() {
      return accountId;
    }

    /**
     * Return the holding
     *
     * @return Holding or null if there is no holding
     */
    public LedgerHolding getHolding() {
      return holding;
    }

    /**
     * Return the holding identifier
     *
     * @return Holding identifier or null if there is no holding identifier
     */
    public Long getHoldingId() {
      return holdingId;
    }

    /**
     * Update the balance change
     *
     * @param amount Change amount
     */
    private void updateChange(long amount) {
      change += amount;
    }

    /**
     * Return the balance change
     *
     * @return Balance changes
     */
    public long getChange() {
      return change;
    }

    /**
     * Set the new balance
     *
     * @param balance New balance
     */
    private void setBalance(long balance) {
      this.balance = balance;
    }

    /**
     * Return the new balance
     *
     * @return New balance
     */
    public long getBalance() {
      return balance;
    }

    /**
     * Return the block identifier
     *
     * @return Block identifier
     */
    public long getBlockId() {
      return blockId;
    }

    /**
     * Return the height
     *
     * @return Height
     */
    public int getHeight() {
      return height;
    }

    /**
     * Return the timestamp
     *
     * @return Timestamp
     */
    public int getTimestamp() {
      return timestamp;
    }

    /**
     * Return the hash code
     *
     * @return Hash code
     */
    @Override
    public int hashCode() {
      return (Long.hashCode(accountId)
          ^ event.getCode()
          ^ Long.hashCode(eventId)
          ^ (holding != null ? holding.getCode() : 0)
          ^ (holdingId != null ? Long.hashCode(holdingId) : 0));
    }

    /**
     * Check if two ledger events are equal
     *
     * @param obj Ledger event to check
     * @return TRUE if the ledger events are the same
     */
    @Override
    public boolean equals(Object obj) {
      return (obj != null
          && (obj instanceof LedgerEntry)
          && accountId == ((LedgerEntry) obj).accountId
          && event == ((LedgerEntry) obj).event
          && eventId == ((LedgerEntry) obj).eventId
          && holding == ((LedgerEntry) obj).holding
          && (holdingId != null
              ? holdingId.equals(((LedgerEntry) obj).holdingId)
              : ((LedgerEntry) obj).holdingId == null));
    }

    /**
     * Save the ledger entry
     *
     * @param con Database connection
     * @throws SQLException Database error occurred
     */
    private void save(Connection con) throws SQLException {
      try (PreparedStatement stmt =
          con.prepareStatement(
              "INSERT INTO account_ledger "
                  + "(account_id, event_type, event_id, holding_type, holding_id, change, balance, "
                  + "block_id, height, timestamp) "
                  + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
              Statement.RETURN_GENERATED_KEYS)) {
        int i = 0;
        stmt.setLong(++i, accountId);
        stmt.setByte(++i, (byte) event.getCode());
        stmt.setLong(++i, eventId);
        if (holding != null) {
          stmt.setByte(++i, (byte) holding.getCode());
        } else {
          stmt.setByte(++i, (byte) -1);
        }
        DbUtils.setLong(stmt, ++i, holdingId);
        stmt.setLong(++i, change);
        stmt.setLong(++i, balance);
        stmt.setLong(++i, blockId);
        stmt.setInt(++i, height);
        stmt.setInt(++i, timestamp);
        stmt.executeUpdate();
        try (ResultSet rs = stmt.getGeneratedKeys()) {
          if (rs.next()) {
            ledgerId = rs.getLong(1);
          }
        }
      }
    }
  }
}