mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Include https://github.com/lightninglabs/lndclient/pull/280 multi: migrate to btcd v2 modules + add WalletKit.SubmitPackage Migrate Loop imports and update Aperture and Taproot Assets to compatible revisions so this commit remains green on its own.
263 lines
6.2 KiB
Go
263 lines
6.2 KiB
Go
package instantout
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
btcaddr "github.com/btcsuite/btcd/address/v2"
|
|
"github.com/btcsuite/btcd/btcutil/v2"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/instantout/reservation"
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
)
|
|
|
|
var (
|
|
defaultStateWaitTime = 30 * time.Second
|
|
defaultCltv = 100
|
|
ErrSwapDoesNotExist = errors.New("swap does not exist")
|
|
)
|
|
|
|
// Manager manages the instantout state machines.
|
|
type Manager struct {
|
|
sync.Mutex
|
|
|
|
// cfg contains all the services that the reservation manager needs to
|
|
// operate.
|
|
cfg *Config
|
|
|
|
// activeInstantOuts contains all the active instantouts.
|
|
activeInstantOuts map[lntypes.Hash]*FSM
|
|
|
|
// currentHeight stores the currently best known block height.
|
|
currentHeight int32
|
|
|
|
// blockEpochChan receives new block heights.
|
|
blockEpochChan chan int32
|
|
|
|
runCtx context.Context
|
|
}
|
|
|
|
// NewInstantOutManager creates a new instantout manager.
|
|
func NewInstantOutManager(cfg *Config, height int32) *Manager {
|
|
return &Manager{
|
|
cfg: cfg,
|
|
activeInstantOuts: make(map[lntypes.Hash]*FSM),
|
|
blockEpochChan: make(chan int32),
|
|
currentHeight: height,
|
|
}
|
|
}
|
|
|
|
// Run runs the instantout manager.
|
|
func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|
log.Debugf("Starting instantout manager")
|
|
defer func() {
|
|
log.Debugf("Stopping instantout manager")
|
|
}()
|
|
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
m.runCtx = runCtx
|
|
|
|
err := m.recoverInstantOuts(runCtx)
|
|
if err != nil {
|
|
close(initChan)
|
|
return err
|
|
}
|
|
|
|
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.
|
|
RegisterBlockEpochNtfn(ctx)
|
|
if err != nil {
|
|
close(initChan)
|
|
return err
|
|
}
|
|
|
|
close(initChan)
|
|
|
|
for {
|
|
select {
|
|
case <-runCtx.Done():
|
|
return nil
|
|
|
|
case height := <-newBlockChan:
|
|
m.Lock()
|
|
m.currentHeight = height
|
|
m.Unlock()
|
|
|
|
case err := <-newBlockErrChan:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// recoverInstantOuts recovers all the active instantouts from the database.
|
|
func (m *Manager) recoverInstantOuts(ctx context.Context) error {
|
|
// Fetch all the active instantouts from the database.
|
|
activeInstantOuts, err := m.cfg.Store.ListInstantLoopOuts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, instantOut := range activeInstantOuts {
|
|
if isFinalState(instantOut.State) {
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Recovering instantout %v", instantOut.SwapHash)
|
|
|
|
instantOutFSM, err := NewFSMFromInstantOut(
|
|
m.cfg, instantOut,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.activeInstantOuts[instantOut.SwapHash] = instantOutFSM
|
|
|
|
// As SendEvent can block, we'll start a goroutine to process
|
|
// the event.
|
|
go func() {
|
|
err := instantOutFSM.SendEvent(ctx, OnRecover, nil)
|
|
if err != nil {
|
|
log.Errorf("FSM %v Error sending recover "+
|
|
"event %v, state: %v",
|
|
instantOutFSM.InstantOut.SwapHash, err,
|
|
instantOutFSM.InstantOut.State)
|
|
}
|
|
}()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewInstantOut creates a new instantout.
|
|
func (m *Manager) NewInstantOut(ctx context.Context,
|
|
reservations []reservation.ID, sweepAddress string) (*FSM, error) {
|
|
|
|
var (
|
|
sweepAddr btcaddr.Address
|
|
err error
|
|
)
|
|
if sweepAddress != "" {
|
|
sweepAddr, err = btcaddr.DecodeAddress(
|
|
sweepAddress, m.cfg.Network,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
m.Lock()
|
|
// Create the instantout request.
|
|
request := &InitInstantOutCtx{
|
|
cltvExpiry: m.currentHeight + int32(defaultCltv),
|
|
reservations: reservations,
|
|
initationHeight: m.currentHeight,
|
|
protocolVersion: CurrentProtocolVersion(),
|
|
sweepAddress: sweepAddr,
|
|
}
|
|
|
|
instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation)
|
|
if err != nil {
|
|
m.Unlock()
|
|
return nil, err
|
|
}
|
|
m.activeInstantOuts[instantOut.InstantOut.SwapHash] = instantOut
|
|
m.Unlock()
|
|
|
|
// Start the instantout FSM.
|
|
go func() {
|
|
err := instantOut.SendEvent(m.runCtx, OnStart, request)
|
|
if err != nil {
|
|
log.Errorf("Error sending event: %v", err)
|
|
}
|
|
}()
|
|
|
|
// If everything went well, we'll wait for the instant out to be
|
|
// waiting for sweepless sweep to be confirmed.
|
|
err = instantOut.DefaultObserver.WaitForState(
|
|
ctx, defaultStateWaitTime, WaitForSweeplessSweepConfirmed,
|
|
fsm.WithAbortEarlyOnErrorOption(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return instantOut, nil
|
|
}
|
|
|
|
// GetActiveInstantOut returns an active instant out.
|
|
func (m *Manager) GetActiveInstantOut(swapHash lntypes.Hash) (*FSM, error) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
fsm, ok := m.activeInstantOuts[swapHash]
|
|
if !ok {
|
|
return nil, ErrSwapDoesNotExist
|
|
}
|
|
|
|
// If the instant out is in a final state, we'll remove it from the
|
|
// active instant outs.
|
|
if isFinalState(fsm.InstantOut.State) {
|
|
delete(m.activeInstantOuts, swapHash)
|
|
}
|
|
|
|
return fsm, nil
|
|
}
|
|
|
|
type Quote struct {
|
|
// ServiceFee is the fee in sat that is paid to the loop service.
|
|
ServiceFee btcutil.Amount
|
|
|
|
// OnChainFee is the estimated on chain fee in sat.
|
|
OnChainFee btcutil.Amount
|
|
}
|
|
|
|
// GetInstantOutQuote returns a quote for an instant out.
|
|
func (m *Manager) GetInstantOutQuote(ctx context.Context,
|
|
amt btcutil.Amount, reservationIDs [][]byte) (Quote, error) {
|
|
|
|
if len(reservationIDs) == 0 {
|
|
return Quote{}, fmt.Errorf("no reservations selected")
|
|
}
|
|
|
|
if amt <= 0 {
|
|
return Quote{}, fmt.Errorf("no amount selected")
|
|
}
|
|
|
|
// Get the service fee.
|
|
quoteRes, err := m.cfg.InstantOutClient.GetInstantOutQuote(
|
|
ctx, &swapserverrpc.GetInstantOutQuoteRequest{
|
|
Amount: uint64(amt),
|
|
ReservationIds: reservationIDs,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return Quote{}, err
|
|
}
|
|
|
|
// Get the offchain fee by getting the fee estimate from the lnd client
|
|
// and multiplying it by the estimated sweepless sweep transaction.
|
|
feeRate, err := m.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget)
|
|
if err != nil {
|
|
return Quote{}, err
|
|
}
|
|
|
|
// The on chain chainFee is the chainFee rate times the estimated
|
|
// sweepless sweep transaction size.
|
|
chainFee := feeRate.FeeForWeight(sweeplessSweepWeight(len(reservationIDs)))
|
|
|
|
return Quote{
|
|
ServiceFee: btcutil.Amount(quoteRes.SwapFee),
|
|
OnChainFee: chainFee,
|
|
}, nil
|
|
}
|
|
|
|
// ListInstantOuts returns all instant outs from the database.
|
|
func (m *Manager) ListInstantOuts(ctx context.Context) ([]*InstantOut, error) {
|
|
return m.cfg.Store.ListInstantLoopOuts(ctx)
|
|
}
|