diff --git a/db/dcrsqlite/apisource.go b/db/dcrsqlite/apisource.go index 6c58947096..acdb624dd1 100644 --- a/db/dcrsqlite/apisource.go +++ b/db/dcrsqlite/apisource.go @@ -1148,13 +1148,18 @@ func (db *wiredDB) GetExplorerTx(txid string) *explorer.TxInfo { inputs := make([]explorer.Vin, 0, len(txraw.Vin)) for i, vin := range txraw.Vin { var addresses []string + // ValueIn is a temporary fix until amountIn is correct from dcrd call + var ValueIn dcrutil.Amount if !(vin.IsCoinBase() || (vin.IsStakeBase() && i == 0)) { - addrs, err := txhelpers.OutPointAddresses(&msgTx.TxIn[i].PreviousOutPoint, db.client, db.params) + var addrs []string + addrs, ValueIn, err = txhelpers.OutPointAddresses(&msgTx.TxIn[i].PreviousOutPoint, db.client, db.params) if err != nil { log.Warnf("Failed to get outpoint address from txid: %v", err) continue } addresses = addrs + } else { + ValueIn, _ = dcrutil.NewAmount(vin.AmountIn) } inputs = append(inputs, explorer.Vin{ Vin: &dcrjson.Vin{ @@ -1162,11 +1167,11 @@ func (db *wiredDB) GetExplorerTx(txid string) *explorer.TxInfo { Coinbase: vin.Coinbase, Stakebase: vin.Stakebase, Vout: vin.Vout, - AmountIn: vin.AmountIn, + AmountIn: ValueIn.ToCoin(), BlockHeight: vin.BlockHeight, }, Addresses: addresses, - FormattedAmount: humanize.Commaf(vin.AmountIn), + FormattedAmount: humanize.Commaf(ValueIn.ToCoin()), }) } tx.Vin = inputs @@ -1360,7 +1365,6 @@ func (db *wiredDB) UnconfirmedTxnsForAddress(address string) (*txhelpers.Address } Tx, err1 := db.client.GetRawTransaction(txhash) - if err1 != nil { log.Warnf("Unable to GetRawTransaction(%s): %v", tx, err1) err = err1 @@ -1387,6 +1391,7 @@ func (db *wiredDB) UnconfirmedTxnsForAddress(address string) (*txhelpers.Address // Merge the I/Os and the transactions into results addressOutpoints.Update(prevTxns, outpoints, prevouts) } + return addressOutpoints, numUnconfirmed, err } diff --git a/explorer/explorerroutes.go b/explorer/explorerroutes.go index d449d6a118..d00cc91ed4 100644 --- a/explorer/explorerroutes.go +++ b/explorer/explorerroutes.go @@ -9,6 +9,7 @@ import ( "io" "math" "net/http" + "sort" "strconv" "strings" @@ -412,90 +413,61 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { // Get addresses table rows for the address addrHist, balance, errH := exp.explorerSource.AddressHistory( address, limitN, offsetAddrOuts, txnType) - // Fallback to RPC if DB query fails - if errH != nil { - log.Errorf("Unable to get address %s history: %v", address, errH) - addrData = exp.blockData.GetExplorerAddress(address, limitN, offsetAddrOuts) + + if errH == nil { + // Generate AddressInfo skeleton from the address table rows + addrData = ReduceAddressHistory(addrHist) if addrData == nil { - log.Errorf("Unable to get address %s", address) - exp.StatusPage(w, defaultErrorCode, "could not find that address", NotFoundStatusType) - return + // Empty history is not expected for credit txnType with any txns. + if txnType != dbtypes.AddrTxnDebit && (balance.NumSpent+balance.NumUnspent) > 0 { + log.Debugf("empty address history (%s): n=%d&start=%d", address, limitN, offsetAddrOuts) + exp.StatusPage(w, defaultErrorCode, "that address has no history", NotFoundStatusType) + return + } + // No mined transactions + addrData = new(AddressInfo) + addrData.Address = address } - - // Set page parameters - addrData.Path = r.URL.Path - addrData.Limit, addrData.Offset = limitN, offsetAddrOuts - addrData.TxnType = txnType.String() - - confirmHeights := make([]int64, len(addrData.Transactions)) - for i, v := range addrData.Transactions { - confirmHeights[i] = exp.NewBlockData.Height - int64(v.Confirmations) + addrData.Fullmode = true + + // Balances and txn counts (partial unless in full mode) + addrData.Balance = balance + addrData.KnownTransactions = (balance.NumSpent * 2) + balance.NumUnspent + addrData.KnownFundingTxns = balance.NumSpent + balance.NumUnspent + addrData.KnownSpendingTxns = balance.NumSpent + + // Transactions to fetch with FillAddressTransactions. This should be a + // noop if ReduceAddressHistory is working right. + switch txnType { + case dbtypes.AddrTxnAll: + case dbtypes.AddrTxnCredit: + addrData.Transactions = addrData.TxnsFunding + case dbtypes.AddrTxnDebit: + addrData.Transactions = addrData.TxnsSpending + default: + log.Warnf("Unknown address transaction type: %v", txnType) } - pageData := AddressPageData{ - Data: addrData, - ConfirmHeight: confirmHeights, - Version: exp.Version, + // Transactions on current page + addrData.NumTransactions = int64(len(addrData.Transactions)) + if addrData.NumTransactions > limitN { + addrData.NumTransactions = limitN } - str, err := exp.templates.execTemplateToString("address", pageData) - if err != nil { - log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) - return - } - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - io.WriteString(w, str) - return - } - // Generate AddressInfo skeleton from the address table rows - addrData = ReduceAddressHistory(addrHist) - if addrData == nil { - // Empty history is not expected for credit txnType with any txns. - if txnType != dbtypes.AddrTxnDebit && (balance.NumSpent+balance.NumUnspent) > 0 { - log.Debugf("empty address history (%s): n=%d&start=%d", address, limitN, offsetAddrOuts) - exp.StatusPage(w, defaultErrorCode, "that address has no history", NotFoundStatusType) + // Query database for transaction details + err = exp.explorerSource.FillAddressTransactions(addrData) + if err != nil { + log.Errorf("Unable to fill address %s transactions: %v", address, err) + exp.StatusPage(w, defaultErrorCode, "could not find transactions for that address", NotFoundStatusType) return } - // No mined transactions + } else { + // We do not have any confirmed transactions. Prep to display ONLY + // unconfirmed transactions (or none at all) addrData = new(AddressInfo) addrData.Address = address - } - addrData.Fullmode = true - - // Balances and txn counts (partial unless in full mode) - addrData.Balance = balance - addrData.KnownTransactions = (balance.NumSpent * 2) + balance.NumUnspent - addrData.KnownFundingTxns = balance.NumSpent + balance.NumUnspent - addrData.KnownSpendingTxns = balance.NumSpent - addrData.KnownMergedSpendingTxns = balance.NumMergedSpent - - // Transactions to fetch with FillAddressTransactions. This should be a - // noop if ReduceAddressHistory is working right. - switch txnType { - case dbtypes.AddrTxnAll, dbtypes.AddrMergedTxnDebit: - case dbtypes.AddrTxnCredit: - addrData.Transactions = addrData.TxnsFunding - case dbtypes.AddrTxnDebit: - addrData.Transactions = addrData.TxnsSpending - default: - log.Warnf("Unknown address transaction type: %v", txnType) - } - - // Transactions on current page - addrData.NumTransactions = int64(len(addrData.Transactions)) - if addrData.NumTransactions > limitN { - addrData.NumTransactions = limitN - } - - // Query database for transaction details - err = exp.explorerSource.FillAddressTransactions(addrData) - if err != nil { - log.Errorf("Unable to fill address %s transactions: %v", address, err) - exp.StatusPage(w, defaultErrorCode, "could not find transactions for that address", ErrorStatusType) - return + addrData.Fullmode = true + addrData.Balance = &AddressBalance{} } // Check for unconfirmed transactions @@ -507,10 +479,20 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { if addrData.UnconfirmedTxns == nil { addrData.UnconfirmedTxns = new(AddressTransactions) } - uctxn := addrData.UnconfirmedTxns - // Funding transactions (unconfirmed) + var received, sent, numReceived, numSent int64 + FUNDING_TX_DUPLICATE_CHECK: for _, f := range addressOuts.Outpoints { + //Mempool transactions stick around for 2 blocks. The first block + //incorporates the transaction and mines it. The second block + //validates it by the stake. However, transactions move into our + //database as soon as they are mined and thus we need to be careful + //to not include those transactions in our list. + for _, b := range addrData.Transactions { + if f.Hash.String() == b.TxID && f.Index == b.InOutID { + continue FUNDING_TX_DUPLICATE_CHECK + } + } fundingTx, ok := addressOuts.TxnsStore[f.Hash] if !ok { log.Errorf("An outpoint's transaction is not available in TxnStore.") @@ -520,19 +502,34 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { log.Errorf("An outpoint's transaction is unexpectedly confirmed.") continue } - addrTx := &AddressTx{ - TxID: fundingTx.Hash().String(), - InOutID: f.Index, - FormattedSize: humanize.Bytes(uint64(fundingTx.Tx.SerializeSize())), - Total: txhelpers.TotalOutFromMsgTx(fundingTx.Tx).ToCoin(), - ReceivedTotal: dcrutil.Amount(fundingTx.Tx.TxOut[f.Index].Value).ToCoin(), + if txnType == dbtypes.AddrTxnAll || txnType == dbtypes.AddrTxnCredit { + addrTx := &AddressTx{ + TxID: fundingTx.Hash().String(), + InOutID: f.Index, + Time: fundingTx.MemPoolTime, + FormattedSize: humanize.Bytes(uint64(fundingTx.Tx.SerializeSize())), + Total: txhelpers.TotalOutFromMsgTx(fundingTx.Tx).ToCoin(), + ReceivedTotal: dcrutil.Amount(fundingTx.Tx.TxOut[f.Index].Value).ToCoin(), + } + addrData.Transactions = append(addrData.Transactions, addrTx) } - uctxn.Transactions = append(uctxn.Transactions, addrTx) - uctxn.TxnsFunding = append(uctxn.TxnsFunding, addrTx) - } + received += fundingTx.Tx.TxOut[f.Index].Value + numReceived++ + } // Spending transactions (unconfirmed) + SPENDING_TX_DUPLICATE_CHECK: for _, f := range addressOuts.PrevOuts { + //Mempool transactions stick around for 2 blocks. The first block + //incorporates the transaction and mines it. The second block + //validates it by the stake. However, transactions move into our + //database as soon as they are mined and thus we need to be careful + //to not include those transactions in our list. + for _, b := range addrData.Transactions { + if f.TxSpending.String() == b.TxID && f.InputIndex == int(b.InOutID) { + continue SPENDING_TX_DUPLICATE_CHECK + } + } spendingTx, ok := addressOuts.TxnsStore[f.TxSpending] if !ok { log.Errorf("An outpoint's transaction is not available in TxnStore.") @@ -542,16 +539,32 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { log.Errorf("An outpoint's transaction is unexpectedly confirmed.") continue } - addrTx := &AddressTx{ - TxID: spendingTx.Hash().String(), - InOutID: uint32(f.InputIndex), - FormattedSize: humanize.Bytes(uint64(spendingTx.Tx.SerializeSize())), - Total: txhelpers.TotalOutFromMsgTx(spendingTx.Tx).ToCoin(), - SentTotal: dcrutil.Amount(spendingTx.Tx.TxIn[f.InputIndex].ValueIn).ToCoin(), + + // sent total sats has to be a lookup of the vout:i prevout value + // because vin:i valuein is not reliable from dcrd at present + prevhash := spendingTx.Tx.TxIn[f.InputIndex].PreviousOutPoint.Hash + previndex := spendingTx.Tx.TxIn[f.InputIndex].PreviousOutPoint.Index + valuein := addressOuts.TxnsStore[prevhash].Tx.TxOut[previndex].Value + + if txnType == dbtypes.AddrTxnAll || txnType == dbtypes.AddrTxnDebit { + addrTx := &AddressTx{ + TxID: spendingTx.Hash().String(), + InOutID: uint32(f.InputIndex), + Time: spendingTx.MemPoolTime, + FormattedSize: humanize.Bytes(uint64(spendingTx.Tx.SerializeSize())), + Total: txhelpers.TotalOutFromMsgTx(spendingTx.Tx).ToCoin(), + SentTotal: dcrutil.Amount(valuein).ToCoin(), + } + addrData.Transactions = append(addrData.Transactions, addrTx) } - uctxn.Transactions = append(uctxn.Transactions, addrTx) - uctxn.TxnsSpending = append(uctxn.TxnsSpending, addrTx) + + sent += valuein + numSent++ } + addrData.Balance.NumSpent += numSent + addrData.Balance.NumUnspent += (numReceived - numSent) + addrData.Balance.TotalSpent += sent + addrData.Balance.TotalUnspent += (received - sent) } // Set page parameters @@ -564,6 +577,21 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { confirmHeights[i] = exp.NewBlockData.Height - int64(v.Confirmations) } + sort.Slice(addrData.Transactions, func(i, j int) bool { + if addrData.Transactions[i].Time == addrData.Transactions[j].Time { + return addrData.Transactions[i].InOutID > addrData.Transactions[j].InOutID + } + return addrData.Transactions[i].Time > addrData.Transactions[j].Time + }) + + // addresscount := len(addressRows) + // if addresscount > 0 { + // calcoffset := int(math.Min(float64(addresscount), float64(offset))) + // calcN := int(math.Min(float64(offset+N), float64(addresscount))) + // log.Infof("Slicing result set which is %d addresses long to offset: %d and N: %d", addresscount, calcoffset, calcN) + // addressRows = addressRows[calcoffset:calcN] + // } + pageData := AddressPageData{ Data: addrData, ConfirmHeight: confirmHeights, diff --git a/txhelpers/txhelpers.go b/txhelpers/txhelpers.go index 2f1b480b4b..2b0437b96d 100644 --- a/txhelpers/txhelpers.go +++ b/txhelpers/txhelpers.go @@ -399,17 +399,17 @@ func BlockReceivesToAddresses(block *dcrutil.Block, addrs map[string]TxAction, // OutPointAddresses gets the addresses paid to by a transaction output. func OutPointAddresses(outPoint *wire.OutPoint, c RawTransactionGetter, - params *chaincfg.Params) ([]string, error) { + params *chaincfg.Params) ([]string, dcrutil.Amount, error) { // The addresses are encoded in the pkScript, so we need to get the // raw transaction, and the TxOut that contains the pkScript. prevTx, err := c.GetRawTransaction(&outPoint.Hash) if err != nil { - return nil, fmt.Errorf("unable to get raw transaction for %s", outPoint.Hash.String()) + return nil, 0, fmt.Errorf("unable to get raw transaction for %s", outPoint.Hash.String()) } txOuts := prevTx.MsgTx().TxOut if len(txOuts) <= int(outPoint.Index) { - return nil, fmt.Errorf("PrevOut index (%d) is beyond the TxOuts slice (length %d)", + return nil, 0, fmt.Errorf("PrevOut index (%d) is beyond the TxOuts slice (length %d)", outPoint.Index, len(txOuts)) } @@ -418,15 +418,15 @@ func OutPointAddresses(outPoint *wire.OutPoint, c RawTransactionGetter, _, txAddrs, _, err := txscript.ExtractPkScriptAddrs( txOut.Version, txOut.PkScript, params) if err != nil { - return nil, fmt.Errorf("ExtractPkScriptAddrs: %v", err.Error()) + return nil, 0, fmt.Errorf("ExtractPkScriptAddrs: %v", err.Error()) } - + value := dcrutil.Amount(txOut.Value) addresses := make([]string, 0, len(txAddrs)) for _, txAddr := range txAddrs { addr := txAddr.EncodeAddress() addresses = append(addresses, addr) } - return addresses, nil + return addresses, value, nil } // OutPointAddressesFromString is the same as OutPointAddresses, but it takes @@ -439,8 +439,8 @@ func OutPointAddressesFromString(txid string, index uint32, tree int8, } outPoint := wire.NewOutPoint(hash, index, tree) - - return OutPointAddresses(outPoint, c, params) + outPointAddress, _, err := OutPointAddresses(outPoint, c, params) + return outPointAddress, err } // MedianAmount gets the median Amount from a slice of Amounts diff --git a/views/address.tmpl b/views/address.tmpl index 3b8515787a..4b72aa6255 100644 --- a/views/address.tmpl +++ b/views/address.tmpl @@ -188,7 +188,7 @@ {{end}} {{end}}