diff --git a/pkg/bitcoin/electrum/electrum.go b/pkg/bitcoin/electrum/electrum.go index 6be8051f8b..0614d9d24b 100644 --- a/pkg/bitcoin/electrum/electrum.go +++ b/pkg/bitcoin/electrum/electrum.go @@ -2,7 +2,9 @@ package electrum import ( "context" + "crypto/sha256" "crypto/tls" + "encoding/hex" "fmt" "strings" "time" @@ -14,6 +16,7 @@ import ( "github.com/keep-network/keep-common/pkg/wrappers" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/internal/byteutils" ) var ( @@ -150,11 +153,121 @@ func (c *Connection) GetTransaction( return result, nil } +// GetTransactionConfirmations gets the number of confirmations for the +// transaction with the given transaction hash. If the transaction with the +// given hash was not found on the chain, this function returns an error. func (c *Connection) GetTransactionConfirmations( transactionHash bitcoin.Hash, ) (uint, error) { - // TODO: Implementation. - panic("not implemented") + txID := transactionHash.Hex(bitcoin.ReversedByteOrder) + + txLogger := logger.With( + zap.String("txID", txID), + ) + + var rawTransaction string + err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error { + // We cannot use `GetTransaction` to get the the transaction details + // as Esplora/Electrs doesn't support verbose transactions. + // See: https://github.com/Blockstream/electrs/pull/36 + rawTx, err := c.client.GetRawTransaction(c.ctx, txID) + if err != nil { + return fmt.Errorf( + "GetRawTransaction failed: [%w]", + err, + ) + } + rawTransaction = rawTx + return nil + }) + if err != nil { + return 0, fmt.Errorf("failed to get raw transaction [%s]: [%w]", txID, err) + } + + tx, err := decodeTransaction(rawTransaction) + if err != nil { + return 0, fmt.Errorf( + "failed to decode the transaction [%s]: [%w]", + rawTransaction, + err, + ) + } + + // As a workaround for the problem described in https://github.com/Blockstream/electrs/pull/36 + // we need to calculate the number of confirmations based on the latest + // block height and block height of the transaction. + // Electrum protocol doesn't expose a function to get the transaction's block + // height (other that the `GetTransaction` that is unsupported by Esplora/Electrs). + // To get the block height of the transaction we query the history of transactions + // for the output script hash, as the history contains the transaction's block + // height. + + // Initialize txBlockHeigh with minimum int32 value to identify a problem when + // a block height was not found in a history of any of the script hashes. + // + // The history is expected to return a block height for confirmed transaction. + // If a transaction is unconfirmed (is still in the mempool) the height will + // have a value of `0` or `-1`. + txBlockHeight := int32(math.MinInt32) +txOutLoop: + for _, txOut := range tx.TxOut { + script := txOut.PkScript + scriptHash := sha256.Sum256(script) + reversedScriptHash := byteutils.Reverse(scriptHash[:]) + reversedScriptHashString := hex.EncodeToString(reversedScriptHash) + + var scriptHashHistory []*electrum.GetMempoolResult + err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error { + history, err := c.client.GetHistory(c.ctx, reversedScriptHashString) + if err != nil { + return fmt.Errorf("GetHistory failed: [%w]", err) + } + + scriptHashHistory = history + + return nil + }) + if err != nil { + // Don't return an error, but continue to the next TxOut entry. + txLogger.Errorf("failed to get history for script hash [%s]: [%w]", err) + continue txOutLoop + } + + for _, transaction := range scriptHashHistory { + if transaction.Hash == txID { + txBlockHeight = transaction.Height + break txOutLoop + } + } + } + + // History querying didn't come up with the transaction's block height. Return + // an error. + if txBlockHeight == math.MinInt32 { + return 0, fmt.Errorf( + "failed to find the transaction block height in script hashes' histories", + ) + } + + // If the block height is greater than `0` the transaction is confirmed. + if txBlockHeight > 0 { + latestBlockHeight, err := c.GetLatestBlockHeight() + if err != nil { + return 0, fmt.Errorf( + "failed to get the latest block height: [%w]", + err, + ) + } + + if latestBlockHeight >= uint(txBlockHeight) { + // Add `1` to the calculated difference as if the transaction block + // height equals the latest block height the transaction is already + // confirmed, so it has one confirmation. + return latestBlockHeight - uint(txBlockHeight) + 1, nil + } + } + + return 0, nil } // BroadcastTransaction broadcasts the given transaction over the