Slyghtning 2 weeks ago committed by GitHub
commit c1f34a2eeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,8 +2,13 @@ package main
import (
"context"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
@ -16,6 +21,8 @@ var staticAddressCommands = cli.Command{
Subcommands: []cli.Command{
newStaticAddressCommand,
listUnspentCommand,
withdrawalCommand,
summaryCommand,
},
}
@ -39,14 +46,14 @@ func newStaticAddress(ctx *cli.Context) error {
return cli.ShowCommandHelp(ctx, "new")
}
client, cleanup, err := getAddressClient(ctx)
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.NewAddress(
ctxb, &looprpc.NewAddressRequest{},
resp, err := client.NewStaticAddress(
ctxb, &looprpc.NewStaticAddressRequest{},
)
if err != nil {
return err
@ -86,15 +93,88 @@ func listUnspent(ctx *cli.Context) error {
return cli.ShowCommandHelp(ctx, "listunspent")
}
client, cleanup, err := getAddressClient(ctx)
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.ListUnspent(ctxb, &looprpc.ListUnspentRequest{
MinConfs: int32(ctx.Int("min_confs")),
MaxConfs: int32(ctx.Int("max_confs")),
resp, err := client.ListUnspentDeposits(
ctxb, &looprpc.ListUnspentDepositsRequest{
MinConfs: int32(ctx.Int("min_confs")),
MaxConfs: int32(ctx.Int("max_confs")),
})
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var withdrawalCommand = cli.Command{
Name: "withdraw",
ShortName: "w",
Usage: "Withdraw from static address deposits.",
Description: `
Withdraws from all or selected static address deposits by sweeping them
back to our lnd wallet.
`,
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "utxo",
Usage: "specify utxos as outpoints(tx:idx) which will" +
"be closed.",
},
cli.BoolFlag{
Name: "all",
Usage: "withdraws all static address deposits.",
},
},
Action: withdraw,
}
func withdraw(ctx *cli.Context) error {
if ctx.NArg() > 0 {
return cli.ShowCommandHelp(ctx, "withdraw")
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
var (
req = &looprpc.WithdrawDepositsRequest{}
isAllSelected = ctx.IsSet("all")
isUtxoSelected = ctx.IsSet("utxo")
outpoints []*looprpc.OutPoint
ctxb = context.Background()
)
switch {
case isAllSelected == isUtxoSelected:
return errors.New("must select either all or some utxos")
case isAllSelected:
case isUtxoSelected:
utxos := ctx.StringSlice("utxo")
outpoints, err = utxosToOutpoints(utxos)
if err != nil {
return err
}
req.Outpoints = outpoints
default:
return fmt.Errorf("unknown withdrawal request")
}
resp, err := client.WithdrawDeposits(ctxb, &looprpc.WithdrawDepositsRequest{
Outpoints: outpoints,
All: isAllSelected,
})
if err != nil {
return err
@ -105,20 +185,117 @@ func listUnspent(ctx *cli.Context) error {
return nil
}
func getAddressClient(ctx *cli.Context) (looprpc.StaticAddressClientClient,
func(), error) {
var summaryCommand = cli.Command{
Name: "summary",
ShortName: "s",
Usage: "Display a summary of static address related data.",
Description: `
Displays various static address related data. Utxos, Deposits,
Withdrawls, loop-ins...
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "filter",
Usage: "specify a filter to only display deposits in " +
"the specified state. The state can be one " +
"of [deposited|withdrawing|withdrawn|" +
"publish_expired_deposit|" +
"wait_for_expiry_sweep|expired|failed].",
},
},
Action: summary,
}
rpcServer := ctx.GlobalString("rpcserver")
tlsCertPath, macaroonPath, err := extractPathArgs(ctx)
func summary(ctx *cli.Context) error {
ctxb := context.Background()
if ctx.NArg() > 0 {
return cli.ShowCommandHelp(ctx, "summary")
}
client, cleanup, err := getClient(ctx)
if err != nil {
return nil, nil, err
return err
}
defer cleanup()
var filterState looprpc.DepositState
switch ctx.String("filter") {
case "":
// If no filter is specified, we'll default to showing all.
case "deposited":
filterState = looprpc.DepositState_DEPOSITED
case "withdrawing":
filterState = looprpc.DepositState_WITHDRAWING
case "withdrawn":
filterState = looprpc.DepositState_WITHDRAWN
case "publish_expired_deposit":
filterState = looprpc.DepositState_PUBLISH_EXPIRED
case "wait_for_expiry_sweep":
filterState = looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP
case "expired":
filterState = looprpc.DepositState_EXPIRED
case "failed":
filterState = looprpc.DepositState_FAILED_STATE
default:
filterState = looprpc.DepositState_UNKNOWN_STATE
}
conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
resp, err := client.GetStaticAddressSummary(
ctxb, &looprpc.StaticAddressSummaryRequest{
StateFilter: filterState,
},
)
if err != nil {
return nil, nil, err
return err
}
printRespJSON(resp)
return nil
}
func utxosToOutpoints(utxos []string) ([]*looprpc.OutPoint, error) {
var outpoints []*looprpc.OutPoint
if len(utxos) == 0 {
return nil, fmt.Errorf("no utxos specified")
}
for _, utxo := range utxos {
outpoint, err := NewProtoOutPoint(utxo)
if err != nil {
return nil, err
}
outpoints = append(outpoints, outpoint)
}
cleanup := func() { conn.Close() }
addressClient := looprpc.NewStaticAddressClientClient(conn)
return addressClient, cleanup, nil
return outpoints, nil
}
// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint
// type.
func NewProtoOutPoint(op string) (*looprpc.OutPoint, error) {
parts := strings.Split(op, ":")
if len(parts) != 2 {
return nil, errors.New("outpoint should be of the form " +
"txid:index")
}
txid := parts[0]
if hex.DecodedLen(len(txid)) != chainhash.HashSize {
return nil, fmt.Errorf("invalid hex-encoded txid %v", txid)
}
outputIndex, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid output index: %v", err)
}
return &looprpc.OutPoint{
TxidStr: txid,
OutputIndex: uint32(outputIndex),
}, nil
}

@ -21,7 +21,9 @@ import (
"github.com/lightninglabs/loop/loopd/perms"
"github.com/lightninglabs/loop/loopdb"
loop_looprpc "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/withdraw"
loop_swaprpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/clock"
@ -68,12 +70,6 @@ type Daemon struct {
// same process.
swapClientServer
// AddressServer is the embedded RPC server that satisfies the
// static address client RPC interface. We embed this struct so the
// Daemon itself can be registered to an existing grpc.Server to run as
// a subserver in the same process.
*staticaddr.AddressServer
// ErrChan is an error channel that users of the Daemon struct must use
// to detect runtime errors and also whether a shutdown is fully
// completed.
@ -239,7 +235,6 @@ func (d *Daemon) startWebServers() error {
grpc.StreamInterceptor(streamInterceptor),
)
loop_looprpc.RegisterSwapClientServer(d.grpcServer, d)
loop_looprpc.RegisterStaticAddressClientServer(d.grpcServer, d)
// Register our debug server if it is compiled in.
d.registerDebugServer()
@ -438,6 +433,17 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
swapClient.Conn,
)
// Create a static address server client.
staticAddressClient := loop_swaprpc.NewStaticAddressServerClient(
swapClient.Conn,
)
// Create a static address client that cooperatively closes deposits
// with the server.
withdrawalClient := loop_swaprpc.NewWithdrawalServerClient(
swapClient.Conn,
)
// Both the client RPC server and the swap server client should stop
// on main context cancel. So we create it early and pass it down.
d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
@ -498,6 +504,10 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
var (
reservationManager *reservation.Manager
instantOutManager *instantout.Manager
staticAddressManager *address.Manager
depositManager *deposit.Manager
withdrawalManager *withdraw.Manager
)
// Create the reservation and instantout managers.
if d.cfg.EnableExperimental {
@ -534,42 +544,62 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
instantOutManager = instantout.NewInstantOutManager(
instantOutConfig,
)
// Static address manager setup.
staticAddressStore := address.NewSqlStore(baseDb)
addrCfg := &address.ManagerConfig{
AddressClient: staticAddressClient,
SwapClient: swapClient,
Store: staticAddressStore,
WalletKit: d.lnd.WalletKit,
ChainParams: d.lnd.ChainParams,
}
staticAddressManager = address.NewManager(addrCfg)
// Static address deposit manager setup.
depositStore := deposit.NewSqlStore(baseDb)
depoCfg := &deposit.ManagerConfig{
AddressClient: staticAddressClient,
AddressManager: staticAddressManager,
SwapClient: swapClient,
Store: depositStore,
WalletKit: d.lnd.WalletKit,
ChainParams: d.lnd.ChainParams,
ChainNotifier: d.lnd.ChainNotifier,
Signer: d.lnd.Signer,
}
depositManager = deposit.NewManager(depoCfg)
// Static address deposit withdrawal manager setup.
closeCfg := &withdraw.ManagerConfig{
WithdrawalServerClient: withdrawalClient,
AddressManager: staticAddressManager,
DepositManager: depositManager,
WalletKit: d.lnd.WalletKit,
ChainParams: d.lnd.ChainParams,
ChainNotifier: d.lnd.ChainNotifier,
Signer: d.lnd.Signer,
}
withdrawalManager = withdraw.NewManager(closeCfg)
}
// Now finally fully initialize the swap client RPC server instance.
d.swapClientServer = swapClientServer{
config: d.cfg,
network: lndclient.Network(d.cfg.Network),
impl: swapClient,
liquidityMgr: getLiquidityManager(swapClient),
lnd: &d.lnd.LndServices,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
subscribers: make(map[int]chan<- interface{}),
statusChan: make(chan loop.SwapInfo),
mainCtx: d.mainCtx,
reservationManager: reservationManager,
instantOutManager: instantOutManager,
}
// Create a static address server client.
staticAddressClient := loop_swaprpc.NewStaticAddressServerClient(
swapClient.Conn,
)
store := staticaddr.NewSqlStore(baseDb)
cfg := &staticaddr.ManagerConfig{
AddressClient: staticAddressClient,
SwapClient: swapClient,
Store: store,
WalletKit: d.lnd.WalletKit,
ChainParams: d.lnd.ChainParams,
config: d.cfg,
network: lndclient.Network(d.cfg.Network),
impl: swapClient,
liquidityMgr: getLiquidityManager(swapClient),
lnd: &d.lnd.LndServices,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
subscribers: make(map[int]chan<- interface{}),
statusChan: make(chan loop.SwapInfo),
mainCtx: d.mainCtx,
reservationManager: reservationManager,
instantOutManager: instantOutManager,
staticAddressManager: staticAddressManager,
depositManager: depositManager,
withdrawalManager: withdrawalManager,
}
staticAddressManager := staticaddr.NewAddressManager(cfg)
d.AddressServer = staticaddr.NewAddressServer(
staticAddressClient, staticAddressManager,
)
// Retrieve all currently existing swaps from the database.
swapsList, err := d.impl.FetchSwaps(d.mainCtx)
@ -662,20 +692,69 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
}
// Start the static address manager.
d.wg.Add(1)
go func() {
defer d.wg.Done()
if staticAddressManager != nil {
d.wg.Add(1)
go func() {
defer d.wg.Done()
log.Info("Starting static address manager...")
err = staticAddressManager.Run(d.mainCtx)
if err != nil && !errors.Is(context.Canceled, err) {
d.internalErrChan <- err
}
log.Info("Starting static address manager...")
err = staticAddressManager.Run(d.mainCtx)
if err != nil && !errors.Is(context.Canceled, err) {
d.internalErrChan <- err
}
log.Info("Static address manager stopped")
}()
}
log.Info("Static address manager stopped")
}()
// Start the static address deposit manager.
if depositManager != nil {
d.wg.Add(1)
go func() {
defer d.wg.Done()
staticAddressManager.WaitInitComplete()
// Lnd's GetInfo call supplies us with the current block
// height.
info, err := d.lnd.Client.GetInfo(d.mainCtx)
if err != nil {
d.internalErrChan <- err
return
}
log.Info("Starting static address deposit manager...")
err = depositManager.Run(d.mainCtx, info.BlockHeight)
if err != nil && !errors.Is(context.Canceled, err) {
d.internalErrChan <- err
}
log.Info("Static address deposit manager stopped")
}()
depositManager.WaitInitComplete()
}
// Start the static address deposit withdrawal manager.
if withdrawalManager != nil {
d.wg.Add(1)
go func() {
defer d.wg.Done()
// Lnd's GetInfo call supplies us with the current block
// height.
info, err := d.lnd.Client.GetInfo(d.mainCtx)
if err != nil {
d.internalErrChan <- err
return
}
log.Info("Starting static address deposit withdrawal " +
"manager...")
err = withdrawalManager.Run(d.mainCtx, info.BlockHeight)
if err != nil && !errors.Is(context.Canceled, err) {
d.internalErrChan <- err
}
log.Info("Static address deposit withdrawal manager " +
"stopped")
}()
withdrawalManager.WaitInitComplete()
}
// Last, start our internal error handler. This will return exactly one
// error or nil on the main error channel to inform the caller that

@ -10,7 +10,8 @@ import (
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/staticaddr"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
@ -38,7 +39,8 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
lnd.AddSubLogger(root, "SWEEP", intercept, sweepbatcher.UseLogger)
lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger)
lnd.AddSubLogger(root, "STORE", intercept, loopdb.UseLogger)
lnd.AddSubLogger(root, "SADDR", intercept, staticaddr.UseLogger)
lnd.AddSubLogger(root, "SADDR", intercept, address.UseLogger)
lnd.AddSubLogger(root, "DEPO", intercept, deposit.UseLogger)
lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger)
lnd.AddSubLogger(
root, liquidity.Subsystem, intercept, liquidity.UseLogger,

@ -69,14 +69,28 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "loop",
Action: "in",
}},
"/looprpc.StaticAddressClient/NewAddress": {{
"/looprpc.SwapClient/NewStaticAddress": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.StaticAddressClient/ListUnspent": {{
"/looprpc.SwapClient/ListUnspentDeposits": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/WithdrawDeposits": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetStaticAddressSummary": {{
Entity: "swap",
Action: "read",
}, {

@ -15,15 +15,20 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
clientrpc "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/withdraw"
"github.com/lightninglabs/loop/swap"
looprpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
@ -76,19 +81,22 @@ type swapClientServer struct {
clientrpc.UnimplementedSwapClientServer
clientrpc.UnimplementedDebugServer
config *Config
network lndclient.Network
impl *loop.Client
liquidityMgr *liquidity.Manager
lnd *lndclient.LndServices
reservationManager *reservation.Manager
instantOutManager *instantout.Manager
swaps map[lntypes.Hash]loop.SwapInfo
subscribers map[int]chan<- interface{}
statusChan chan loop.SwapInfo
nextSubscriberID int
swapsLock sync.Mutex
mainCtx context.Context
config *Config
network lndclient.Network
impl *loop.Client
liquidityMgr *liquidity.Manager
lnd *lndclient.LndServices
reservationManager *reservation.Manager
instantOutManager *instantout.Manager
staticAddressManager *address.Manager
depositManager *deposit.Manager
withdrawalManager *withdraw.Manager
swaps map[lntypes.Hash]loop.SwapInfo
subscribers map[int]chan<- interface{}
statusChan chan loop.SwapInfo
nextSubscriberID int
swapsLock sync.Mutex
mainCtx context.Context
}
// LoopOut initiates a loop out swap with the given parameters. The call returns
@ -1231,6 +1239,267 @@ func (s *swapClientServer) InstantOutQuote(ctx context.Context,
}, nil
}
// NewStaticAddress is the rpc endpoint for loop clients to request a new static
// address.
func (s *swapClientServer) NewStaticAddress(ctx context.Context,
_ *clientrpc.NewStaticAddressRequest) (
*clientrpc.NewStaticAddressResponse, error) {
staticAddress, err := s.staticAddressManager.NewAddress(ctx)
if err != nil {
return nil, err
}
return &clientrpc.NewStaticAddressResponse{
Address: staticAddress.String(),
}, nil
}
// ListUnspentDeposits returns a list of utxos behind the static address.
func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
req *clientrpc.ListUnspentDepositsRequest) (
*clientrpc.ListUnspentDepositsResponse, error) {
// List all unspent utxos the wallet sees, regardless of the number of
// confirmations.
staticAddress, utxos, err := s.staticAddressManager.ListUnspentRaw(
ctx, req.MinConfs, req.MaxConfs,
)
if err != nil {
return nil, err
}
// Prepare the list response.
var respUtxos []*clientrpc.Utxo
for _, u := range utxos {
utxo := &clientrpc.Utxo{
StaticAddress: staticAddress.String(),
AmountSat: int64(u.Value),
Confirmations: u.Confirmations,
Outpoint: u.OutPoint.String(),
}
respUtxos = append(respUtxos, utxo)
}
return &clientrpc.ListUnspentDepositsResponse{Utxos: respUtxos}, nil
}
// WithdrawDeposits tries to obtain a partial signature from the server to spend
// the selected deposits to the client's wallet.
func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
req *clientrpc.WithdrawDepositsRequest) (
*clientrpc.WithdrawDepositsResponse, error) {
var (
isAllSelected = req.All
isUtxoSelected = req.Outpoints != nil
outpoints []wire.OutPoint
err error
)
switch {
case isAllSelected == isUtxoSelected:
return nil, fmt.Errorf("must select either all or some utxos")
case isAllSelected:
deposits, err := s.depositManager.GetActiveDepositsInState(
deposit.Deposited,
)
if err != nil {
return nil, err
}
for _, d := range deposits {
outpoints = append(outpoints, d.OutPoint)
}
case isUtxoSelected:
outpoints, err = toServerOutpoints(req.Outpoints)
if err != nil {
return nil, err
}
}
err = s.withdrawalManager.WithdrawDeposits(ctx, outpoints)
if err != nil {
return nil, err
}
return &clientrpc.WithdrawDepositsResponse{}, err
}
func (s *swapClientServer) GetStaticAddressSummary(ctx context.Context,
req *clientrpc.StaticAddressSummaryRequest) (*clientrpc.StaticAddressSummaryResponse,
error) {
allDeposits, err := s.depositManager.GetAllDeposits()
if err != nil {
return nil, err
}
return s.depositSummary(ctx, allDeposits, req.StateFilter)
}
func (s *swapClientServer) depositSummary(ctx context.Context,
deposits []*deposit.Deposit,
filter clientrpc.DepositState) (*clientrpc.StaticAddressSummaryResponse,
error) {
var (
totalNumDeposits = len(deposits)
valueUnconfirmed int64
valueDeposited int64
valueExpired int64
valueWithdrawn int64
)
// Value unconfirmed.
utxos, err := s.staticAddressManager.ListUnspent(
ctx, 0, deposit.MinConfs-1,
)
if err != nil {
return nil, err
}
for _, u := range utxos {
valueUnconfirmed += int64(u.Value)
}
for _, d := range deposits {
value := int64(d.Value)
switch d.State {
case deposit.Deposited:
valueDeposited += value
case deposit.Expired:
valueExpired += value
case deposit.Withdrawn:
valueWithdrawn += value
}
}
clientDeposits, err := s.filterClientDeposits(deposits, filter)
if err != nil {
return nil, err
}
params, err := s.staticAddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, err
}
address, err := s.staticAddressManager.GetTaprootAddress(
params.ClientPubkey, params.ServerPubkey, int64(params.Expiry),
)
return &clientrpc.StaticAddressSummaryResponse{
StaticAddress: address.String(),
TotalNumDeposits: uint32(totalNumDeposits),
ValueUnconfirmed: valueUnconfirmed,
ValueDeposited: valueDeposited,
ValueExpired: valueExpired,
ValueWithdrawn: valueWithdrawn,
FilteredDeposits: clientDeposits,
}, nil
}
func (s *swapClientServer) filterClientDeposits(deposits []*deposit.Deposit,
filterState clientrpc.DepositState) ([]*clientrpc.Deposit, error) {
var clientDeposits []*clientrpc.Deposit
for _, d := range deposits {
if filterState != clientrpc.DepositState_UNKNOWN_STATE &&
d.State != toServerState(filterState) {
continue
}
outpoint := wire.NewOutPoint(&d.Hash, d.Index).String()
clientDeposits = append(clientDeposits, &clientrpc.Deposit{
Id: d.ID[:],
State: toClientState(d.State),
Outpoint: outpoint,
Value: int64(d.Value),
ConfirmationHeight: d.ConfirmationHeight,
})
}
return clientDeposits, nil
}
func toClientState(state fsm.StateType) clientrpc.DepositState {
switch state {
case deposit.Deposited:
return clientrpc.DepositState_DEPOSITED
case deposit.Withdrawing:
return clientrpc.DepositState_WITHDRAWING
case deposit.Withdrawn:
return clientrpc.DepositState_WITHDRAWN
case deposit.PublishExpiredDeposit:
return clientrpc.DepositState_PUBLISH_EXPIRED
case deposit.WaitForExpirySweep:
return clientrpc.DepositState_WAIT_FOR_EXPIRY_SWEEP
case deposit.Expired:
return clientrpc.DepositState_EXPIRED
case deposit.Failed:
return clientrpc.DepositState_FAILED_STATE
default:
return clientrpc.DepositState_UNKNOWN_STATE
}
}
func toServerState(state clientrpc.DepositState) fsm.StateType {
switch state {
case clientrpc.DepositState_DEPOSITED:
return deposit.Deposited
case clientrpc.DepositState_WITHDRAWING:
return deposit.Withdrawing
case clientrpc.DepositState_WITHDRAWN:
return deposit.Withdrawn
case clientrpc.DepositState_PUBLISH_EXPIRED:
return deposit.PublishExpiredDeposit
case clientrpc.DepositState_WAIT_FOR_EXPIRY_SWEEP:
return deposit.WaitForExpirySweep
case clientrpc.DepositState_EXPIRED:
return deposit.Expired
case clientrpc.DepositState_FAILED_STATE:
return deposit.Failed
default:
return fsm.EmptyState
}
}
func toServerOutpoints(outpoints []*clientrpc.OutPoint) ([]wire.OutPoint,
error) {
var serverOutpoints []wire.OutPoint
for _, o := range outpoints {
outpointStr := fmt.Sprintf("%s:%d", o.TxidStr, o.OutputIndex)
newOutpoint, err := wire.NewOutPointFromString(outpointStr)
if err != nil {
return nil, err
}
serverOutpoints = append(serverOutpoints, *newOutpoint)
}
return serverOutpoints, nil
}
func rpcAutoloopReason(reason liquidity.Reason) (clientrpc.AutoReason, error) {
switch reason {
case liquidity.ReasonNone:

@ -0,0 +1,47 @@
-- deposits stores historic and unspent static address outputs.
CREATE TABLE IF NOT EXISTS deposits (
-- id is the auto-incrementing primary key for a static address.
id INTEGER PRIMARY KEY,
-- deposit_id is the unique identifier for the deposit.
deposit_id BLOB NOT NULL UNIQUE,
-- tx_hash is the transaction hash of the deposit.
tx_hash BYTEA NOT NULL,
-- output_index is the index of the output in the transaction.
out_index INT NOT NULL,
-- amount is the amount of the deposit.
amount BIGINT NOT NULL,
-- confirmation_height is the absolute height at which the deposit was
-- confirmed.
confirmation_height BIGINT NOT NULL,
-- timeout_sweep_pk_script is the public key script that will be used to
-- sweep the deposit after has expired.
timeout_sweep_pk_script BYTEA NOT NULL,
-- expiry_sweep_txid is the transaction id of the expiry sweep.
expiry_sweep_txid BLOB,
-- withdrawal_sweep_pk_script is the address that will be used to sweep the
-- deposit cooperatively with the server before it has expired.
withdrawal_sweep_address TEXT
);
-- deposit_updates contains all the updates to a deposit.
CREATE TABLE IF NOT EXISTS deposit_updates (
-- id is the auto incrementing primary key.
id INTEGER PRIMARY KEY,
-- deposit_id is the unique identifier for the deposit.
deposit_id BLOB NOT NULL REFERENCES deposits(deposit_id),
-- update_state is the state of the deposit at the time of the update.
update_state TEXT NOT NULL,
-- update_timestamp is the timestamp of the update.
update_timestamp TIMESTAMP NOT NULL
);

@ -9,6 +9,25 @@ import (
"time"
)
type Deposit struct {
ID int32
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
WithdrawalSweepAddress sql.NullString
}
type DepositUpdate struct {
ID int32
DepositID []byte
UpdateState string
UpdateTimestamp time.Time
}
type HtlcKey struct {
SwapHash []byte
SenderScriptPubkey []byte

@ -9,15 +9,19 @@ import (
)
type Querier interface {
AllDeposits(ctx context.Context) ([]Deposit, error)
AllStaticAddresses(ctx context.Context) ([]StaticAddress, error)
ConfirmBatch(ctx context.Context, id int32) error
CreateDeposit(ctx context.Context, arg CreateDepositParams) error
CreateReservation(ctx context.Context, arg CreateReservationParams) error
CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error
FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]GetBatchSweepsRow, error)
GetDeposit(ctx context.Context, depositID []byte) (Deposit, error)
GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error)
GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error)
GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error)
GetLatestDepositUpdate(ctx context.Context, depositID []byte) (DepositUpdate, error)
GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error)
GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error)
GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error)
@ -30,6 +34,7 @@ type Querier interface {
GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error)
GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error)
InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error)
InsertDepositUpdate(ctx context.Context, arg InsertDepositUpdateParams) error
InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error
InsertInstantOut(ctx context.Context, arg InsertInstantOutParams) error
InsertInstantOutUpdate(ctx context.Context, arg InsertInstantOutUpdateParams) error
@ -39,6 +44,7 @@ type Querier interface {
InsertSwap(ctx context.Context, arg InsertSwapParams) error
InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error
UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error
UpdateReservation(ctx context.Context, arg UpdateReservationParams) error
UpsertLiquidityParams(ctx context.Context, params []byte) error

@ -0,0 +1,69 @@
-- name: CreateDeposit :exec
INSERT INTO deposits (
deposit_id,
tx_hash,
out_index,
amount,
confirmation_height,
timeout_sweep_pk_script,
expiry_sweep_txid,
withdrawal_sweep_address
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8
);
-- name: UpdateDeposit :exec
UPDATE deposits
SET
tx_hash = $2,
out_index = $3,
confirmation_height = $4,
expiry_sweep_txid = $5,
withdrawal_sweep_address = $6
WHERE
deposits.deposit_id = $1;
-- name: InsertDepositUpdate :exec
INSERT INTO deposit_updates (
deposit_id,
update_state,
update_timestamp
) VALUES (
$1,
$2,
$3
);
-- name: GetDeposit :one
SELECT
*
FROM
deposits
WHERE
deposit_id = $1;
-- name: AllDeposits :many
SELECT
*
FROM
deposits
ORDER BY
id ASC;
-- name: GetLatestDepositUpdate :one
SELECT
*
FROM
deposit_updates
WHERE
deposit_id = $1
ORDER BY
update_timestamp DESC
LIMIT 1;

@ -0,0 +1,207 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: static_address_deposits.sql
package sqlc
import (
"context"
"database/sql"
"time"
)
const allDeposits = `-- name: AllDeposits :many
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, withdrawal_sweep_address
FROM
deposits
ORDER BY
id ASC
`
func (q *Queries) AllDeposits(ctx context.Context) ([]Deposit, error) {
rows, err := q.db.QueryContext(ctx, allDeposits)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Deposit
for rows.Next() {
var i Deposit
if err := rows.Scan(
&i.ID,
&i.DepositID,
&i.TxHash,
&i.OutIndex,
&i.Amount,
&i.ConfirmationHeight,
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.WithdrawalSweepAddress,
); 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 createDeposit = `-- name: CreateDeposit :exec
INSERT INTO deposits (
deposit_id,
tx_hash,
out_index,
amount,
confirmation_height,
timeout_sweep_pk_script,
expiry_sweep_txid,
withdrawal_sweep_address
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8
)
`
type CreateDepositParams struct {
DepositID []byte
TxHash []byte
OutIndex int32
Amount int64
ConfirmationHeight int64
TimeoutSweepPkScript []byte
ExpirySweepTxid []byte
WithdrawalSweepAddress sql.NullString
}
func (q *Queries) CreateDeposit(ctx context.Context, arg CreateDepositParams) error {
_, err := q.db.ExecContext(ctx, createDeposit,
arg.DepositID,
arg.TxHash,
arg.OutIndex,
arg.Amount,
arg.ConfirmationHeight,
arg.TimeoutSweepPkScript,
arg.ExpirySweepTxid,
arg.WithdrawalSweepAddress,
)
return err
}
const getDeposit = `-- name: GetDeposit :one
SELECT
id, deposit_id, tx_hash, out_index, amount, confirmation_height, timeout_sweep_pk_script, expiry_sweep_txid, withdrawal_sweep_address
FROM
deposits
WHERE
deposit_id = $1
`
func (q *Queries) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) {
row := q.db.QueryRowContext(ctx, getDeposit, depositID)
var i Deposit
err := row.Scan(
&i.ID,
&i.DepositID,
&i.TxHash,
&i.OutIndex,
&i.Amount,
&i.ConfirmationHeight,
&i.TimeoutSweepPkScript,
&i.ExpirySweepTxid,
&i.WithdrawalSweepAddress,
)
return i, err
}
const getLatestDepositUpdate = `-- name: GetLatestDepositUpdate :one
SELECT
id, deposit_id, update_state, update_timestamp
FROM
deposit_updates
WHERE
deposit_id = $1
ORDER BY
update_timestamp DESC
LIMIT 1
`
func (q *Queries) GetLatestDepositUpdate(ctx context.Context, depositID []byte) (DepositUpdate, error) {
row := q.db.QueryRowContext(ctx, getLatestDepositUpdate, depositID)
var i DepositUpdate
err := row.Scan(
&i.ID,
&i.DepositID,
&i.UpdateState,
&i.UpdateTimestamp,
)
return i, err
}
const insertDepositUpdate = `-- name: InsertDepositUpdate :exec
INSERT INTO deposit_updates (
deposit_id,
update_state,
update_timestamp
) VALUES (
$1,
$2,
$3
)
`
type InsertDepositUpdateParams struct {
DepositID []byte
UpdateState string
UpdateTimestamp time.Time
}
func (q *Queries) InsertDepositUpdate(ctx context.Context, arg InsertDepositUpdateParams) error {
_, err := q.db.ExecContext(ctx, insertDepositUpdate, arg.DepositID, arg.UpdateState, arg.UpdateTimestamp)
return err
}
const updateDeposit = `-- name: UpdateDeposit :exec
UPDATE deposits
SET
tx_hash = $2,
out_index = $3,
confirmation_height = $4,
expiry_sweep_txid = $5,
withdrawal_sweep_address = $6
WHERE
deposits.deposit_id = $1
`
type UpdateDepositParams struct {
DepositID []byte
TxHash []byte
OutIndex int32
ConfirmationHeight int64
ExpirySweepTxid []byte
WithdrawalSweepAddress sql.NullString
}
func (q *Queries) UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error {
_, err := q.db.ExecContext(ctx, updateDeposit,
arg.DepositID,
arg.TxHash,
arg.OutIndex,
arg.ConfirmationHeight,
arg.ExpirySweepTxid,
arg.WithdrawalSweepAddress,
)
return err
}

File diff suppressed because it is too large Load Diff

@ -127,6 +127,27 @@ service SwapClient {
*/
rpc InstantOutQuote (InstantOutQuoteRequest)
returns (InstantOutQuoteResponse);
/* loop: `static newstaticaddress`
NewStaticAddress requests a new static address for loop-ins from the server.
*/
rpc NewStaticAddress (NewStaticAddressRequest)
returns (NewStaticAddressResponse);
/* loop: `static listunspentdeposits`
ListUnspentDeposits returns a list of utxos deposited at a static address.
*/
rpc ListUnspentDeposits (ListUnspentDepositsRequest)
returns (ListUnspentDepositsResponse);
/* loop:`static withdraw`
WithdrawDeposits withdraws a selection or all deposits of a static address.
*/
rpc WithdrawDeposits (WithdrawDepositsRequest)
returns (WithdrawDepositsResponse);
rpc GetStaticAddressSummary (StaticAddressSummaryRequest)
returns (StaticAddressSummaryResponse);
}
message LoopOutRequest {
@ -1387,25 +1408,14 @@ message InstantOutQuoteResponse {
int64 sweep_fee_sat = 2;
}
service StaticAddressClient {
/*
NewAddress requests a new static address for loop-ins from the server.
*/
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse);
/*
ListUnspent returns a list of utxos behind a static address.
*/
rpc ListUnspent (ListUnspentRequest) returns (ListUnspentResponse);
}
message NewAddressRequest {
message NewStaticAddressRequest {
/*
The client's public key for the 2-of-2 MuSig2 taproot static address.
*/
bytes client_key = 1;
}
message NewAddressResponse {
message NewStaticAddressResponse {
/*
The taproot static address.
*/
@ -1417,7 +1427,7 @@ message NewAddressResponse {
uint32 expiry = 2;
}
message ListUnspentRequest {
message ListUnspentDepositsRequest {
/*
The number of minimum confirmations a utxo must have to be listed.
*/
@ -1430,7 +1440,7 @@ message ListUnspentRequest {
int32 max_confs = 2;
}
message ListUnspentResponse {
message ListUnspentDepositsResponse {
/*
A list of utxos behind the static address.
*/
@ -1457,4 +1467,73 @@ message Utxo {
The number of confirmations for the Utxo.
*/
int64 confirmations = 4;
}
message WithdrawDepositsRequest {
/*
The outpoints of the deposits to withdraw.
*/
repeated OutPoint outpoints = 1;
/*
If set to true, all deposits will be withdrawn.
*/
bool all = 2;
}
message WithdrawDepositsResponse {
}
message OutPoint {
/*
Raw bytes representing the transaction id.
*/
bytes txid_bytes = 1;
/*
Reversed, hex-encoded string representing the transaction id.
*/
string txid_str = 2;
/*
The index of the output on the transaction.
*/
uint32 output_index = 3;
}
message StaticAddressSummaryRequest {
DepositState state_filter = 1;
}
message StaticAddressSummaryResponse {
string static_address = 1;
uint32 total_num_deposits = 2;
int64 value_unconfirmed = 3;
int64 value_deposited = 4;
int64 value_expired = 5;
int64 value_withdrawn = 6;
repeated Deposit filtered_deposits = 7;
}
enum DepositState {
UNKNOWN_STATE = 0;
DEPOSITED = 1;
WITHDRAWING = 2;
WITHDRAWN = 3;
PUBLISH_EXPIRED = 4;
WAIT_FOR_EXPIRY_SWEEP = 5;
EXPIRED = 6;
FAILED_STATE = 7;
}
message Deposit {
bytes id = 1;
DepositState state = 2;
string outpoint = 3;
int64 value = 4;
int64 confirmation_height = 5;
}

@ -7,9 +7,6 @@
"tags": [
{
"name": "SwapClient"
},
{
"name": "StaticAddressClient"
}
],
"consumes": [
@ -621,6 +618,43 @@
}
}
},
"looprpcDeposit": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "byte"
},
"state": {
"$ref": "#/definitions/looprpcDepositState"
},
"outpoint": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
},
"confirmation_height": {
"type": "string",
"format": "int64"
}
}
},
"looprpcDepositState": {
"type": "string",
"enum": [
"UNKNOWN_STATE",
"DEPOSITED",
"WITHDRAWING",
"WITHDRAWN",
"PUBLISH_EXPIRED",
"WAIT_FOR_EXPIRY_SWEEP",
"EXPIRED",
"FAILED_STATE"
],
"default": "UNKNOWN_STATE"
},
"looprpcDisqualified": {
"type": "object",
"properties": {
@ -1013,7 +1047,7 @@
}
}
},
"looprpcListUnspentResponse": {
"looprpcListUnspentDepositsResponse": {
"type": "object",
"properties": {
"utxos": {
@ -1250,7 +1284,7 @@
}
}
},
"looprpcNewAddressResponse": {
"looprpcNewStaticAddressResponse": {
"type": "object",
"properties": {
"address": {
@ -1264,6 +1298,25 @@
}
}
},
"looprpcOutPoint": {
"type": "object",
"properties": {
"txid_bytes": {
"type": "string",
"format": "byte",
"description": "Raw bytes representing the transaction id."
},
"txid_str": {
"type": "string",
"description": "Reversed, hex-encoded string representing the transaction id."
},
"output_index": {
"type": "integer",
"format": "int64",
"description": "The index of the output on the transaction."
}
}
},
"looprpcOutQuoteResponse": {
"type": "object",
"properties": {
@ -1351,6 +1404,40 @@
"looprpcSetLiquidityParamsResponse": {
"type": "object"
},
"looprpcStaticAddressSummaryResponse": {
"type": "object",
"properties": {
"static_address": {
"type": "string"
},
"total_num_deposits": {
"type": "integer",
"format": "int64"
},
"value_unconfirmed": {
"type": "string",
"format": "int64"
},
"value_deposited": {
"type": "string",
"format": "int64"
},
"value_expired": {
"type": "string",
"format": "int64"
},
"value_withdrawn": {
"type": "string",
"format": "int64"
},
"filtered_deposits": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcDeposit"
}
}
}
},
"looprpcSuggestSwapsResponse": {
"type": "object",
"properties": {
@ -1549,6 +1636,9 @@
}
}
},
"looprpcWithdrawDepositsResponse": {
"type": "object"
},
"protobufAny": {
"type": "object",
"properties": {

@ -93,6 +93,16 @@ type SwapClientClient interface {
//InstantOutQuote returns a quote for an instant out swap with the provided
//parameters.
InstantOutQuote(ctx context.Context, in *InstantOutQuoteRequest, opts ...grpc.CallOption) (*InstantOutQuoteResponse, error)
// loop: `static newstaticaddress`
//NewStaticAddress requests a new static address for loop-ins from the server.
NewStaticAddress(ctx context.Context, in *NewStaticAddressRequest, opts ...grpc.CallOption) (*NewStaticAddressResponse, error)
// loop: `static listunspentdeposits`
//ListUnspentDeposits returns a list of utxos deposited at a static address.
ListUnspentDeposits(ctx context.Context, in *ListUnspentDepositsRequest, opts ...grpc.CallOption) (*ListUnspentDepositsResponse, error)
// loop:`static withdraw`
//WithdrawDeposits withdraws a selection or all deposits of a static address.
WithdrawDeposits(ctx context.Context, in *WithdrawDepositsRequest, opts ...grpc.CallOption) (*WithdrawDepositsResponse, error)
GetStaticAddressSummary(ctx context.Context, in *StaticAddressSummaryRequest, opts ...grpc.CallOption) (*StaticAddressSummaryResponse, error)
}
type swapClientClient struct {
@ -297,6 +307,42 @@ func (c *swapClientClient) InstantOutQuote(ctx context.Context, in *InstantOutQu
return out, nil
}
func (c *swapClientClient) NewStaticAddress(ctx context.Context, in *NewStaticAddressRequest, opts ...grpc.CallOption) (*NewStaticAddressResponse, error) {
out := new(NewStaticAddressResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/NewStaticAddress", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) ListUnspentDeposits(ctx context.Context, in *ListUnspentDepositsRequest, opts ...grpc.CallOption) (*ListUnspentDepositsResponse, error) {
out := new(ListUnspentDepositsResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/ListUnspentDeposits", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) WithdrawDeposits(ctx context.Context, in *WithdrawDepositsRequest, opts ...grpc.CallOption) (*WithdrawDepositsResponse, error) {
out := new(WithdrawDepositsResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/WithdrawDeposits", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) GetStaticAddressSummary(ctx context.Context, in *StaticAddressSummaryRequest, opts ...grpc.CallOption) (*StaticAddressSummaryResponse, error) {
out := new(StaticAddressSummaryResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetStaticAddressSummary", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapClientServer is the server API for SwapClient service.
// All implementations must embed UnimplementedSwapClientServer
// for forward compatibility
@ -376,6 +422,16 @@ type SwapClientServer interface {
//InstantOutQuote returns a quote for an instant out swap with the provided
//parameters.
InstantOutQuote(context.Context, *InstantOutQuoteRequest) (*InstantOutQuoteResponse, error)
// loop: `static newstaticaddress`
//NewStaticAddress requests a new static address for loop-ins from the server.
NewStaticAddress(context.Context, *NewStaticAddressRequest) (*NewStaticAddressResponse, error)
// loop: `static listunspentdeposits`
//ListUnspentDeposits returns a list of utxos deposited at a static address.
ListUnspentDeposits(context.Context, *ListUnspentDepositsRequest) (*ListUnspentDepositsResponse, error)
// loop:`static withdraw`
//WithdrawDeposits withdraws a selection or all deposits of a static address.
WithdrawDeposits(context.Context, *WithdrawDepositsRequest) (*WithdrawDepositsResponse, error)
GetStaticAddressSummary(context.Context, *StaticAddressSummaryRequest) (*StaticAddressSummaryResponse, error)
mustEmbedUnimplementedSwapClientServer()
}
@ -440,6 +496,18 @@ func (UnimplementedSwapClientServer) InstantOut(context.Context, *InstantOutRequ
func (UnimplementedSwapClientServer) InstantOutQuote(context.Context, *InstantOutQuoteRequest) (*InstantOutQuoteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InstantOutQuote not implemented")
}
func (UnimplementedSwapClientServer) NewStaticAddress(context.Context, *NewStaticAddressRequest) (*NewStaticAddressResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NewStaticAddress not implemented")
}
func (UnimplementedSwapClientServer) ListUnspentDeposits(context.Context, *ListUnspentDepositsRequest) (*ListUnspentDepositsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUnspentDeposits not implemented")
}
func (UnimplementedSwapClientServer) WithdrawDeposits(context.Context, *WithdrawDepositsRequest) (*WithdrawDepositsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method WithdrawDeposits not implemented")
}
func (UnimplementedSwapClientServer) GetStaticAddressSummary(context.Context, *StaticAddressSummaryRequest) (*StaticAddressSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStaticAddressSummary not implemented")
}
func (UnimplementedSwapClientServer) mustEmbedUnimplementedSwapClientServer() {}
// UnsafeSwapClientServer may be embedded to opt out of forward compatibility for this service.
@ -798,6 +866,78 @@ func _SwapClient_InstantOutQuote_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _SwapClient_NewStaticAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NewStaticAddressRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).NewStaticAddress(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/NewStaticAddress",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).NewStaticAddress(ctx, req.(*NewStaticAddressRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_ListUnspentDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUnspentDepositsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).ListUnspentDeposits(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/ListUnspentDeposits",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).ListUnspentDeposits(ctx, req.(*ListUnspentDepositsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_WithdrawDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(WithdrawDepositsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).WithdrawDeposits(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/WithdrawDeposits",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).WithdrawDeposits(ctx, req.(*WithdrawDepositsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_GetStaticAddressSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StaticAddressSummaryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).GetStaticAddressSummary(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/GetStaticAddressSummary",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetStaticAddressSummary(ctx, req.(*StaticAddressSummaryRequest))
}
return interceptor(ctx, in, info, handler)
}
// SwapClient_ServiceDesc is the grpc.ServiceDesc for SwapClient service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -877,6 +1017,22 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
MethodName: "InstantOutQuote",
Handler: _SwapClient_InstantOutQuote_Handler,
},
{
MethodName: "NewStaticAddress",
Handler: _SwapClient_NewStaticAddress_Handler,
},
{
MethodName: "ListUnspentDeposits",
Handler: _SwapClient_ListUnspentDeposits_Handler,
},
{
MethodName: "WithdrawDeposits",
Handler: _SwapClient_WithdrawDeposits_Handler,
},
{
MethodName: "GetStaticAddressSummary",
Handler: _SwapClient_GetStaticAddressSummary_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@ -887,133 +1043,3 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
},
Metadata: "client.proto",
}
// StaticAddressClientClient is the client API for StaticAddressClient service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type StaticAddressClientClient interface {
//
//NewAddress requests a new static address for loop-ins from the server.
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
//
//ListUnspent returns a list of utxos behind a static address.
ListUnspent(ctx context.Context, in *ListUnspentRequest, opts ...grpc.CallOption) (*ListUnspentResponse, error)
}
type staticAddressClientClient struct {
cc grpc.ClientConnInterface
}
func NewStaticAddressClientClient(cc grpc.ClientConnInterface) StaticAddressClientClient {
return &staticAddressClientClient{cc}
}
func (c *staticAddressClientClient) NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error) {
out := new(NewAddressResponse)
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressClient/NewAddress", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *staticAddressClientClient) ListUnspent(ctx context.Context, in *ListUnspentRequest, opts ...grpc.CallOption) (*ListUnspentResponse, error) {
out := new(ListUnspentResponse)
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressClient/ListUnspent", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// StaticAddressClientServer is the server API for StaticAddressClient service.
// All implementations must embed UnimplementedStaticAddressClientServer
// for forward compatibility
type StaticAddressClientServer interface {
//
//NewAddress requests a new static address for loop-ins from the server.
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
//
//ListUnspent returns a list of utxos behind a static address.
ListUnspent(context.Context, *ListUnspentRequest) (*ListUnspentResponse, error)
mustEmbedUnimplementedStaticAddressClientServer()
}
// UnimplementedStaticAddressClientServer must be embedded to have forward compatible implementations.
type UnimplementedStaticAddressClientServer struct {
}
func (UnimplementedStaticAddressClientServer) NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NewAddress not implemented")
}
func (UnimplementedStaticAddressClientServer) ListUnspent(context.Context, *ListUnspentRequest) (*ListUnspentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUnspent not implemented")
}
func (UnimplementedStaticAddressClientServer) mustEmbedUnimplementedStaticAddressClientServer() {}
// UnsafeStaticAddressClientServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StaticAddressClientServer will
// result in compilation errors.
type UnsafeStaticAddressClientServer interface {
mustEmbedUnimplementedStaticAddressClientServer()
}
func RegisterStaticAddressClientServer(s grpc.ServiceRegistrar, srv StaticAddressClientServer) {
s.RegisterService(&StaticAddressClient_ServiceDesc, srv)
}
func _StaticAddressClient_NewAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NewAddressRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StaticAddressClientServer).NewAddress(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.StaticAddressClient/NewAddress",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StaticAddressClientServer).NewAddress(ctx, req.(*NewAddressRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StaticAddressClient_ListUnspent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUnspentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StaticAddressClientServer).ListUnspent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.StaticAddressClient/ListUnspent",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StaticAddressClientServer).ListUnspent(ctx, req.(*ListUnspentRequest))
}
return interceptor(ctx, in, info, handler)
}
// StaticAddressClient_ServiceDesc is the grpc.ServiceDesc for StaticAddressClient service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var StaticAddressClient_ServiceDesc = grpc.ServiceDesc{
ServiceName: "looprpc.StaticAddressClient",
HandlerType: (*StaticAddressClientServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "NewAddress",
Handler: _StaticAddressClient_NewAddress_Handler,
},
{
MethodName: "ListUnspent",
Handler: _StaticAddressClient_ListUnspent_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "client.proto",
}

@ -1,73 +0,0 @@
// Code generated by falafel 0.9.1. DO NOT EDIT.
// source: client.proto
package looprpc
import (
"context"
gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
)
func RegisterStaticAddressClientJSONCallbacks(registry map[string]func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error))) {
marshaler := &gateway.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
},
}
registry["looprpc.StaticAddressClient.NewAddress"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &NewAddressRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewStaticAddressClientClient(conn)
resp, err := client.NewAddress(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.StaticAddressClient.ListUnspent"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &ListUnspentRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewStaticAddressClientClient(conn)
resp, err := client.ListUnspent(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
}

@ -512,4 +512,104 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.NewStaticAddress"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &NewStaticAddressRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.NewStaticAddress(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.ListUnspentDeposits"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &ListUnspentDepositsRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.ListUnspentDeposits(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.WithdrawDeposits"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &WithdrawDepositsRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.WithdrawDeposits(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.GetStaticAddressSummary"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &StaticAddressSummaryRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.GetStaticAddressSummary(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
}

@ -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
}

@ -0,0 +1,29 @@
package address
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the sub system name of this package.
const Subsystem = "SADDR"
// log is a logger that is initialized with no output filters. This means the
// package will not perform any logging by default until the caller requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// UseLogger uses a specified Logger to output package logging info. This should
// be used in preference to SetLogWriter if the caller is also using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}
// GetLogger returns the logger for this package.
func GetLogger() btclog.Logger {
return log
}

@ -1,4 +1,4 @@
package staticaddr
package address
import (
"bytes"
@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/staticaddr"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
@ -31,7 +32,7 @@ type ManagerConfig struct {
// Store is the database store that is used to store static address
// related records.
Store AddressStore
Store Store
// WalletKit is the wallet client that is used to derive new keys from
// lnd's wallet.
@ -46,31 +47,20 @@ type ManagerConfig struct {
type Manager struct {
cfg *ManagerConfig
initChan chan struct{}
sync.Mutex
}
// NewAddressManager creates a new address manager.
func NewAddressManager(cfg *ManagerConfig) *Manager {
// NewManager creates a new address manager.
func NewManager(cfg *ManagerConfig) *Manager {
return &Manager{
cfg: cfg,
initChan: make(chan struct{}),
cfg: cfg,
}
}
// Run runs the address manager.
func (m *Manager) Run(ctx context.Context) error {
log.Debugf("Starting address manager.")
defer log.Debugf("Address manager stopped.")
// Communicate to the caller that the address manager has completed its
// initialization.
close(m.initChan)
<-ctx.Done()
return nil
return ctx.Err()
}
// NewAddress starts a new address creation flow.
@ -93,7 +83,7 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
m.Unlock()
return m.getTaprootAddress(clientPubKey, serverPubKey, expiry)
return m.GetTaprootAddress(clientPubKey, serverPubKey, expiry)
}
m.Unlock()
@ -113,7 +103,7 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
// Send our clientPubKey to the server and wait for the server to
// respond with he serverPubKey and the static address CSV expiry.
protocolVersion := CurrentRPCProtocolVersion()
protocolVersion := staticaddr.CurrentRPCProtocolVersion()
resp, err := m.cfg.AddressClient.ServerNewAddress(
ctx, &staticaddressrpc.ServerNewAddressRequest{
ProtocolVersion: protocolVersion,
@ -146,7 +136,7 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
// Create the static address from the parameters the server provided and
// store all parameters in the database.
addrParams := &AddressParameters{
addrParams := &Parameters{
ClientPubkey: clientPubKey.PubKey,
ServerPubkey: serverPubKey,
PkScript: pkScript,
@ -155,7 +145,9 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
Family: clientPubKey.Family,
Index: clientPubKey.Index,
},
ProtocolVersion: AddressProtocolVersion(protocolVersion),
ProtocolVersion: staticaddr.AddressProtocolVersion(
protocolVersion,
),
}
err = m.cfg.Store.CreateStaticAddress(ctx, addrParams)
if err != nil {
@ -175,12 +167,12 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
log.Infof("imported static address taproot script to lnd wallet: %v",
addr)
return m.getTaprootAddress(
return m.GetTaprootAddress(
clientPubKey.PubKey, serverPubKey, int64(serverParams.Expiry),
)
}
func (m *Manager) getTaprootAddress(clientPubkey,
func (m *Manager) GetTaprootAddress(clientPubkey,
serverPubkey *btcec.PublicKey, expiry int64) (*btcutil.AddressTaproot,
error) {
@ -197,12 +189,6 @@ func (m *Manager) getTaprootAddress(clientPubkey,
)
}
// WaitInitComplete waits until the address manager has completed its setup.
func (m *Manager) WaitInitComplete() {
defer log.Debugf("Address manager initiation complete.")
<-m.initChan
}
// ListUnspentRaw returns a list of utxos at the static address.
func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
maxConfs int32) (*btcutil.AddressTaproot, []*lnwallet.Utxo, error) {
@ -213,7 +199,7 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
return nil, nil, err
case len(addresses) == 0:
return nil, nil, fmt.Errorf("no address found")
return nil, nil, nil
case len(addresses) > 1:
return nil, nil, fmt.Errorf("more than one address found")
@ -239,7 +225,7 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
}
}
taprootAddress, err := m.getTaprootAddress(
taprootAddress, err := m.GetTaprootAddress(
staticAddress.ClientPubkey, staticAddress.ServerPubkey,
int64(staticAddress.Expiry),
)
@ -249,3 +235,52 @@ func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
return taprootAddress, filteredUtxos, nil
}
// GetStaticAddressParameters returns the parameters of the static address.
func (m *Manager) GetStaticAddressParameters(ctx context.Context) (*Parameters,
error) {
params, err := m.cfg.Store.GetAllStaticAddresses(ctx)
if err != nil {
return nil, err
}
if len(params) == 0 {
return nil, fmt.Errorf("no static address parameters found")
}
return params[0], nil
}
// GetStaticAddress returns a taproot address for the given client and server
// public keys and expiry.
func (m *Manager) GetStaticAddress(ctx context.Context) (*script.StaticAddress,
error) {
params, err := m.GetStaticAddressParameters(ctx)
if err != nil {
return nil, err
}
address, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry),
params.ClientPubkey, params.ServerPubkey,
)
if err != nil {
return nil, err
}
return address, nil
}
// ListUnspent returns a list of utxos at the static address.
func (m *Manager) ListUnspent(ctx context.Context, minConfs,
maxConfs int32) ([]*lnwallet.Utxo, error) {
_, utxos, err := m.ListUnspentRaw(ctx, minConfs, maxConfs)
if err != nil {
return nil, err
}
return utxos, nil
}

@ -1,4 +1,4 @@
package staticaddr
package address
import (
"context"
@ -8,6 +8,7 @@ import (
"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"
)
@ -63,7 +64,7 @@ func (s *SqlStore) ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
// 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 +81,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 +92,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 +121,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 +133,7 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) (
return nil, err
}
return &AddressParameters{
return &Parameters{
ClientPubkey: clientPubkey,
ServerPubkey: serverPubkey,
PkScript: row.Pkscript,
@ -141,6 +142,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
}

@ -0,0 +1,164 @@
package deposit
import (
"errors"
"fmt"
"strings"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/script"
)
const (
defaultConfTarget = 3
)
// PublishDepositExpirySweepAction creates and publishes the timeout transaction
// that spends the deposit from the static address timeout leaf to the
// predefined timeout sweep pkscript.
func (f *FSM) PublishDepositExpirySweepAction(_ fsm.EventContext) fsm.EventType {
msgTx := wire.NewMsgTx(2)
params, err := f.cfg.AddressManager.GetStaticAddressParameters(f.ctx)
if err != nil {
return fsm.OnError
}
// Add the deposit outpoint as input to the transaction.
msgTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: f.deposit.OutPoint,
Sequence: params.Expiry,
SignatureScript: nil,
})
// Estimate the fee rate of an expiry spend transaction.
feeRateEstimator, err := f.cfg.WalletKit.EstimateFeeRate(
f.ctx, defaultConfTarget,
)
if err != nil {
return f.HandleError(fmt.Errorf("timeout sweep fee "+
"estimation failed: %v", err))
}
weight := script.ExpirySpendWeight()
fee := feeRateEstimator.FeeForWeight(weight)
// We cap the fee at 20% of the deposit value.
if fee > f.deposit.Value/5 {
return f.HandleError(errors.New("fee is greater than 20% of " +
"the deposit value"))
}
output := &wire.TxOut{
Value: int64(f.deposit.Value - fee),
PkScript: f.deposit.TimeOutSweepPkScript,
}
msgTx.AddTxOut(output)
txOut := &wire.TxOut{
Value: int64(f.deposit.Value),
PkScript: params.PkScript,
}
prevOut := []*wire.TxOut{txOut}
signDesc, err := f.SignDescriptor()
if err != nil {
return f.HandleError(err)
}
rawSigs, err := f.cfg.Signer.SignOutputRaw(
f.ctx, msgTx, []*lndclient.SignDescriptor{signDesc}, prevOut,
)
if err != nil {
return f.HandleError(err)
}
address, err := f.cfg.AddressManager.GetStaticAddress(f.ctx)
if err != nil {
return f.HandleError(err)
}
sig := rawSigs[0]
msgTx.TxIn[0].Witness, err = address.GenTimeoutWitness(sig)
if err != nil {
return f.HandleError(err)
}
txLabel := fmt.Sprintf("timeout sweep for deposit %v",
f.deposit.OutPoint)
err = f.cfg.WalletKit.PublishTransaction(f.ctx, msgTx, txLabel)
if err != nil {
if !strings.Contains(err.Error(), "output already spent") {
log.Errorf("%v: %v", txLabel, err)
f.LastActionError = err
return fsm.OnError
}
} else {
f.Debugf("published timeout sweep with txid: %v",
msgTx.TxHash())
}
return OnExpiryPublished
}
// WaitForExpirySweepAction waits for a sufficient number of confirmations
// before a timeout sweep is considered successful.
func (f *FSM) WaitForExpirySweepAction(_ fsm.EventContext) fsm.EventType {
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
f.ctx, nil, f.deposit.TimeOutSweepPkScript, defaultConfTarget,
int32(f.deposit.ConfirmationHeight),
)
if err != nil {
return f.HandleError(err)
}
select {
case err := <-errSpendChan:
log.Debugf("error while sweeping expired deposit: %v", err)
return fsm.OnError
case confirmedTx := <-spendChan:
f.deposit.ExpirySweepTxid = confirmedTx.Tx.TxHash()
return OnExpirySwept
case <-f.ctx.Done():
return fsm.OnError
}
}
// SweptExpiredDepositAction is the final action of the FSM. It signals to the
// manager that the deposit has been swept and the FSM can be removed. It also
// ends the state machine main loop by cancelling its context.
func (f *FSM) SweptExpiredDepositAction(_ fsm.EventContext) fsm.EventType {
select {
case <-f.ctx.Done():
return fsm.OnError
default:
f.finalizedDepositChan <- f.deposit.OutPoint
f.ctx.Done()
}
return fsm.NoOp
}
// WithdrawnDepositAction is the final action after a withdrawal. It signals to
// the manager that the deposit has been swept and the FSM can be removed. It
// also ends the state machine main loop by cancelling its context.
func (f *FSM) WithdrawnDepositAction(_ fsm.EventContext) fsm.EventType {
select {
case <-f.ctx.Done():
return fsm.OnError
default:
f.finalizedDepositChan <- f.deposit.OutPoint
f.ctx.Done()
}
return fsm.NoOp
}

@ -0,0 +1,78 @@
package deposit
import (
"crypto/rand"
"fmt"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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
// 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
// ExpirySweepTxid is the transaction id of the expiry sweep.
ExpirySweepTxid chainhash.Hash
// WithdrawalSweepAddress is the address that is used to
// cooperatively sweep the deposit to before it is expired.
WithdrawalSweepAddress string
}
// IsInPendingState returns true if the deposit is pending.
func (d *Deposit) IsInPendingState() bool {
return !d.IsInFinalState()
}
// IsInFinalState returns true if the deposit is final.
func (d *Deposit) IsInFinalState() bool {
return d.State == Expired || d.State == Failed
}
func (d *Deposit) isExpired(currentHeight, expiry uint32) bool {
return currentHeight >= uint32(d.ConfirmationHeight)+expiry
}
// GetRandomDepositID generates a random deposit ID.
func GetRandomDepositID() (ID, error) {
var id ID
_, err := rand.Read(id[:])
return id, err
}

@ -0,0 +1,349 @@
package deposit
import (
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
const (
DefaultObserverSize = 20
)
var (
ErrProtocolVersionNotSupported = errors.New("protocol version not " +
"supported")
)
// States.
var (
Deposited = fsm.StateType("Deposited")
Withdrawing = fsm.StateType("Withdrawing")
Withdrawn = fsm.StateType("Withdrawn")
PublishExpiredDeposit = fsm.StateType("PublishExpiredDeposit")
WaitForExpirySweep = fsm.StateType("WaitForExpirySweep")
Expired = fsm.StateType("Expired")
Failed = fsm.StateType("Failed")
)
// Events.
var (
OnStart = fsm.EventType("OnStart")
OnWithdraw = fsm.EventType("OnWithdraw")
OnWithdrawn = fsm.EventType("OnWithdrawn")
OnExpiry = fsm.EventType("OnExpiry")
OnExpiryPublished = fsm.EventType("OnExpiryPublished")
OnExpirySwept = fsm.EventType("OnExpirySwept")
OnRecover = fsm.EventType("OnRecover")
)
// FSM is the state machine that handles the instant out.
type FSM struct {
*fsm.StateMachine
cfg *ManagerConfig
deposit *Deposit
params *address.Parameters
address *script.StaticAddress
ctx context.Context
blockNtfnChan chan uint32
finalizedDepositChan chan wire.OutPoint
}
// NewFSM creates a new state machine that can action on all static address
// feature requests.
func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
finalizedDepositChan chan wire.OutPoint,
recoverStateMachine bool) (*FSM, error) {
params, err := cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address "+
"parameters: %v", err)
}
address, err := cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get static address: %v", err)
}
depoFsm := &FSM{
cfg: cfg,
deposit: deposit,
params: params,
address: address,
ctx: ctx,
blockNtfnChan: make(chan uint32),
finalizedDepositChan: finalizedDepositChan,
}
depositStates := depoFsm.DepositStatesV0()
switch params.ProtocolVersion {
case staticaddr.ProtocolVersion_V0:
default:
return nil, ErrProtocolVersionNotSupported
}
if recoverStateMachine {
depoFsm.StateMachine = fsm.NewStateMachineWithState(
depositStates, deposit.State,
DefaultObserverSize,
)
} else {
depoFsm.StateMachine = fsm.NewStateMachine(
depositStates, DefaultObserverSize,
)
}
depoFsm.ActionEntryFunc = depoFsm.updateDeposit
go func() {
for {
select {
case currentHeight := <-depoFsm.blockNtfnChan:
err := depoFsm.handleBlockNotification(
currentHeight,
)
if err != nil {
log.Errorf("error handling block "+
"notification: %v", err)
}
case <-ctx.Done():
return
}
}
}()
return depoFsm, nil
}
// handleBlockNotification inspects the current block height and sends the
// OnExpiry event to publish the expiry sweep transaction if the deposit timed
// out, or it republishes the expiry sweep transaction if it was not yet swept.
func (f *FSM) handleBlockNotification(currentHeight uint32) error {
params, err := f.cfg.AddressManager.GetStaticAddressParameters(f.ctx)
if err != nil {
return err
}
// If the deposit is expired but not yet sufficiently confirmed, we
// republish the expiry sweep transaction.
if f.deposit.isExpired(currentHeight, params.Expiry) {
if f.deposit.State == WaitForExpirySweep {
f.PublishDepositExpirySweepAction(nil)
} else {
go func() {
err := f.SendEvent(OnExpiry, nil)
if err != nil {
log.Debugf("error sending OnExpiry "+
"event: %v", err)
}
}()
}
}
return nil
}
// DepositStatesV0 returns the states a deposit can be in.
func (f *FSM) DepositStatesV0() fsm.States {
return fsm.States{
fsm.EmptyState: fsm.State{
Transitions: fsm.Transitions{
OnStart: Deposited,
},
Action: fsm.NoOpAction,
},
Deposited: fsm.State{
Transitions: fsm.Transitions{
OnWithdraw: Withdrawing,
OnExpiry: PublishExpiredDeposit,
OnRecover: Deposited,
},
Action: fsm.NoOpAction,
},
Withdrawing: fsm.State{
Transitions: fsm.Transitions{
OnWithdrawn: Withdrawn,
// Upon recovery, we go back to the Deposited
// state. The deposit by then has a withdrawal
// address stamped to it which will cause it to
// transition into the Withdrawing state again.
OnRecover: Deposited,
// A precondition for the Withdrawing state is
// that the withdrawal transaction has been
// broadcast. If the deposit expires while the
// withdrawal isn't confirmed, we can ignore the
// expiry.
OnExpiry: Withdrawing,
// If the withdrawal failed we go back to
// Deposited, hoping that another withdrawal
// attempt will be successful. Alternatively,
// the client can wait for the timeout sweep.
fsm.OnError: Deposited,
},
Action: fsm.NoOpAction,
},
PublishExpiredDeposit: fsm.State{
Transitions: fsm.Transitions{
OnRecover: PublishExpiredDeposit,
OnExpiryPublished: WaitForExpirySweep,
// If the timeout sweep failed we go back to
// Deposited, hoping that another timeout sweep
// attempt will be successful. Alternatively,
// the client can try to coop-spend the deposit.
fsm.OnError: Deposited,
},
Action: f.PublishDepositExpirySweepAction,
},
WaitForExpirySweep: fsm.State{
Transitions: fsm.Transitions{
OnExpirySwept: Expired,
// Upon recovery, we republish the sweep tx.
OnRecover: PublishExpiredDeposit,
// If the timeout sweep failed we go back to
// Deposited, hoping that another timeout sweep
// attempt will be successful. Alternatively,
// the client can try to coop-spend the deposit.
fsm.OnError: Deposited,
},
Action: f.WaitForExpirySweepAction,
},
Expired: fsm.State{
Transitions: fsm.Transitions{
OnExpiry: Expired,
},
Action: f.SweptExpiredDepositAction,
},
Withdrawn: fsm.State{
Action: f.WithdrawnDepositAction,
},
Failed: fsm.State{
Transitions: fsm.Transitions{
OnExpiry: Failed,
},
Action: fsm.NoOpAction,
},
}
}
// DepositEntryFunction is called after every action and updates the deposit in
// the db.
func (f *FSM) updateDeposit(notification fsm.Notification) {
if f.deposit == nil {
return
}
f.Debugf("NextState: %v, PreviousState: %v, Event: %v",
notification.NextState, notification.PreviousState,
notification.Event,
)
f.deposit.State = notification.NextState
// Don't update the deposit if we are in an initial state or if we
// are transitioning from an initial state to a failed state.
state := f.deposit.State
prevState := notification.PreviousState
if state == fsm.EmptyState ||
state == Deposited && !isRecoverable(prevState) ||
(prevState == Deposited && state == Failed) {
return
}
err := f.cfg.Store.UpdateDeposit(f.ctx, f.deposit)
if err != nil {
f.Errorf("unable to update deposit: %v", err)
}
}
func isRecoverable(state fsm.StateType) bool {
if state == Withdrawing || state == PublishExpiredDeposit ||
state == WaitForExpirySweep {
return true
}
return false
}
// Infof logs an info message with the deposit outpoint.
func (f *FSM) Infof(format string, args ...interface{}) {
log.Infof(
"Deposit %v: "+format,
append(
[]interface{}{f.deposit.OutPoint},
args...,
)...,
)
}
// Debugf logs a debug message with the deposit outpoint.
func (f *FSM) Debugf(format string, args ...interface{}) {
log.Debugf(
"Deposit %v: "+format,
append(
[]interface{}{f.deposit.OutPoint},
args...,
)...,
)
}
// Errorf logs an error message with the deposit outpoint.
func (f *FSM) Errorf(format string, args ...interface{}) {
log.Errorf(
"Deposit %v: "+format,
append(
[]interface{}{f.deposit.OutPoint},
args...,
)...,
)
}
// SignDescriptor returns the sign descriptor for the static address output.
func (f *FSM) SignDescriptor() (*lndclient.SignDescriptor, error) {
address, err := f.cfg.AddressManager.GetStaticAddress(f.ctx)
if err != nil {
return nil, err
}
return &lndclient.SignDescriptor{
WitnessScript: address.TimeoutLeaf.Script,
KeyDesc: keychain.KeyDescriptor{
PubKey: f.params.ClientPubkey,
},
Output: wire.NewTxOut(
int64(f.deposit.Value), f.params.PkScript,
),
HashType: txscript.SigHashDefault,
InputIndex: 0,
SignMethod: input.TaprootScriptSpendSignMethod,
}, nil
}

@ -0,0 +1,51 @@
package deposit
import (
"context"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"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)
// GetTaprootAddress returns a taproot address.
GetTaprootAddress(clientPubkey,
serverPubkey *btcec.PublicKey,
expiry int64) (*btcutil.AddressTaproot, error)
}

@ -1,4 +1,4 @@
package staticaddr
package deposit
import (
"github.com/btcsuite/btclog"
@ -6,7 +6,7 @@ import (
)
// Subsystem defines the sub system name of this package.
const Subsystem = "SADDR"
const Subsystem = "DEPO"
// log is a logger that is initialized with no output filters. This means the
// package will not perform any logging by default until the caller requests it.

@ -0,0 +1,503 @@
package deposit
import (
"context"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
)
const (
// PollInterval is the interval in which we poll for new deposits to our
// static address.
PollInterval = 10 * time.Second
// MinConfs is the minimum number of confirmations we require for a
// deposit to be considered available for loop-ins, coop-spends and
// timeouts.
MinConfs = 3
// MaxConfs is unset since we don't require a max number of
// confirmations for deposits.
MaxConfs = 0
)
// ManagerConfig holds the configuration for the address manager.
type ManagerConfig struct {
// AddressClient is the client that communicates with the loop server
// to manage static addresses.
AddressClient staticaddressrpc.StaticAddressServerClient
AddressManager AddressManager
// SwapClient provides loop rpc functionality.
SwapClient *loop.Client
// Store is the database store that is used to store static address
// related records.
Store Store
// WalletKit is the wallet client that is used to derive new keys from
// lnd's wallet.
WalletKit lndclient.WalletKitClient
// ChainParams is the chain configuration(mainnet, testnet...) this
// manager uses.
ChainParams *chaincfg.Params
// ChainNotifier is the chain notifier that is used to listen for new
// blocks.
ChainNotifier lndclient.ChainNotifierClient
// Signer is the signer client that is used to sign transactions.
Signer lndclient.SignerClient
}
// Manager manages the address state machines.
type Manager struct {
cfg *ManagerConfig
runCtx context.Context
sync.Mutex
// initChan signals the daemon that the address manager has completed
// its initialization.
initChan chan struct{}
// activeDeposits contains all the active static address outputs.
activeDeposits map[wire.OutPoint]*FSM
// initiationHeight stores the currently best known block height.
initiationHeight uint32
// currentHeight stores the currently best known block height.
currentHeight uint32
// deposits contains all the deposits that have ever been made to the
// static address. This field is used to store and recover deposits. It
// also serves as basis for reconciliation of newly detected deposits by
// matching them against deposits in this map that were already seen.
deposits map[wire.OutPoint]*Deposit
// finalizedDepositChan is a channel that receives deposits that have
// been finalized. The manager will adjust its internal state and flush
// finalized deposits from its memory.
finalizedDepositChan chan wire.OutPoint
}
// NewManager creates a new deposit manager.
func NewManager(cfg *ManagerConfig) *Manager {
return &Manager{
cfg: cfg,
initChan: make(chan struct{}),
activeDeposits: make(map[wire.OutPoint]*FSM),
deposits: make(map[wire.OutPoint]*Deposit),
finalizedDepositChan: make(chan wire.OutPoint),
}
}
// Run runs the address manager.
func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
m.runCtx = ctx
m.Lock()
m.currentHeight, m.initiationHeight = currentHeight, currentHeight
m.Unlock()
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.RegisterBlockEpochNtfn(m.runCtx) //nolint:lll
if err != nil {
return err
}
// Recover previous deposits and static address parameters from the DB.
err = m.recover(m.runCtx)
if err != nil {
return err
}
// Start the deposit notifier.
m.pollDeposits(ctx)
// Communicate to the caller that the address manager has completed its
// initialization.
close(m.initChan)
for {
select {
case height := <-newBlockChan:
m.Lock()
m.currentHeight = uint32(height)
m.Unlock()
// Inform all active deposits about a new block arrival.
for _, fsm := range m.activeDeposits {
select {
case fsm.blockNtfnChan <- uint32(height):
case <-m.runCtx.Done():
return m.runCtx.Err()
}
}
case outpoint := <-m.finalizedDepositChan:
// If deposits notify us about their finalization, we
// update the manager's internal state and flush the
// finalized deposit from memory.
m.finalizeDeposit(outpoint)
case err := <-newBlockErrChan:
return err
case <-m.runCtx.Done():
return m.runCtx.Err()
}
}
}
// recover recovers static address parameters, previous deposits and state
// machines from the database and starts the deposit notifier.
func (m *Manager) recover(ctx context.Context) error {
log.Infof("Recovering static address parameters and deposits...")
// Recover deposits.
deposits, err := m.cfg.Store.AllDeposits(ctx)
if err != nil {
return err
}
for i, d := range deposits {
m.deposits[d.OutPoint] = deposits[i]
// If the current deposit is final it wasn't active when we
// shut down the client last. So we don't need to start a fsm
// for it.
if d.IsInFinalState() {
continue
}
log.Debugf("Recovering deposit %x", d.ID)
// Create a state machine for a given deposit.
fsm, err := NewFSM(
m.runCtx, d, m.cfg,
m.finalizedDepositChan, true,
)
if err != nil {
return err
}
// Send the OnRecover event to the state machine.
go func() {
err = fsm.SendEvent(OnRecover, nil)
if err != nil {
log.Errorf("Error sending OnStart event: %v",
err)
}
}()
m.activeDeposits[d.OutPoint] = fsm
}
return nil
}
// WaitInitComplete waits until the address manager has completed its setup.
func (m *Manager) WaitInitComplete() {
defer log.Debugf("Static address deposit manager initiation complete.")
<-m.initChan
}
// pollDeposits polls new deposits to our static address and notifies the
// manager's event loop about them.
func (m *Manager) pollDeposits(ctx context.Context) {
log.Debugf("waiting for new static address deposits...")
go func() {
ticker := time.NewTicker(PollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
err := m.reconcileDeposits(ctx)
if err != nil {
log.Errorf("unable to reconcile "+
"deposits: %v", err)
}
case <-ctx.Done():
return
}
}
}()
}
// reconcileDeposits fetches all spends to our static address from our lnd
// wallet and matches it against the deposits in our memory that we've seen so
// far. It picks the newly identified deposits and starts a state machine per
// deposit to track its progress.
func (m *Manager) reconcileDeposits(ctx context.Context) error {
log.Tracef("Reconciling new deposits...")
utxos, err := m.cfg.AddressManager.ListUnspent(
ctx, MinConfs, MaxConfs,
)
if err != nil {
return fmt.Errorf("unable to list new deposits: %v", err)
}
newDeposits := m.filterNewDeposits(utxos)
if err != nil {
return fmt.Errorf("unable to filter new deposits: %v", err)
}
if len(newDeposits) == 0 {
log.Tracef("No new deposits...")
return nil
}
for _, utxo := range newDeposits {
deposit, err := m.createNewDeposit(ctx, utxo)
if err != nil {
return fmt.Errorf("unable to retain new deposit: %v",
err)
}
log.Debugf("Received deposit: %v", deposit)
err = m.startDepositFsm(deposit)
if err != nil {
return fmt.Errorf("unable to start new deposit FSM: %v",
err)
}
}
return nil
}
// createNewDeposit transforms the wallet utxo into a deposit struct and stores
// it in our database and manager memory.
func (m *Manager) createNewDeposit(ctx context.Context,
utxo *lnwallet.Utxo) (*Deposit, error) {
blockHeight, err := m.getBlockHeight(ctx, utxo)
if err != nil {
return nil, err
}
// Get the sweep pk script.
addr, err := m.cfg.WalletKit.NextAddr(
ctx, lnwallet.DefaultAccountName,
walletrpc.AddressType_TAPROOT_PUBKEY, false,
)
if err != nil {
return nil, err
}
timeoutSweepPkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, err
}
id, err := GetRandomDepositID()
if err != nil {
return nil, err
}
deposit := &Deposit{
ID: id,
State: Deposited,
OutPoint: utxo.OutPoint,
Value: utxo.Value,
ConfirmationHeight: int64(blockHeight),
TimeOutSweepPkScript: timeoutSweepPkScript,
}
err = m.cfg.Store.CreateDeposit(ctx, deposit)
if err != nil {
return nil, err
}
m.Lock()
m.deposits[deposit.OutPoint] = deposit
m.Unlock()
return deposit, nil
}
// getBlockHeight retrieves the block height of a given utxo.
func (m *Manager) getBlockHeight(ctx context.Context,
utxo *lnwallet.Utxo) (uint32, error) {
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return 0, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %v", err)
}
notifChan, errChan, err := m.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll
ctx, &utxo.OutPoint.Hash, addressParams.PkScript, MinConfs,
int32(m.initiationHeight),
)
if err != nil {
return 0, err
}
select {
case tx := <-notifChan:
return tx.BlockHeight, nil
case err := <-errChan:
return 0, err
case <-ctx.Done():
return 0, ctx.Err()
}
}
// filterNewDeposits filters the given utxos for new deposits that we haven't
// seen before.
func (m *Manager) filterNewDeposits(utxos []*lnwallet.Utxo) []*lnwallet.Utxo {
m.Lock()
defer m.Unlock()
var newDeposits []*lnwallet.Utxo
for _, utxo := range utxos {
_, ok := m.deposits[utxo.OutPoint]
if !ok {
newDeposits = append(newDeposits, utxo)
}
}
return newDeposits
}
// startDepositFsm creates a new state machine flow from the latest deposit to
// our static address.
func (m *Manager) startDepositFsm(deposit *Deposit) error {
// Create a state machine for a given deposit.
fsm, err := NewFSM(
m.runCtx, deposit, m.cfg, m.finalizedDepositChan, false,
)
if err != nil {
return err
}
// Send the start event to the state machine.
go func() {
err = fsm.SendEvent(OnStart, nil)
if err != nil {
log.Errorf("Error sending OnStart event: %v", err)
}
}()
err = fsm.DefaultObserver.WaitForState(m.runCtx, time.Minute, Deposited)
if err != nil {
return err
}
// Add the FSM to the active FSMs map.
m.Lock()
m.activeDeposits[deposit.OutPoint] = fsm
m.Unlock()
return nil
}
func (m *Manager) finalizeDeposit(outpoint wire.OutPoint) {
m.Lock()
delete(m.activeDeposits, outpoint)
delete(m.deposits, outpoint)
m.Unlock()
}
// GetActiveDepositsInState returns all active deposits.
func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) (
[]*Deposit, error) {
m.Lock()
defer m.Unlock()
var deposits []*Deposit
for _, fsm := range m.activeDeposits {
if fsm.deposit.State != stateFilter {
continue
}
deposits = append(deposits, fsm.deposit)
}
return deposits, nil
}
// GetAllDeposits returns all active deposits.
func (m *Manager) GetAllDeposits() ([]*Deposit, error) {
return m.cfg.Store.AllDeposits(m.runCtx)
}
// AllOutpointsActiveDeposits checks if all deposits referenced by the outpoints
// are active and in the specified state.
func (m *Manager) AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
stateFilter fsm.StateType) (
[]*Deposit, bool) {
m.Lock()
defer m.Unlock()
var deposits []*Deposit
for _, o := range outpoints {
if _, ok := m.activeDeposits[o]; !ok {
return nil, false
}
deposit := m.deposits[o]
if deposit.State != stateFilter {
return nil, false
}
deposits = append(deposits, m.deposits[o])
}
return deposits, true
}
// TransitionDeposits allows a caller to transition a set of deposits to a new
// state.
func (m *Manager) TransitionDeposits(deposits []*Deposit, event fsm.EventType,
expectedFinalState fsm.StateType) error {
for _, d := range deposits {
m.Lock()
sm, ok := m.activeDeposits[d.OutPoint]
m.Unlock()
if !ok {
return fmt.Errorf("deposit not found")
}
err := sm.SendEvent(event, nil)
if err != nil {
return err
}
err = sm.DefaultObserver.WaitForState(
m.runCtx, time.Minute, expectedFinalState,
)
if err != nil {
return err
}
}
return nil
}
func (m *Manager) UpdateDeposit(d *Deposit) error {
return m.cfg.Store.UpdateDeposit(m.runCtx, d)
}

@ -0,0 +1,221 @@
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 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(),
}
}
// 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,
WithdrawalSweepAddress: sql.NullString{
String: deposit.WithdrawalSweepAddress,
},
}
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[:],
WithdrawalSweepAddress: sql.NullString{
String: deposit.WithdrawalSweepAddress,
Valid: deposit.WithdrawalSweepAddress != "",
},
}
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,
WithdrawalSweepAddress: row.WithdrawalSweepAddress.String,
}, nil
}
// Close closes the database connection.
func (s *SqlStore) Close() {
s.baseDB.DB.Close()
}

@ -1,70 +0,0 @@
package staticaddr
import (
"context"
"github.com/lightninglabs/loop/looprpc"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
)
// AddressServer holds all fields for the address rpc server.
type AddressServer struct {
addressClient staticaddressrpc.StaticAddressServerClient
manager *Manager
looprpc.UnimplementedStaticAddressClientServer
}
// NewAddressServer creates a new static address server.
func NewAddressServer(addressClient staticaddressrpc.StaticAddressServerClient,
manager *Manager) *AddressServer {
return &AddressServer{
addressClient: addressClient,
manager: manager,
}
}
// NewAddress is the rpc endpoint for loop clients to request a new static
// address.
func (s *AddressServer) NewAddress(ctx context.Context,
_ *looprpc.NewAddressRequest) (*looprpc.NewAddressResponse, error) {
address, err := s.manager.NewAddress(ctx)
if err != nil {
return nil, err
}
log.Infof("New static loop-in address: %s\n", address.String())
return &looprpc.NewAddressResponse{
Address: address.String(),
}, nil
}
// ListUnspent returns a list of utxos behind the static address.
func (s *AddressServer) ListUnspent(ctx context.Context,
req *looprpc.ListUnspentRequest) (*looprpc.ListUnspentResponse, error) {
// List all unspent utxos the wallet sees, regardless of the number of
// confirmations.
staticAddress, utxos, err := s.manager.ListUnspentRaw(
ctx, req.MinConfs, req.MaxConfs,
)
if err != nil {
return nil, err
}
// Prepare the list response.
var respUtxos []*looprpc.Utxo
for _, u := range utxos {
utxo := &looprpc.Utxo{
StaticAddress: staticAddress.String(),
AmountSat: int64(u.Value),
Confirmations: u.Confirmations,
Outpoint: u.OutPoint.String(),
}
respUtxos = append(respUtxos, utxo)
}
return &looprpc.ListUnspentResponse{Utxos: respUtxos}, nil
}

@ -0,0 +1,44 @@
package withdraw
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/lnwallet"
)
const (
IdLength = 32
)
// 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)
}
type DepositManager interface {
GetActiveDepositsInState(stateFilter fsm.StateType) ([]*deposit.Deposit,
error)
AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
stateFilter fsm.StateType) ([]*deposit.Deposit, bool)
TransitionDeposits(deposits []*deposit.Deposit, event fsm.EventType,
expectedFinalState fsm.StateType) error
UpdateDeposit(d *deposit.Deposit) error
}

@ -0,0 +1,24 @@
package withdraw
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the sub system name of this package.
const Subsystem = "CLOS"
// log is a logger that is initialized with no output filters. This means the
// package will not perform any logging by default until the caller requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// UseLogger uses a specified Logger to output package logging info. This should
// be used in preference to SetLogWriter if the caller is also using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

@ -0,0 +1,649 @@
package withdraw
import (
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"reflect"
"sync"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/deposit"
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
var (
// ErrWithdrawingInactiveDeposits is returned when the user tries to
// withdraw inactive deposits.
ErrWithdrawingInactiveDeposits = errors.New("deposits to be withdrawn " +
"are unknown or inactive")
// MinConfs is the minimum number of confirmations we require for a
// deposit to be considered withdrawn.
MinConfs int32 = 3
// Is the default confirmation target for the fee estimation of the
// withdrawal transaction.
defaultConfTarget int32 = 3
)
// ManagerConfig holds the configuration for the address manager.
type ManagerConfig struct {
WithdrawalServerClient staticaddressrpc.WithdrawalServerClient
AddressManager AddressManager
DepositManager DepositManager
// WalletKit is the wallet client that is used to derive new keys from
// lnd's wallet.
WalletKit lndclient.WalletKitClient
// ChainParams is the chain configuration(mainnet, testnet...) this
// manager uses.
ChainParams *chaincfg.Params
// ChainNotifier is the chain notifier that is used to listen for new
// blocks.
ChainNotifier lndclient.ChainNotifierClient
// Signer is the signer client that is used to sign transactions.
Signer lndclient.SignerClient
}
// Manager manages the address state machines.
type Manager struct {
cfg *ManagerConfig
runCtx context.Context
sync.Mutex
// initChan signals the daemon that the address manager has completed
// its initialization.
initChan chan struct{}
// initiationHeight stores the currently best known block height.
initiationHeight uint32
// currentHeight stores the currently best known block height.
currentHeight uint32
}
// NewManager creates a new deposit withdrawal manager.
func NewManager(cfg *ManagerConfig) *Manager {
return &Manager{
cfg: cfg,
initChan: make(chan struct{}),
}
}
// Run runs the deposit withdrawal manager.
func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
m.runCtx = ctx
m.Lock()
m.currentHeight, m.initiationHeight = currentHeight, currentHeight
m.Unlock()
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.RegisterBlockEpochNtfn(m.runCtx) //nolint:lll
if err != nil {
return err
}
err = m.recover()
if err != nil {
return err
}
// Communicate to the caller that the address manager has completed its
// initialization.
close(m.initChan)
for {
select {
case height := <-newBlockChan:
m.Lock()
m.currentHeight = uint32(height)
m.Unlock()
case err := <-newBlockErrChan:
return err
case <-m.runCtx.Done():
return m.runCtx.Err()
}
}
}
func (m *Manager) recover() error {
// To recover withdrawals we skim through all active deposits and check
// if they have a withdrawal address set. For the ones that do we
// cluster those with equal withdrawal addresses and kick-off
// their withdrawal. Each cluster represents a separate withdrawal by
// the user.
activeDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
deposit.Deposited,
)
if err != nil {
return err
}
// Group the deposits by their withdrawal address.
depositsByWithdrawalAddress := make(map[string][]*deposit.Deposit)
for _, d := range activeDeposits {
sweepAddress := d.WithdrawalSweepAddress
if sweepAddress == "" {
continue
}
depositsByWithdrawalAddress[sweepAddress] = append(
depositsByWithdrawalAddress[sweepAddress], d,
)
}
// We can now reinstate each cluster of deposits for a withdrawal.
for address, deposits := range depositsByWithdrawalAddress {
sweepAddress, err := btcutil.DecodeAddress(
address, m.cfg.ChainParams,
)
if err != nil {
return err
}
err = m.cfg.DepositManager.TransitionDeposits(
deposits, deposit.OnWithdraw, deposit.Withdrawing,
)
if err != nil {
return err
}
err = m.publishWithdrawal(m.runCtx, deposits, sweepAddress)
if err != nil {
return err
}
}
return nil
}
// WaitInitComplete waits until the address manager has completed its setup.
func (m *Manager) WaitInitComplete() {
defer log.Debugf("Static address withdrawal manager initiation " +
"complete.")
<-m.initChan
}
// WithdrawDeposits starts a deposits withdrawal flow.
func (m *Manager) WithdrawDeposits(ctx context.Context,
outpoints []wire.OutPoint) error {
if len(outpoints) == 0 {
return fmt.Errorf("no outpoints selected to withdraw")
}
// Ensure that the deposits are in a state in which they can be
// withdrawn.
deposits, allActive := m.cfg.DepositManager.AllOutpointsActiveDeposits(
outpoints, deposit.Deposited)
if !allActive {
return ErrWithdrawingInactiveDeposits
}
// Generate the withdrawal address from our local lnd wallet.
withdrawalAddress, err := m.cfg.WalletKit.NextAddr(
ctx, lnwallet.DefaultAccountName,
walletrpc.AddressType_TAPROOT_PUBKEY, false,
)
if err != nil {
return err
}
// Attach the withdrawal address to the deposit. After a client restart
// we can use this address as an indicator to continue the withdrawal.
// If there are multiple deposits with the same withdrawal address, we
// bundle them together in the same withdrawal transaction.
for _, d := range deposits {
d.WithdrawalSweepAddress = withdrawalAddress.String()
/*err := m.cfg.DepositManager.UpdateDeposit(d)
if err != nil {
return err
}*/
}
// Transition the deposits to the withdrawing state. This updates each
// deposits withdrawal address. If a transition fails, we'll return an
// error and abort the withdrawal. An error in transition is likely due
// to an error in the state machine. The already transitioned deposits
// should be reset to the deposit state after a restart.
err = m.cfg.DepositManager.TransitionDeposits(
deposits, deposit.OnWithdraw, deposit.Withdrawing,
)
if err != nil {
return err
}
return m.publishWithdrawal(ctx, deposits, withdrawalAddress)
}
func (m *Manager) publishWithdrawal(ctx context.Context,
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address) error {
// Create a musig2 session for each deposit.
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
ctx, deposits,
)
if err != nil {
return err
}
// Get the fee rate for the withdrawal sweep.
withdrawalSweepFeeRate, err := m.cfg.WalletKit.EstimateFeeRate(
m.runCtx, defaultConfTarget,
)
if err != nil {
return err
}
outpoints := toOutpoints(deposits)
resp, err := m.cfg.WithdrawalServerClient.WithdrawDeposits(
m.runCtx,
&staticaddressrpc.ServerWithdrawRequest{
Outpoints: toServerOutpoints(outpoints),
ClientNonces: clientNonces,
ClientSweepAddr: withdrawalAddress.String(),
MusigTxFeeRate: uint64(withdrawalSweepFeeRate),
},
)
if err != nil {
return err
}
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return fmt.Errorf("couldn't get confirmation height for "+
"deposit, %v", err)
}
prevOuts := m.toPrevOuts(deposits, addressParams.PkScript)
totalValue := withdrawalValue(prevOuts)
withdrawalTx, err := m.createWithdrawalTx(
prevOuts, totalValue, withdrawalAddress, withdrawalSweepFeeRate,
)
if err != nil {
return err
}
coopServerNonces, err := toNonces(resp.ServerNonces)
if err != nil {
return err
}
// Next we'll get our sweep tx signatures.
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
_, err = m.signMusig2Tx(
m.runCtx, prevOutFetcher, outpoints, m.cfg.Signer, withdrawalTx,
withdrawalSessions, coopServerNonces,
)
if err != nil {
return err
}
// Now we'll finalize the sweepless sweep transaction.
finalizedWithdrawalTx, err := m.finalizeMusig2Transaction(
m.runCtx, outpoints, m.cfg.Signer, withdrawalSessions,
withdrawalTx, resp.Musig2SweepSigs,
)
if err != nil {
return err
}
txLabel := fmt.Sprintf("deposit-withdrawal-%v",
finalizedWithdrawalTx.TxHash())
// Publish the sweepless sweep transaction.
err = m.cfg.WalletKit.PublishTransaction(
m.runCtx, finalizedWithdrawalTx, txLabel,
)
if err != nil {
return err
}
txHash := finalizedWithdrawalTx.TxHash()
if err != nil {
return err
}
withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress)
confChan, errChan, err := m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
m.runCtx, &txHash, withdrawalPkScript, MinConfs,
int32(m.initiationHeight),
)
if err != nil {
return err
}
go func() {
select {
case <-confChan:
err = m.cfg.DepositManager.TransitionDeposits(
deposits, deposit.OnWithdrawn,
deposit.Withdrawn,
)
if err != nil {
log.Errorf("Error transitioning deposits: %v",
err)
}
case err := <-errChan:
log.Errorf("Error waiting for confirmation: %v", err)
case <-m.runCtx.Done():
log.Errorf("Withdrawal tx confirmation wait canceled")
}
}()
return nil
}
func toOutpoints(deposits []*deposit.Deposit) []wire.OutPoint {
outpoints := make([]wire.OutPoint, len(deposits))
for i, d := range deposits {
outpoints[i] = wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
}
return outpoints
}
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
// transaction.
func (m *Manager) signMusig2Tx(ctx context.Context,
prevOutFetcher *txscript.MultiPrevOutFetcher, outpoints []wire.OutPoint,
signer lndclient.SignerClient, tx *wire.MsgTx,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
sigs := make([][]byte, len(outpoints))
for idx, outpoint := range outpoints {
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
outpoint) {
return nil, fmt.Errorf("tx input does not match " +
"deposits")
}
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault, tx, idx,
prevOutFetcher,
)
if err != nil {
return nil, err
}
var digest [32]byte
copy(digest[:], taprootSigHash)
// Register the server's nonce before attempting to create our
// partial signature.
haveAllNonces, err := signer.MuSig2RegisterNonces(
ctx, musig2sessions[idx].SessionID,
[][musig2.PubNonceSize]byte{counterPartyNonces[idx]},
)
if err != nil {
return nil, err
}
// Sanity check that we have all the nonces.
if !haveAllNonces {
return nil, fmt.Errorf("invalid MuSig2 session: " +
"nonces missing")
}
// Since our MuSig2 session has all nonces, we can now create
// the local partial signature by signing the sig hash.
sig, err := signer.MuSig2Sign(
ctx, musig2sessions[idx].SessionID, digest, false,
)
if err != nil {
return nil, err
}
sigs[idx] = sig
}
return sigs, nil
}
func withdrawalValue(prevOuts map[wire.OutPoint]*wire.TxOut) btcutil.Amount {
var totalValue btcutil.Amount
for _, prevOut := range prevOuts {
totalValue += btcutil.Amount(prevOut.Value)
}
return totalValue
}
// toNonces converts a byte slice to a 66 byte slice.
func toNonces(nonces [][]byte) ([][66]byte, error) {
res := make([][66]byte, 0, len(nonces))
for _, n := range nonces {
n := n
nonce, err := byteSliceTo66ByteSlice(n)
if err != nil {
return nil, err
}
res = append(res, nonce)
}
return res, nil
}
// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
func byteSliceTo66ByteSlice(b []byte) ([66]byte, error) {
if len(b) != 66 {
return [66]byte{}, fmt.Errorf("invalid byte slice length")
}
var res [66]byte
copy(res[:], b)
return res, nil
}
func (m *Manager) createWithdrawalTx(prevOuts map[wire.OutPoint]*wire.TxOut,
withdrawlAmount btcutil.Amount, clientSweepAddress btcutil.Address,
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
// First Create the tx.
msgTx := wire.NewMsgTx(2)
// Add the deposit inputs to the transaction.
for o, _ := range prevOuts {
msgTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: o,
})
}
// Estimate the fee
weight, err := withdrawalFee(len(prevOuts), clientSweepAddress)
if err != nil {
return nil, err
}
pkscript, err := txscript.PayToAddrScript(clientSweepAddress)
if err != nil {
return nil, err
}
fee := feeRate.FeeForWeight(weight)
// Create the sweep output
sweepOutput := &wire.TxOut{
Value: int64(withdrawlAmount) - int64(fee),
PkScript: pkscript,
}
msgTx.AddTxOut(sweepOutput)
return msgTx, nil
}
// withdrawalFee returns the weight for the withdrawal transaction.
func withdrawalFee(numInputs int, sweepAddress btcutil.Address) (int64,
error) {
var weightEstimator input.TxWeightEstimator
for i := 0; i < numInputs; i++ {
weightEstimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
}
// Get the weight of the sweep output.
switch sweepAddress.(type) {
case *btcutil.AddressWitnessPubKeyHash:
weightEstimator.AddP2WKHOutput()
case *btcutil.AddressTaproot:
weightEstimator.AddP2TROutput()
default:
return 0, fmt.Errorf("invalid sweep address type %T",
sweepAddress)
}
return int64(weightEstimator.Weight()), nil
}
// finalizeMusig2Transaction creates the finalized transactions for either
// the htlc or the cooperative close.
func (m *Manager) finalizeMusig2Transaction(ctx context.Context,
outpoints []wire.OutPoint, signer lndclient.SignerClient,
musig2Sessions []*input.MuSig2SessionInfo,
tx *wire.MsgTx, serverSigs [][]byte) (*wire.MsgTx, error) {
for idx := range outpoints {
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
ctx, musig2Sessions[idx].SessionID,
[][]byte{serverSigs[idx]},
)
if err != nil {
return nil, err
}
if !haveAllSigs {
return nil, fmt.Errorf("missing sigs")
}
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
}
return tx, nil
}
func toServerOutpoints(outpoints []wire.OutPoint) []*staticaddressrpc.ServerOutPoint { //nolint:lll
var result []*staticaddressrpc.ServerOutPoint
for _, o := range outpoints {
outP := o
outpoint := &staticaddressrpc.ServerOutPoint{
TxidBytes: outP.Hash[:],
OutputIndex: outP.Index,
}
result = append(result, outpoint)
}
return result
}
// createMusig2Sessions creates a musig2 session for a number of deposits.
func (m *Manager) createMusig2Sessions(ctx context.Context,
deposits []*deposit.Deposit) ([]*input.MuSig2SessionInfo, [][]byte,
error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
clientNonces := make([][]byte, len(deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(deposits); i++ {
session, err := m.createMusig2Session(ctx)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// Musig2CreateSession creates a musig2 session for the deposit.
func (m *Manager) createMusig2Session(ctx context.Context) (
*input.MuSig2SessionInfo, error) {
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
ctx,
)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %v", err)
}
signers := [][]byte{
addressParams.ClientPubkey.SerializeCompressed(),
addressParams.ServerPubkey.SerializeCompressed(),
}
address, err := m.cfg.AddressManager.GetStaticAddress(ctx)
if err != nil {
return nil, fmt.Errorf("couldn't get confirmation height for "+
"deposit, %v", err)
}
expiryLeaf := address.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return m.cfg.Signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &addressParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) map[wire.OutPoint]*wire.TxOut {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
prevOuts[outpoint] = txOut
}
return prevOuts
}

@ -0,0 +1,363 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.6.1
// source: withdraw.proto
// We can't change this to swapserverrpc, it would be a breaking change because
// the package name is also contained in the HTTP URIs and old clients would
// call the wrong endpoints. Luckily with the go_package option we can have
// different golang and RPC package names to fix protobuf namespace conflicts.
package swapserverrpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ServerWithdrawRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The deposit outpoints the client whishes to close.
Outpoints []*ServerOutPoint `protobuf:"bytes,1,rep,name=outpoints,proto3" json:"outpoints,omitempty"`
// The nonces that the client used to generate the coop close tx sigs.
ClientNonces [][]byte `protobuf:"bytes,2,rep,name=client_nonces,json=clientNonces,proto3" json:"client_nonces,omitempty"`
// The address that the client wants to sweep the static address deposits
// to.
ClientSweepAddr string `protobuf:"bytes,3,opt,name=client_sweep_addr,json=clientSweepAddr,proto3" json:"client_sweep_addr,omitempty"`
// The fee rate in sat/kw that the client wants to use for the sweep.
MusigTxFeeRate uint64 `protobuf:"varint,4,opt,name=musig_tx_fee_rate,json=musigTxFeeRate,proto3" json:"musig_tx_fee_rate,omitempty"`
}
func (x *ServerWithdrawRequest) Reset() {
*x = ServerWithdrawRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_withdraw_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ServerWithdrawRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServerWithdrawRequest) ProtoMessage() {}
func (x *ServerWithdrawRequest) ProtoReflect() protoreflect.Message {
mi := &file_withdraw_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServerWithdrawRequest.ProtoReflect.Descriptor instead.
func (*ServerWithdrawRequest) Descriptor() ([]byte, []int) {
return file_withdraw_proto_rawDescGZIP(), []int{0}
}
func (x *ServerWithdrawRequest) GetOutpoints() []*ServerOutPoint {
if x != nil {
return x.Outpoints
}
return nil
}
func (x *ServerWithdrawRequest) GetClientNonces() [][]byte {
if x != nil {
return x.ClientNonces
}
return nil
}
func (x *ServerWithdrawRequest) GetClientSweepAddr() string {
if x != nil {
return x.ClientSweepAddr
}
return ""
}
func (x *ServerWithdrawRequest) GetMusigTxFeeRate() uint64 {
if x != nil {
return x.MusigTxFeeRate
}
return 0
}
type ServerWithdrawResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The sweep sigs that the server generated for the htlc.
Musig2SweepSigs [][]byte `protobuf:"bytes,1,rep,name=musig2_sweep_sigs,json=musig2SweepSigs,proto3" json:"musig2_sweep_sigs,omitempty"`
// The nonces that the server used to generate the sweepless sweep sigs.
ServerNonces [][]byte `protobuf:"bytes,2,rep,name=server_nonces,json=serverNonces,proto3" json:"server_nonces,omitempty"`
}
func (x *ServerWithdrawResponse) Reset() {
*x = ServerWithdrawResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_withdraw_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ServerWithdrawResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServerWithdrawResponse) ProtoMessage() {}
func (x *ServerWithdrawResponse) ProtoReflect() protoreflect.Message {
mi := &file_withdraw_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServerWithdrawResponse.ProtoReflect.Descriptor instead.
func (*ServerWithdrawResponse) Descriptor() ([]byte, []int) {
return file_withdraw_proto_rawDescGZIP(), []int{1}
}
func (x *ServerWithdrawResponse) GetMusig2SweepSigs() [][]byte {
if x != nil {
return x.Musig2SweepSigs
}
return nil
}
func (x *ServerWithdrawResponse) GetServerNonces() [][]byte {
if x != nil {
return x.ServerNonces
}
return nil
}
type ServerOutPoint struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
//
//Raw bytes representing the transaction id.
TxidBytes []byte `protobuf:"bytes,1,opt,name=txid_bytes,json=txidBytes,proto3" json:"txid_bytes,omitempty"`
//
//Reversed, hex-encoded string representing the transaction id.
TxidStr string `protobuf:"bytes,2,opt,name=txid_str,json=txidStr,proto3" json:"txid_str,omitempty"`
//
//The index of the output on the transaction.
OutputIndex uint32 `protobuf:"varint,3,opt,name=output_index,json=outputIndex,proto3" json:"output_index,omitempty"`
}
func (x *ServerOutPoint) Reset() {
*x = ServerOutPoint{}
if protoimpl.UnsafeEnabled {
mi := &file_withdraw_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ServerOutPoint) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServerOutPoint) ProtoMessage() {}
func (x *ServerOutPoint) ProtoReflect() protoreflect.Message {
mi := &file_withdraw_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServerOutPoint.ProtoReflect.Descriptor instead.
func (*ServerOutPoint) Descriptor() ([]byte, []int) {
return file_withdraw_proto_rawDescGZIP(), []int{2}
}
func (x *ServerOutPoint) GetTxidBytes() []byte {
if x != nil {
return x.TxidBytes
}
return nil
}
func (x *ServerOutPoint) GetTxidStr() string {
if x != nil {
return x.TxidStr
}
return ""
}
func (x *ServerOutPoint) GetOutputIndex() uint32 {
if x != nil {
return x.OutputIndex
}
return 0
}
var File_withdraw_proto protoreflect.FileDescriptor
var file_withdraw_proto_rawDesc = []byte{
0x0a, 0x0e, 0x77, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0xca, 0x01, 0x0a, 0x15, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52,
0x09, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c,
0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x0c, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12,
0x2a, 0x0a, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f,
0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6c, 0x69, 0x65,
0x6e, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x41, 0x64, 0x64, 0x72, 0x12, 0x29, 0x0a, 0x11, 0x6d,
0x75, 0x73, 0x69, 0x67, 0x5f, 0x74, 0x78, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65,
0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x69, 0x67, 0x54, 0x78, 0x46,
0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x22, 0x69, 0x0a, 0x16, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x75, 0x73, 0x69, 0x67, 0x32, 0x5f, 0x73, 0x77, 0x65, 0x65, 0x70,
0x5f, 0x73, 0x69, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x6d, 0x75, 0x73,
0x69, 0x67, 0x32, 0x53, 0x77, 0x65, 0x65, 0x70, 0x53, 0x69, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x0d,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20,
0x03, 0x28, 0x0c, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63, 0x65,
0x73, 0x22, 0x6d, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x50, 0x6f,
0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x69, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65,
0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x78, 0x69, 0x64, 0x42, 0x79, 0x74,
0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x78, 0x69, 0x64, 0x5f, 0x73, 0x74, 0x72, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x78, 0x69, 0x64, 0x53, 0x74, 0x72, 0x12, 0x21, 0x0a,
0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78,
0x32, 0x67, 0x0a, 0x10, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x61, 0x6c, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x10, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77,
0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61,
0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72,
0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61,
0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e,
0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x73, 0x77, 0x61, 0x70, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_withdraw_proto_rawDescOnce sync.Once
file_withdraw_proto_rawDescData = file_withdraw_proto_rawDesc
)
func file_withdraw_proto_rawDescGZIP() []byte {
file_withdraw_proto_rawDescOnce.Do(func() {
file_withdraw_proto_rawDescData = protoimpl.X.CompressGZIP(file_withdraw_proto_rawDescData)
})
return file_withdraw_proto_rawDescData
}
var file_withdraw_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_withdraw_proto_goTypes = []interface{}{
(*ServerWithdrawRequest)(nil), // 0: looprpc.ServerWithdrawRequest
(*ServerWithdrawResponse)(nil), // 1: looprpc.ServerWithdrawResponse
(*ServerOutPoint)(nil), // 2: looprpc.ServerOutPoint
}
var file_withdraw_proto_depIdxs = []int32{
2, // 0: looprpc.ServerWithdrawRequest.outpoints:type_name -> looprpc.ServerOutPoint
0, // 1: looprpc.WithdrawalServer.WithdrawDeposits:input_type -> looprpc.ServerWithdrawRequest
1, // 2: looprpc.WithdrawalServer.WithdrawDeposits:output_type -> looprpc.ServerWithdrawResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_withdraw_proto_init() }
func file_withdraw_proto_init() {
if File_withdraw_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_withdraw_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerWithdrawRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_withdraw_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerWithdrawResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_withdraw_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerOutPoint); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_withdraw_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_withdraw_proto_goTypes,
DependencyIndexes: file_withdraw_proto_depIdxs,
MessageInfos: file_withdraw_proto_msgTypes,
}.Build()
File_withdraw_proto = out.File
file_withdraw_proto_rawDesc = nil
file_withdraw_proto_goTypes = nil
file_withdraw_proto_depIdxs = nil
}

@ -0,0 +1,57 @@
syntax = "proto3";
// We can't change this to swapserverrpc, it would be a breaking change because
// the package name is also contained in the HTTP URIs and old clients would
// call the wrong endpoints. Luckily with the go_package option we can have
// different golang and RPC package names to fix protobuf namespace conflicts.
package looprpc;
option go_package = "github.com/lightninglabs/loop/swapserverrpc";
service WithdrawalServer {
// WithdrawDeposits allows to cooperatively sweep deposits that haven't
// timed out yet to the client's wallet. The server will generate the
// partial sigs for the client's selected deposits.
rpc WithdrawDeposits (ServerWithdrawRequest)
returns (ServerWithdrawResponse);
}
message ServerWithdrawRequest {
// The deposit outpoints the client whishes to close.
repeated ServerOutPoint outpoints = 1;
// The nonces that the client used to generate the coop close tx sigs.
repeated bytes client_nonces = 2;
// The address that the client wants to sweep the static address deposits
// to.
string client_sweep_addr = 3;
// The fee rate in sat/kw that the client wants to use for the sweep.
uint64 musig_tx_fee_rate = 4;
}
message ServerWithdrawResponse {
// The sweep sigs that the server generated for the htlc.
repeated bytes musig2_sweep_sigs = 1;
// The nonces that the server used to generate the sweepless sweep sigs.
repeated bytes server_nonces = 2;
}
message ServerOutPoint {
/*
Raw bytes representing the transaction id.
*/
bytes txid_bytes = 1;
/*
Reversed, hex-encoded string representing the transaction id.
*/
string txid_str = 2;
/*
The index of the output on the transaction.
*/
uint32 output_index = 3;
}

@ -0,0 +1,107 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package swapserverrpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// WithdrawalServerClient is the client API for WithdrawalServer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type WithdrawalServerClient interface {
// WithdrawDeposits allows to cooperatively sweep deposits that haven't
// timed out yet to the client's wallet. The server will generate the
// partial sigs for the client's selected deposits.
WithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error)
}
type withdrawalServerClient struct {
cc grpc.ClientConnInterface
}
func NewWithdrawalServerClient(cc grpc.ClientConnInterface) WithdrawalServerClient {
return &withdrawalServerClient{cc}
}
func (c *withdrawalServerClient) WithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error) {
out := new(ServerWithdrawResponse)
err := c.cc.Invoke(ctx, "/looprpc.WithdrawalServer/WithdrawDeposits", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// WithdrawalServerServer is the server API for WithdrawalServer service.
// All implementations must embed UnimplementedWithdrawalServerServer
// for forward compatibility
type WithdrawalServerServer interface {
// WithdrawDeposits allows to cooperatively sweep deposits that haven't
// timed out yet to the client's wallet. The server will generate the
// partial sigs for the client's selected deposits.
WithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error)
mustEmbedUnimplementedWithdrawalServerServer()
}
// UnimplementedWithdrawalServerServer must be embedded to have forward compatible implementations.
type UnimplementedWithdrawalServerServer struct {
}
func (UnimplementedWithdrawalServerServer) WithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method WithdrawDeposits not implemented")
}
func (UnimplementedWithdrawalServerServer) mustEmbedUnimplementedWithdrawalServerServer() {}
// UnsafeWithdrawalServerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to WithdrawalServerServer will
// result in compilation errors.
type UnsafeWithdrawalServerServer interface {
mustEmbedUnimplementedWithdrawalServerServer()
}
func RegisterWithdrawalServerServer(s grpc.ServiceRegistrar, srv WithdrawalServerServer) {
s.RegisterService(&WithdrawalServer_ServiceDesc, srv)
}
func _WithdrawalServer_WithdrawDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerWithdrawRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WithdrawalServerServer).WithdrawDeposits(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.WithdrawalServer/WithdrawDeposits",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WithdrawalServerServer).WithdrawDeposits(ctx, req.(*ServerWithdrawRequest))
}
return interceptor(ctx, in, info, handler)
}
// WithdrawalServer_ServiceDesc is the grpc.ServiceDesc for WithdrawalServer service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var WithdrawalServer_ServiceDesc = grpc.ServiceDesc{
ServiceName: "looprpc.WithdrawalServer",
HandlerType: (*WithdrawalServerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "WithdrawDeposits",
Handler: _WithdrawalServer_WithdrawDeposits_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "withdraw.proto",
}
Loading…
Cancel
Save