diff --git a/api/insight/apiroutes.go b/api/insight/apiroutes.go index b6d39b8ae0..54458c8e89 100644 --- a/api/insight/apiroutes.go +++ b/api/insight/apiroutes.go @@ -874,7 +874,7 @@ func (c *insightApiContext) getAddressInfo(w http.ResponseWriter, r *http.Reques // Get Confirmed Balances var unconfirmedBalanceSat int64 - _, _, totalSpent, totalUnspent, err := c.BlockData.ChainDB.RetrieveAddressSpentUnspent(address) + _, _, totalSpent, totalUnspent, _, err := c.BlockData.ChainDB.RetrieveAddressSpentUnspent(address) if err != nil { return } diff --git a/db/dbtypes/types.go b/db/dbtypes/types.go index b8e003a225..ccc19cf591 100644 --- a/db/dbtypes/types.go +++ b/db/dbtypes/types.go @@ -44,15 +44,17 @@ const ( AddrTxnAll AddrTxnType = iota AddrTxnCredit AddrTxnDebit + AddrMergedTxnDebit AddrTxnUnknown ) // AddrTxnTypes is the canonical mapping from AddrTxnType to string. var AddrTxnTypes = map[AddrTxnType]string{ - AddrTxnAll: "all", - AddrTxnCredit: "credit", - AddrTxnDebit: "debit", - AddrTxnUnknown: "unknown", + AddrTxnAll: "all", + AddrTxnCredit: "credit", + AddrTxnDebit: "debit", + AddrMergedTxnDebit: "merged debit", + AddrTxnUnknown: "unknown", } func (a AddrTxnType) String() string { @@ -73,6 +75,8 @@ func AddrTxnTypeFromStr(txnType string) AddrTxnType { fallthrough case "debits": return AddrTxnDebit + case "merged debit": + return AddrMergedTxnDebit default: return AddrTxnUnknown } @@ -297,15 +301,16 @@ type Vout struct { type AddressRow struct { // id int64 Address string - // MatchingTxHash provides the relationship between spending tx inputs and - // funding tx outputs. - MatchingTxHash string - IsFunding bool - TxBlockTime uint64 - TxHash string - TxVinVoutIndex uint32 - Value uint64 - VinVoutDbID uint64 + // MatchingTxHash that provides the relationship + // between spending tx inputs and funding tx outputs + MatchingTxHash string + IsFunding bool + TxBlockTime uint64 + TxHash string + TxVinVoutIndex uint32 + Value uint64 + VinVoutDbID uint64 + MergedDebitCount uint64 } // ChartsData defines the fields that store the values needed to plot the charts diff --git a/db/dcrpg/insightapi.go b/db/dcrpg/insightapi.go index 8de6028a7c..c3995079a2 100644 --- a/db/dcrpg/insightapi.go +++ b/db/dcrpg/insightapi.go @@ -65,7 +65,7 @@ func (pgb *ChainDB) InsightPgGetAddressTransactions(addr []string, // RetrieveAddressSpentUnspent retrieves balance information for a specific // address. -func (pgb *ChainDB) RetrieveAddressSpentUnspent(address string) (int64, int64, int64, int64, error) { +func (pgb *ChainDB) RetrieveAddressSpentUnspent(address string) (int64, int64, int64, int64, int64, error) { return RetrieveAddressSpentUnspent(pgb.db, address) } diff --git a/db/dcrpg/internal/addrstmts.go b/db/dcrpg/internal/addrstmts.go index 8bc47dd998..b6111a6bda 100644 --- a/db/dcrpg/internal/addrstmts.go +++ b/db/dcrpg/internal/addrstmts.go @@ -54,7 +54,10 @@ const ( WHERE address = $1 and is_funding = TRUE and matching_tx_hash = '';` SelectAddressSpentCountAndValue = `SELECT COUNT(*), SUM(value) FROM addresses - WHERE address = $1 and is_funding = FALSE and matching_tx_hash != '';` + WHERE address = $1 and is_funding = FALSE and matching_tx_hash != '';` + + SelectAddressesMergedSpentCount = `SELECT COUNT( distinct tx_hash ) FROM addresses + WHERE address = $1 and is_funding = false` SelectAddressUnspentWithTxn = `SELECT addresses.address, addresses.tx_hash, addresses.value, transactions.block_height, addresses.block_time, tx_vin_vout_index, pkscript @@ -72,7 +75,11 @@ const ( SelectAddressLimitNByAddressSubQry = `WITH these as (SELECT ` + addrsColumnNames + ` FROM addresses WHERE address=$1) - SELECT * FROM these ORDER BY block_time DESC LIMIT $2 OFFSET $3;` + SELECT * FROM these order by block_time desc limit $2 offset $3;` + + SelectAddressMergedDebitView = `SELECT tx_hash, block_time, sum(value), + COUNT(*) FROM addresses WHERE address=$1 AND is_funding = FALSE + GROUP BY (tx_hash, block_time) ORDER BY block_time DESC LIMIT $2 OFFSET $3;` SelectAddressDebitsLimitNByAddress = `SELECT ` + addrsColumnNames + ` FROM addresses WHERE address=$1 and is_funding = FALSE diff --git a/db/dcrpg/pgblockchain.go b/db/dcrpg/pgblockchain.go index bf8c7138d7..cad3cd3a96 100644 --- a/db/dcrpg/pgblockchain.go +++ b/db/dcrpg/pgblockchain.go @@ -412,6 +412,9 @@ func (pgb *ChainDB) AddressTransactions(address string, N, offset int64, } case dbtypes.AddrTxnDebit: addrFunc = RetrieveAddressDebitTxns + + case dbtypes.AddrMergedTxnDebit: + addrFunc = RetrieveAddressMergedDebitTxns default: return nil, fmt.Errorf("unknown AddrTxnType %v", txnType) } @@ -540,18 +543,19 @@ func (pgb *ChainDB) addressBalance(address string) (*explorer.AddressBalance, er } if !fresh { - var numSpent, numUnspent, totalSpent, totalUnspent int64 - numSpent, numUnspent, totalSpent, totalUnspent, err = + var numSpent, numUnspent, totalSpent, totalUnspent, totalMergedSpent int64 + numSpent, numUnspent, totalSpent, totalUnspent, totalMergedSpent, err = RetrieveAddressSpentUnspent(pgb.db, address) if err != nil { return nil, err } balanceInfo = explorer.AddressBalance{ - Address: address, - NumSpent: numSpent, - NumUnspent: numUnspent, - TotalSpent: totalSpent, - TotalUnspent: totalUnspent, + Address: address, + NumSpent: numSpent, + NumUnspent: numUnspent, + NumMergedSpent: totalMergedSpent, + TotalSpent: totalSpent, + TotalUnspent: totalUnspent, } totals.balance[address] = balanceInfo @@ -616,18 +620,19 @@ func (pgb *ChainDB) AddressHistory(address string, N, offset int64, TotalUnspent: int64(addrInfo.AmountUnspent), } } else { - var numSpent, numUnspent, totalSpent, totalUnspent int64 - numSpent, numUnspent, totalSpent, totalUnspent, err = + var numSpent, numUnspent, totalSpent, totalUnspent, totalMergedSpent int64 + numSpent, numUnspent, totalSpent, totalUnspent, totalMergedSpent, err = RetrieveAddressSpentUnspent(pgb.db, address) if err != nil { return nil, nil, err } balanceInfo = explorer.AddressBalance{ - Address: address, - NumSpent: numSpent, - NumUnspent: numUnspent, - TotalSpent: totalSpent, - TotalUnspent: totalUnspent, + Address: address, + NumSpent: numSpent, + NumUnspent: numUnspent, + NumMergedSpent: totalMergedSpent, + TotalSpent: totalSpent, + TotalUnspent: totalUnspent, } } @@ -737,7 +742,7 @@ func (pgb *ChainDB) addressInfo(addr string, count, skip int64, // Transactions to fetch with FillAddressTransactions. This should be a // noop if AddressHistory/ReduceAddressHistory are working right. switch txnType { - case dbtypes.AddrTxnAll: + case dbtypes.AddrTxnAll, dbtypes.AddrMergedTxnDebit: case dbtypes.AddrTxnCredit: addrData.Transactions = addrData.TxnsFunding case dbtypes.AddrTxnDebit: diff --git a/db/dcrpg/queries.go b/db/dcrpg/queries.go index cc75f4df58..34473123df 100644 --- a/db/dcrpg/queries.go +++ b/db/dcrpg/queries.go @@ -746,7 +746,7 @@ func RetrieveAddressSpent(db *sql.DB, address string) (count, totalAmount int64, } func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspent, - totalSpent, totalUnspent int64, err error) { + totalSpent, totalUnspent, totalMergedSpent int64, err error) { dbtx, err := db.Begin() if err != nil { err = fmt.Errorf("unable to begin database transaction: %v", err) @@ -777,6 +777,23 @@ func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspe } numSpent, totalSpent = ns.Int64, ts.Int64 + var nms sql.NullInt64 + err = dbtx.QueryRow(internal.SelectAddressesMergedSpentCount, address). + Scan(&nms) + if err != nil && err != sql.ErrNoRows { + if errRoll := dbtx.Rollback(); errRoll != nil { + log.Errorf("Rollback failed: %v", errRoll) + } + err = fmt.Errorf("unable to QueryRow for merged spent count: %v", err) + return + } + + totalMergedSpent = nms.Int64 + + if !nms.Valid { + log.Debug("Merged debit spent count is not valid") + } + err = dbtx.Rollback() return } @@ -797,26 +814,31 @@ func RetrieveAllAddressTxns(db *sql.DB, address string) ([]uint64, []*dbtypes.Ad func RetrieveAddressTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { return retrieveAddressTxns(db, address, N, offset, - internal.SelectAddressLimitNByAddressSubQry) + internal.SelectAddressLimitNByAddressSubQry, false) } func RetrieveAddressTxnsAlt(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { return retrieveAddressTxns(db, address, N, offset, - internal.SelectAddressLimitNByAddress) + internal.SelectAddressLimitNByAddress, false) } func RetrieveAddressDebitTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { return retrieveAddressTxns(db, address, N, offset, - internal.SelectAddressDebitsLimitNByAddress) + internal.SelectAddressDebitsLimitNByAddress, false) } func RetrieveAddressCreditTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { return retrieveAddressTxns(db, address, N, offset, - internal.SelectAddressCreditsLimitNByAddress) + internal.SelectAddressCreditsLimitNByAddress, false) +} + +func RetrieveAddressMergedDebitTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { + return retrieveAddressTxns(db, address, N, offset, + internal.SelectAddressMergedDebitView, true) } func retrieveAddressTxns(db *sql.DB, address string, N, offset int64, - statement string) ([]uint64, []*dbtypes.AddressRow, error) { + statement string, isMergedDebitView bool) ([]uint64, []*dbtypes.AddressRow, error) { rows, err := db.Query(statement, address, N, offset) if err != nil { return nil, nil, err @@ -827,9 +849,27 @@ func retrieveAddressTxns(db *sql.DB, address string, N, offset int64, } }() + if isMergedDebitView { + addr, err := scanPartialAddressQueryRows(rows, address) + return nil, addr, err + } return scanAddressQueryRows(rows) } +func scanPartialAddressQueryRows(rows *sql.Rows, addr string) (addressRows []*dbtypes.AddressRow, err error) { + for rows.Next() { + var addr = dbtypes.AddressRow{Address: addr} + + err = rows.Scan(&addr.TxHash, &addr.TxBlockTime, + &addr.Value, &addr.MergedDebitCount) + if err != nil { + return + } + addressRows = append(addressRows, &addr) + } + return +} + func scanAddressQueryRows(rows *sql.Rows) (ids []uint64, addressRows []*dbtypes.AddressRow, err error) { for rows.Next() { var id uint64 diff --git a/explorer/explorerroutes.go b/explorer/explorerroutes.go index 11df4c81db..d449d6a118 100644 --- a/explorer/explorerroutes.go +++ b/explorer/explorerroutes.go @@ -470,11 +470,12 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { 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: + case dbtypes.AddrTxnAll, dbtypes.AddrMergedTxnDebit: case dbtypes.AddrTxnCredit: addrData.Transactions = addrData.TxnsFunding case dbtypes.AddrTxnDebit: diff --git a/explorer/explorertypes.go b/explorer/explorertypes.go index f339993548..78f9144151 100644 --- a/explorer/explorertypes.go +++ b/explorer/explorertypes.go @@ -61,24 +61,29 @@ type ChartDataCounter struct { // AddressTx models data for transactions on the address page type AddressTx struct { - TxID string - InOutID uint32 - Size uint32 - FormattedSize string - Total float64 - Confirmations uint64 - Time int64 - FormattedTime string - ReceivedTotal float64 - SentTotal float64 - IsFunding bool - MatchedTx string - BlockTime uint64 + TxID string + InOutID uint32 + Size uint32 + FormattedSize string + Total float64 + Confirmations uint64 + Time int64 + FormattedTime string + ReceivedTotal float64 + SentTotal float64 + IsFunding bool + MatchedTx string + BlockTime uint64 + MergedTxnCount uint64 `json:",omitempty"` } // IOID formats an identification string for the transaction input (or output) // represented by the AddressTx. -func (a *AddressTx) IOID() string { +func (a *AddressTx) IOID(txType ...string) string { + // if transaction is of type merged debit, return unformatted transaction ID + if len(txType) > 0 && dbtypes.AddrTxnTypeFromStr(txType[0]) == dbtypes.AddrMergedTxnDebit { + return a.TxID + } // When AddressTx is used properly, at least one of ReceivedTotal or // SentTotal should be zero. if a.IsFunding { @@ -248,6 +253,10 @@ type AddressInfo struct { KnownTransactions int64 KnownFundingTxns int64 KnownSpendingTxns int64 + + // KnownMergedSpendingTxns refers to the total count of unique debit transactions + // that appear in the merged debit view. + KnownMergedSpendingTxns int64 } // TxnCount returns the number of transaction "rows" available. @@ -262,6 +271,8 @@ func (a *AddressInfo) TxnCount() int64 { return a.KnownFundingTxns case dbtypes.AddrTxnDebit: return a.KnownSpendingTxns + case dbtypes.AddrMergedTxnDebit: + return a.KnownMergedSpendingTxns default: log.Warnf("Unknown address transaction type: %v", a.TxnType) return 0 @@ -271,11 +282,12 @@ func (a *AddressInfo) TxnCount() int64 { // AddressBalance represents the number and value of spent and unspent outputs // for an address. type AddressBalance struct { - Address string `json:"address"` - NumSpent int64 `json:"num_stxos"` - NumUnspent int64 `json:"num_utxos"` - TotalSpent int64 `json:"amount_spent"` - TotalUnspent int64 `json:"amount_unspent"` + Address string `json:"address"` + NumSpent int64 `json:"num_stxos"` + NumUnspent int64 `json:"num_utxos"` + TotalSpent int64 `json:"amount_spent"` + TotalUnspent int64 `json:"amount_unspent"` + NumMergedSpent int64 `json:"num_merged_spent,omitempty"` } // HomeInfo represents data used for the home page @@ -370,6 +382,8 @@ func ReduceAddressHistory(addrHist []*dbtypes.AddressRow) *AddressInfo { // Spending transaction sent += int64(addrOut.Value) tx.SentTotal = coin + tx.MergedTxnCount = addrOut.MergedDebitCount + debitTxns = append(debitTxns, &tx) } diff --git a/views/address.tmpl b/views/address.tmpl index 81baead424..3b8515787a 100644 --- a/views/address.tmpl +++ b/views/address.tmpl @@ -8,6 +8,7 @@ {{with .Data}} {{$heights := $.ConfirmHeight}} {{$TxnCount := .TxnCount}} + {{$txType := .TxnType}}
@@ -109,14 +110,14 @@
  • -
  • +
  • @@ -132,9 +133,17 @@ - - - + {{if eq $txType "merged debit"}} + + {{else}} + + {{end}} + {{if eq $txType "merged debit"}} + + {{else}} + + {{end}} + @@ -143,23 +152,31 @@ {{range $i, $v := .Transactions}} {{with $v}} - - {{if ne .ReceivedTotal 0.0}} - + + {{if eq $txType "merged debit"}} + {{else}} - {{if eq .SentTotal 0.0}} - + {{if ne .ReceivedTotal 0.0}} + {{else}} - {{if ne .MatchedTx ""}} - + {{if eq .SentTotal 0.0}} + {{else}} - + {{if ne .MatchedTx ""}} + + {{else}} + + {{end}} {{end}} {{end}} {{end}} {{if ne .SentTotal 0.0}} {{if lt 0.0 .SentTotal}} - + {{if eq $txType "merged debit"}} + + {{else}} + + {{end}} {{else}} {{end}} @@ -212,9 +229,10 @@ class="form-control-sm mb-2 mr-sm-2 mb-sm-0 {{if not .Fullmode}}disabled{{end}}" {{if not .Fullmode}}disabled="disabled"{{end}} > - - - + + + + {{if and (not .Fullmode) (ge .KnownTransactions .MaxTxLimit)}} @@ -246,7 +264,7 @@ window.location.pathname + "?txntype="+ $(ev.currentTarget).val() + "&n="+ parseInt($("#pagesize").val()) - + "&start=" + {{.Offset}} + + "&start=0" ) })
    Input/Output IDCredit DCRDebit DCRTime# of Input(s)Credit DCRDebit DCRDebit DCRTime UTC Age Confirms Size
    {{.IOID}}{{template "decimalParts" (float64AsDecimalParts .ReceivedTotal false)}}{{.IOID $txType}}{{.MergedTxnCount}}sstxcommitment{{template "decimalParts" (float64AsDecimalParts .ReceivedTotal false)}}sourcesstxcommitmentN/AsourceN/A{{template "decimalParts" (float64AsDecimalParts .SentTotal false)}} {{template "decimalParts" (float64AsDecimalParts .SentTotal false)}}{{template "decimalParts" (float64AsDecimalParts .SentTotal false)}}N/A