Skip to content

Commit 3db34f0

Browse files
committed
Implement GetTransactionConfirmations function
The function returns number of confirmations for the given transaction. As Esplora/Electrs doesn't support verbose transactions details in the response, that would contain the confirmations number we need to workaround it. (See: Blockstream/electrs#36) The workaround: 1. Get the raw transaction 2. Deserialize the raw transaction 3. Find transaction block height by finding it in a history of transactions for the output script included in the transaction. 4. Get the latest block height 5. Calculate number of confirmations by subtracting the transaction block height from the latest block height and adding one.
1 parent a573d52 commit 3db34f0

File tree

1 file changed

+115
-2
lines changed

1 file changed

+115
-2
lines changed

pkg/bitcoin/electrum/electrum.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package electrum
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"crypto/tls"
7+
"encoding/hex"
68
"fmt"
79
"strings"
810
"time"
@@ -14,6 +16,7 @@ import (
1416

1517
"github.com/keep-network/keep-common/pkg/wrappers"
1618
"github.com/keep-network/keep-core/pkg/bitcoin"
19+
"github.com/keep-network/keep-core/pkg/internal/byteutils"
1720
)
1821

1922
var (
@@ -150,11 +153,121 @@ func (c *Connection) GetTransaction(
150153
return result, nil
151154
}
152155

156+
// GetTransactionConfirmations gets the number of confirmations for the
157+
// transaction with the given transaction hash. If the transaction with the
158+
// given hash was not found on the chain, this function returns an error.
153159
func (c *Connection) GetTransactionConfirmations(
154160
transactionHash bitcoin.Hash,
155161
) (uint, error) {
156-
// TODO: Implementation.
157-
panic("not implemented")
162+
txID := transactionHash.Hex(bitcoin.ReversedByteOrder)
163+
164+
txLogger := logger.With(
165+
zap.String("txID", txID),
166+
)
167+
168+
var rawTransaction string
169+
err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error {
170+
// We cannot use `GetTransaction` to get the the transaction details
171+
// as Esplora/Electrs doesn't support verbose transactions.
172+
// See: https://github.com/Blockstream/electrs/pull/36
173+
rawTx, err := c.client.GetRawTransaction(c.ctx, txID)
174+
if err != nil {
175+
return fmt.Errorf(
176+
"GetRawTransaction failed: [%w]",
177+
err,
178+
)
179+
}
180+
rawTransaction = rawTx
181+
return nil
182+
})
183+
if err != nil {
184+
return 0, fmt.Errorf("failed to get raw transaction [%s]: [%w]", txID, err)
185+
}
186+
187+
tx, err := decodeTransaction(rawTransaction)
188+
if err != nil {
189+
return 0, fmt.Errorf(
190+
"failed to decode the transaction [%s]: [%w]",
191+
rawTransaction,
192+
err,
193+
)
194+
}
195+
196+
// As a workaround for the problem described in https://github.com/Blockstream/electrs/pull/36
197+
// we need to calculate the number of confirmations based on the latest
198+
// block height and block height of the transaction.
199+
// Electrum protocol doesn't expose a function to get the transaction's block
200+
// height (other that the `GetTransaction` that is unsupported by Esplora/Electrs).
201+
// To get the block height of the transaction we query the history of transactions
202+
// for the output script hash, as the history contains the transaction's block
203+
// height.
204+
205+
// Initialize txBlockHeigh with minimum int32 value to identify a problem when
206+
// a block height was not found in a history of any of the script hashes.
207+
//
208+
// The history is expected to return a block height for confirmed transaction.
209+
// If a transaction is unconfirmed (is still in the mempool) the height will
210+
// have a value of `0` or `-1`.
211+
txBlockHeight := int32(math.MinInt32)
212+
txOutLoop:
213+
for _, txOut := range tx.TxOut {
214+
script := txOut.PkScript
215+
scriptHash := sha256.Sum256(script)
216+
reversedScriptHash := byteutils.Reverse(scriptHash[:])
217+
reversedScriptHashString := hex.EncodeToString(reversedScriptHash)
218+
219+
var scriptHashHistory []*electrum.GetMempoolResult
220+
err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error {
221+
history, err := c.client.GetHistory(c.ctx, reversedScriptHashString)
222+
if err != nil {
223+
return fmt.Errorf("GetHistory failed: [%w]", err)
224+
}
225+
226+
scriptHashHistory = history
227+
228+
return nil
229+
})
230+
if err != nil {
231+
// Don't return an error, but continue to the next TxOut entry.
232+
txLogger.Errorf("failed to get history for script hash [%s]: [%w]", err)
233+
continue txOutLoop
234+
}
235+
236+
for _, transaction := range scriptHashHistory {
237+
if transaction.Hash == txID {
238+
txBlockHeight = transaction.Height
239+
break txOutLoop
240+
}
241+
}
242+
}
243+
244+
// History querying didn't come up with the transaction's block height. Return
245+
// an error.
246+
if txBlockHeight == math.MinInt32 {
247+
return 0, fmt.Errorf(
248+
"failed to find the transaction block height in script hashes' histories",
249+
)
250+
}
251+
252+
// If the block height is greater than `0` the transaction is confirmed.
253+
if txBlockHeight > 0 {
254+
latestBlockHeight, err := c.GetLatestBlockHeight()
255+
if err != nil {
256+
return 0, fmt.Errorf(
257+
"failed to get the latest block height: [%w]",
258+
err,
259+
)
260+
}
261+
262+
if latestBlockHeight >= uint(txBlockHeight) {
263+
// Add `1` to the calculated difference as if the transaction block
264+
// height equals the latest block height the transaction is already
265+
// confirmed, so it has one confirmation.
266+
return latestBlockHeight - uint(txBlockHeight) + 1, nil
267+
}
268+
}
269+
270+
return 0, nil
158271
}
159272

160273
// BroadcastTransaction broadcasts the given transaction over the

0 commit comments

Comments
 (0)