diff --git a/staticaddr/interface.go b/staticaddr/address/interface.go similarity index 66% rename from staticaddr/interface.go rename to staticaddr/address/interface.go index 8b424fb..99021e8 100644 --- a/staticaddr/interface.go +++ b/staticaddr/address/interface.go @@ -1,39 +1,38 @@ -package staticaddr +package address import ( "context" "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/loop/staticaddr" "github.com/lightningnetwork/lnd/keychain" ) var ( ErrAddressAlreadyExists = fmt.Errorf("address already exists") - ErrAddressNotFound = fmt.Errorf("address not found") ) -// AddressStore is the database interface that is used to store and retrieve +// Store is the database interface that is used to store and retrieve // static addresses. -type AddressStore interface { +type Store interface { // CreateStaticAddress inserts a new static address with its parameters // into the store. - CreateStaticAddress(ctx context.Context, - addrParams *AddressParameters) error + CreateStaticAddress(ctx context.Context, addrParams *Parameters) error // GetStaticAddress fetches static address parameters for a given // address ID. - GetStaticAddress(ctx context.Context, - pkScript []byte) (*AddressParameters, error) + GetStaticAddress(ctx context.Context, pkScript []byte) (*Parameters, + error) // GetAllStaticAddresses retrieves all static addresses from the store. - GetAllStaticAddresses(ctx context.Context) ( - []*AddressParameters, error) + GetAllStaticAddresses(ctx context.Context) ([]*Parameters, + error) } -// AddressParameters holds all the necessary information for the 2-of-2 multisig +// Parameters holds all the necessary information for the 2-of-2 multisig // address. -type AddressParameters struct { +type Parameters struct { // ClientPubkey is the client's pubkey for the static address. It is // used for the 2-of-2 funding output as well as for the client's // timeout path. @@ -54,5 +53,5 @@ type AddressParameters struct { KeyLocator keychain.KeyLocator // ProtocolVersion is the protocol version of the static address. - ProtocolVersion AddressProtocolVersion + ProtocolVersion staticaddr.AddressProtocolVersion } diff --git a/staticaddr/sql_store.go b/staticaddr/address/sql_store.go similarity index 63% rename from staticaddr/sql_store.go rename to staticaddr/address/sql_store.go index 7f14f7c..9d15fc9 100644 --- a/staticaddr/sql_store.go +++ b/staticaddr/address/sql_store.go @@ -1,13 +1,12 @@ -package staticaddr +package address import ( "context" - "errors" "github.com/btcsuite/btcd/btcec/v2" - "github.com/jackc/pgx/v4" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightninglabs/loop/staticaddr" "github.com/lightningnetwork/lnd/keychain" ) @@ -24,46 +23,9 @@ func NewSqlStore(db *loopdb.BaseDB) *SqlStore { } } -// ExecTx is a wrapper for txBody to abstract the creation and commit of a db -// transaction. The db transaction is embedded in a `*sqlc.Queries` that txBody -// needs to use when executing each one of the queries that need to be applied -// atomically. -func (s *SqlStore) ExecTx(ctx context.Context, txOptions loopdb.TxOptions, - txBody func(queries *sqlc.Queries) error) error { - - // Create the db transaction. - tx, err := s.baseDB.BeginTx(ctx, txOptions) - if err != nil { - return err - } - - // Rollback is safe to call even if the tx is already closed, so if the - // tx commits successfully, this is a no-op. - defer func() { - err := tx.Rollback() - switch { - // If the tx was already closed (it was successfully executed) - // we do not need to log that error. - case errors.Is(err, pgx.ErrTxClosed): - return - - // If this is an unexpected error, log it. - case err != nil: - log.Errorf("unable to rollback db tx: %v", err) - } - }() - - if err := txBody(s.baseDB.Queries.WithTx(tx)); err != nil { - return err - } - - // Commit transaction. - return tx.Commit() -} - // CreateStaticAddress creates a static address record in the database. func (s *SqlStore) CreateStaticAddress(ctx context.Context, - addrParams *AddressParameters) error { + addrParams *Parameters) error { createArgs := sqlc.CreateStaticAddressParams{ ClientPubkey: addrParams.ClientPubkey.SerializeCompressed(), @@ -80,7 +42,7 @@ func (s *SqlStore) CreateStaticAddress(ctx context.Context, // GetStaticAddress retrieves static address parameters for a given pkScript. func (s *SqlStore) GetStaticAddress(ctx context.Context, - pkScript []byte) (*AddressParameters, error) { + pkScript []byte) (*Parameters, error) { staticAddress, err := s.baseDB.Queries.GetStaticAddress(ctx, pkScript) if err != nil { @@ -91,15 +53,15 @@ func (s *SqlStore) GetStaticAddress(ctx context.Context, } // GetAllStaticAddresses returns all address known to the server. -func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) ( - []*AddressParameters, error) { +func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) ([]*Parameters, + error) { staticAddresses, err := s.baseDB.Queries.AllStaticAddresses(ctx) if err != nil { return nil, err } - var result []*AddressParameters + var result []*Parameters for _, address := range staticAddresses { res, err := s.toAddressParameters(address) if err != nil { @@ -120,7 +82,7 @@ func (s *SqlStore) Close() { // toAddressParameters transforms a database representation of a static address // to an AddressParameters struct. func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( - *AddressParameters, error) { + *Parameters, error) { clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey) if err != nil { @@ -132,7 +94,7 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( return nil, err } - return &AddressParameters{ + return &Parameters{ ClientPubkey: clientPubkey, ServerPubkey: serverPubkey, PkScript: row.Pkscript, @@ -141,6 +103,6 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( Family: keychain.KeyFamily(row.ClientKeyFamily), Index: uint32(row.ClientKeyIndex), }, - ProtocolVersion: AddressProtocolVersion(row.ProtocolVersion), + ProtocolVersion: staticaddr.AddressProtocolVersion(row.ProtocolVersion), }, nil } diff --git a/staticaddr/deposit/interface.go b/staticaddr/deposit/interface.go new file mode 100644 index 0000000..dc98f16 --- /dev/null +++ b/staticaddr/deposit/interface.go @@ -0,0 +1,44 @@ +package deposit + +import ( + "context" + + "github.com/lightninglabs/loop/staticaddr/address" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightningnetwork/lnd/lnwallet" +) + +const ( + IdLength = 32 +) + +// Store is the database interface that is used to store and retrieve +// static address deposits. +type Store interface { + // CreateDeposit inserts a new deposit into the store. + CreateDeposit(ctx context.Context, deposit *Deposit) error + + // UpdateDeposit updates the deposit in the database. + UpdateDeposit(ctx context.Context, deposit *Deposit) error + + // GetDeposit retrieves a deposit with depositID from the database. + GetDeposit(ctx context.Context, depositID ID) (*Deposit, error) + + // AllDeposits retrieves all deposits from the store. + AllDeposits(ctx context.Context) ([]*Deposit, error) +} + +// AddressManager handles fetching of address parameters. +type AddressManager interface { + // GetStaticAddressParameters returns the static address parameters. + GetStaticAddressParameters(ctx context.Context) (*address.Parameters, + error) + + // GetStaticAddress returns the deposit address for the given + // client and server public keys. + GetStaticAddress(ctx context.Context) (*script.StaticAddress, error) + + // ListUnspent returns a list of utxos at the static address. + ListUnspent(ctx context.Context, minConfs, + maxConfs int32) ([]*lnwallet.Utxo, error) +} diff --git a/staticaddr/deposit/sql_store.go b/staticaddr/deposit/sql_store.go new file mode 100644 index 0000000..08c1d38 --- /dev/null +++ b/staticaddr/deposit/sql_store.go @@ -0,0 +1,213 @@ +package deposit + +import ( + "context" + "database/sql" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightningnetwork/lnd/clock" +) + +// SqlStore is the backing store for static address deposits. +type SqlStore struct { + baseDB *loopdb.BaseDB + + clock clock.Clock +} + +// NewSqlStore constructs a new SQLStore from a BaseDB. The BaseDB is agnostic +// to the underlying driver which can be postgres or sqlite. +func NewSqlStore(db *loopdb.BaseDB) *SqlStore { + return &SqlStore{ + baseDB: db, + + clock: clock.NewDefaultClock(), + } +} + +// CreateDeposit creates a static address deposit record in the database. +func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { + createArgs := sqlc.CreateDepositParams{ + DepositID: deposit.ID[:], + TxHash: deposit.Hash[:], + OutIndex: int32(deposit.Index), + Amount: int64(deposit.Value), + ConfirmationHeight: deposit.ConfirmationHeight, + TimeoutSweepPkScript: deposit.TimeOutSweepPkScript, + } + + updateArgs := sqlc.InsertDepositUpdateParams{ + DepositID: deposit.ID[:], + UpdateTimestamp: s.clock.Now().UTC(), + UpdateState: string(deposit.State), + } + + return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{}, + func(q *sqlc.Queries) error { + err := q.CreateDeposit(ctx, createArgs) + if err != nil { + return err + } + + return q.InsertDepositUpdate(ctx, updateArgs) + }) +} + +// UpdateDeposit updates the deposit in the database. +func (s *SqlStore) UpdateDeposit(ctx context.Context, deposit *Deposit) error { + insertUpdateArgs := sqlc.InsertDepositUpdateParams{ + DepositID: deposit.ID[:], + UpdateTimestamp: s.clock.Now().UTC(), + UpdateState: string(deposit.State), + } + + var ( + txHash = deposit.Hash[:] + outIndex = sql.NullInt32{ + Int32: int32(deposit.Index), + Valid: true, + } + confirmationHeight = sql.NullInt64{ + Int64: deposit.ConfirmationHeight, + Valid: deposit.ConfirmationHeight != 0, + } + ) + + updateArgs := sqlc.UpdateDepositParams{ + DepositID: deposit.ID[:], + TxHash: txHash, + OutIndex: outIndex.Int32, + ConfirmationHeight: confirmationHeight.Int64, + ExpirySweepTxid: deposit.ExpirySweepTxid[:], + } + + return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{}, + func(q *sqlc.Queries) error { + err := q.UpdateDeposit(ctx, updateArgs) + if err != nil { + return err + } + + return q.InsertDepositUpdate(ctx, insertUpdateArgs) + }) +} + +// GetDeposit retrieves the deposit from the database. +func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { + var deposit *Deposit + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), + func(q *sqlc.Queries) error { + row, err := q.GetDeposit(ctx, id[:]) + if err != nil { + return err + } + + latestUpdate, err := q.GetLatestDepositUpdate( + ctx, id[:], + ) + if err != nil { + return err + } + + deposit, err = s.toDeposit(row, latestUpdate) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return deposit, nil +} + +// AllDeposits retrieves all known deposits to our static address. +func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { + var allDeposits []*Deposit + + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), + func(q *sqlc.Queries) error { + var err error + + deposits, err := q.AllDeposits(ctx) + if err != nil { + return err + } + + for _, deposit := range deposits { + latestUpdate, err := q.GetLatestDepositUpdate( + ctx, deposit.DepositID, + ) + if err != nil { + return err + } + + d, err := s.toDeposit(deposit, latestUpdate) + if err != nil { + return err + } + + allDeposits = append(allDeposits, d) + } + + return nil + }) + if err != nil { + return nil, err + } + + return allDeposits, nil +} + +// toDeposit converts an sql deposit to a deposit. +func (s *SqlStore) toDeposit(row sqlc.Deposit, + lastUpdate sqlc.DepositUpdate) (*Deposit, error) { + + id := ID{} + err := id.FromByteSlice(row.DepositID) + if err != nil { + return nil, err + } + + var txHash *chainhash.Hash + if row.TxHash != nil { + txHash, err = chainhash.NewHash(row.TxHash) + if err != nil { + return nil, err + } + } + + var expirySweepTxid chainhash.Hash + if row.ExpirySweepTxid != nil { + hash, err := chainhash.NewHash(row.ExpirySweepTxid) + if err != nil { + return nil, err + } + expirySweepTxid = *hash + } + + return &Deposit{ + ID: id, + State: fsm.StateType(lastUpdate.UpdateState), + OutPoint: wire.OutPoint{ + Hash: *txHash, + Index: uint32(row.OutIndex), + }, + Value: btcutil.Amount(row.Amount), + ConfirmationHeight: row.ConfirmationHeight, + TimeOutSweepPkScript: row.TimeoutSweepPkScript, + ExpirySweepTxid: expirySweepTxid, + }, nil +} + +// Close closes the database connection. +func (s *SqlStore) Close() { + s.baseDB.DB.Close() +}