Skip to content

Commit 552b436

Browse files
authored
Merge pull request #82 from fystack/feat/enhance-bitcoin-extraction
Feat/enhance bitcoin extraction
2 parents bebc2be + 0522e97 commit 552b436

10 files changed

Lines changed: 1300 additions & 74 deletions

File tree

configs/config.example.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ chains:
8989
start_block: 4440000 # recent testnet block
9090
poll_interval: "60s" # Bitcoin blocks ~10 minutes
9191
reorg_rollback_window: 100
92-
index_change_output: false # Enable change output event extraction and emission (Bitcoin only)
9392
index_utxo: false # Enable UTXO event extraction and emission (Bitcoin only)
9493
nodes:
9594
- url: "https://bitcoin-testnet-rpc.publicnode.com"
@@ -112,7 +111,6 @@ chains:
112111
start_block: 850000
113112
poll_interval: "60s"
114113
reorg_rollback_window: 100
115-
index_change_output: false
116114
index_utxo: false # Enable UTXO event extraction and emission (Bitcoin only)
117115
nodes:
118116
- url: "https://bitcoin-rpc.publicnode.com"

internal/indexer/bitcoin.go

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ func (b *BitcoinIndexer) convertBlockWithPrevoutResolution(ctx context.Context,
108108
if tx.IsCoinbase() {
109109
continue
110110
}
111-
if len(tx.Vin) > 0 && tx.Vin[0].PrevOut == nil && tx.Vin[0].TxID != "" {
111+
needsAny := false
112+
for _, vin := range tx.Vin {
113+
if vin.TxID != "" && vin.PrevOut == nil {
114+
needsAny = true
115+
break
116+
}
117+
}
118+
if needsAny {
112119
needsResolution = append(needsResolution, i)
113120
}
114121
}
@@ -171,7 +178,7 @@ func (b *BitcoinIndexer) convertBlockWithPrevoutResolution(ctx context.Context,
171178
continue
172179
}
173180

174-
transfers := b.extractTransfersFromTx(tx, btcBlock.Height, btcBlock.Time, latestBlock)
181+
transfers := b.extractTransfersFromTx(tx, btcBlock.Hash, btcBlock.Height, btcBlock.Time, latestBlock)
175182
allTransfers = append(allTransfers, transfers...)
176183

177184
if b.config.IndexUTXO {
@@ -276,6 +283,7 @@ func (b *BitcoinIndexer) GetBlocksByNumbers(
276283
// extractTransfersFromTx extracts all transfers from a transaction.
277284
func (b *BitcoinIndexer) extractTransfersFromTx(
278285
tx *bitcoin.Transaction,
286+
blockHash string,
279287
blockNumber, ts, latestBlock uint64,
280288
) []types.Transaction {
281289
var transfers []types.Transaction
@@ -293,39 +301,19 @@ func (b *BitcoinIndexer) extractTransfersFromTx(
293301
status = types.StatusConfirmed
294302
}
295303

296-
fromAddr := b.getFirstInputAddress(tx)
297-
298-
// Build set of all normalized input addresses for change output detection.
299-
inputAddrs := make(map[string]bool, len(tx.Vin))
300-
for _, vin := range tx.Vin {
301-
addr := bitcoin.GetInputAddress(&vin)
302-
if addr == "" {
303-
continue
304-
}
305-
if normalized, err := bitcoin.NormalizeBTCAddress(addr); err == nil {
306-
addr = normalized
307-
}
308-
inputAddrs[addr] = true
304+
allInputAddrs := b.getAllInputAddresses(tx)
305+
fromAddr := ""
306+
if len(allInputAddrs) > 0 {
307+
fromAddr = allInputAddrs[0]
309308
}
310309

311310
feeAssigned := false
312-
for _, vout := range tx.Vout {
313-
toAddr := bitcoin.GetOutputAddress(&vout)
314-
if toAddr == "" {
311+
for voutIdx, vout := range tx.Vout {
312+
toAddrs := bitcoin.GetOutputAddresses(&vout)
313+
if len(toAddrs) == 0 {
315314
continue // Skip unspendable outputs (OP_RETURN, etc.)
316315
}
317316

318-
if normalized, err := bitcoin.NormalizeBTCAddress(toAddr); err == nil {
319-
toAddr = normalized
320-
}
321-
322-
// For Transfer events, respect index_change_output config
323-
// (This filters what goes to transfer.event.dispatch)
324-
if !b.config.IndexChangeOutput && len(inputAddrs) > 0 && inputAddrs[toAddr] {
325-
continue
326-
}
327-
328-
// Convert BTC to satoshis (multiply by 1e8)
329317
amountSat := satoshisFromFloat(vout.Value)
330318

331319
txFee := decimal.Zero
@@ -334,22 +322,30 @@ func (b *BitcoinIndexer) extractTransfersFromTx(
334322
feeAssigned = true
335323
}
336324

337-
transfer := types.Transaction{
338-
TxHash: tx.TxID,
339-
NetworkId: b.config.NetworkId,
340-
BlockNumber: blockNumber,
341-
FromAddress: fromAddr,
342-
ToAddress: toAddr,
343-
AssetAddress: "", // Empty for native BTC
344-
Amount: strconv.FormatInt(amountSat, 10),
345-
Type: constant.TxTypeNativeTransfer,
346-
TxFee: txFee,
347-
Timestamp: ts,
348-
Confirmations: confirmations,
349-
Status: status,
350-
}
325+
for addrIdx, toAddr := range toAddrs {
326+
if normalized, err := bitcoin.NormalizeBTCAddress(toAddr); err == nil {
327+
toAddr = normalized
328+
}
351329

352-
transfers = append(transfers, transfer)
330+
transfer := types.Transaction{
331+
TxHash: tx.TxID,
332+
NetworkId: b.config.NetworkId,
333+
BlockHash: blockHash,
334+
BlockNumber: blockNumber,
335+
TransferIndex: fmt.Sprintf("%d:%d", voutIdx, addrIdx),
336+
FromAddress: fromAddr,
337+
FromAddresses: allInputAddrs,
338+
ToAddress: toAddr,
339+
AssetAddress: "",
340+
Amount: strconv.FormatInt(amountSat, 10),
341+
Type: constant.TxTypeNativeTransfer,
342+
TxFee: txFee,
343+
Timestamp: ts,
344+
Confirmations: confirmations,
345+
Status: status,
346+
}
347+
transfers = append(transfers, transfer)
348+
}
353349
}
354350

355351
return transfers
@@ -371,24 +367,26 @@ func (b *BitcoinIndexer) extractUTXOEvent(
371367
// Extract ALL created UTXOs (vouts) without filtering
372368
// Filtering happens at emission level based on monitored addresses
373369
for i, vout := range tx.Vout {
374-
addr := bitcoin.GetOutputAddress(&vout)
375-
if addr == "" {
370+
addrs := bitcoin.GetOutputAddresses(&vout)
371+
if len(addrs) == 0 {
376372
continue
377373
}
378374

379-
if normalized, err := bitcoin.NormalizeBTCAddress(addr); err == nil {
380-
addr = normalized
381-
}
382-
383375
amountSat := satoshisFromFloat(vout.Value)
384376

385-
created = append(created, types.UTXO{
386-
TxHash: tx.TxID,
387-
Vout: uint32(i),
388-
Address: addr,
389-
Amount: strconv.FormatInt(amountSat, 10),
390-
ScriptPubKey: vout.ScriptPubKey.Hex,
391-
})
377+
for _, addr := range addrs {
378+
if normalized, err := bitcoin.NormalizeBTCAddress(addr); err == nil {
379+
addr = normalized
380+
}
381+
382+
created = append(created, types.UTXO{
383+
TxHash: tx.TxID,
384+
Vout: uint32(i),
385+
Address: addr,
386+
Amount: strconv.FormatInt(amountSat, 10),
387+
ScriptPubKey: vout.ScriptPubKey.Hex,
388+
})
389+
}
392390
}
393391

394392
// Extract ALL spent UTXOs (vins) without filtering
@@ -445,16 +443,25 @@ func (b *BitcoinIndexer) extractUTXOEvent(
445443
}
446444
}
447445

448-
func (b *BitcoinIndexer) getFirstInputAddress(tx *bitcoin.Transaction) string {
446+
// getAllInputAddresses returns deduplicated, normalized input addresses for a transaction,
447+
// preserving the order of first appearance. Returns an empty slice if no inputs have prevout data.
448+
func (b *BitcoinIndexer) getAllInputAddresses(tx *bitcoin.Transaction) []string {
449+
seen := make(map[string]bool)
450+
var addrs []string
449451
for _, vin := range tx.Vin {
450-
if addr := bitcoin.GetInputAddress(&vin); addr != "" {
451-
if normalized, err := bitcoin.NormalizeBTCAddress(addr); err == nil {
452-
return normalized
453-
}
454-
return addr
452+
addr := bitcoin.GetInputAddress(&vin)
453+
if addr == "" {
454+
continue
455+
}
456+
if normalized, err := bitcoin.NormalizeBTCAddress(addr); err == nil {
457+
addr = normalized
458+
}
459+
if !seen[addr] {
460+
seen[addr] = true
461+
addrs = append(addrs, addr)
455462
}
456463
}
457-
return ""
464+
return addrs
458465
}
459466

460467
// calculateConfirmations calculates the number of confirmations for a transaction
@@ -513,7 +520,7 @@ func (b *BitcoinIndexer) GetMempoolTransactions(ctx context.Context) ([]types.Tr
513520
continue
514521
}
515522

516-
transfers := b.extractTransfersFromTx(tx, 0, currentTime, latestBlock)
523+
transfers := b.extractTransfersFromTx(tx, "", 0, currentTime, latestBlock)
517524
allTransfers = append(allTransfers, transfers...)
518525

519526
if b.config.IndexUTXO {

0 commit comments

Comments
 (0)