From fb145faeca2d8e79dc8ceef6e548fc62820d57b2 Mon Sep 17 00:00:00 2001 From: martonp Date: Fri, 5 Jan 2024 11:55:14 -0500 Subject: [PATCH] client,app: Transaction History UI This diff adds a UI to the wallets page displaying the transaction history of a wallet. Modifications are also made to the WalletTransaction type. The ID is now a string rather than a []byte. `BalanceDelta int64` is now `Amount uint64` as the sign is obvious from the type of the transaction and negatives are a hassle to deal with. A `Recipient` field for `Send` transactions and a `BondInfo` field for bond related transaction are also added. --- client/asset/btc/btc.go | 132 +++---- client/asset/btc/txdb.go | 22 +- client/asset/btc/txdb_test.go | 33 +- client/asset/eth/eth.go | 140 ++++--- client/asset/eth/eth_test.go | 85 ++--- client/asset/eth/txdb.go | 41 ++- client/asset/eth/txdb_test.go | 84 +++-- client/asset/interface.go | 57 ++- client/core/core.go | 2 +- client/core/wallet.go | 2 +- client/mm/mm_test.go | 3 - client/rpcserver/rpcserver.go | 2 +- client/rpcserver/rpcserver_test.go | 2 +- client/rpcserver/types.go | 11 +- client/webserver/api.go | 30 ++ client/webserver/live_test.go | 3 + client/webserver/locales/en-us.go | 12 + .../webserver/site/src/css/application.scss | 3 +- client/webserver/site/src/css/wallets.scss | 66 +++- .../webserver/site/src/css/wallets_dark.scss | 13 +- client/webserver/site/src/html/forms.tmpl | 2 +- client/webserver/site/src/html/wallets.tmpl | 110 +++++- client/webserver/site/src/js/app.ts | 119 +++++- client/webserver/site/src/js/locales.ts | 28 +- client/webserver/site/src/js/registry.ts | 32 ++ client/webserver/site/src/js/wallets.ts | 343 +++++++++++++++++- client/webserver/webserver.go | 2 + client/webserver/webserver_test.go | 4 + 28 files changed, 1081 insertions(+), 302 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 521a8f2385..174d970839 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -2272,7 +2272,6 @@ func (btc *baseWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, ui } return nil, nil, 0, err } else if split { - fmt.Printf("original coins: %s, split coins %s\n", coins, splitCoins) return splitCoins, []dex.Bytes{nil}, splitFees, nil // no redeem script required for split tx output } return coins, redeemScripts, 0, nil // splitCoins == coins @@ -2419,7 +2418,7 @@ func (btc *baseWallet) submitMultiSplitTx(fundingCoins asset.Coins, spents []*Ou for _, txOut := range tx.TxOut { totalOut += uint64(txOut.Value) } - btc.addTxToHistory(asset.Split, txHash[:], 0, totalIn-totalOut, true) + btc.addTxToHistory(asset.Split, txHash, 0, totalIn-totalOut, nil, nil, true) success = true return coins, totalIn - totalOut, nil @@ -2675,7 +2674,7 @@ func (btc *baseWallet) split(value uint64, lots uint64, outputs []*Output, input totalOut += uint64(msgTx.TxOut[i].Value) } - btc.addTxToHistory(asset.Split, txHash[:], 0, coinSum-totalOut, true) + btc.addTxToHistory(asset.Split, txHash, 0, coinSum-totalOut, nil, nil, true) fundingCoins = map[OutPoint]*UTxO{op.Pt: { TxHash: op.txHash(), @@ -3110,7 +3109,7 @@ func accelerateOrder(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, } txHash := btc.hashTx(signedTx) - btc.addTxToHistory(asset.Acceleration, txHash[:], 0, fees, true) + btc.addTxToHistory(asset.Acceleration, txHash, 0, fees, nil, nil, true) // Delete the old change from the cache btc.cm.ReturnOutPoint(NewOutPoint(changeTxHash, changeVout)) @@ -3414,14 +3413,14 @@ func (btc *baseWallet) txDB() txDB { return dbi.(txDB) } -func (btc *baseWallet) markTxAsSubmitted(id dex.Bytes) { +func (btc *baseWallet) markTxAsSubmitted(txID string) { txHistoryDB := btc.txDB() if txHistoryDB == nil { return } var txHash chainhash.Hash - copy(txHash[:], id) + copy(txHash[:], txID) btc.pendingTxsMtx.Lock() wt, found := btc.pendingTxs[txHash] @@ -3432,25 +3431,28 @@ func (btc *baseWallet) markTxAsSubmitted(id dex.Bytes) { } btc.pendingTxsMtx.Unlock() - err := txHistoryDB.markTxAsSubmitted(id) + err := txHistoryDB.markTxAsSubmitted(txID) if err != nil { btc.log.Errorf("failed to mark tx as submitted in tx history db: %v", err) } + + btc.emit.TransactionNote(wt.WalletTransaction, true) } -func (btc *baseWallet) removeTxFromHistory(id dex.Bytes) { +func (btc *baseWallet) removeTxFromHistory(txID string) { txHistoryDB := btc.txDB() if txHistoryDB == nil { return } - err := txHistoryDB.removeTx(id) + err := txHistoryDB.removeTx(txID) if err != nil { btc.log.Errorf("failed to remove tx from tx history db: %v", err) } } -func (btc *baseWallet) addTxToHistory(txType asset.TransactionType, id dex.Bytes, balanceDelta int64, fees uint64, submitted bool) { +func (btc *baseWallet) addTxToHistory(txType asset.TransactionType, txHash *chainhash.Hash, amount uint64, fees uint64, + bondInfo *asset.BondTxInfo, recipient *string, submitted bool) { txHistoryDB := btc.txDB() if txHistoryDB == nil { return @@ -3458,25 +3460,28 @@ func (btc *baseWallet) addTxToHistory(txType asset.TransactionType, id dex.Bytes wt := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: txType, - ID: id, - BalanceDelta: balanceDelta, - Fees: fees, + Type: txType, + ID: txHash.String(), + Amount: amount, + Fees: fees, + BondInfo: bondInfo, + Recipient: recipient, }, Submitted: submitted, } - var txHash chainhash.Hash - copy(txHash[:], wt.ID) - btc.pendingTxsMtx.Lock() - btc.pendingTxs[txHash] = wt + btc.pendingTxs[*txHash] = wt btc.pendingTxsMtx.Unlock() err := txHistoryDB.storeTx(wt) if err != nil { btc.log.Errorf("failed to store tx in tx history db: %v", err) } + + if submitted { + btc.emit.TransactionNote(wt.WalletTransaction, true) + } } // Swap sends the swaps in a single transaction and prepares the receipts. The @@ -3607,7 +3612,7 @@ func (btc *baseWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, ui return nil, nil, 0, err } - btc.addTxToHistory(asset.Swap, txHash[:], -int64(totalOut), fees, true) + btc.addTxToHistory(asset.Swap, txHash, totalOut, fees, nil, nil, true) // If change is nil, return a nil asset.Coin. var changeCoin asset.Coin @@ -3771,7 +3776,7 @@ func (btc *baseWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, return nil, nil, 0, err } - btc.addTxToHistory(asset.Redeem, txHash[:], int64(totalIn), fee, true) + btc.addTxToHistory(asset.Redeem, txHash, totalIn, fee, nil, nil, true) // Log the change output. coinIDs := make([]dex.Bytes, 0, len(form.Redemptions)) @@ -4028,7 +4033,7 @@ func (btc *baseWallet) Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.B if len(msgTx.TxOut) > 0 { // something went very wrong if not true fee = uint64(utxo.Value - msgTx.TxOut[0].Value) } - btc.addTxToHistory(asset.Refund, txHash[:], utxo.Value, fee, true) + btc.addTxToHistory(asset.Refund, txHash, uint64(utxo.Value), fee, nil, nil, true) return ToCoinID(refundHash, 0), nil } @@ -4240,7 +4245,7 @@ func (btc *baseWallet) SendTransaction(rawTx []byte) ([]byte, error) { return nil, err } - btc.markTxAsSubmitted(txHash[:]) + btc.markTxAsSubmitted(txHash.String()) return ToCoinID(txHash, 0), nil } @@ -4264,11 +4269,6 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract return nil, 0, 0, fmt.Errorf("PayToAddrScript error: %w", err) } - selfSend, err := btc.OwnsDepositAddress(address) - if err != nil { - return nil, 0, 0, fmt.Errorf("error checking address ownership: %w", err) - } - baseSize := dexbtc.MinimumTxOverhead if btc.segwit { baseSize += dexbtc.P2WPKHOutputSize * 2 @@ -4314,12 +4314,16 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract totalOut += uint64(txOut.Value) } - var amtSent int64 - if !selfSend { - amtSent = -int64(toSend) + selfSend, err := btc.OwnsDepositAddress(address) + if err != nil { + return nil, 0, 0, fmt.Errorf("error checking address ownership: %w", err) + } + txType := asset.Send + if selfSend { + txType = asset.SelfSend } - btc.addTxToHistory(asset.Send, txHash[:], amtSent, totalIn-totalOut, true) + btc.addTxToHistory(txType, txHash, toSend, totalIn-totalOut, nil, &address, true) return txHash, 0, toSend, nil } @@ -4853,7 +4857,7 @@ func (btc *baseWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time btc.log.Errorf("error returning coins for unused bond tx: %v", coins) } if txIDToRemoveFromHistory != nil { - btc.removeTxFromHistory(txIDToRemoveFromHistory[:]) + btc.removeTxFromHistory(txIDToRemoveFromHistory.String()) } } @@ -4911,7 +4915,12 @@ func (btc *baseWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time } success = true - btc.addTxToHistory(asset.CreateBond, txid[:], -int64(amt), fee, false) + bondInfo := &asset.BondTxInfo{ + AccountID: acctID, + LockTime: uint64(lockTimeSec), + BondID: pkh, + } + btc.addTxToHistory(asset.CreateBond, txid, amt, fee, bondInfo, nil, false) txIDToRemoveFromHistory = txid return bond, abandon, nil @@ -5000,6 +5009,10 @@ func (btc *baseWallet) RefundBond(ctx context.Context, ver uint16, coinID, scrip if ver != 0 { return nil, errors.New("only version 0 bonds supported") } + lockTime, pkhPush, err := dexbtc.ExtractBondDetailsV0(0, script) + if err != nil { + return nil, err + } txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, err @@ -5021,8 +5034,11 @@ func (btc *baseWallet) RefundBond(ctx context.Context, ver uint16, coinID, scrip if len(msgTx.TxOut) == 1 { fees = amt - uint64(msgTx.TxOut[0].Value) } - btc.addTxToHistory(asset.RedeemBond, txID[:], int64(amt), fees, true) - + bondInfo := &asset.BondTxInfo{ + LockTime: uint64(lockTime), + BondID: pkhPush, + } + btc.addTxToHistory(asset.RedeemBond, txID, amt, fees, bondInfo, nil, true) return NewOutput(txHash, 0, uint64(msgTx.TxOut[0].Value)), nil } @@ -5179,8 +5195,7 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { btc.log.Errorf("Error decoding txid %s: %v", tx.TxID, err) continue } - txID := dex.Bytes(txHash[:]) - _, err = txHistoryDB.getTx(txID) + _, err = txHistoryDB.getTx(txHash.String()) if err == nil { continue } @@ -5199,25 +5214,8 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { fee = toSatoshi(*tx.Fee) } } - wt := &extendedWalletTx{ - WalletTransaction: &asset.WalletTransaction{ - Type: asset.Receive, - ID: txID, - BalanceDelta: int64(toSatoshi(tx.Amount)), - Fees: fee, - }, - Submitted: true, - } - err = txHistoryDB.storeTx(wt) - if err != nil { - btc.log.Errorf("Error storing tx %s: %v", tx.TxID, err) - } - if !wt.Confirmed || err != nil { - btc.pendingTxsMtx.Lock() - btc.pendingTxs[*txHash] = wt - btc.pendingTxsMtx.Unlock() - } + btc.addTxToHistory(asset.Receive, txHash, toSatoshi(tx.Amount), fee, nil, nil, true) } } } @@ -5229,29 +5227,30 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { } btc.pendingTxsMtx.RUnlock() - handlePendingTx := func(hash chainhash.Hash, tx *extendedWalletTx) { + handlePendingTx := func(txHash chainhash.Hash, tx *extendedWalletTx) { tx.mtx.Lock() defer tx.mtx.Unlock() if !tx.Submitted { return } - gtr, err := btc.node.getWalletTransaction(&hash) + + gtr, err := btc.node.getWalletTransaction(&txHash) if errors.Is(err, asset.CoinNotFoundError) { - err = txHistoryDB.removeTx(hash[:]) + err = txHistoryDB.removeTx(txHash.String()) if err == nil { btc.pendingTxsMtx.Lock() - delete(btc.pendingTxs, hash) + delete(btc.pendingTxs, txHash) btc.pendingTxsMtx.Unlock() } else { // Leave it in the pendingPendingTxs and attempt to remove it // again next time. - btc.log.Errorf("Error removing tx %s from the history store: %v", hash, err) + btc.log.Errorf("Error removing tx %s from the history store: %v", txHash, err) } return } if err != nil { - btc.log.Errorf("Error getting transaction %s: %v", hash, err) + btc.log.Errorf("Error getting transaction %s: %v", txHash, err) return } @@ -5267,12 +5266,14 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { btc.log.Errorf("Error getting block height for %s: %v", blockHash, err) return } - if tx.BlockNumber != uint64(blockHeight) { + if tx.BlockNumber != uint64(blockHeight) || tx.Timestamp != gtr.BlockTime { tx.BlockNumber = uint64(blockHeight) + tx.Timestamp = gtr.BlockTime updated = true } } else if gtr.BlockHash == "" && tx.BlockNumber != 0 { tx.BlockNumber = 0 + tx.Timestamp = 0 updated = true } @@ -5288,14 +5289,15 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { if updated { err = txHistoryDB.storeTx(tx) if err != nil { - btc.log.Errorf("Error updating tx %s: %v", hash, err) + btc.log.Errorf("Error updating tx %s: %v", txHash, err) return } if tx.Confirmed { btc.pendingTxsMtx.Lock() - delete(btc.pendingTxs, hash) + delete(btc.pendingTxs, txHash) btc.pendingTxsMtx.Unlock() } + btc.emit.TransactionNote(tx.WalletTransaction, false) } } @@ -5309,7 +5311,7 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) { // If past is true, the transactions prior to the refID are returned, otherwise // the transactions after the refID are returned. n is the number of // transactions to return. If n is <= 0, all the transactions will be returned. -func (btc *ExchangeWalletSPV) TxHistory(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (btc *ExchangeWalletSPV) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { txHistoryDB := btc.txDB() if txHistoryDB == nil { return nil, fmt.Errorf("tx database not initialized") diff --git a/client/asset/btc/txdb.go b/client/asset/btc/txdb.go index 14f6d5704a..63ae77d2d5 100644 --- a/client/asset/btc/txdb.go +++ b/client/asset/btc/txdb.go @@ -63,26 +63,26 @@ func parseBlockKey(key []byte) (blockHeight, index uint64) { } // txKey maps a txid to a blockKey or pendingKey. -func txKey(txid []byte) []byte { - key := make([]byte, len(txPrefix)+len(txid)) +func txKey(txid string) []byte { + key := make([]byte, len(txPrefix)+len([]byte(txid))) copy(key, txPrefix) - copy(key[len(txPrefix):], txid) + copy(key[len(txPrefix):], []byte(txid)) return key } type txDB interface { storeTx(tx *extendedWalletTx) error - markTxAsSubmitted(txID dex.Bytes) error + markTxAsSubmitted(txID string) error // getTxs retrieves n transactions from the database. refID optionally // takes a transaction ID, and returns that transaction and the at most // (n - 1) transactions that were made either before or after it, depending // on the value of past. If refID is nil, the most recent n transactions // are returned, and the value of past is ignored. If the transaction with // ID refID is not in the database, asset.CoinNotFoundError is returned. - getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) - getTx(txID dex.Bytes) (*asset.WalletTransaction, error) + getTxs(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) + getTx(txID string) (*asset.WalletTransaction, error) getPendingTxs() ([]*extendedWalletTx, error) - removeTx(hash dex.Bytes) error + removeTx(txID string) error // setLastReceiveTxQuery stores the last time the wallet was queried for // receive transactions. This is required to know how far back to query // for incoming transactions that were received while the wallet is @@ -213,7 +213,7 @@ func (db *badgerTxDB) storeTx(tx *extendedWalletTx) error { }) } -func (db *badgerTxDB) markTxAsSubmitted(txID dex.Bytes) error { +func (db *badgerTxDB) markTxAsSubmitted(txID string) error { return db.Update(func(txn *badger.Txn) error { txKey := txKey(txID) txKeyItem, err := txn.Get(txKey) @@ -257,7 +257,7 @@ func (db *badgerTxDB) markTxAsSubmitted(txID dex.Bytes) error { // on the value of past. If refID is nil, the most recent n transactions // are returned, and the value of past is ignored. If the transaction with // ID refID is not in the database, asset.CoinNotFoundError is returned. -func (db *badgerTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (db *badgerTxDB) getTxs(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { var txs []*asset.WalletTransaction err := db.View(func(txn *badger.Txn) error { var startKey []byte @@ -310,7 +310,7 @@ func (db *badgerTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.Walle return txs, err } -func (db *badgerTxDB) getTx(txID dex.Bytes) (*asset.WalletTransaction, error) { +func (db *badgerTxDB) getTx(txID string) (*asset.WalletTransaction, error) { txs, err := db.getTxs(1, &txID, false) if err != nil { return nil, err @@ -352,7 +352,7 @@ func (db *badgerTxDB) getPendingTxs() ([]*extendedWalletTx, error) { return txs, err } -func (db *badgerTxDB) removeTx(txID dex.Bytes) error { +func (db *badgerTxDB) removeTx(txID string) error { return db.Update(func(txn *badger.Txn) error { txKey := txKey(txID) txKeyItem, err := txn.Get(txKey) diff --git a/client/asset/btc/txdb_test.go b/client/asset/btc/txdb_test.go index f1f7b6f54c..4a5048eac5 100644 --- a/client/asset/btc/txdb_test.go +++ b/client/asset/btc/txdb_test.go @@ -4,6 +4,7 @@ package btc import ( + "encoding/hex" "errors" "reflect" "testing" @@ -33,35 +34,35 @@ func TestTxDB(t *testing.T) { tx1 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Send, - ID: encode.RandomBytes(32), - BalanceDelta: -1e8, - Fees: 1e5, - BlockNumber: 0, + Type: asset.Send, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 1e8, + Fees: 1e5, + BlockNumber: 0, }, } tx2 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Receive, - ID: encode.RandomBytes(32), - BalanceDelta: 1e8, - Fees: 3e5, - BlockNumber: 0, + Type: asset.Receive, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 1e8, + Fees: 3e5, + BlockNumber: 0, }, } tx3 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Swap, - ID: encode.RandomBytes(32), - BalanceDelta: -1e8, - Fees: 2e5, - BlockNumber: 0, + Type: asset.Swap, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 1e8, + Fees: 2e5, + BlockNumber: 0, }, } - getTxsAndCheck := func(n int, refID *dex.Bytes, past bool, expected []*asset.WalletTransaction) { + getTxsAndCheck := func(n int, refID *string, past bool, expected []*asset.WalletTransaction) { t.Helper() txs, err = txHistoryStore.getTxs(n, refID, past) diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index d82ea6f572..ec8d38f5f8 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -1968,7 +1968,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 } txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), -int64(swapVal), swaps.FeeRate*gasLimit, 0, w.assetID, txHash[:], asset.Swap) + w.addToTxHistory(tx.Nonce(), swapVal, swaps.FeeRate*gasLimit, w.assetID, txHash, asset.Swap, nil) receipts := make([]asset.Receipt, 0, n) for _, swap := range swaps.Contracts { @@ -2059,7 +2059,7 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin contractAddr := w.netToken.SwapContracts[swaps.Version].Address.String() txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), -int64(swapVal), swaps.FeeRate*gasLimit, 0, w.assetID, txHash[:], asset.Swap) + w.addToTxHistory(tx.Nonce(), swapVal, swaps.FeeRate*gasLimit, w.assetID, txHash, asset.Swap, nil) receipts := make([]asset.Receipt, 0, n) for _, swap := range swaps.Contracts { @@ -2232,7 +2232,7 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non } txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), int64(redeemedValue), gasFeeCap*gasLimit, 0, w.assetID, txHash[:], asset.Redeem) + w.addToTxHistory(tx.Nonce(), redeemedValue, gasFeeCap*gasLimit, w.assetID, txHash, asset.Redeem, nil) txs := make([]dex.Bytes, len(form.Redemptions)) for i := range txs { @@ -2309,7 +2309,11 @@ func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate, gasLimit uint64, dex.BipIDSymbol(w.assetID), c.tokenAddress(), txOpts.Nonce, tx.Hash().Hex()) txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), 0, maxFeeRate*gasLimit, 0, w.assetID, txHash[:], asset.ApproveToken) + txType := asset.ApproveToken + if amount.Cmp(big.NewInt(0)) == 0 { + txType = asset.RevokeTokenApproval + } + w.addToTxHistory(tx.Nonce(), 0, maxFeeRate*gasLimit, w.assetID, txHash, txType, nil) return nil }) @@ -2892,7 +2896,7 @@ func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, txHash := tx.Hash() refundValue := dexeth.WeiToGwei(swap.Value) - w.addToTxHistory(tx.Nonce(), int64(refundValue), fees, 0, w.assetID, txHash[:], asset.Refund) + w.addToTxHistory(tx.Nonce(), refundValue, fees, w.assetID, txHash, asset.Refund, nil) return txHash[:], nil } @@ -3168,7 +3172,12 @@ func (w *ETHWallet) Send(addr string, value, _ uint64) (asset.Coin, error) { } txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), -int64(value), maxFee, 0, w.assetID, txHash[:], asset.Send) + txType := asset.Send + if strings.EqualFold(addr, w.addr.String()) { + txType = asset.SelfSend + } + + w.addToTxHistory(tx.Nonce(), value, maxFee, w.assetID, txHash, txType, &addr) return &coin{id: txHash, value: value}, nil } @@ -3192,7 +3201,7 @@ func (w *TokenWallet) Send(addr string, value, _ uint64) (asset.Coin, error) { } txHash := tx.Hash() - w.addToTxHistory(tx.Nonce(), -int64(value), maxFee, 0, w.assetID, txHash[:], asset.Send) + w.addToTxHistory(tx.Nonce(), value, maxFee, w.assetID, txHash, asset.Send, &addr) return &coin{id: txHash, value: value}, nil } @@ -4058,10 +4067,10 @@ func (w *assetWallet) sumPendingTxs(bal *big.Int) (out, in uint64) { addPendingTx := func(txAssetID uint32, pt *extendedWalletTx) { if txAssetID == w.assetID { - if pt.BalanceDelta > 0 { - in += uint64(pt.BalanceDelta) + if asset.IncomingTxType(pt.Type) { + in += pt.Amount } else { - out += uint64(-1 * pt.BalanceDelta) + out += pt.Amount } } if !isToken { @@ -4565,10 +4574,29 @@ func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error { return nil } -// checkPendingTx checks the confirmation status of a transaction. The BlockNumber -// and Fees fields of the extendedWalletTx are updated if the transaction is confirmed, -// and if the transaction has reached the required number of confirmations, it is removed -// from w.pendingTxs. +func (w *baseWallet) emitTransactionNote(tx *asset.WalletTransaction, new bool) { + w.walletsMtx.RLock() + baseWallet, found := w.wallets[w.baseChainID] + var tokenWallet *assetWallet + if tx.TokenID != nil { + tokenWallet = w.wallets[*tx.TokenID] + } + w.walletsMtx.RUnlock() + + if found { + baseWallet.emit.TransactionNote(tx, new) + } else { + w.log.Error("emitTransactionNote: base wallet not found") + } + if tokenWallet != nil { + tokenWallet.emit.TransactionNote(tx, new) + } +} + +// checkPendingTx checks the confirmation status of a transaction. The +// BlockNumber, Fees, and Timestamp fields of the extendedWalletTx are updated +// if the transaction is confirmed, and if the transaction has reached the +// required number of confirmations, it is removed from w.pendingTxs. // True is returned from this function if we have given up on the transaction, and it // should not be considered in the pending tx calculation. // @@ -4607,6 +4635,8 @@ func (w *baseWallet) checkPendingTx(nonce uint64, pendingTx *extendedWalletTx) ( delete(w.pendingTxs, nonce) w.pendingTxsMtx.Unlock() } + + w.emitTransactionNote(pendingTx.WalletTransaction, false) } }() @@ -4615,19 +4645,24 @@ func (w *baseWallet) checkPendingTx(nonce uint64, pendingTx *extendedWalletTx) ( } pendingTx.lastCheck = tip - var txHash common.Hash - copy(txHash[:], pendingTx.ID) + h, err := common.ParseHexOrString(pendingTx.ID) + if err != nil { + w.log.Errorf("error parsing tx hash %s: %v", pendingTx.ID, err) + return + } + txHash := common.BytesToHash(h) receipt, tx, err := w.node.transactionReceipt(w.ctx, txHash) if err != nil { if errors.Is(err, asset.CoinNotFoundError) && pendingTx.BlockNumber > 0 { w.log.Warnf("TxID %v was previously confirmed but now cannot be found", pendingTx.ID) pendingTx.BlockNumber = 0 + pendingTx.Timestamp = 0 updated = true } if !errors.Is(err, asset.CoinNotFoundError) { w.log.Errorf("Error getting confirmations for pending tx %s: %v", txHash, err) } - if time.Since(time.Unix(int64(pendingTx.TimeStamp), 0)) > time.Minute*6 { + if time.Since(time.Unix(int64(pendingTx.SubmissionTime), 0)) > time.Minute*6 { givenUp = true } @@ -4638,6 +4673,7 @@ func (w *baseWallet) checkPendingTx(nonce uint64, pendingTx *extendedWalletTx) ( if pendingTx.BlockNumber > 0 { w.log.Warnf("TxID %v was previously confirmed but now is not confirmed", pendingTx.ID) pendingTx.BlockNumber = 0 + pendingTx.Timestamp = 0 updated = true } return @@ -4654,12 +4690,12 @@ func (w *baseWallet) checkPendingTx(nonce uint64, pendingTx *extendedWalletTx) ( effectiveGasPrice := new(big.Int).Add(hdr.BaseFee, tx.EffectiveGasTipValue(hdr.BaseFee)) bigFees := new(big.Int).Mul(effectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) - fees := dexeth.WeiToGwei(bigFees) blockNumber := receipt.BlockNumber.Uint64() - if pendingTx.BlockNumber != blockNumber || pendingTx.Fees != fees { + if pendingTx.BlockNumber != blockNumber || pendingTx.Fees != fees || pendingTx.Timestamp != hdr.Time { pendingTx.Fees = dexeth.WeiToGwei(bigFees) pendingTx.BlockNumber = blockNumber + pendingTx.Timestamp = hdr.Time updated = true } @@ -4695,16 +4731,7 @@ func (w *baseWallet) checkPendingTxs() { const txHistoryNonceKey = "Nonce" -func (w *baseWallet) addToTxHistory(nonce uint64, balanceDelta int64, fees, blockNumber uint64, - assetID uint32, id dex.Bytes, typ asset.TransactionType) { - w.tipMtx.RLock() - tip := w.currentTip.Number.Uint64() - w.tipMtx.RUnlock() - var confs uint64 - if blockNumber > 0 && tip >= blockNumber { - confs = tip - blockNumber + 1 - } - +func (w *baseWallet) addToTxHistory(nonce, amount, fees uint64, assetID uint32, txHash common.Hash, typ asset.TransactionType, recipient *string) { var tokenAssetID *uint32 if assetID != w.baseChainID { tokenAssetID = &assetID @@ -4712,37 +4739,31 @@ func (w *baseWallet) addToTxHistory(nonce uint64, balanceDelta int64, fees, bloc wt := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: typ, - ID: id, - BalanceDelta: balanceDelta, - Fees: fees, - BlockNumber: blockNumber, - TokenID: tokenAssetID, + Type: typ, + ID: txHash.String(), + Amount: amount, + Fees: fees, + TokenID: tokenAssetID, + Recipient: recipient, AdditionalData: map[string]string{ txHistoryNonceKey: strconv.FormatUint(nonce, 10), }, }, - TimeStamp: uint64(time.Now().Unix()), - Confirmed: confs >= txConfsNeededToConfirm, - } - - if !wt.Confirmed { - w.pendingTxsMtx.Lock() - w.pendingTxs[nonce] = wt - w.pendingTxsMtx.Unlock() + SubmissionTime: uint64(time.Now().Unix()), + savedToDB: true, } err := w.txDB.storeTx(nonce, wt) if err != nil { - if wt.Confirmed { - // If it's confirmed but we failed to store it in the db, add - // it to the map so we can retry. - w.pendingTxsMtx.Lock() - w.pendingTxs[nonce] = wt - w.pendingTxsMtx.Unlock() - } w.log.Errorf("error storing tx in db: %v", err) + wt.savedToDB = false } + + w.pendingTxsMtx.Lock() + w.pendingTxs[nonce] = wt + w.pendingTxsMtx.Unlock() + + w.emitTransactionNote(wt.WalletTransaction, true) } // TxHistory returns all the transactions the wallet has made. This @@ -4751,13 +4772,28 @@ func (w *baseWallet) addToTxHistory(nonce uint64, balanceDelta int64, fees, bloc // If past is true, the transactions prior to the refID are returned, otherwise // the transactions after the refID are returned. n is the number of // transactions to return. If n is <= 0, all the transactions will be returned. -func (w *baseWallet) TxHistory(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (w *ETHWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { + baseChainWallet := w.wallet(w.baseChainID) + if baseChainWallet == nil || !baseChainWallet.connected.Load() { + return nil, fmt.Errorf("wallet not connected") + } + + return w.txDB.getTxs(n, refID, past, nil) +} + +// TxHistory returns all the transactions the token wallet has made. If refID +// is nil, then transactions starting from the most recent are returned (past +// is ignored). If past is true, the transactions prior to the refID are +// returned, otherwise the transactions after the refID are returned. n is the +// number of transactions to return. If n is <= 0, all the transactions will be +// returned. +func (w *TokenWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { baseChainWallet := w.wallet(w.baseChainID) if baseChainWallet == nil || !baseChainWallet.connected.Load() { return nil, fmt.Errorf("wallet not connected") } - return w.txDB.getTxs(n, refID, past) + return w.txDB.getTxs(n, refID, past, &w.assetID) } // providersFile reads a file located at ~/dextest/credentials.json. diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 141e482a2f..2ce29a961c 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -431,11 +431,11 @@ func (db *tTxDB) storeTx(nonce uint64, wt *extendedWalletTx) error { db.storeTxCalled = true return db.storeTxErr } -func (db *tTxDB) removeTx(id dex.Bytes) error { +func (db *tTxDB) removeTx(id string) error { db.removeTxCalled = true return db.removeTxErr } -func (db *tTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (db *tTxDB) getTxs(n int, refID *string, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error) { return nil, nil } func (db *tTxDB) getPendingTxs() (map[uint64]*extendedWalletTx, error) { @@ -481,8 +481,8 @@ func TestCheckUnconfirmedTxs(t *testing.T) { TokenID: tokenID, Fees: maxFees, }, - TimeStamp: uint64(timeStamp), - savedToDB: savedToDB, + SubmissionTime: uint64(timeStamp), + savedToDB: savedToDB, }, confs: txReceiptConfs, gasUsed: txReceiptGasUsed, @@ -519,12 +519,12 @@ func TestCheckUnconfirmedTxs(t *testing.T) { }, }, { - name: "tx was nonce replaced", + name: "not nonce replaced, but still cannot find after 7 mins", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(5*60+1), true), + 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(7*60+1), true), }, - confirmedNonce: 1, + confirmedNonce: 0, expTxsAfter: map[uint64]*extendedWalletTx{}, expRemoveTxCalled: true, }, @@ -532,25 +532,15 @@ func TestCheckUnconfirmedTxs(t *testing.T) { name: "leave in unconfirmed txs if txDB.removeTx fails", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(5*60+1), true), + 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(7*60+1), true), }, confirmedNonce: 1, expTxsAfter: map[uint64]*extendedWalletTx{ - 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(5*60+1), true).wt, + 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(7*60+1), true).wt, }, removeTxErr: errors.New(""), expRemoveTxCalled: true, }, - { - name: "not nonce replaced, but still cannot find after 10 mins", - assetID: BipID, - unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 1: newExtendedWalletTx(BipID, 1e7, 0, 0, 0, asset.CoinNotFoundError, now-(10*60+1), true), - }, - confirmedNonce: 0, - expTxsAfter: map[uint64]*extendedWalletTx{}, - expRemoveTxCalled: true, - }, { name: "still in mempool", assetID: BipID, @@ -607,9 +597,6 @@ func TestCheckUnconfirmedTxs(t *testing.T) { } for _, tt := range tests { - if tt.name != "1 confirmation" { - continue - } t.Run(tt.name, func(t *testing.T) { _, eth, node, shutdown := tassetWallet(tt.assetID) defer shutdown() @@ -635,7 +622,7 @@ func TestCheckUnconfirmedTxs(t *testing.T) { for nonce, pt := range tt.unconfirmedTxs { txHash := common.BytesToHash(encode.RandomBytes(32)) - pt.wt.ID = txHash[:] + pt.wt.ID = txHash.String() eth.pendingTxs[nonce] = pt.wt var blockNumber *big.Int if pt.confs > 0 { @@ -881,6 +868,17 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co } } + emitChan := make(chan asset.WalletNotification, 8) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-emitChan: + } + } + }() + aw := &assetWallet{ baseWallet: &baseWallet{ baseChainID: BipID, @@ -910,7 +908,8 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), // move up after review - wi: WalletInfo, + wi: WalletInfo, + emit: asset.NewWalletEmitter(emitChan, BipID, tLogger), } aw.wallets = map[uint32]*assetWallet{ BipID: aw, @@ -931,6 +930,7 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co atomize: dexeth.WeiToGwei, pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), + emit: asset.NewWalletEmitter(emitChan, BipID, tLogger), } w = &TokenWallet{ assetWallet: aw, @@ -1109,7 +1109,7 @@ func TestBalanceNoMempool(t *testing.T) { confs uint32 } - newExtendedWalletTx := func(assetID uint32, out, in, maxFees uint64, currBlockNumber uint64, txReceiptConfs uint32) *tExtendedWalletTx { + newExtendedWalletTx := func(assetID uint32, amt, maxFees uint64, currBlockNumber uint64, txReceiptConfs uint32, txType asset.TransactionType) *tExtendedWalletTx { var tokenID *uint32 if assetID != BipID { tokenID = &assetID @@ -1118,10 +1118,11 @@ func TestBalanceNoMempool(t *testing.T) { return &tExtendedWalletTx{ wt: &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - BalanceDelta: int64(in) - int64(out), - BlockNumber: 0, - TokenID: tokenID, - Fees: maxFees, + Type: txType, + Amount: amt, + BlockNumber: 0, + TokenID: tokenID, + Fees: maxFees, }, lastCheck: lastCheck, }, @@ -1141,7 +1142,7 @@ func TestBalanceNoMempool(t *testing.T) { name: "single eth tx", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(BipID, 1, 0, 2, 0, 0), + 0: newExtendedWalletTx(BipID, 1, 2, 0, 0, asset.Send), }, expPendingOut: 3, expCountAfter: 1, @@ -1150,7 +1151,7 @@ func TestBalanceNoMempool(t *testing.T) { name: "single tx expired", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(BipID, 1, 0, 1, 0, 1), + 0: newExtendedWalletTx(BipID, 1, 1, 0, 1, asset.Send), }, expCountAfter: 1, }, @@ -1158,14 +1159,14 @@ func TestBalanceNoMempool(t *testing.T) { name: "single tx expired, txConfsNeededToConfirm confs", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(BipID, 1, 0, 1, 0, txConfsNeededToConfirm), + 0: newExtendedWalletTx(BipID, 1, 1, 0, txConfsNeededToConfirm, asset.Send), }, }, { name: "eth with token fees", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(simnetTokenID, 4, 0, 5, 0, 0), + 0: newExtendedWalletTx(simnetTokenID, 4, 5, 0, 0, asset.Send), }, expPendingOut: 5, expCountAfter: 1, @@ -1174,8 +1175,8 @@ func TestBalanceNoMempool(t *testing.T) { name: "token with 1 tx and other ignored assets", assetID: simnetTokenID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(simnetTokenID, 4, 0, 5, 0, 0), - 1: newExtendedWalletTx(simnetTokenID+1, 8, 0, 9, 0, 0), + 0: newExtendedWalletTx(simnetTokenID, 4, 5, 0, 0, asset.Send), + 1: newExtendedWalletTx(simnetTokenID+1, 8, 9, 0, 0, asset.Send), }, expPendingOut: 4, expCountAfter: 2, @@ -1184,7 +1185,7 @@ func TestBalanceNoMempool(t *testing.T) { name: "token with 1 tx incoming", assetID: simnetTokenID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(simnetTokenID, 0, 15, 5, 0, 0), + 0: newExtendedWalletTx(simnetTokenID, 15, 5, 0, 0, asset.Redeem), }, expPendingIn: 15, expCountAfter: 1, @@ -1193,10 +1194,10 @@ func TestBalanceNoMempool(t *testing.T) { name: "eth mixed txs", assetID: BipID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(BipID, 1, 0, 2, 0, 0), // 3 eth out - 1: newExtendedWalletTx(simnetTokenID, 3, 0, 4, 0, txConfsNeededToConfirm), // confirmed - 2: newExtendedWalletTx(simnetTokenID, 5, 0, 6, 0, 0), // 6 eth out - 3: newExtendedWalletTx(BipID, 0, 7, 1, 0, 0), // 1 eth out, 7 eth in + 0: newExtendedWalletTx(BipID, 1, 2, 0, 0, asset.Swap), // 3 eth out + 1: newExtendedWalletTx(simnetTokenID, 3, 4, 0, txConfsNeededToConfirm, asset.Send), // confirmed + 2: newExtendedWalletTx(simnetTokenID, 5, 6, 0, 0, asset.Swap), // 6 eth out + 3: newExtendedWalletTx(BipID, 7, 1, 0, 0, asset.Refund), // 1 eth out, 7 eth in }, expPendingOut: 10, expPendingIn: 7, @@ -1206,7 +1207,7 @@ func TestBalanceNoMempool(t *testing.T) { name: "already confirmed, but still waiting for txConfsNeededToConfirm", assetID: simnetTokenID, unconfirmedTxs: map[uint64]*tExtendedWalletTx{ - 0: newExtendedWalletTx(simnetTokenID, 0, 15, 5, tipHeight, 1), + 0: newExtendedWalletTx(simnetTokenID, 15, 5, tipHeight, 1, asset.Redeem), }, expCountAfter: 1, }, @@ -1231,7 +1232,7 @@ func TestBalanceNoMempool(t *testing.T) { for nonce, pt := range tt.unconfirmedTxs { txHash := common.BytesToHash(encode.RandomBytes(32)) - pt.wt.ID = txHash[:] + pt.wt.ID = txHash.String() eth.pendingTxs[nonce] = pt.wt var blockNumber *big.Int if pt.confs > 0 { diff --git a/client/asset/eth/txdb.go b/client/asset/eth/txdb.go index 4d52a9c583..c91ca34c21 100644 --- a/client/asset/eth/txdb.go +++ b/client/asset/eth/txdb.go @@ -30,7 +30,7 @@ type extendedWalletTx struct { // Confirmed will be set to true once the transaction has 3 confirmations. Confirmed bool `json:"confirmed"` BlockSubmitted uint64 `json:"blockSubmitted"` - TimeStamp uint64 `json:"timeStamp"` + SubmissionTime uint64 `json:"timeStamp"` lastCheck uint64 savedToDB bool @@ -138,10 +138,10 @@ func nonceFromKey(nk []byte) (uint64, error) { return binary.BigEndian.Uint64(nk[len(noncePrefix):]), nil } -func txHashKey(txHash dex.Bytes) []byte { - key := make([]byte, len(txHashPrefix)+len(txHash)) +func txIDKey(txID string) []byte { + key := make([]byte, len(txHashPrefix)+len([]byte(txID))) copy(key, txHashPrefix) - copy(key[len(txHashPrefix):], txHash) + copy(key[len(txHashPrefix):], []byte(txID)) return key } @@ -176,8 +176,8 @@ const txDBVersion = prefixDBVersion type txDB interface { storeTx(nonce uint64, wt *extendedWalletTx) error - removeTx(id dex.Bytes) error - getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) + removeTx(id string) error + getTxs(n int, refID *string, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error) getPendingTxs() (map[uint64]*extendedWalletTx, error) storeMonitoredTx(txHash common.Hash, tx *monitoredTx) error getMonitoredTxs() (map[common.Hash]*monitoredTx, error) @@ -332,7 +332,7 @@ func (s *badgerTxDB) storeTx(nonce uint64, wt *extendedWalletTx) error { return err } nk := nonceKey(nonce) - tk := txHashKey(wt.ID) + tk := txIDKey(wt.ID) return s.Update(func(txn *badger.Txn) error { oldWtItem, err := txn.Get(nk) @@ -351,10 +351,10 @@ func (s *badgerTxDB) storeTx(nonce uint64, wt *extendedWalletTx) error { } return err }) - if err == nil && !bytes.Equal(oldWt.ID, wt.ID) { - err = txn.Delete(txHashKey(oldWt.ID)) + if err == nil && oldWt.ID != wt.ID { + err = txn.Delete(txIDKey(oldWt.ID)) if err != nil { - s.log.Errorf("failed to delete old tx id: %s: %v", oldWt.ID.String(), err) + s.log.Errorf("failed to delete old tx id: %s: %v", oldWt.ID, err) } } } @@ -370,8 +370,8 @@ func (s *badgerTxDB) storeTx(nonce uint64, wt *extendedWalletTx) error { } // removeTx removes a tx from the db. -func (s *badgerTxDB) removeTx(id dex.Bytes) error { - tk := txHashKey(id) +func (s *badgerTxDB) removeTx(id string) error { + tk := txIDKey(id) return s.Update(func(txn *badger.Txn) error { txIDEntry, err := txn.Get(tk) @@ -394,15 +394,15 @@ func (s *badgerTxDB) removeTx(id dex.Bytes) error { // getTxs returns the n more recent transaction if refID is nil, or the // n transactions before/after refID depending on the value of past. The -// transactions are returned in chronological order. -func (s *badgerTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +// transactions are returned in reverse chronological order. +func (s *badgerTxDB) getTxs(n int, refID *string, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error) { var txs []*asset.WalletTransaction err := s.View(func(txn *badger.Txn) error { var startNonceKey []byte if refID != nil { // Get the nonce for the provided tx hash. - tk := txHashKey(*refID) + tk := txIDKey(*refID) item, err := txn.Get(tk) if err != nil { return err @@ -423,7 +423,7 @@ func (s *badgerTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.Wallet it := txn.NewIterator(opts) defer it.Close() - for it.Seek(startNonceKey); it.Valid() && n <= 0 || len(txs) < n; it.Next() { + for it.Seek(startNonceKey); it.Valid() && (n <= 0 || len(txs) < n); it.Next() { item := it.Item() err := item.Value(func(wtB []byte) error { wt := new(asset.WalletTransaction) @@ -432,13 +432,16 @@ func (s *badgerTxDB) getTxs(n int, refID *dex.Bytes, past bool) ([]*asset.Wallet s.log.Errorf("unable to unmarhsal wallet transaction: %s: %v", string(wtB), err) return err } - if refID != nil && bytes.Equal(wt.ID, *refID) { + if refID != nil && wt.ID == *refID { + return nil + } + if tokenID != nil && (wt.TokenID == nil || *tokenID != *wt.TokenID) { return nil } if past { - txs = append([]*asset.WalletTransaction{wt}, txs...) - } else { txs = append(txs, wt) + } else { + txs = append([]*asset.WalletTransaction{wt}, txs...) } return nil }) diff --git a/client/asset/eth/txdb_test.go b/client/asset/eth/txdb_test.go index 0c6c451cea..06ca79efdd 100644 --- a/client/asset/eth/txdb_test.go +++ b/client/asset/eth/txdb_test.go @@ -1,6 +1,7 @@ package eth import ( + "encoding/hex" "reflect" "testing" @@ -21,7 +22,7 @@ func TestTxDB(t *testing.T) { t.Fatalf("error creating tx history store: %v", err) } - txs, err := txHistoryStore.getTxs(0, nil, true) + txs, err := txHistoryStore.getTxs(0, nil, true, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } @@ -31,11 +32,11 @@ func TestTxDB(t *testing.T) { wt1 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Send, - ID: encode.RandomBytes(32), - BalanceDelta: -100, - Fees: 300, - BlockNumber: 123, + Type: asset.Send, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 100, + Fees: 300, + BlockNumber: 123, AdditionalData: map[string]string{ "Nonce": "1", }, @@ -46,11 +47,11 @@ func TestTxDB(t *testing.T) { wt2 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Swap, - ID: encode.RandomBytes(32), - BalanceDelta: -200, - Fees: 100, - BlockNumber: 124, + Type: asset.Swap, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 200, + Fees: 100, + BlockNumber: 124, AdditionalData: map[string]string{ "Nonce": "2", }, @@ -59,11 +60,11 @@ func TestTxDB(t *testing.T) { wt3 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Redeem, - ID: encode.RandomBytes(32), - BalanceDelta: 200, - Fees: 200, - BlockNumber: 125, + Type: asset.Redeem, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 200, + Fees: 200, + BlockNumber: 125, AdditionalData: map[string]string{ "Nonce": "3", }, @@ -72,11 +73,11 @@ func TestTxDB(t *testing.T) { wt4 := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ - Type: asset.Redeem, - ID: encode.RandomBytes(32), - BalanceDelta: 200, - Fees: 300, - BlockNumber: 125, + Type: asset.Redeem, + ID: hex.EncodeToString(encode.RandomBytes(32)), + Amount: 200, + Fees: 300, + BlockNumber: 125, AdditionalData: map[string]string{ "Nonce": "3", }, @@ -87,7 +88,7 @@ func TestTxDB(t *testing.T) { if err != nil { t.Fatalf("error storing tx: %v", err) } - txs, err = txHistoryStore.getTxs(0, nil, true) + txs, err = txHistoryStore.getTxs(0, nil, true, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } @@ -100,11 +101,11 @@ func TestTxDB(t *testing.T) { if err != nil { t.Fatalf("error storing tx: %v", err) } - txs, err = txHistoryStore.getTxs(0, nil, true) + txs, err = txHistoryStore.getTxs(0, nil, true, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } - expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction, wt2.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt2.WalletTransaction, wt1.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } @@ -113,16 +114,16 @@ func TestTxDB(t *testing.T) { if err != nil { t.Fatalf("error storing tx: %v", err) } - txs, err = txHistoryStore.getTxs(2, nil, true) + txs, err = txHistoryStore.getTxs(2, nil, true, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } - expectedTxs = []*asset.WalletTransaction{wt2.WalletTransaction, wt3.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt3.WalletTransaction, wt2.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } - txs, err = txHistoryStore.getTxs(0, &wt2.ID, true) + txs, err = txHistoryStore.getTxs(0, &wt2.ID, true, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } @@ -131,7 +132,7 @@ func TestTxDB(t *testing.T) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } - txs, err = txHistoryStore.getTxs(0, &wt2.ID, false) + txs, err = txHistoryStore.getTxs(0, &wt2.ID, false, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } @@ -145,14 +146,14 @@ func TestTxDB(t *testing.T) { if err != nil { t.Fatalf("error storing tx: %v", err) } - txs, err = txHistoryStore.getTxs(0, nil, false) + txs, err = txHistoryStore.getTxs(0, nil, false, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } if len(txs) != 3 { t.Fatalf("expected 3 txs but got %d", len(txs)) } - expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction, wt2.WalletTransaction, wt4.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt4.WalletTransaction, wt2.WalletTransaction, wt1.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } @@ -163,11 +164,11 @@ func TestTxDB(t *testing.T) { if err != nil { t.Fatalf("error storing tx: %v", err) } - txs, err = txHistoryStore.getTxs(0, nil, false) + txs, err = txHistoryStore.getTxs(0, nil, false, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } - expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction, wt2.WalletTransaction, wt4.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt4.WalletTransaction, wt2.WalletTransaction, wt1.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } @@ -180,11 +181,11 @@ func TestTxDB(t *testing.T) { } defer txHistoryStore.close() - txs, err = txHistoryStore.getTxs(0, nil, false) + txs, err = txHistoryStore.getTxs(0, nil, false, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } - expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction, wt2.WalletTransaction, wt4.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt4.WalletTransaction, wt2.WalletTransaction, wt1.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } @@ -206,19 +207,28 @@ func TestTxDB(t *testing.T) { t.Fatalf("error removing tx: %v", err) } - txs, err = txHistoryStore.getTxs(0, nil, false) + txs, err = txHistoryStore.getTxs(0, nil, false, nil) if err != nil { t.Fatalf("error retrieving txs: %v", err) } - expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction, wt4.WalletTransaction} + expectedTxs = []*asset.WalletTransaction{wt4.WalletTransaction, wt1.WalletTransaction} if !reflect.DeepEqual(expectedTxs, txs) { t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) } + + txs, err = txHistoryStore.getTxs(0, nil, false, &simnetTokenID) + if err != nil { + t.Fatalf("error retrieving txs: %v", err) + } + expectedTxs = []*asset.WalletTransaction{wt1.WalletTransaction} + if !reflect.DeepEqual(expectedTxs, txs) { + t.Fatalf("expected txs %+v but got %+v", expectedTxs, txs) + } + txHashes := make([]common.Hash, 3) for i := range txHashes { txHashes[i] = common.BytesToHash(encode.RandomBytes(32)) } - monitoredTx1 := &monitoredTx{ tx: types.NewTx(&types.LegacyTx{Data: []byte{1}}), replacementTx: &txHashes[1], diff --git a/client/asset/interface.go b/client/asset/interface.go index c404ff757a..f19ded50fd 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -1083,21 +1083,45 @@ const ( RedeemBond ApproveToken Acceleration + SelfSend + RevokeTokenApproval ) +// IncomingTxType returns true if the wallet's balance increases due to a +// transaction. +func IncomingTxType(typ TransactionType) bool { + return typ == Receive || typ == Redeem || typ == Refund || typ == RedeemBond +} + +// BondTxInfo contains information about a CreateBond or RedeemBond +// transaction. +type BondTxInfo struct { + // AccountID is the account is the account ID that the bond is applied to. + AccountID dex.Bytes `json:"accountID"` + // LockTime is the time until which the bond is locked. + LockTime uint64 `json:"lockTime"` + // BondID is the ID of the bond. + BondID dex.Bytes `json:"bondID"` +} + // WalletTransaction represents a transaction that was made by a wallet. type WalletTransaction struct { - Type TransactionType `json:"type"` - ID dex.Bytes `json:"id"` - // BalanceDelta is the amount the wallet balance changed as a result of - // the transaction, excluding fees. - BalanceDelta int64 `json:"balanceDelta"` - Fees uint64 `json:"fees"` + Type TransactionType `json:"type"` + ID string `json:"id"` + Amount uint64 `json:"amount"` + Fees uint64 `json:"fees"` // BlockNumber is 0 for txs in the mempool. BlockNumber uint64 `json:"blockNumber"` + // Timestamp is the time the transaction was mined. + Timestamp uint64 `json:"timestamp"` // TokenID will be non-nil if the BalanceDelta applies to the balance // of a token. TokenID *uint32 `json:"tokenID,omitempty"` + // Recipient wil be non-nil for Send transactions, and specifies the + // recipient of the send. + Recipient *string `json:"recipient,omitempty"` + // BondInfo will be non-nil for CreateBond and RedeemBond transactions. + BondInfo *BondTxInfo `json:"bondInfo,omitempty"` // AdditionalData contains asset specific information, i.e. nonce // for ETH. AdditionalData map[string]string `json:"additionalData"` @@ -1112,7 +1136,7 @@ type WalletHistorian interface { // refID are returned, otherwise the transactions after the refID are // returned. n is the number of transactions to return. If n is <= 0, // all the transactions will be returned. - TxHistory(n int, refID *dex.Bytes, past bool) ([]*WalletTransaction, error) + TxHistory(n int, refID *string, past bool) ([]*WalletTransaction, error) } // Bond is the fidelity bond info generated for a certain account ID, amount, @@ -1438,6 +1462,13 @@ type BalanceChangeNote struct { Balance *Balance } +// TransactionNote is sent when a transaction is made, seen, or updated. +type TransactionNote struct { + baseWalletNotification + Transaction *WalletTransaction `json:"transaction"` + New bool `json:"new"` +} + // CustomWalletNote is any other information the wallet wishes to convey to // the user. type CustomWalletNote struct { @@ -1505,3 +1536,15 @@ func (e *WalletEmitter) BalanceChange(bal *Balance) { Balance: bal, }) } + +// TransactionNote sends a TransactionNote. +func (e *WalletEmitter) TransactionNote(tx *WalletTransaction, new bool) { + e.emit(&TransactionNote{ + baseWalletNotification: baseWalletNotification{ + AssetID: e.assetID, + Route: "transaction", + }, + Transaction: tx, + New: new, + }) +} diff --git a/client/core/core.go b/client/core/core.go index 267214e11f..86c9ef11f3 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -5914,7 +5914,7 @@ func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) ([]*Order, error) { // refID are returned, otherwise the transactions after the refID are // returned. n is the number of transactions to return. If n is <= 0, // all the transactions will be returned -func (c *Core) TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (c *Core) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { wallet, found := c.wallet(assetID) if !found { return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) diff --git a/client/core/wallet.go b/client/core/wallet.go index 40f9795a5c..8e8f080143 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -539,7 +539,7 @@ func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contrac // refID are returned, otherwise the transactions after the refID are // returned. n is the number of transactions to return. If n is <= 0, // all the transactions will be returned. -func (w *xcWallet) TxHistory(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (w *xcWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { if !w.connected() { return nil, errWalletNotConnected } diff --git a/client/mm/mm_test.go b/client/mm/mm_test.go index 95ba31d116..bf9e60bf1b 100644 --- a/client/mm/mm_test.go +++ b/client/mm/mm_test.go @@ -1599,9 +1599,6 @@ func TestSegregatedCoreMaxBuy(t *testing.T) { tempDir := t.TempDir() for _, test := range tests { - if test.name != "1 lot with refund fees, account locker" { - continue - } tCore.setAssetBalances(test.assetBalances) tCore.market = test.market tCore.buySwapFees = test.swapFees diff --git a/client/rpcserver/rpcserver.go b/client/rpcserver/rpcserver.go index 8843f4b61a..b71893cfb8 100644 --- a/client/rpcserver/rpcserver.go +++ b/client/rpcserver/rpcserver.go @@ -85,7 +85,7 @@ type clientCore interface { RemoveWalletPeer(assetID uint32, host string) error Notifications(int) ([]*db.Notification, error) MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core.Order, error) - TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) + TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) // These are core's ticket buying interface. StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) diff --git a/client/rpcserver/rpcserver_test.go b/client/rpcserver/rpcserver_test.go index 94895d125a..8e3f253d09 100644 --- a/client/rpcserver/rpcserver_test.go +++ b/client/rpcserver/rpcserver_test.go @@ -192,7 +192,7 @@ func (c *TCore) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) func (c *TCore) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error { return c.setVotingPrefErr } -func (c *TCore) TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) { +func (c *TCore) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { return nil, nil } diff --git a/client/rpcserver/types.go b/client/rpcserver/types.go index fb7a7ef109..b5b414df82 100644 --- a/client/rpcserver/types.go +++ b/client/rpcserver/types.go @@ -217,7 +217,7 @@ type setVotingPreferencesForm struct { type txHistoryForm struct { assetID uint32 num int - refID *dex.Bytes + refID *string past bool } @@ -984,19 +984,14 @@ func parseTxHistoryArgs(params *RawParams) (*txHistoryForm, error) { } } - var refID *dex.Bytes + var refID *string var past bool if len(params.Args) > 2 { if len(params.Args) != 4 { return nil, fmt.Errorf("refID provided without past") } - id, err := hex.DecodeString(params.Args[2]) - if err != nil { - return nil, fmt.Errorf("invalid refID: %v", err) - } - idDB := dex.Bytes(id) - refID = &idDB + refID = ¶ms.Args[2] past, err = checkBoolArg(params.Args[3], "past") if err != nil { diff --git a/client/webserver/api.go b/client/webserver/api.go index be7659b9ce..e3eebd949c 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -1886,6 +1886,36 @@ func (s *WebServer) apiMarketMakingStatus(w http.ResponseWriter, r *http.Request }, s.indent) } +func (s *WebServer) apiTxHistory(w http.ResponseWriter, r *http.Request) { + var form struct { + AssetID uint32 `json:"assetID"` + N int `json:"n"` + RefID string `json:"refID"` + Past bool `json:"past"` + } + if !readPost(w, r, &form) { + return + } + + var refID *string + if len(form.RefID) > 0 { + refID = &form.RefID + } + + txs, err := s.core.TxHistory(form.AssetID, form.N, refID, form.Past) + if err != nil { + s.writeAPIError(w, fmt.Errorf("error getting transaction history: %w", err)) + return + } + writeJSON(w, &struct { + OK bool `json:"ok"` + Txs []*asset.WalletTransaction `json:"txs"` + }{ + OK: true, + Txs: txs, + }, s.indent) +} + // writeAPIError logs the formatted error and sends a standardResponse with the // error message. func (s *WebServer) writeAPIError(w http.ResponseWriter, err error) { diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index ff6ce31d17..2814778ef4 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -863,6 +863,9 @@ func (c *TCore) UnshieldFunds(assetID uint32, amt uint64) ([]byte, error) { func (c *TCore) SendShielded(appPW []byte, assetID uint32, toAddr string, amt uint64) ([]byte, error) { return nil, nil } +func (c *TCore) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { + return nil, nil +} func coreCoin() *core.Coin { b := make([]byte, 36) diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 51b8b2d104..c39b84af1d 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -501,4 +501,16 @@ var EnUS = map[string]string{ "Yes": "Yes", "Treasury Keys": "Treasury Keys", "bonds_pending_refund": "Bonds Pending Refund", + "asset_name tx_history": " Transaction History", + "ID": "ID", + "no_tx_history": "No transactions to show", + "tx_details": "Transaction Details", + "fee": "Fee", + "tx_id": "Transaction ID", + "bond_id": "Bond ID", + "locktime": "Lock Time", + "recipient": "Recipient", + "block": "Block", + "timestamp": "Timestamp", + "nonce": "Nonce", } diff --git a/client/webserver/site/src/css/application.scss b/client/webserver/site/src/css/application.scss index 4d9ed2e8de..e0383ac860 100644 --- a/client/webserver/site/src/css/application.scss +++ b/client/webserver/site/src/css/application.scss @@ -26,7 +26,8 @@ $grid-breakpoints: ( md: 768px, lg: 992px, xl: 1200px, - xxl: 1750px + xxl: 1750px, + xxxl: 2150px ); // Bootstrap diff --git a/client/webserver/site/src/css/wallets.scss b/client/webserver/site/src/css/wallets.scss index f39a82ab50..12ae6375bc 100644 --- a/client/webserver/site/src/css/wallets.scss +++ b/client/webserver/site/src/css/wallets.scss @@ -29,7 +29,7 @@ width: 400px; } - #copyAddressBtn:hover { + .copy-btn:hover { cursor: pointer; color: #a8a8a8; } @@ -149,7 +149,7 @@ } } - table#ordersTable { + table#ordersTable, #txHistoryTable { th { font-size: 15px; } @@ -165,15 +165,19 @@ tbody { font-size: 15px; + } + } - // tr:nth-child(even) { - // background-color: #7772; - // } + #txHistoryTableContainer { + max-height: 350px; + overflow-y: auto; + width: 100%; + text-align: center; + } - tr:hover { - background-color: #7774; - } - } + #earlierTxs, #txViewBlockExplorer { + text-decoration: underline; + cursor: pointer; } #checkmarkBox { @@ -181,9 +185,10 @@ height: 100px; } - #marketsOverviewBox { - border-left: 1px solid $light_border_color; - border-bottom: 1px solid $light_border_color; + @include media-breakpoint-up(lg) { + #txHistoryBox, #orderActivityBox { + border-left: 1px solid $light_border_color; + } } #walletDetailsBox { @@ -257,13 +262,17 @@ } } -@include media-breakpoint-up(xxl) { - .no-overflow-xxl { +@include media-breakpoint-up(xxxl) { + .no-overflow-xxxl { overflow: hidden; } + #marketsOverviewBox { + border-left: 1px solid $light_border_color; + } + .walletspage { - .flex-nowrap-xxl { + .flex-nowrap-xxxl { flex-wrap: nowrap; } @@ -288,8 +297,8 @@ } #marketsOverviewBox { - border-right: 1px solid $light-border-color; - border-bottom: none; + border-top: 1px solid $light-border-color; + border-bottom: 1px solid $light_border_color; } #walletDetailsBox { @@ -332,6 +341,11 @@ width: 450px; } +#txDetails { + width: 450px; + font-size: 17px; +} + .warning-text { text-decoration: underline; text-decoration-color: #d11414; @@ -347,3 +361,21 @@ #submitReconfig[disabled] { cursor: not-allowed; } + +.ease-color { + transition: color 1s ease; +} + +#txDetailsBondSection, #txDetailsRecipientSection, #txDetailsNonceSection { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #777; +} + +.positive-tx { + color: #1e7d11; +} + +.negative-tx { + color: #d11414; +} diff --git a/client/webserver/site/src/css/wallets_dark.scss b/client/webserver/site/src/css/wallets_dark.scss index fd193522c3..e32b4b6c4c 100644 --- a/client/webserver/site/src/css/wallets_dark.scss +++ b/client/webserver/site/src/css/wallets_dark.scss @@ -1,10 +1,5 @@ body.dark { .walletspage { - #marketsOverviewBox { - border-left: 1px solid $dark_border_color; - border-bottom: 1px solid $dark_border_color; - } - #walletDetailsBox { border-bottom: $dark_border_color; } @@ -57,11 +52,7 @@ body.dark { } } - @include media-breakpoint-up(lg) { - .walletspage { - #marketsOverviewBox { - border-color: $dark-border-color; - } - } + #marketsOverviewBox, #orderActivityBox, #txHistoryBox { + border-color: $dark-border-color; } } diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 9c48da4d75..d8df3c86fb 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -146,7 +146,7 @@
- + [[[copied]]]
diff --git a/client/webserver/site/src/html/wallets.tmpl b/client/webserver/site/src/html/wallets.tmpl index 2406a4219a..dc71d190f3 100644 --- a/client/webserver/site/src/html/wallets.tmpl +++ b/client/webserver/site/src/html/wallets.tmpl @@ -23,9 +23,9 @@
-
+
{{- /* WALLET DETAILS */ -}} -