Skip to content

Commit a85f6a0

Browse files
committed
staticaddr: address and deposit adjustments for withdrawals
1 parent 20c329f commit a85f6a0

File tree

8 files changed

+200
-35
lines changed

8 files changed

+200
-35
lines changed

staticaddr/address/manager.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/btcsuite/btcd/btcutil"
1212
"github.com/btcsuite/btcd/chaincfg"
1313
"github.com/lightninglabs/lndclient"
14-
"github.com/lightninglabs/loop/staticaddr"
1514
"github.com/lightninglabs/loop/staticaddr/script"
1615
"github.com/lightninglabs/loop/staticaddr/version"
1716
"github.com/lightninglabs/loop/swap"

staticaddr/address/manager_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ type mockStaticAddressClient struct {
3232
mock.Mock
3333
}
3434

35+
func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
36+
in *swapserverrpc.ServerWithdrawRequest,
37+
opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse,
38+
error) {
39+
40+
args := m.Called(ctx, in, opts)
41+
42+
return args.Get(0).(*swapserverrpc.ServerWithdrawResponse),
43+
args.Error(1)
44+
}
45+
3546
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
3647
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
3748
*swapserverrpc.ServerNewAddressResponse, error) {

staticaddr/deposit/actions.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,17 @@ func (f *FSM) SweptExpiredDepositAction(_ fsm.EventContext) fsm.EventType {
147147

148148
return fsm.NoOp
149149
}
150+
151+
// WithdrawnDepositAction is the final action after a withdrawal. It signals to
152+
// the manager that the deposit has been swept and the FSM can be removed.
153+
func (f *FSM) WithdrawnDepositAction(_ fsm.EventContext) fsm.EventType {
154+
select {
155+
case <-f.ctx.Done():
156+
return fsm.OnError
157+
158+
default:
159+
f.finalizedDepositChan <- f.deposit.OutPoint
160+
}
161+
162+
return fsm.NoOp
163+
}

staticaddr/deposit/deposit.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type Deposit struct {
3535
// state is the current state of the deposit.
3636
state fsm.StateType
3737

38-
// The outpoint of the deposit.
38+
// Outpoint of the deposit.
3939
wire.OutPoint
4040

4141
// Value is the amount of the deposit.
@@ -52,6 +52,10 @@ type Deposit struct {
5252
// ExpirySweepTxid is the transaction id of the expiry sweep.
5353
ExpirySweepTxid chainhash.Hash
5454

55+
// WithdrawalSweepAddress is the address that is used to
56+
// cooperatively sweep the deposit to before it is expired.
57+
WithdrawalSweepAddress string
58+
5559
sync.Mutex
5660
}
5761

@@ -68,7 +72,7 @@ func (d *Deposit) IsInFinalState() bool {
6872
d.Lock()
6973
defer d.Unlock()
7074

71-
return d.state == Expired || d.state == Failed
75+
return d.state == Expired || d.state == Withdrawn || d.state == Failed
7276
}
7377

7478
func (d *Deposit) IsExpired(currentHeight, expiry uint32) bool {

staticaddr/deposit/fsm.go

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ var (
2929
var (
3030
Deposited = fsm.StateType("Deposited")
3131

32+
Withdrawing = fsm.StateType("Withdrawing")
33+
34+
Withdrawn = fsm.StateType("Withdrawn")
35+
3236
PublishExpiredDeposit = fsm.StateType("PublishExpiredDeposit")
3337

3438
WaitForExpirySweep = fsm.StateType("WaitForExpirySweep")
@@ -40,11 +44,13 @@ var (
4044

4145
// Events.
4246
var (
43-
OnStart = fsm.EventType("OnStart")
44-
OnExpiry = fsm.EventType("OnExpiry")
45-
OnExpiryPublished = fsm.EventType("OnExpiryPublished")
46-
OnExpirySwept = fsm.EventType("OnExpirySwept")
47-
OnRecover = fsm.EventType("OnRecover")
47+
OnStart = fsm.EventType("OnStart")
48+
OnWithdrawInitiated = fsm.EventType("OnWithdrawInitiated")
49+
OnWithdrawn = fsm.EventType("OnWithdrawn")
50+
OnExpiry = fsm.EventType("OnExpiry")
51+
OnExpiryPublished = fsm.EventType("OnExpiryPublished")
52+
OnExpirySwept = fsm.EventType("OnExpirySwept")
53+
OnRecover = fsm.EventType("OnRecover")
4854
)
4955

5056
// FSM is the state machine that handles the instant out.
@@ -118,13 +124,7 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
118124
for {
119125
select {
120126
case currentHeight := <-depoFsm.blockNtfnChan:
121-
err := depoFsm.handleBlockNotification(
122-
currentHeight,
123-
)
124-
if err != nil {
125-
log.Errorf("error handling block "+
126-
"notification: %v", err)
127-
}
127+
depoFsm.handleBlockNotification(currentHeight)
128128

129129
case <-ctx.Done():
130130
return
@@ -138,15 +138,10 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
138138
// handleBlockNotification inspects the current block height and sends the
139139
// OnExpiry event to publish the expiry sweep transaction if the deposit timed
140140
// out, or it republishes the expiry sweep transaction if it was not yet swept.
141-
func (f *FSM) handleBlockNotification(currentHeight uint32) error {
142-
params, err := f.cfg.AddressManager.GetStaticAddressParameters(f.ctx)
143-
if err != nil {
144-
return err
145-
}
146-
141+
func (f *FSM) handleBlockNotification(currentHeight uint32) {
147142
// If the deposit is expired but not yet sufficiently confirmed, we
148143
// republish the expiry sweep transaction.
149-
if f.deposit.IsExpired(currentHeight, params.Expiry) {
144+
if f.deposit.IsExpired(currentHeight, f.params.Expiry) {
150145
if f.deposit.IsInState(WaitForExpirySweep) {
151146
f.PublishDepositExpirySweepAction(nil)
152147
} else {
@@ -159,8 +154,6 @@ func (f *FSM) handleBlockNotification(currentHeight uint32) error {
159154
}()
160155
}
161156
}
162-
163-
return nil
164157
}
165158

166159
// DepositStatesV0 returns the states a deposit can be in.
@@ -174,8 +167,9 @@ func (f *FSM) DepositStatesV0() fsm.States {
174167
},
175168
Deposited: fsm.State{
176169
Transitions: fsm.Transitions{
177-
OnExpiry: PublishExpiredDeposit,
178-
OnRecover: Deposited,
170+
OnExpiry: PublishExpiredDeposit,
171+
OnWithdrawInitiated: Withdrawing,
172+
OnRecover: Deposited,
179173
},
180174
Action: fsm.NoOpAction,
181175
},
@@ -210,6 +204,36 @@ func (f *FSM) DepositStatesV0() fsm.States {
210204
},
211205
Action: f.SweptExpiredDepositAction,
212206
},
207+
Withdrawing: fsm.State{
208+
Transitions: fsm.Transitions{
209+
OnWithdrawn: Withdrawn,
210+
// Upon recovery, we go back to the Deposited
211+
// state. The deposit by then has a withdrawal
212+
// address stamped to it which will cause it to
213+
// transition into the Withdrawing state again.
214+
OnRecover: Deposited,
215+
216+
// A precondition for the Withdrawing state is
217+
// that the withdrawal transaction has been
218+
// broadcast. If the deposit expires while the
219+
// withdrawal isn't confirmed, we can ignore the
220+
// expiry.
221+
OnExpiry: Withdrawing,
222+
223+
// If the withdrawal failed we go back to
224+
// Deposited, hoping that another withdrawal
225+
// attempt will be successful. Alternatively,
226+
// the client can wait for the timeout sweep.
227+
fsm.OnError: Deposited,
228+
},
229+
Action: fsm.NoOpAction,
230+
},
231+
Withdrawn: fsm.State{
232+
Transitions: fsm.Transitions{
233+
OnExpiry: Expired,
234+
},
235+
Action: f.WithdrawnDepositAction,
236+
},
213237
Failed: fsm.State{
214238
Transitions: fsm.Transitions{
215239
OnExpiry: Failed,

staticaddr/deposit/manager.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package deposit
33
import (
44
"context"
55
"fmt"
6+
"sort"
67
"sync"
78
"time"
89

@@ -11,6 +12,7 @@ import (
1112
"github.com/btcsuite/btcd/wire"
1213
"github.com/lightninglabs/lndclient"
1314
"github.com/lightninglabs/loop"
15+
"github.com/lightninglabs/loop/fsm"
1416
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
1517
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
1618
"github.com/lightningnetwork/lnd/lnwallet"
@@ -29,6 +31,10 @@ const (
2931
// MaxConfs is unset since we don't require a max number of
3032
// confirmations for deposits.
3133
MaxConfs = 0
34+
35+
// DefaultTransitionTimeout is the default timeout for transitions in
36+
// the deposit state machine.
37+
DefaultTransitionTimeout = 1 * time.Minute
3238
)
3339

3440
// ManagerConfig holds the configuration for the address manager.
@@ -128,7 +134,7 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
128134
}
129135

130136
// Start the deposit notifier.
131-
m.pollDeposits(ctx)
137+
m.pollDeposits(m.runCtx)
132138

133139
// Communicate to the caller that the address manager has completed its
134140
// initialization.
@@ -420,3 +426,89 @@ func (m *Manager) finalizeDeposit(outpoint wire.OutPoint) {
420426
delete(m.deposits, outpoint)
421427
m.Unlock()
422428
}
429+
430+
// GetActiveDepositsInState returns all active deposits.
431+
func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) (
432+
[]*Deposit, error) {
433+
434+
m.Lock()
435+
defer m.Unlock()
436+
437+
var deposits []*Deposit
438+
for _, fsm := range m.activeDeposits {
439+
if fsm.deposit.GetState() != stateFilter {
440+
continue
441+
}
442+
deposits = append(deposits, fsm.deposit)
443+
}
444+
445+
sort.Slice(deposits, func(i, j int) bool {
446+
return deposits[i].ConfirmationHeight <
447+
deposits[j].ConfirmationHeight
448+
})
449+
450+
return deposits, nil
451+
}
452+
453+
// GetAllDeposits returns all active deposits.
454+
func (m *Manager) GetAllDeposits() ([]*Deposit, error) {
455+
return m.cfg.Store.AllDeposits(m.runCtx)
456+
}
457+
458+
// AllOutpointsActiveDeposits checks if all deposits referenced by the outpoints
459+
// are active and in the specified state.
460+
func (m *Manager) AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
461+
stateFilter fsm.StateType) ([]*Deposit, bool) {
462+
463+
m.Lock()
464+
defer m.Unlock()
465+
466+
deposits := make([]*Deposit, 0, len(outpoints))
467+
for _, o := range outpoints {
468+
if _, ok := m.activeDeposits[o]; !ok {
469+
return nil, false
470+
}
471+
472+
deposit := m.deposits[o]
473+
if deposit.GetState() != stateFilter {
474+
return nil, false
475+
}
476+
477+
deposits = append(deposits, m.deposits[o])
478+
}
479+
480+
return deposits, true
481+
}
482+
483+
// TransitionDeposits allows a caller to transition a set of deposits to a new
484+
// state.
485+
func (m *Manager) TransitionDeposits(deposits []*Deposit, event fsm.EventType,
486+
expectedFinalState fsm.StateType) error {
487+
488+
for _, d := range deposits {
489+
m.Lock()
490+
sm, ok := m.activeDeposits[d.OutPoint]
491+
m.Unlock()
492+
if !ok {
493+
return fmt.Errorf("deposit not found")
494+
}
495+
496+
err := sm.SendEvent(event, nil)
497+
if err != nil {
498+
return err
499+
}
500+
err = sm.DefaultObserver.WaitForState(
501+
m.runCtx, DefaultTransitionTimeout, expectedFinalState,
502+
)
503+
if err != nil {
504+
return err
505+
}
506+
}
507+
508+
return nil
509+
}
510+
511+
// UpdateDeposit overrides all fields of the deposit with given ID in the store.
512+
func (m *Manager) UpdateDeposit(d *Deposit) error {
513+
return m.cfg.Store.UpdateDeposit(m.runCtx, d)
514+
}

staticaddr/deposit/manager_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ type mockStaticAddressClient struct {
4949
mock.Mock
5050
}
5151

52+
func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
53+
in *swapserverrpc.ServerWithdrawRequest,
54+
opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse,
55+
error) {
56+
57+
args := m.Called(ctx, in, opts)
58+
59+
return args.Get(0).(*swapserverrpc.ServerWithdrawResponse),
60+
args.Error(1)
61+
}
62+
5263
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
5364
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
5465
*swapserverrpc.ServerNewAddressResponse, error) {
@@ -149,16 +160,15 @@ func (m *MockChainNotifier) RegisterSpendNtfn(ctx context.Context,
149160
// TestManager checks that the manager processes the right channel notifications
150161
// while a deposit is expiring.
151162
func TestManager(t *testing.T) {
152-
ctxb, cancel := context.WithCancel(context.Background())
153-
defer cancel()
163+
ctx := context.Background()
154164

155165
// Create the test context with required mocks.
156166
testContext := newManagerTestContext(t)
157167

158168
// Start the deposit manager.
159169
go func() {
160170
err := testContext.manager.Run(
161-
ctxb, uint32(testContext.mockLnd.Height),
171+
ctx, uint32(testContext.mockLnd.Height),
162172
)
163173
require.NoError(t, err)
164174
}()
@@ -191,6 +201,9 @@ func TestManager(t *testing.T) {
191201
BlockHeight: defaultDepositConfirmations + defaultExpiry + 3,
192202
Tx: expiryTx,
193203
}
204+
205+
// Ensure that the deposit is finalized.
206+
<-finalizedDepositChan
194207
}
195208

196209
// ManagerTestContext is a helper struct that contains all the necessary

0 commit comments

Comments
 (0)