@@ -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.
277284func (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