Skip to content

Commit

Permalink
Unconfirmed tx in address list and input amounts on unconfirmed tx (d…
Browse files Browse the repository at this point in the history
…ecred#489)

This addresses the unconfirmed transactions not visible in address view and input amounts not visible in transaction view.

There were two major areas of work:
* Blending unconfirmed transactions with confirmed transactions in the current address view. Mostly extending what @chappjc already started.
* Compensating for AmountIn values not set in dcrd calls to mempool transactions.
  • Loading branch information
papacarp authored and JFixby committed Aug 1, 2018
1 parent 4aa6f65 commit c6f9d7e
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 106 deletions.
13 changes: 9 additions & 4 deletions db/dcrsqlite/apisource.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,25 +1148,30 @@ 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{
Txid: vin.Txid,
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
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
214 changes: 121 additions & 93 deletions explorer/explorerroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"math"
"net/http"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -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
Expand All @@ -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.")
Expand All @@ -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.")
Expand All @@ -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
Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions txhelpers/txhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion views/address.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
{{end}}
{{end}}
<td>
{{if eq .Time 0}}
{{if eq .Confirmations 0}}
Unconfirmed
{{else}}
{{.FormattedTime}}
Expand Down

0 comments on commit c6f9d7e

Please sign in to comment.