From acb0400296ae276e44de984122da3c61a2f039e8 Mon Sep 17 00:00:00 2001 From: strmci Date: Mon, 30 Jun 2025 15:40:00 +0200 Subject: [PATCH] frontend: Add received date to coin control UTXO list Add the received date (block header timestamp) to the UTXO detail in the coin control view. This makes it easier for users to view the date, which can be helpful for various reasons, without having to open each UTXO individually in the block explorer. --- CHANGELOG.md | 1 + backend/coins/btc/account.go | 2 +- backend/coins/btc/handlers/handlers.go | 26 ++++++++++++------- .../coins/btc/transactions/transactions.go | 8 ++++-- .../btc/transactions/transactions_test.go | 14 +++++----- frontends/web/src/api/account.ts | 1 + .../src/routes/account/send/utxos.module.css | 1 + .../web/src/routes/account/send/utxos.tsx | 15 +++++++++-- 8 files changed, 47 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4eb4b1d1f..6a3abd6e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added support to show on the BitBox when a transaction's recipient is an address of a different account on the device. - Persist third party widget sessions - Change notes export file type to JSON Lines +- Add received date to coin control transaction details ## v4.47.3 - Upgrade Etherscan API to V2 diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index b622e70e3e..0cb5fda67a 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -840,7 +840,7 @@ func sortByAddresses(result []*SpendableOutput) []*SpendableOutput { for _, s := range sums { outputs := grouped[s.address] sort.Slice(outputs, func(i, j int) bool { - return outputs[i].Value > outputs[j].Value + return outputs[i].TxOut.Value > outputs[j].TxOut.Value }) newResult = append(newResult, outputs...) } diff --git a/backend/coins/btc/handlers/handlers.go b/backend/coins/btc/handlers/handlers.go index 3cf503aec8..4b2e5dbfd4 100644 --- a/backend/coins/btc/handlers/handlers.go +++ b/backend/coins/btc/handlers/handlers.go @@ -311,18 +311,24 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) { for _, output := range spendableOutputs { address := output.Address.EncodeForHumans() addressReused := addressCounts[address] > 1 - + var formattedTime *string + timestamp := output.HeaderTimestamp + if timestamp != nil { + t := timestamp.Format(time.RFC3339) + formattedTime = &t + } result = append(result, map[string]interface{}{ - "outPoint": output.OutPoint.String(), - "txId": output.OutPoint.Hash.String(), - "txOutput": output.OutPoint.Index, - "amount": coin.ConvertBTCAmount(handlers.account.Coin(), btcutil.Amount(output.TxOut.Value), false, accountConfig.RateUpdater), - "address": address, - "scriptType": output.Address.AccountConfiguration.ScriptType(), - "note": handlers.account.TxNote(output.OutPoint.Hash.String()), - "addressReused": addressReused, - "isChange": output.IsChange, + "outPoint": output.OutPoint.String(), + "txId": output.OutPoint.Hash.String(), + "txOutput": output.OutPoint.Index, + "amount": coin.ConvertBTCAmount(handlers.account.Coin(), btcutil.Amount(output.TxOut.Value), false, accountConfig.RateUpdater), + "address": address, + "scriptType": output.Address.AccountConfiguration.ScriptType(), + "note": handlers.account.TxNote(output.OutPoint.Hash.String()), + "addressReused": addressReused, + "isChange": output.IsChange, + "headerTimestamp": formattedTime, }) } diff --git a/backend/coins/btc/transactions/transactions.go b/backend/coins/btc/transactions/transactions.go index 911b43119f..e940c53899 100644 --- a/backend/coins/btc/transactions/transactions.go +++ b/backend/coins/btc/transactions/transactions.go @@ -15,6 +15,8 @@ package transactions import ( + "time" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/headers" @@ -34,7 +36,8 @@ import ( // SpendableOutput is an unspent coin. type SpendableOutput struct { - *wire.TxOut + TxOut *wire.TxOut + HeaderTimestamp *time.Time } // ScriptHashHex returns the hash of the PkScript of the output, in hex format. @@ -235,7 +238,8 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab if confirmed || transactions.allInputsOurs(dbTx, txInfo.Tx) { result[outPoint] = &SpendableOutput{ - TxOut: txOut, + TxOut: txOut, + HeaderTimestamp: txInfo.HeaderTimestamp, } } } diff --git a/backend/coins/btc/transactions/transactions_test.go b/backend/coins/btc/transactions/transactions_test.go index b6a20b0cad..c9614a19b0 100644 --- a/backend/coins/btc/transactions/transactions_test.go +++ b/backend/coins/btc/transactions/transactions_test.go @@ -177,12 +177,14 @@ func (s *transactionsSuite) TestUpdateAddressHistorySingleTxReceive() { } spendableOutputs, err := s.transactions.SpendableOutputs() s.Require().NoError(err) - s.Require().Equal( - map[wire.OutPoint]*transactions.SpendableOutput{ - {Hash: tx1.TxHash(), Index: 0}: utxo, - }, - spendableOutputs, - ) + expected := map[wire.OutPoint]*wire.TxOut{ + {Hash: tx1.TxHash(), Index: 0}: utxo.TxOut, + } + actual := make(map[wire.OutPoint]*wire.TxOut) + for outpoint, spendable := range spendableOutputs { + actual[outpoint] = spendable.TxOut + } + s.Require().Equal(expected, actual) transactions, err := s.transactions.Transactions(func(blockchainpkg.ScriptHashHex) bool { return false }) s.Require().NoError(err) s.Require().Len(transactions, 1) diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index fb5614bc01..060ed99f22 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -433,6 +433,7 @@ export type TUTXO = { scriptType: ScriptType; addressReused: boolean; isChange: boolean; + headerTimestamp: string | null; }; export const getUTXOs = (code: AccountCode): Promise => { diff --git a/frontends/web/src/routes/account/send/utxos.module.css b/frontends/web/src/routes/account/send/utxos.module.css index f4f9c8b2d1..2022ce08dc 100644 --- a/frontends/web/src/routes/account/send/utxos.module.css +++ b/frontends/web/src/routes/account/send/utxos.module.css @@ -69,6 +69,7 @@ } .address, +.date, .transaction { display: flex; font-size: var(--size-default); diff --git a/frontends/web/src/routes/account/send/utxos.tsx b/frontends/web/src/routes/account/send/utxos.tsx index c514e705f2..7a24e9ac4e 100644 --- a/frontends/web/src/routes/account/send/utxos.tsx +++ b/frontends/web/src/routes/account/send/utxos.tsx @@ -33,8 +33,9 @@ import { Amount } from '@/components/amount/amount'; import { getScriptName } from '@/routes/account/utils'; import { Message } from '@/components/message/message'; import { Badge } from '@/components/badge/badge'; -import style from './utxos.module.css'; +import { parseTimeShort } from '@/utils/date'; import { AmountWithUnit } from '@/components/amount/amount-with-unit'; +import style from './utxos.module.css'; export type TSelectedUTXOs = { [key: string]: boolean; @@ -55,7 +56,7 @@ export const UTXOs = ({ onChange, onClose, }: Props) => { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const [utxos, setUtxos] = useState([]); const [selectedUTXOs, setSelectedUTXOs] = useState({}); const [reusedAddressUTXOs, setReusedAddressUTXOs] = useState(0); @@ -128,6 +129,16 @@ export const UTXOs = ({ +
+ + {t('transaction.details.date')}: + + + {utxo.headerTimestamp + ? parseTimeShort(utxo.headerTimestamp, i18n.language) + : t('transaction.status.pending')} + +
{t('send.coincontrol.address')}: