diff --git a/wallet/wallet.go b/wallet/wallet.go index 911185519c..1023908cda 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -2373,6 +2373,175 @@ func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, return &res, err } +// GetTransactionResult returns a transaction in the UnminedTransaction +// field if it is unmined, or in the MinedTransaction field as a Block +// structure for a mined transaction. +type GetTransactionResult struct { + MinedTransaction *Block + UnminedTransaction *TransactionSummary +} + +// GetTransaction returns data of any transaction given its id. A mined +// transaction is returned in a Block structure which records properties about +// the block. A bool is also returned to denote if the transaction previously +// existed in the database or not. +func (w *Wallet) GetTransaction(txHash *chainhash.Hash) (*GetTransactionResult, + bool, error) { + + var res GetTransactionResult + chainClient := w.chainClient + if chainClient == nil { + return nil, false, errors.New("no chain server client") + } + + // In case the transaction is unconfirmed it could due to race condition + // confirm during the time we receive it from the backend and check it + // in the database. + // We therefore store the best block height and will use it for quering the + // database. + _, bestHeight, err := chainClient.GetBestBlock() + if err != nil { + return nil, false, errors.New("cannot get best block") + } + + // Get the transaction information from directly calling the backend + // endpoint. + var txResult *btcjson.TxRawResult + switch client := chainClient.(type) { + case *chain.RPCClient: + txResult, err = client.GetRawTransactionVerbose(txHash) + if err != nil { + return nil, false, err + } + case *chain.BitcoindClient: + txResult, err = client.GetRawTransactionVerbose(txHash) + if err != nil { + return nil, false, err + } + case *chain.NeutrinoClient: + return nil, false, errors.New("not supported with neutrino client") + } + + // Set the block hash from the provided string and get the block height + // from it. + // + // TODO: Fetching block heights by their hashes is inherently racy + // because not all block headers are saved but when they are for SPV the + // db can be queried directly without this. + var blockHash *chainhash.Hash + var height int32 = -1 + if txResult.BlockHash != "" { + blockHashBytes, err := hex.DecodeString(txResult.BlockHash) + if err != nil { + return nil, false, err + } + + // The byte is reversed due to different endianness in response + // and call. + reversed := make([]byte, len(blockHashBytes)) + for i, n := range blockHashBytes { + j := len(blockHashBytes) - i - 1 + reversed[j] = n + } + blockHash, err = chainhash.NewHash(reversed) + if err != nil { + return nil, false, err + } + + // Obtain the block height from the backend. + switch client := chainClient.(type) { + case *chain.RPCClient: + header, err := client.GetBlockHeaderVerbose(blockHash) + if err != nil { + return nil, false, err + } + height = header.Height + case *chain.BitcoindClient: + height, err = client.GetBlockHeight(blockHash) + if err != nil { + return nil, false, err + } + case *chain.NeutrinoClient: + height, err = client.GetBlockHeight(blockHash) + if err != nil { + return nil, false, err + } + } + + // We know that the transaction definitively is in a specific + // block height and set both start and end height to it. + bestHeight = height + } + + // Populate with additional data if this transaction exists in the + // database. + var summary *TransactionSummary + var inDB bool + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // TODO: probably should make RangeTransactions not + // reuse the details backing array memory. + dets := make([]wtxmgr.TxDetails, len(details)) + copy(dets, details) + details = dets + + for i := range details { + dbTx := makeTxSummary(dbtx, w, &details[i]) + + // We are only interested in the specific + // transaction and can return early. + if txHash.IsEqual(dbTx.Hash) { + summary, inDB = &dbTx, true + height = details[i].Block.Height + blockHash = &details[i].Block.Hash + return true, nil + } + } + return false, nil + } + + return w.TxStore.RangeTransactions(txmgrNs, bestHeight, + height, rangeFn) + }) + if err != nil { + return nil, false, err + } + + // We need to build the transaction to return as the response if it was + // not found in the database. + if summary == nil { + txRaw, err := hex.DecodeString(txResult.Hex) + if err != nil { + return nil, false, err + } + summary = &TransactionSummary{ + Hash: txHash, + Transaction: txRaw, + MyInputs: make([]TransactionSummaryInput, 0), + MyOutputs: make([]TransactionSummaryOutput, 0), + Timestamp: txResult.Time, + } + } + + // Add the transaction either as confirmed or unconfirmed to the + // response. + switch blockHash { + case nil: + res.UnminedTransaction = summary + default: + res.MinedTransaction = &Block{ + Hash: blockHash, + Height: height, + Timestamp: summary.Timestamp, + Transactions: []TransactionSummary{*summary}, + } + } + + return &res, inDB, nil +} + // AccountResult is a single account result for the AccountsResult type. type AccountResult struct { waddrmgr.AccountProperties