From b2f5e51e380c20c6e06d92fbc271e0845a130998 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 30 Oct 2023 10:56:36 -0700 Subject: [PATCH] Merge pull request #890 from ErikEk/add-gettransaction wallet: add gettransaction --- wallet/wallet.go | 53 ++++++++++++++++++++++ wallet/wallet_test.go | 101 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/wallet/wallet.go b/wallet/wallet.go index 2c9d950d7c..c4fdc05b3c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -77,6 +77,9 @@ var ( // to true. ErrTxLabelExists = errors.New("transaction already labelled") + // ErrNoTx is returned when a transaction can not be found. + ErrNoTx = errors.New("can not find transaction") + // ErrTxUnsigned is returned when a transaction is created in the // watch-only mode where we can select coins but not sign any inputs. ErrTxUnsigned = errors.New("watch-only wallet, transaction not signed") @@ -2464,6 +2467,56 @@ func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, return &res, err } +// GetTransactionResult returns a summary of the transaction along with +// other block properties. +type GetTransactionResult struct { + Summary TransactionSummary + Height int32 + BlockHash *chainhash.Hash + Confirmations int32 + Timestamp int64 +} + +// GetTransaction returns detailed data of a transaction given its id. In addition it +// returns properties about its block. +func (w *Wallet) GetTransaction(txHash chainhash.Hash) (*GetTransactionResult, + error) { + + var res GetTransactionResult + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + txDetail, err := w.TxStore.TxDetails(txmgrNs, &txHash) + if err != nil { + return err + } + + // If the transaction was not found we return an error. + if txDetail == nil { + return fmt.Errorf("%w: txid %v", ErrNoTx, txHash) + } + + res = GetTransactionResult{ + Summary: makeTxSummary(dbtx, w, txDetail), + Timestamp: txDetail.Block.Time.Unix(), + Confirmations: txDetail.Block.Height, + } + + // If it is a confirmed transaction we set the corresponding + // block height and hash. + if txDetail.Block.Height != -1 { + res.Height = txDetail.Block.Height + res.BlockHash = &txDetail.Block.Hash + } + + return nil + }) + if err != nil { + return nil, err + } + return &res, nil +} + // AccountResult is a single account result for the AccountsResult type. type AccountResult struct { waddrmgr.AccountProperties diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 0ec2293d82..5bb438aa44 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -7,14 +7,24 @@ import ( "github.com/dcrlabs/ltcwallet/walletdb" "github.com/dcrlabs/ltcwallet/wtxmgr" - + "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" + "github.com/stretchr/testify/require" ) var ( TstSerializedTx, _ = hex.DecodeString("010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb7373000000008c493046022100995447baec31ee9f6d4ec0e05cb2a44f6b817a99d5f6de167d1c75354a946410022100c9ffc23b64d770b0e01e7ff4d25fbc2f1ca8091053078a247905c39fce3760b601410458b8e267add3c1e374cf40f1de02b59213a82e1d84c2b94096e22e2f09387009c96debe1d0bcb2356ffdcf65d2a83d4b34e72c62eccd8490dbf2110167783b2bffffffff0280969800000000001976a914479ed307831d0ac19ebc5f63de7d5f1a430ddb9d88ac38bfaa00000000001976a914dadf9e3484f28b385ddeaa6c575c0c0d18e9788a88ac00000000") TstTx, _ = ltcutil.NewTxFromBytes(TstSerializedTx) TstTxHash = TstTx.Hash() + + TstMinedTxBlockHeight = int32(279143) + TstMinedSignedTxBlockDetails = &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *TstTxHash, + Height: TstMinedTxBlockHeight, + }, + Time: time.Now(), + } ) // TestLocateBirthdayBlock ensures we can properly map a block in the chain to a @@ -205,3 +215,92 @@ func TestLabelTransaction(t *testing.T) { }) } } + +// TestGetTransaction tests if we can fetch a mined, an existing +// and a non-existing transaction from the wallet like we expect. +func TestGetTransaction(t *testing.T) { + t.Parallel() + rec, err := wtxmgr.NewTxRecord(TstSerializedTx, time.Now()) + require.NoError(t, err) + + tests := []struct { + name string + + // Transaction id. + txid chainhash.Hash + + // Expected height. + expectedHeight int32 + + // Store function. + f func(*wtxmgr.Store, walletdb.ReadWriteBucket) (*wtxmgr.Store, error) + + // The error we expect to be returned. + expectedErr error + }{ + { + name: "existing unmined transaction", + txid: *TstTxHash, + // We write txdetail for the tx to disk. + f: func(s *wtxmgr.Store, ns walletdb.ReadWriteBucket) ( + *wtxmgr.Store, error) { + + err = s.InsertTx(ns, rec, nil) + return s, err + }, + expectedErr: nil, + }, + { + name: "existing mined transaction", + txid: *TstTxHash, + // We write txdetail for the tx to disk. + f: func(s *wtxmgr.Store, ns walletdb.ReadWriteBucket) ( + *wtxmgr.Store, error) { + + err = s.InsertTx(ns, rec, TstMinedSignedTxBlockDetails) + return s, err + }, + expectedHeight: TstMinedTxBlockHeight, + expectedErr: nil, + }, + { + name: "non-existing transaction", + txid: *TstTxHash, + // Write no txdetail to disk. + f: func(s *wtxmgr.Store, ns walletdb.ReadWriteBucket) ( + *wtxmgr.Store, error) { + + return s, nil + }, + expectedErr: ErrNoTx, + }, + } + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + w, cleanup := testWallet(t) + defer cleanup() + + err := walletdb.Update(w.db, func(rw walletdb.ReadWriteTx) error { + ns := rw.ReadWriteBucket(wtxmgrNamespaceKey) + _, err := test.f(w.TxStore, ns) + return err + }) + require.NoError(t, err) + tx, err := w.GetTransaction(test.txid) + require.ErrorIs(t, err, test.expectedErr) + + // Discontinue if no transaction were found. + if err != nil { + return + } + + // Check if we get the expected hash. + require.Equal(t, &test.txid, tx.Summary.Hash) + + // Check the block height. + require.Equal(t, test.expectedHeight, tx.Height) + }) + } +}