Skip to content

Commit f22553f

Browse files
committed
wallet: route relevant tx notifications
Send relevant transaction notifications through db.Store so the dispatcher no longer reads or writes directly through legacy txstore paths.
1 parent c2bb778 commit f22553f

2 files changed

Lines changed: 172 additions & 1 deletion

File tree

wallet/syncer.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,79 @@ func (s *syncer) updateSyncTip(ctx context.Context,
657657
return nil
658658
}
659659

660+
// putTxNotifications records relevant transaction notifications through the
661+
// store when configured, falling back to the legacy walletdb path otherwise.
662+
func (s *syncer) putTxNotifications(ctx context.Context,
663+
matches TxEntries, blockMeta *wtxmgr.BlockMeta) error {
664+
665+
if s.store == nil {
666+
return s.DBPutTxns(ctx, matches, blockMeta)
667+
}
668+
669+
var block *db.Block
670+
if blockMeta != nil {
671+
var err error
672+
673+
block, err = storeBlockFromBlockMeta(*blockMeta)
674+
if err != nil {
675+
return err
676+
}
677+
}
678+
679+
transactions := make([]db.CreateTxParams, 0, len(matches))
680+
for i := range matches {
681+
match := matches[i]
682+
credits := make(map[uint32]btcutil.Address, len(match.Entries))
683+
684+
for _, entry := range match.Entries {
685+
index := entry.Credit.Index
686+
if uint64(index) >= uint64(len(match.Rec.MsgTx.TxOut)) {
687+
return fmt.Errorf("credit output %d: %w", index,
688+
db.ErrInvalidParam)
689+
}
690+
691+
pkScript := match.Rec.MsgTx.TxOut[index].PkScript
692+
_, err := s.store.GetAddress(
693+
ctx, db.GetAddressQuery{
694+
WalletID: s.walletID,
695+
ScriptPubKey: pkScript,
696+
},
697+
)
698+
if errors.Is(err, db.ErrAddressNotFound) {
699+
continue
700+
}
701+
702+
if err != nil {
703+
return fmt.Errorf("resolve tx credit %d: %w", index,
704+
err)
705+
}
706+
707+
credits[index] = entry.Address
708+
}
709+
710+
transactions = append(transactions, db.CreateTxParams{
711+
WalletID: s.walletID,
712+
Tx: &match.Rec.MsgTx,
713+
Received: match.Rec.Received,
714+
Block: block,
715+
Status: db.TxStatusPublished,
716+
Credits: credits,
717+
})
718+
}
719+
720+
err := s.store.ApplyTxBatch(
721+
ctx, db.TxBatchParams{
722+
WalletID: s.walletID,
723+
Transactions: transactions,
724+
},
725+
)
726+
if err != nil {
727+
return fmt.Errorf("apply tx notifications: %w", err)
728+
}
729+
730+
return nil
731+
}
732+
660733
// storeBlockFromBlockMeta converts chain notification block metadata into the
661734
// store block shape.
662735
func storeBlockFromBlockMeta(block wtxmgr.BlockMeta) (*db.Block, error) {
@@ -1287,7 +1360,7 @@ func (s *syncer) processChainUpdate(ctx context.Context, update any) error {
12871360
// handled atomically via FilteredBlockConnected.
12881361
case chain.RelevantTx:
12891362
matches := s.prepareTxMatches([]*wtxmgr.TxRecord{n.TxRecord})
1290-
return s.DBPutTxns(ctx, matches, n.Block)
1363+
return s.putTxNotifications(ctx, matches, n.Block)
12911364

12921365
case chain.FilteredBlockConnected:
12931366
matches := s.prepareTxMatches(n.RelevantTxs)

wallet/syncer_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,104 @@ func TestHandleChainUpdate(t *testing.T) {
831831
require.NoError(t, err)
832832
}
833833

834+
// matchGetAddressByScript returns a matcher asserting a GetAddress query
835+
// targets the given wallet and script.
836+
func matchGetAddressByScript(walletID uint32, pkScript []byte) any {
837+
return mock.MatchedBy(func(query db.GetAddressQuery) bool {
838+
return query.WalletID == walletID &&
839+
bytes.Equal(query.ScriptPubKey, pkScript)
840+
})
841+
}
842+
843+
// matchRelevantTxParams reports whether the single batched transaction carries
844+
// the expected wallet, hash, receive time, credit address, and published
845+
// status. Block confirmation is validated separately by the caller.
846+
func matchRelevantTxParams(txParams db.CreateTxParams, walletID uint32,
847+
tx *wire.MsgTx, addr btcutil.Address, received time.Time) bool {
848+
849+
creditAddr, ok := txParams.Credits[0]
850+
if !ok || creditAddr == nil || txParams.Tx == nil {
851+
return false
852+
}
853+
854+
return txParams.WalletID == walletID &&
855+
txParams.Tx.TxHash() == tx.TxHash() &&
856+
txParams.Received.Equal(received) &&
857+
txParams.Status == db.TxStatusPublished &&
858+
creditAddr.EncodeAddress() == addr.EncodeAddress()
859+
}
860+
861+
// matchUnminedTxBatch returns a matcher asserting an unmined relevant
862+
// transaction is written as a single store batch with the expected credit and
863+
// no confirming block.
864+
func matchUnminedTxBatch(walletID uint32, tx *wire.MsgTx,
865+
addr btcutil.Address, received time.Time) any {
866+
867+
return mock.MatchedBy(func(params db.TxBatchParams) bool {
868+
if params.WalletID != walletID ||
869+
len(params.Transactions) != 1 {
870+
871+
return false
872+
}
873+
874+
txParams := params.Transactions[0]
875+
if txParams.Block != nil {
876+
return false
877+
}
878+
879+
return matchRelevantTxParams(
880+
txParams, walletID, tx, addr, received,
881+
)
882+
})
883+
}
884+
885+
// TestProcessRelevantTxUsesStore verifies that relevant transaction
886+
// notifications are routed through the store when store wiring is available.
887+
func TestProcessRelevantTxUsesStore(t *testing.T) {
888+
t.Parallel()
889+
890+
// Arrange: Create a syncer with store wiring and a transaction paying to a
891+
// wallet-owned address.
892+
const walletID uint32 = 7
893+
894+
store := &walletmock.Store{}
895+
publisher := &mockTxPublisher{}
896+
s := newSyncer(
897+
Config{ChainParams: &chainParams}, nil, nil, publisher,
898+
syncerStoreConfig{store: store, walletID: walletID},
899+
)
900+
901+
addr, err := btcutil.NewAddressPubKeyHash(
902+
make([]byte, 20), &chainParams,
903+
)
904+
require.NoError(t, err)
905+
906+
pkScript, err := txscript.PayToAddrScript(addr)
907+
require.NoError(t, err)
908+
909+
tx := wire.NewMsgTx(1)
910+
tx.AddTxOut(&wire.TxOut{Value: 1000, PkScript: pkScript})
911+
912+
received := time.Unix(123, 0).UTC()
913+
rec, err := wtxmgr.NewTxRecordFromMsgTx(tx, received)
914+
require.NoError(t, err)
915+
916+
store.On("GetAddress", mock.Anything,
917+
matchGetAddressByScript(walletID, pkScript),
918+
).Return(&db.AddressInfo{ScriptPubKey: pkScript}, nil).Once()
919+
920+
store.On("ApplyTxBatch", mock.Anything,
921+
matchUnminedTxBatch(walletID, tx, addr, received),
922+
).Return(nil).Once()
923+
924+
// Act: Process an unconfirmed relevant transaction notification.
925+
err = s.processChainUpdate(t.Context(), chain.RelevantTx{TxRecord: rec})
926+
927+
// Assert: The notification was written through the store batch API.
928+
require.NoError(t, err)
929+
store.AssertExpectations(t)
930+
}
931+
834932
// TestExtractAddrEntries verifies address extraction from outputs.
835933
func TestExtractAddrEntries(t *testing.T) {
836934
t.Parallel()

0 commit comments

Comments
 (0)