Skip to content

Commit

Permalink
staticaddr: deposit interface and sql store
Browse files Browse the repository at this point in the history
  • Loading branch information
hieblmi committed Mar 4, 2024
1 parent c1661d0 commit dcf2151
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 0 deletions.
66 changes: 66 additions & 0 deletions staticaddr/deposit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package staticaddr

import (
"crypto/rand"
"fmt"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
)

// ID is a unique identifier for a deposit.
type ID [IdLength]byte

// FromByteSlice creates a deposit id from a byte slice.
func (r *ID) FromByteSlice(b []byte) error {
if len(b) != IdLength {
return fmt.Errorf("deposit id must be 32 bytes, got %d, %x",
len(b), b)
}

copy(r[:], b)

return nil
}

// Deposit bundles an utxo at a static address together with manager-relevant
// data.
type Deposit struct {
// ID is the unique identifier of the deposit.
ID ID

// State is the current state of the deposit.
State fsm.StateType

// The outpoint of the deposit.
wire.OutPoint

// Value is the amount of the deposit.
Value btcutil.Amount

// ConfirmationHeight is the absolute height at which the deposit was
// first confirmed.
ConfirmationHeight int64

// TimeOutSweepPkScript is the pk script that is used to sweep the
// deposit to after it is expired.
TimeOutSweepPkScript []byte
}

// IsPending returns true if the deposit is pending.
func (d *Deposit) IsPending() bool {
return !d.IsFinal()
}

// IsFinal returns true if the deposit is final.
func (d *Deposit) IsFinal() bool {
return d.State == SweptExpiredDeposit || d.State == Failed
}

// GetRandomDepositID generates a random deposit ID.
func GetRandomDepositID() (ID, error) {
var id ID
_, err := rand.Read(id[:])
return id, err
}
16 changes: 16 additions & 0 deletions staticaddr/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ var (
ErrAddressNotFound = fmt.Errorf("address not found")
)

const (
IdLength = 32
)

// AddressStore is the database interface that is used to store and retrieve
// static addresses.
type AddressStore interface {
Expand All @@ -29,6 +33,18 @@ type AddressStore interface {
// GetAllStaticAddresses retrieves all static addresses from the store.
GetAllStaticAddresses(ctx context.Context) (
[]*AddressParameters, error)

// 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)
}

// AddressParameters holds all the necessary information for the 2-of-2 multisig
Expand Down
191 changes: 191 additions & 0 deletions staticaddr/sql_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,35 @@ package staticaddr

import (
"context"
"database/sql"
"errors"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/jackc/pgx/v4"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/keychain"
)

// SqlStore is the backing store for static addresses.
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(),
}
}

Expand Down Expand Up @@ -112,6 +122,179 @@ func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) (
return result, nil
}

// CreateDeposit creates a static address 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,
}
)

updateArgs := sqlc.UpdateDepositParams{
DepositID: deposit.ID[:],
TxHash: txHash,
OutIndex: outIndex.Int32,
ConfirmationHeight: marshalSqlNullInt64(
deposit.ConfirmationHeight,
).Int64,
}

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 {
var err error
row, err := q.GetDeposit(ctx, id[:])
if err != nil {
return err
}

updates, err := q.GetDepositUpdates(ctx, id[:])
if err != nil {
return err
}

if len(updates) == 0 {
return errors.New("no deposit updates")
}

deposit, err = s.toDeposit(row, updates[len(updates)-1])
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 {
updates, err := q.GetDepositUpdates(
ctx, deposit.DepositID,
)
if err != nil {
return err
}

if len(updates) == 0 {
return errors.New("no deposit updates")
}

lastUpdate := updates[len(updates)-1]

d, err := s.toDeposit(deposit, lastUpdate)
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
}
}

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,
}, nil
}

// Close closes the database connection.
func (s *SqlStore) Close() {
s.baseDB.DB.Close()
Expand Down Expand Up @@ -144,3 +327,11 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) (
ProtocolVersion: AddressProtocolVersion(row.ProtocolVersion),
}, nil
}

// marshalSqlNullInt64 converts an int64 to a sql.NullInt64.
func marshalSqlNullInt64(i int64) sql.NullInt64 {
return sql.NullInt64{
Int64: i,
Valid: i != 0,
}
}

0 comments on commit dcf2151

Please sign in to comment.