@@ -2982,3 +2982,167 @@ func TestListOutputsToWatchBareMultisigUsesOutputScript(t *testing.T) {
29822982 require .Equal (t , multiSigScript , utxos [0 ].PkScript )
29832983 require .NotEqual (t , memberScript , utxos [0 ].PkScript )
29842984}
2985+
2986+ // TestApplyTxBatchStoresTxAndSyncTip verifies that a runtime batch can persist
2987+ // transaction history and advance the wallet sync tip atomically.
2988+ func TestApplyTxBatchStoresTxAndSyncTip (t * testing.T ) {
2989+ t .Parallel ()
2990+
2991+ store := NewTestStore (t )
2992+ walletName := "wallet-apply-tx-batch"
2993+ walletID := newWallet (t , store , walletName )
2994+ createDerivedAccount (t , store , walletID , db .KeyScopeBIP0084 , "default" )
2995+
2996+ addr := newDerivedAddress (
2997+ t , store , walletID , db .KeyScopeBIP0084 , "default" , false ,
2998+ )
2999+ tx := newRegularTx (
3000+ []wire.OutPoint {randomOutPoint ()},
3001+ []* wire.TxOut {{Value : 7000 , PkScript : addr .ScriptPubKey }},
3002+ )
3003+ syncedTo := NewBlockFixture (212 )
3004+
3005+ err := store .ApplyTxBatch (t .Context (), db.TxBatchParams {
3006+ WalletID : walletID ,
3007+ Transactions : []db.CreateTxParams {{
3008+ WalletID : walletID ,
3009+ Tx : tx ,
3010+ Received : time .Unix (1710000150 , 0 ),
3011+ Status : db .TxStatusPending ,
3012+ Credits : map [uint32 ]btcutil.Address {0 : nil },
3013+ }},
3014+ SyncedTo : & syncedTo ,
3015+ })
3016+ require .NoError (t , err )
3017+
3018+ txInfo , err := store .GetTx (t .Context (), db.GetTxQuery {
3019+ WalletID : walletID ,
3020+ Txid : tx .TxHash (),
3021+ })
3022+ require .NoError (t , err )
3023+ require .Equal (t , db .TxStatusPending , txInfo .Status )
3024+ require .Nil (t , txInfo .Block )
3025+ require .True (t , walletUtxoExists (t , store , walletID , wire.OutPoint {
3026+ Hash : tx .TxHash (), Index : 0 ,
3027+ }))
3028+
3029+ walletInfo , err := store .GetWallet (t .Context (), walletName )
3030+ require .NoError (t , err )
3031+ require .NotNil (t , walletInfo .SyncedTo )
3032+ require .Equal (t , syncedTo .Hash , walletInfo .SyncedTo .Hash )
3033+ require .Equal (t , syncedTo .Height , walletInfo .SyncedTo .Height )
3034+ require .Equal (t , syncedTo .Timestamp .Unix (),
3035+ walletInfo .SyncedTo .Timestamp .Unix ())
3036+ }
3037+
3038+ // TestApplyTxBatchConfirmsTxInSameBlock verifies that a batch can record a
3039+ // transaction confirmed in the very block the same batch introduces as the new
3040+ // sync tip. The confirming block row does not exist before the batch, so the
3041+ // batch must create the sync-tip block before recording the confirmed
3042+ // transaction; otherwise the confirmed insert fails with ErrBlockNotFound.
3043+ func TestApplyTxBatchConfirmsTxInSameBlock (t * testing.T ) {
3044+ t .Parallel ()
3045+
3046+ store := NewTestStore (t )
3047+ walletName := "wallet-apply-tx-batch-confirmed"
3048+ walletID := newWallet (t , store , walletName )
3049+ createDerivedAccount (t , store , walletID , db .KeyScopeBIP0084 , "default" )
3050+
3051+ addr := newDerivedAddress (
3052+ t , store , walletID , db .KeyScopeBIP0084 , "default" , false ,
3053+ )
3054+ tx := newRegularTx (
3055+ []wire.OutPoint {randomOutPoint ()},
3056+ []* wire.TxOut {{Value : 7000 , PkScript : addr .ScriptPubKey }},
3057+ )
3058+
3059+ // The confirming block is also the batch's new sync tip. It is not
3060+ // inserted ahead of time, so the batch itself must create it before the
3061+ // confirmed transaction is recorded.
3062+ block := NewBlockFixture (213 )
3063+
3064+ err := store .ApplyTxBatch (t .Context (), db.TxBatchParams {
3065+ WalletID : walletID ,
3066+ Transactions : []db.CreateTxParams {{
3067+ WalletID : walletID ,
3068+ Tx : tx ,
3069+ Received : time .Unix (1710000160 , 0 ),
3070+ Block : & block ,
3071+ Status : db .TxStatusPublished ,
3072+ Credits : map [uint32 ]btcutil.Address {0 : nil },
3073+ }},
3074+ SyncedTo : & block ,
3075+ })
3076+ require .NoError (t , err )
3077+
3078+ // The transaction is recorded as confirmed in the batch's block and its
3079+ // credited output is in the wallet UTXO set.
3080+ txInfo , err := store .GetTx (t .Context (), db.GetTxQuery {
3081+ WalletID : walletID ,
3082+ Txid : tx .TxHash (),
3083+ })
3084+ require .NoError (t , err )
3085+ require .Equal (t , db .TxStatusPublished , txInfo .Status )
3086+ require .NotNil (t , txInfo .Block )
3087+ require .Equal (t , block .Height , txInfo .Block .Height )
3088+ require .Equal (t , block .Hash , txInfo .Block .Hash )
3089+ require .True (t , walletUtxoExists (t , store , walletID , wire.OutPoint {
3090+ Hash : tx .TxHash (), Index : 0 ,
3091+ }))
3092+
3093+ // The sync tip advanced to the same block.
3094+ walletInfo , err := store .GetWallet (t .Context (), walletName )
3095+ require .NoError (t , err )
3096+ require .NotNil (t , walletInfo .SyncedTo )
3097+ require .Equal (t , block .Height , walletInfo .SyncedTo .Height )
3098+ require .Equal (t , block .Hash , walletInfo .SyncedTo .Hash )
3099+ }
3100+
3101+ // TestApplyTxBatchRejectsMismatchedWalletID verifies that a batch is rejected
3102+ // when any transaction is owned by a wallet other than the batch wallet, and
3103+ // that the rejection commits nothing: the sync tip is not advanced and no
3104+ // transaction row is written.
3105+ func TestApplyTxBatchRejectsMismatchedWalletID (t * testing.T ) {
3106+ t .Parallel ()
3107+
3108+ store := NewTestStore (t )
3109+ walletName := "wallet-apply-tx-batch-mismatch"
3110+ walletID := newWallet (t , store , walletName )
3111+ createDerivedAccount (t , store , walletID , db .KeyScopeBIP0084 , "default" )
3112+
3113+ addr := newDerivedAddress (
3114+ t , store , walletID , db .KeyScopeBIP0084 , "default" , false ,
3115+ )
3116+ tx := newRegularTx (
3117+ []wire.OutPoint {randomOutPoint ()},
3118+ []* wire.TxOut {{Value : 7000 , PkScript : addr .ScriptPubKey }},
3119+ )
3120+ syncedTo := NewBlockFixture (214 )
3121+
3122+ // The batch targets walletID, but the lone transaction claims a different
3123+ // wallet. The whole batch must be rejected before any write.
3124+ err := store .ApplyTxBatch (t .Context (), db.TxBatchParams {
3125+ WalletID : walletID ,
3126+ Transactions : []db.CreateTxParams {{
3127+ WalletID : walletID + 99 ,
3128+ Tx : tx ,
3129+ Received : time .Unix (1710000170 , 0 ),
3130+ Status : db .TxStatusPending ,
3131+ Credits : map [uint32 ]btcutil.Address {0 : nil },
3132+ }},
3133+ SyncedTo : & syncedTo ,
3134+ })
3135+ require .ErrorIs (t , err , db .ErrInvalidParam )
3136+
3137+ // The sync tip was not advanced: the wallet is still unsynced.
3138+ walletInfo , err := store .GetWallet (t .Context (), walletName )
3139+ require .NoError (t , err )
3140+ require .Nil (t , walletInfo .SyncedTo )
3141+
3142+ // No transaction row was written for either wallet.
3143+ _ , err = store .GetTx (t .Context (), db.GetTxQuery {
3144+ WalletID : walletID ,
3145+ Txid : tx .TxHash (),
3146+ })
3147+ require .ErrorIs (t , err , db .ErrTxNotFound )
3148+ }
0 commit comments