From 35901d1247db4773330f695ccb91eee08d30b2b2 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 1 Aug 2025 10:16:52 +0200 Subject: [PATCH 1/6] staticaddr: don't append to public variable PendingStates --- staticaddr/loopin/fsm.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/staticaddr/loopin/fsm.go b/staticaddr/loopin/fsm.go index 43e7cdf8f..ecd663b77 100644 --- a/staticaddr/loopin/fsm.go +++ b/staticaddr/loopin/fsm.go @@ -138,7 +138,9 @@ var FinalStates = []fsm.StateType{ HtlcTimeoutSwept, Succeeded, SucceededTransitioningFailed, Failed, } -var AllStates = append(PendingStates, FinalStates...) +var AllStates = append( + append([]fsm.StateType{}, PendingStates...), FinalStates..., +) // Events. var ( From 6e7441ba8dbff1e2939ab5e4447c1a23dc1ab1d6 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 30 Jul 2025 12:00:03 +0200 Subject: [PATCH 2/6] staticaddr: migrate swap hashes to deposits --- go.mod | 2 +- loopd/daemon.go | 10 + .../000017_deposit_swaphash.down.sql | 1 + .../migrations/000017_deposit_swaphash.up.sql | 1 + loopdb/sqlc/models.go | 1 + loopdb/sqlc/querier.go | 3 + loopdb/sqlc/queries/static_address_loopin.sql | 27 +++ loopdb/sqlc/static_address_deposits.sql.go | 9 +- loopdb/sqlc/static_address_loopin.sql.go | 67 ++++++ .../loopin/deposit_swaphash_migration.go | 88 ++++++++ .../loopin/deposit_swaphash_migration_test.go | 195 ++++++++++++++++++ staticaddr/loopin/sql_store.go | 99 +++++++++ 12 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 loopdb/sqlc/migrations/000017_deposit_swaphash.down.sql create mode 100644 loopdb/sqlc/migrations/000017_deposit_swaphash.up.sql create mode 100644 staticaddr/loopin/deposit_swaphash_migration.go create mode 100644 staticaddr/loopin/deposit_swaphash_migration_test.go diff --git a/go.mod b/go.mod index 97a54488c..c8a42e0ac 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.6.0 github.com/jessevdk/go-flags v1.4.0 github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.3.13-beta @@ -104,7 +105,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.2 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle v1.3.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackpal/gateway v1.0.5 // indirect diff --git a/loopd/daemon.go b/loopd/daemon.go index 7212933ad..c5f1dc70b 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -626,6 +626,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error { clock.NewDefaultClock(), d.lnd.ChainParams, ) + // Run the deposit swap hash migration. + err = loopin.MigrateDepositSwapHash( + d.mainCtx, swapDb, depositStore, staticAddressLoopInStore, + ) + if err != nil { + errorf("Deposit swap hash migration failed: %v", err) + + return err + } + staticLoopInManager = loopin.NewManager(&loopin.Config{ Server: staticAddressClient, QuoteGetter: swapClient.Server, diff --git a/loopdb/sqlc/migrations/000017_deposit_swaphash.down.sql b/loopdb/sqlc/migrations/000017_deposit_swaphash.down.sql new file mode 100644 index 000000000..b2090f839 --- /dev/null +++ b/loopdb/sqlc/migrations/000017_deposit_swaphash.down.sql @@ -0,0 +1 @@ +ALTER TABLE deposits DROP COLUMN swap_hash; diff --git a/loopdb/sqlc/migrations/000017_deposit_swaphash.up.sql b/loopdb/sqlc/migrations/000017_deposit_swaphash.up.sql new file mode 100644 index 000000000..d400b047e --- /dev/null +++ b/loopdb/sqlc/migrations/000017_deposit_swaphash.up.sql @@ -0,0 +1 @@ +ALTER TABLE deposits ADD swap_hash BLOB; diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 6728bb420..46766aca1 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -19,6 +19,7 @@ type Deposit struct { TimeoutSweepPkScript []byte ExpirySweepTxid []byte FinalizedWithdrawalTx sql.NullString + SwapHash []byte } type DepositUpdate struct { diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 26e4c0554..d40bd7628 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -19,6 +19,7 @@ type Querier interface { CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) + DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) FetchLiquidityParams(ctx context.Context) ([]byte, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) @@ -62,7 +63,9 @@ type Querier interface { InsertSwap(ctx context.Context, arg InsertSwapParams) error InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error IsStored(ctx context.Context, swapHash []byte) (bool, error) + MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error diff --git a/loopdb/sqlc/queries/static_address_loopin.sql b/loopdb/sqlc/queries/static_address_loopin.sql index ea4344b75..9baadfbee 100644 --- a/loopdb/sqlc/queries/static_address_loopin.sql +++ b/loopdb/sqlc/queries/static_address_loopin.sql @@ -93,3 +93,30 @@ SELECT EXISTS ( FROM static_address_swaps WHERE swap_hash = $1 ); + +-- name: MapDepositToSwap :exec +UPDATE + deposits +SET + swap_hash = $2 +WHERE + deposit_id = $1; + +-- name: SwapHashForDepositID :one +SELECT + swap_hash +FROM + deposits +WHERE + deposit_id = $1; + +-- name: DepositIDsForSwapHash :many +SELECT + deposit_id +FROM + deposits +WHERE + swap_hash = $1; + + + diff --git a/loopdb/sqlc/static_address_deposits.sql.go b/loopdb/sqlc/static_address_deposits.sql.go index 3a5c8076b..191f1f563 100644 --- a/loopdb/sqlc/static_address_deposits.sql.go +++ b/loopdb/sqlc/static_address_deposits.sql.go @@ -13,7 +13,7 @@ import ( const allDeposits = `-- name: AllDeposits :many SELECT - id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx + id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash FROM deposits ORDER BY @@ -39,6 +39,7 @@ func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) { &i.TimeoutSweepPkScript, &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, + &i.SwapHash, ); err != nil { return nil, err } @@ -102,7 +103,7 @@ func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) er const depositForOutpoint = `-- name: DepositForOutpoint :one SELECT - id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx + id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash FROM deposits WHERE @@ -129,13 +130,14 @@ func (q *Queries) DepositForOutpoint(ctx context.Context, arg DepositForOutpoint &i.TimeoutSweepPkScript, &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, + &i.SwapHash, ) return i, err } const getDeposit = `-- name: GetDeposit :one SELECT - id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx + id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, finalized_withdrawal_tx, swap_hash FROM deposits WHERE @@ -155,6 +157,7 @@ func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, er &i.TimeoutSweepPkScript, &i.ExpirySweepTxid, &i.FinalizedWithdrawalTx, + &i.SwapHash, ) return i, err } diff --git a/loopdb/sqlc/static_address_loopin.sql.go b/loopdb/sqlc/static_address_loopin.sql.go index 22ee6438a..6ce177302 100644 --- a/loopdb/sqlc/static_address_loopin.sql.go +++ b/loopdb/sqlc/static_address_loopin.sql.go @@ -11,6 +11,38 @@ import ( "time" ) +const depositIDsForSwapHash = `-- name: DepositIDsForSwapHash :many +SELECT + deposit_id +FROM + deposits +WHERE + swap_hash = $1 +` + +func (q *Queries) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) { + rows, err := q.db.QueryContext(ctx, depositIDsForSwapHash, swapHash) + if err != nil { + return nil, err + } + defer rows.Close() + var items [][]byte + for rows.Next() { + var deposit_id []byte + if err := rows.Scan(&deposit_id); err != nil { + return nil, err + } + items = append(items, deposit_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getLoopInSwapUpdates = `-- name: GetLoopInSwapUpdates :many SELECT static_address_swap_updates.id, static_address_swap_updates.swap_hash, static_address_swap_updates.update_state, static_address_swap_updates.update_timestamp @@ -328,6 +360,41 @@ func (q *Queries) IsStored(ctx context.Context, swapHash []byte) (bool, error) { return exists, err } +const mapDepositToSwap = `-- name: MapDepositToSwap :exec +UPDATE + deposits +SET + swap_hash = $2 +WHERE + deposit_id = $1 +` + +type MapDepositToSwapParams struct { + DepositID []byte + SwapHash []byte +} + +func (q *Queries) MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error { + _, err := q.db.ExecContext(ctx, mapDepositToSwap, arg.DepositID, arg.SwapHash) + return err +} + +const swapHashForDepositID = `-- name: SwapHashForDepositID :one +SELECT + swap_hash +FROM + deposits +WHERE + deposit_id = $1 +` + +func (q *Queries) SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) { + row := q.db.QueryRowContext(ctx, swapHashForDepositID, depositID) + var swap_hash []byte + err := row.Scan(&swap_hash) + return swap_hash, err +} + const updateStaticAddressLoopIn = `-- name: UpdateStaticAddressLoopIn :exec UPDATE static_address_swaps SET diff --git a/staticaddr/loopin/deposit_swaphash_migration.go b/staticaddr/loopin/deposit_swaphash_migration.go new file mode 100644 index 000000000..512cb06ca --- /dev/null +++ b/staticaddr/loopin/deposit_swaphash_migration.go @@ -0,0 +1,88 @@ +package loopin + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightningnetwork/lnd/lntypes" +) + +const ( + // depositSwapHashMigrationID is the identifier for the deposit swap + // hash migration. + depositSwapHashMigrationID = "deposit_swap_hash" +) + +// MigrateDepositSwapHash will retrieve the comma separated deposit list of +// past and pending swaps and map them to the swap hash in the deposits table. +func MigrateDepositSwapHash(ctx context.Context, db loopdb.SwapStore, + depositStore *deposit.SqlStore, swapStore *SqlStore) error { + + migrationDone, err := db.HasMigration( + ctx, depositSwapHashMigrationID, + ) + if err != nil { + return fmt.Errorf("unable to check migration status: %w", err) + } + if migrationDone { + log.Infof("Deposit swap hash migration already done, " + + "skipping") + + return nil + } + + log.Infof("Starting deposit swap hash migration") + startTs := time.Now() + defer func() { + log.Infof("Finished deposit swap hash migration in %v", + time.Since(startTs)) + }() + + // First we'll fetch all past loop in swaps from the database. + swaps, err := swapStore.GetStaticAddressLoopInSwapsByStates( + ctx, AllStates, + ) + if err != nil { + return err + } + + // Now we'll map each deposit of a swap to its respective swap hash. + depositsToSwapHashes := make(map[deposit.ID]lntypes.Hash) + for _, swap := range swaps { + for _, outpoint := range swap.DepositOutpoints { + deposit, err := depositStore.DepositForOutpoint( + ctx, outpoint, + ) + if err != nil { + return fmt.Errorf("unable to fetch deposit "+ + "for outpoint %s: %w", outpoint, err) + } + if deposit == nil { + return fmt.Errorf("deposit for outpoint %s "+ + "not found", outpoint) + } + + if _, ok := depositsToSwapHashes[deposit.ID]; !ok { + depositsToSwapHashes[deposit.ID] = swap.SwapHash + } else { + log.Warnf("Duplicate deposit ID %s found for "+ + "outpoint %s, skipping", + deposit.ID, outpoint) + } + } + } + + log.Infof("Batch-mapping %d deposits to swap hashes", + len(depositsToSwapHashes)) + + err = swapStore.BatchMapDepositsToSwapHashes(ctx, depositsToSwapHashes) + if err != nil { + return err + } + + // Finally mark the migration as done. + return db.SetMigration(ctx, depositSwapHashMigrationID) +} diff --git a/staticaddr/loopin/deposit_swaphash_migration_test.go b/staticaddr/loopin/deposit_swaphash_migration_test.go new file mode 100644 index 000000000..1db77fe96 --- /dev/null +++ b/staticaddr/loopin/deposit_swaphash_migration_test.go @@ -0,0 +1,195 @@ +package loopin + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +const ( + P2wkhAddr = "bcrt1qq68r6ff4k4pjx39efs44gcyccf7unqnu5qtjjz" +) + +// TestDepositSwapHashMigration tests deposit to swap hash migration. +func TestDepositSwapHashMigration(t *testing.T) { + // Set up test context objects. + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + db := loopdb.NewStoreMock(t) + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.RegressionNetParams, + ) + + newID := func() deposit.ID { + did, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + return did + } + + d1, d2 := &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, + Index: 0, + }, + Value: btcutil.Amount(100_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + }, + &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e}, + Index: 1, + }, + Value: btcutil.Amount(200_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, + }, + } + + err := depositStore.CreateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d2) + require.NoError(t, err) + + outpoints := []string{ + d1.OutPoint.String(), + d2.OutPoint.String(), + } + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + addr, err := btcutil.DecodeAddress(P2wkhAddr, nil) + require.NoError(t, err) + + swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4} + loopIn := StaticAddressLoopIn{ + SwapHash: swapHash, + DepositOutpoints: outpoints, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + loopIn.SetState(Succeeded) + + // Insert the swap without the deposit mapping. + err = swapStore.baseDB.ExecTx(ctxb, loopdb.NewSqlWriteOpts(), + func(q Querier) error { + swapArgs := sqlc.InsertSwapParams{ + SwapHash: loopIn.SwapHash[:], + Preimage: loopIn.SwapPreimage[:], + InitiationTime: loopIn.InitiationTime, + AmountRequested: int64(loopIn.TotalDepositAmount()), + CltvExpiry: loopIn.HtlcCltvExpiry, + MaxSwapFee: int64(loopIn.MaxSwapFee), + InitiationHeight: int32(loopIn.InitiationHeight), + ProtocolVersion: int32(loopIn.ProtocolVersion), + Label: loopIn.Label, + } + + htlcKeyArgs := sqlc.InsertHtlcKeysParams{ + SwapHash: loopIn.SwapHash[:], + SenderScriptPubkey: loopIn.ClientPubkey.SerializeCompressed(), + ReceiverScriptPubkey: loopIn.ServerPubkey.SerializeCompressed(), + ClientKeyFamily: int32(loopIn.HtlcKeyLocator.Family), + ClientKeyIndex: int32(loopIn.HtlcKeyLocator.Index), + } + + // Sanity check, if any of the outpoints contain the outpoint separator. + // If so, we reject the loop-in to prevent potential issues with + // parsing. + for _, outpoint := range loopIn.DepositOutpoints { + if strings.Contains(outpoint, outpointSeparator) { + return ErrInvalidOutpoint + } + } + + joinedOutpoints := strings.Join( + loopIn.DepositOutpoints, outpointSeparator, + ) + staticAddressLoopInParams := sqlc.InsertStaticAddressLoopInParams{ + SwapHash: loopIn.SwapHash[:], + SwapInvoice: loopIn.SwapInvoice, + LastHop: loopIn.LastHop, + QuotedSwapFeeSatoshis: int64(loopIn.QuotedSwapFee), + HtlcTimeoutSweepAddress: loopIn.HtlcTimeoutSweepAddress.String(), + HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate), + DepositOutpoints: joinedOutpoints, + PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds), + } + + updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{ + SwapHash: loopIn.SwapHash[:], + UpdateTimestamp: testClock.Now(), + UpdateState: string(loopIn.GetState()), + } + err := q.InsertSwap(ctxb, swapArgs) + if err != nil { + return err + } + + err = q.InsertHtlcKeys(ctxb, htlcKeyArgs) + if err != nil { + return err + } + + err = q.InsertStaticAddressLoopIn( + ctxb, staticAddressLoopInParams, + ) + if err != nil { + return err + } + + return q.InsertStaticAddressMetaUpdate(ctxb, updateArgs) + }, + ) + require.NoError(t, err) + + depositIDs, err := swapStore.DepositIDsForSwapHash(ctxb, swapHash) + require.NoError(t, err) + require.Len(t, depositIDs, 0) + + swapHashes, err := swapStore.SwapHashesForDepositIDs( + ctxb, []deposit.ID{d1.ID, d2.ID}, + ) + require.NoError(t, err) + require.Len(t, swapHashes, 0) + + err = MigrateDepositSwapHash(ctxb, db, depositStore, swapStore) + require.NoError(t, err) + + depositIDs, err = swapStore.DepositIDsForSwapHash(ctxb, swapHash) + require.NoError(t, err) + require.Len(t, depositIDs, 2) + require.Contains(t, depositIDs, d1.ID) + require.Contains(t, depositIDs, d2.ID) + + swapHashes, err = swapStore.SwapHashesForDepositIDs( + ctxb, []deposit.ID{d1.ID, d2.ID}, + ) + require.NoError(t, err) + require.Len(t, swapHashes, 1) + require.Len(t, swapHashes[swapHash], 2) + require.Contains(t, swapHashes[swapHash], d1.ID) + require.Contains(t, swapHashes[swapHash], d2.ID) +} diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index e6ba5aadd..b50fc4d05 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -10,9 +10,11 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/jackc/pgx/v5" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/version" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/keychain" @@ -68,6 +70,20 @@ type Querier interface { // IsStored returns true if a swap with the given hash is stored in the // database, false otherwise. IsStored(ctx context.Context, swapHash []byte) (bool, error) + + // MapDepositToSwap maps a deposit to a swap in the database. + MapDepositToSwap(ctx context.Context, + arg sqlc.MapDepositToSwapParams) error + + // SwapHashForDepositID retrieves the swap hash for the given deposit + // ID. + SwapHashForDepositID(ctx context.Context, + depositID []byte) ([]byte, error) + + // DepositIDsForSwapHash retrieves all deposit IDs for a given swap + // hash. + DepositIDsForSwapHash(ctx context.Context, + swapHash []byte) ([][]byte, error) } // BaseDB is the interface that contains all the queries generated by sqlc for @@ -306,6 +322,89 @@ func (s *SqlStore) IsStored(ctx context.Context, swapHash lntypes.Hash) (bool, return s.baseDB.IsStored(ctx, swapHash[:]) } +// BatchMapDepositsToSwapHashes maps multiple deposits to their respective swap +// hashes in a single transaction. +func (s *SqlStore) BatchMapDepositsToSwapHashes(ctx context.Context, + depositsToHashes map[deposit.ID]lntypes.Hash) error { + + return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + func(q Querier) error { + for deposit, swapHash := range depositsToHashes { + err := q.MapDepositToSwap( + ctx, sqlc.MapDepositToSwapParams{ + DepositID: deposit[:], + SwapHash: swapHash[:], + }, + ) + if err != nil { + return err + } + } + + return nil + }) +} + +// SwapHashesForDepositIDs retrieves the swap hashes for the given deposit IDs. +func (s *SqlStore) SwapHashesForDepositIDs(ctx context.Context, + depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) { + + swapHashes := make(map[lntypes.Hash][]deposit.ID) + for _, id := range depositIDs { + swapHash, err := s.baseDB.SwapHashForDepositID(ctx, id[:]) + if err != nil { + if errors.Is(err, sql.ErrNoRows) || + errors.Is(err, pgx.ErrNoRows) { + + return nil, nil + } + + return nil, err + } + + if swapHash == nil { + return nil, nil + } + + if len(swapHash) != lntypes.HashSize { + return nil, errors.New("invalid swap hash length") + } + + swapHashParsed, err := lntypes.MakeHash(swapHash) + if err != nil { + return nil, err + } + + // Place the deposit ID in the map under the + // corresponding swap hash. + swapHashes[swapHashParsed] = append( + swapHashes[swapHashParsed], id, + ) + } + + return swapHashes, nil +} + +// DepositIDsForSwapHash retrieves all deposit IDs for a given swap hash. +func (s *SqlStore) DepositIDsForSwapHash(ctx context.Context, + swapHash lntypes.Hash) ([]deposit.ID, error) { + + byteIDs, err := s.baseDB.DepositIDsForSwapHash(ctx, swapHash[:]) + if err != nil { + return nil, err + } + + depositIDs := make([]deposit.ID, len(byteIDs)) + for i, id := range byteIDs { + if len(id) != deposit.IdLength { + return nil, errors.New("invalid deposit ID length") + } + copy(depositIDs[i][:], id) + } + + return depositIDs, nil +} + // toStaticAddressLoopIn converts sql rows to an instant out struct. func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, row sqlc.GetStaticAddressLoopInSwapRow, From a1af860527c99848b73efb497f8a5947dcfc5334 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 31 Jul 2025 12:38:16 +0200 Subject: [PATCH 3/6] staticaddr: remove unused fsm transition --- staticaddr/loopin/fsm.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/staticaddr/loopin/fsm.go b/staticaddr/loopin/fsm.go index ecd663b77..a47253590 100644 --- a/staticaddr/loopin/fsm.go +++ b/staticaddr/loopin/fsm.go @@ -130,8 +130,7 @@ var ( var PendingStates = []fsm.StateType{ InitHtlcTx, SignHtlcTx, MonitorInvoiceAndHtlcTx, PaymentReceived, - SweepHtlcTimeout, MonitorHtlcTimeoutSweep, - UnlockDeposits, + SweepHtlcTimeout, MonitorHtlcTimeoutSweep, UnlockDeposits, } var FinalStates = []fsm.StateType{ @@ -151,7 +150,6 @@ var ( OnHtlcTimeoutSweepPublished = fsm.EventType("OnHtlcTimeoutSweepPublished") OnHtlcTimeoutSwept = fsm.EventType("OnHtlcTimeoutSwept") OnPaymentReceived = fsm.EventType("OnPaymentReceived") - OnPaymentDeadlineExceeded = fsm.EventType("OnPaymentDeadlineExceeded") OnSwapTimedOut = fsm.EventType("OnSwapTimedOut") OnSucceeded = fsm.EventType("OnSucceeded") OnRecover = fsm.EventType("OnRecover") From d7ac3148c923eb36923bac155067c82aba014be8 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 31 Jul 2025 12:39:43 +0200 Subject: [PATCH 4/6] staticaddr: relax scope of ToDeposit --- staticaddr/deposit/sql_store.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/staticaddr/deposit/sql_store.go b/staticaddr/deposit/sql_store.go index 1303c6a70..05c89776f 100644 --- a/staticaddr/deposit/sql_store.go +++ b/staticaddr/deposit/sql_store.go @@ -135,7 +135,7 @@ func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { return err } - deposit, err = s.toDeposit(row, latestUpdate) + deposit, err = ToDeposit(row, latestUpdate) if err != nil { return err } @@ -177,7 +177,7 @@ func (s *SqlStore) DepositForOutpoint(ctx context.Context, return err } - deposit, err = s.toDeposit(row, latestUpdate) + deposit, err = ToDeposit(row, latestUpdate) if err != nil { return err } @@ -212,7 +212,7 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { return err } - d, err := s.toDeposit(deposit, latestUpdate) + d, err := ToDeposit(deposit, latestUpdate) if err != nil { return err } @@ -229,9 +229,9 @@ func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { return allDeposits, nil } -// toDeposit converts an sql deposit to a deposit. -func (s *SqlStore) toDeposit(row sqlc.Deposit, - lastUpdate sqlc.DepositUpdate) (*Deposit, error) { +// ToDeposit converts an sql deposit to a deposit. +func ToDeposit(row sqlc.Deposit, lastUpdate sqlc.DepositUpdate) (*Deposit, + error) { id := ID{} err := id.FromByteSlice(row.DepositID) From d5e3075d045a7cfc9d45ae4ef7c5ae0533cccb1f Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 31 Jul 2025 12:40:26 +0200 Subject: [PATCH 5/6] loopdb: add query DepositsForSwapHash --- .../000010_static_address_deposits.up.sql | 2 +- loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/static_address_loopin.sql | 21 ++++++ loopdb/sqlc/static_address_loopin.sql.go | 70 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/loopdb/sqlc/migrations/000010_static_address_deposits.up.sql b/loopdb/sqlc/migrations/000010_static_address_deposits.up.sql index 2952e7e53..b996ea9bd 100644 --- a/loopdb/sqlc/migrations/000010_static_address_deposits.up.sql +++ b/loopdb/sqlc/migrations/000010_static_address_deposits.up.sql @@ -45,4 +45,4 @@ CREATE TABLE IF NOT EXISTS deposit_updates ( -- update_timestamp is the timestamp of the update. update_timestamp TIMESTAMP NOT NULL -); +); \ No newline at end of file diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index d40bd7628..0a380e426 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -20,6 +20,7 @@ type Querier interface { CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error DepositForOutpoint(ctx context.Context, arg DepositForOutpointParams) (Deposit, error) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) + DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error) FetchLiquidityParams(ctx context.Context) ([]byte, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) diff --git a/loopdb/sqlc/queries/static_address_loopin.sql b/loopdb/sqlc/queries/static_address_loopin.sql index 9baadfbee..36a6cfdc5 100644 --- a/loopdb/sqlc/queries/static_address_loopin.sql +++ b/loopdb/sqlc/queries/static_address_loopin.sql @@ -118,5 +118,26 @@ FROM WHERE swap_hash = $1; +-- name: DepositsForSwapHash :many +SELECT + d.*, + u.update_state, + u.update_timestamp +FROM + deposits d + LEFT JOIN + deposit_updates u ON u.id = ( + SELECT id + FROM deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 + ) +WHERE + d.swap_hash = $1; + + + + diff --git a/loopdb/sqlc/static_address_loopin.sql.go b/loopdb/sqlc/static_address_loopin.sql.go index 6ce177302..3d582dea9 100644 --- a/loopdb/sqlc/static_address_loopin.sql.go +++ b/loopdb/sqlc/static_address_loopin.sql.go @@ -43,6 +43,76 @@ func (q *Queries) DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([ return items, nil } +const depositsForSwapHash = `-- name: DepositsForSwapHash :many +SELECT + d.id, d.deposit_id, d.tx_hash, d.out_index, d.amount, d.confirmation_height, d.timeout_sweep_pk_script, d.expiry_sweep_txid, d.finalized_withdrawal_tx, d.swap_hash, + u.update_state, + u.update_timestamp +FROM + deposits d + LEFT JOIN + deposit_updates u ON u.id = ( + SELECT id + FROM deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 + ) +WHERE + d.swap_hash = $1 +` + +type DepositsForSwapHashRow struct { + ID int32 + DepositID []byte + TxHash []byte + OutIndex int32 + Amount int64 + ConfirmationHeight int64 + TimeoutSweepPkScript []byte + ExpirySweepTxid []byte + FinalizedWithdrawalTx sql.NullString + SwapHash []byte + UpdateState sql.NullString + UpdateTimestamp sql.NullTime +} + +func (q *Queries) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, error) { + rows, err := q.db.QueryContext(ctx, depositsForSwapHash, swapHash) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DepositsForSwapHashRow + for rows.Next() { + var i DepositsForSwapHashRow + if err := rows.Scan( + &i.ID, + &i.DepositID, + &i.TxHash, + &i.OutIndex, + &i.Amount, + &i.ConfirmationHeight, + &i.TimeoutSweepPkScript, + &i.ExpirySweepTxid, + &i.FinalizedWithdrawalTx, + &i.SwapHash, + &i.UpdateState, + &i.UpdateTimestamp, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getLoopInSwapUpdates = `-- name: GetLoopInSwapUpdates :many SELECT static_address_swap_updates.id, static_address_swap_updates.swap_hash, static_address_swap_updates.update_state, static_address_swap_updates.update_timestamp From fe7622db8706c44223fc81fbfe230908b6a27148 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 31 Jul 2025 12:42:11 +0200 Subject: [PATCH 6/6] staticaddr: fetch deposits as part of swap retrieval since deposits have been normalized in the db in the context of a static address swap this commit now retrieves the deposit information as part of GetLoopInByHash and GetStaticAddressLoopInSwapsByStates. --- staticaddr/loopin/sql_store.go | 130 ++++++++++--- staticaddr/loopin/sql_store_test.go | 273 ++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+), 29 deletions(-) create mode 100644 staticaddr/loopin/sql_store_test.go diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index b50fc4d05..78a323122 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -84,6 +84,10 @@ type Querier interface { // hash. DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) + + // DepositsForSwapHash retrieves all deposits for a given swap hash. + DepositsForSwapHash(ctx context.Context, + swapHash []byte) ([]sqlc.DepositsForSwapHashRow, error) } // BaseDB is the interface that contains all the queries generated by sqlc for @@ -122,10 +126,15 @@ func (s *SqlStore) GetLoopInByHash(ctx context.Context, var ( err error - row sqlc.GetStaticAddressLoopInSwapRow + swap sqlc.GetStaticAddressLoopInSwapRow updates []sqlc.StaticAddressSwapUpdate ) - row, err = s.baseDB.GetStaticAddressLoopInSwap(ctx, swapHash[:]) + swap, err = s.baseDB.GetStaticAddressLoopInSwap(ctx, swapHash[:]) + if err != nil { + return nil, err + } + + deposits, err := s.baseDB.DepositsForSwapHash(ctx, swapHash[:]) if err != nil { return nil, err } @@ -135,9 +144,7 @@ func (s *SqlStore) GetLoopInByHash(ctx context.Context, return nil, err } - return toStaticAddressLoopIn( - ctx, s.network, row, updates, - ) + return toStaticAddressLoopIn(ctx, s.network, swap, deposits, updates) } // GetStaticAddressLoopInSwapsByStates returns all static address loop-ins from @@ -165,6 +172,11 @@ func (s *SqlStore) GetStaticAddressLoopInSwapsByStates(ctx context.Context, loopIns := make([]*StaticAddressLoopIn, 0, len(rows)) for _, row := range rows { + deposits, err := s.baseDB.DepositsForSwapHash(ctx, row.SwapHash) + if err != nil { + return nil, err + } + updates, err = s.baseDB.GetLoopInSwapUpdates( ctx, row.SwapHash, ) @@ -174,7 +186,7 @@ func (s *SqlStore) GetStaticAddressLoopInSwapsByStates(ctx context.Context, loopIn, err = toStaticAddressLoopIn( ctx, s.network, sqlc.GetStaticAddressLoopInSwapRow(row), - updates, + deposits, updates, ) if err != nil { return nil, err @@ -206,6 +218,14 @@ func toStrings(states []fsm.StateType) []string { func (s *SqlStore) CreateLoopIn(ctx context.Context, loopIn *StaticAddressLoopIn) error { + if loopIn == nil { + return errors.New("loop-in cannot be nil") + } + + if len(loopIn.Deposits) == 0 { + return errors.New("loop-in must have at least one deposit") + } + swapArgs := sqlc.InsertSwapParams{ SwapHash: loopIn.SwapHash[:], Preimage: loopIn.SwapPreimage[:], @@ -274,8 +294,24 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, return err } + // Map each deposit to the swap hash in the + // deposit_to_swap table. This allows us to track which + // deposits are used for which swaps. + for _, d := range loopIn.Deposits { + err = q.MapDepositToSwap( + ctx, sqlc.MapDepositToSwapParams{ + DepositID: d.ID[:], + SwapHash: loopIn.SwapHash[:], + }, + ) + if err != nil { + return err + } + } + return q.InsertStaticAddressMetaUpdate(ctx, updateArgs) - }) + }, + ) } // UpdateLoopIn updates the loop-in in the database. @@ -407,33 +443,34 @@ func (s *SqlStore) DepositIDsForSwapHash(ctx context.Context, // toStaticAddressLoopIn converts sql rows to an instant out struct. func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, - row sqlc.GetStaticAddressLoopInSwapRow, + swap sqlc.GetStaticAddressLoopInSwapRow, + deposits []sqlc.DepositsForSwapHashRow, updates []sqlc.StaticAddressSwapUpdate) (*StaticAddressLoopIn, error) { - swapHash, err := lntypes.MakeHash(row.SwapHash) + swapHash, err := lntypes.MakeHash(swap.SwapHash) if err != nil { return nil, err } - swapPreImage, err := lntypes.MakePreimage(row.Preimage) + swapPreImage, err := lntypes.MakePreimage(swap.Preimage) if err != nil { return nil, err } - clientKey, err := btcec.ParsePubKey(row.SenderScriptPubkey) + clientKey, err := btcec.ParsePubKey(swap.SenderScriptPubkey) if err != nil { return nil, err } - serverKey, err := btcec.ParsePubKey(row.ReceiverScriptPubkey) + serverKey, err := btcec.ParsePubKey(swap.ReceiverScriptPubkey) if err != nil { return nil, err } var htlcTimeoutSweepTxHash *chainhash.Hash - if row.HtlcTimeoutSweepTxID.Valid { + if swap.HtlcTimeoutSweepTxID.Valid { htlcTimeoutSweepTxHash, err = chainhash.NewHashFromStr( - row.HtlcTimeoutSweepTxID.String, + swap.HtlcTimeoutSweepTxID.String, ) if err != nil { return nil, err @@ -441,10 +478,10 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, } depositOutpoints := strings.Split( - row.DepositOutpoints, outpointSeparator, + swap.DepositOutpoints, outpointSeparator, ) - timeoutAddressString := row.HtlcTimeoutSweepAddress + timeoutAddressString := swap.HtlcTimeoutSweepAddress var timeoutAddress btcutil.Address if timeoutAddressString != "" { timeoutAddress, err = btcutil.DecodeAddress( @@ -455,33 +492,68 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, } } + depositList := make([]*deposit.Deposit, 0, len(deposits)) + for _, d := range deposits { + id := deposit.ID{} + err = id.FromByteSlice(d.DepositID) + if err != nil { + return nil, err + } + + sqlcDeposit := sqlc.Deposit{ + DepositID: id[:], + TxHash: d.TxHash, + Amount: d.Amount, + OutIndex: d.OutIndex, + ConfirmationHeight: d.ConfirmationHeight, + TimeoutSweepPkScript: d.TimeoutSweepPkScript, + ExpirySweepTxid: d.ExpirySweepTxid, + FinalizedWithdrawalTx: d.FinalizedWithdrawalTx, + } + + sqlcDepositUpdate := sqlc.DepositUpdate{ + DepositID: id[:], + UpdateState: d.UpdateState.String, + UpdateTimestamp: d.UpdateTimestamp.Time, + } + deposit, err := deposit.ToDeposit( + sqlcDeposit, sqlcDepositUpdate, + ) + if err != nil { + return nil, err + } + + depositList = append(depositList, deposit) + } + loopIn := &StaticAddressLoopIn{ SwapHash: swapHash, SwapPreimage: swapPreImage, - HtlcCltvExpiry: row.CltvExpiry, - MaxSwapFee: btcutil.Amount(row.MaxSwapFee), - InitiationHeight: uint32(row.InitiationHeight), - InitiationTime: row.InitiationTime, + HtlcCltvExpiry: swap.CltvExpiry, + MaxSwapFee: btcutil.Amount(swap.MaxSwapFee), + InitiationHeight: uint32(swap.InitiationHeight), + InitiationTime: swap.InitiationTime, ProtocolVersion: version.AddressProtocolVersion( - row.ProtocolVersion, + swap.ProtocolVersion, ), - Label: row.Label, + Label: swap.Label, ClientPubkey: clientKey, ServerPubkey: serverKey, HtlcKeyLocator: keychain.KeyLocator{ - Family: keychain.KeyFamily(row.ClientKeyFamily), - Index: uint32(row.ClientKeyIndex), + Family: keychain.KeyFamily(swap.ClientKeyFamily), + Index: uint32(swap.ClientKeyIndex), }, - SwapInvoice: row.SwapInvoice, - PaymentTimeoutSeconds: uint32(row.PaymentTimeoutSeconds), - LastHop: row.LastHop, - QuotedSwapFee: btcutil.Amount(row.QuotedSwapFeeSatoshis), + SwapInvoice: swap.SwapInvoice, + PaymentTimeoutSeconds: uint32(swap.PaymentTimeoutSeconds), + LastHop: swap.LastHop, + QuotedSwapFee: btcutil.Amount(swap.QuotedSwapFeeSatoshis), DepositOutpoints: depositOutpoints, HtlcTxFeeRate: chainfee.SatPerKWeight( - row.HtlcTxFeeRateSatKw, + swap.HtlcTxFeeRateSatKw, ), HtlcTimeoutSweepAddress: timeoutAddress, HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash, + Deposits: depositList, } if len(updates) > 0 { diff --git a/staticaddr/loopin/sql_store_test.go b/staticaddr/loopin/sql_store_test.go new file mode 100644 index 000000000..356049bc7 --- /dev/null +++ b/staticaddr/loopin/sql_store_test.go @@ -0,0 +1,273 @@ +package loopin + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +// TestGetStaticAddressLoopInSwapsByStates tests that we can retrieve +// StaticAddressLoopIn swaps by their states and that the deposits +// associated with the swaps are correctly populated. +func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { + // Set up test context objects. + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.RegressionNetParams, + ) + + newID := func() deposit.ID { + did, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + return did + } + + loopingDepositID := newID() + loopedInDepositID := newID() + d1, d2 := &deposit.Deposit{ + ID: loopingDepositID, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, + Index: 0, + }, + Value: btcutil.Amount(100_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + }, + &deposit.Deposit{ + ID: loopedInDepositID, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e}, + Index: 1, + }, + Value: btcutil.Amount(200_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, + }, + } + + err := depositStore.CreateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d2) + require.NoError(t, err) + + // Add two updates per deposit, expect the last to be retrieved. + d1.SetState(deposit.Deposited) + d2.SetState(deposit.Deposited) + + err = depositStore.UpdateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d2) + require.NoError(t, err) + + d1.SetState(deposit.LoopingIn) + d2.SetState(deposit.LoopedIn) + + err = depositStore.UpdateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d2) + require.NoError(t, err) + + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + addr, err := btcutil.DecodeAddress(P2wkhAddr, nil) + require.NoError(t, err) + + // Create pending swap. + swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4} + swapPending := StaticAddressLoopIn{ + SwapHash: swapHashPending, + SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4}, + DepositOutpoints: []string{d1.OutPoint.String()}, + Deposits: []*deposit.Deposit{d1}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swapPending.SetState(SignHtlcTx) + + err = swapStore.CreateLoopIn(ctxb, &swapPending) + require.NoError(t, err) + + // Create succeeded swap. + swapHashSucceeded := lntypes.Hash{0x2, 0x2, 0x3, 0x5} + swapSucceeded := StaticAddressLoopIn{ + SwapHash: swapHashSucceeded, + SwapPreimage: lntypes.Preimage{0x2, 0x2, 0x3, 0x5}, + DepositOutpoints: []string{d2.OutPoint.String()}, + Deposits: []*deposit.Deposit{d2}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swapSucceeded.SetState(Succeeded) + + err = swapStore.CreateLoopIn(ctxb, &swapSucceeded) + require.NoError(t, err) + + pendingSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, PendingStates) + require.NoError(t, err) + + require.Len(t, pendingSwaps, 1) + require.Equal(t, swapHashPending, pendingSwaps[0].SwapHash) + require.Equal(t, []string{d1.OutPoint.String()}, pendingSwaps[0].DepositOutpoints) + require.Equal(t, SignHtlcTx, pendingSwaps[0].GetState()) + + pendingDeposits := pendingSwaps[0].Deposits + require.Len(t, pendingDeposits, 1) + require.Equal(t, d1.ID, pendingDeposits[0].ID) + require.Equal(t, d1.OutPoint, pendingDeposits[0].OutPoint) + require.Equal(t, d1.Value, pendingDeposits[0].Value) + require.Equal(t, deposit.LoopingIn, pendingDeposits[0].GetState()) + + finalizedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, FinalStates) + require.NoError(t, err) + + require.Len(t, finalizedSwaps, 1) + require.Equal(t, swapHashSucceeded, finalizedSwaps[0].SwapHash) + require.Equal(t, []string{d2.OutPoint.String()}, finalizedSwaps[0].DepositOutpoints) + require.Equal(t, Succeeded, finalizedSwaps[0].GetState()) + + finalizedDeposits := finalizedSwaps[0].Deposits + require.Len(t, finalizedDeposits, 1) + require.Equal(t, d2.ID, finalizedDeposits[0].ID) + require.Equal(t, d2.OutPoint, finalizedDeposits[0].OutPoint) + require.Equal(t, d2.Value, finalizedDeposits[0].Value) + require.Equal(t, deposit.LoopedIn, finalizedDeposits[0].GetState()) +} + +// TestCreateLoopIn tests that CreateLoopIn correctly creates a new +// StaticAddressLoopIn swap and associates it with the provided deposits. +func TestCreateLoopIn(t *testing.T) { + // Set up test context objects. + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.RegressionNetParams, + ) + + newID := func() deposit.ID { + did, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + return did + } + + d1, d2 := &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, + Index: 0, + }, + Value: btcutil.Amount(100_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + }, + &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e}, + Index: 1, + }, + Value: btcutil.Amount(200_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, + }, + } + + err := depositStore.CreateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d2) + require.NoError(t, err) + + d1.SetState(deposit.LoopingIn) + d2.SetState(deposit.LoopingIn) + + err = depositStore.UpdateDeposit(ctxb, d1) + require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d2) + require.NoError(t, err) + + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + addr, err := btcutil.DecodeAddress(P2wkhAddr, nil) + require.NoError(t, err) + + // Create pending swap. + swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4} + swapPending := StaticAddressLoopIn{ + SwapHash: swapHashPending, + SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4}, + DepositOutpoints: []string{d1.OutPoint.String(), + d2.OutPoint.String()}, + Deposits: []*deposit.Deposit{d1, d2}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swapPending.SetState(SignHtlcTx) + + err = swapStore.CreateLoopIn(ctxb, &swapPending) + require.NoError(t, err) + + depositIDs, err := swapStore.DepositIDsForSwapHash( + ctxb, swapHashPending, + ) + require.NoError(t, err) + require.Len(t, depositIDs, 2) + require.Contains(t, depositIDs, d1.ID) + require.Contains(t, depositIDs, d2.ID) + + swapHashes, err := swapStore.SwapHashesForDepositIDs( + ctxb, []deposit.ID{depositIDs[0], depositIDs[1]}, + ) + require.NoError(t, err) + require.Len(t, swapHashes, 1) + require.Len(t, swapHashes[swapHashPending], 2) + require.Contains(t, swapHashes[swapHashPending], depositIDs[0]) + require.Contains(t, swapHashes[swapHashPending], depositIDs[1]) + + swap, err := swapStore.GetLoopInByHash(ctxb, swapHashPending) + require.NoError(t, err) + require.Equal(t, swapHashPending, swap.SwapHash) + require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()}, + swap.DepositOutpoints) + require.Equal(t, SignHtlcTx, swap.GetState()) + + require.Len(t, swap.Deposits, 2) + + require.Equal(t, d1.ID, swap.Deposits[0].ID) + require.Equal(t, d1.OutPoint, swap.Deposits[0].OutPoint) + require.Equal(t, d1.Value, swap.Deposits[0].Value) + require.Equal(t, deposit.LoopingIn, swap.Deposits[0].GetState()) + + require.Equal(t, d2.ID, swap.Deposits[1].ID) + require.Equal(t, d2.OutPoint, swap.Deposits[1].OutPoint) + require.Equal(t, d2.Value, swap.Deposits[1].Value) + require.Equal(t, deposit.LoopingIn, swap.Deposits[1].GetState()) +}