Skip to content

Commit 370e094

Browse files
committed
feat: add hot wallet low liquidity alert
1 parent df3463b commit 370e094

File tree

10 files changed

+383
-32
lines changed

10 files changed

+383
-32
lines changed

internal/adapters/entrypoints/watcher/liquidity_check.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,34 @@ package watcher
22

33
import (
44
"context"
5+
"time"
6+
57
"github.com/rsksmart/liquidity-provider-server/internal/entities/utils"
68
"github.com/rsksmart/liquidity-provider-server/internal/usecases/liquidity_provider"
79
log "github.com/sirupsen/logrus"
8-
"time"
910
)
1011

1112
type LiquidityCheckWatcher struct {
12-
checkLiquidityUseCase *liquidity_provider.CheckLiquidityUseCase
13-
watcherStopChannel chan bool
14-
ticker utils.Ticker
15-
validationTimeout time.Duration
13+
checkLiquidityUseCase *liquidity_provider.CheckLiquidityUseCase
14+
lowLiquidityAlertUseCase *liquidity_provider.LowLiquidityAlertUseCase
15+
watcherStopChannel chan bool
16+
ticker utils.Ticker
17+
validationTimeout time.Duration
1618
}
1719

1820
func NewLiquidityCheckWatcher(
1921
checkLiquidityUseCase *liquidity_provider.CheckLiquidityUseCase,
22+
lowLiquidityAlertUseCase *liquidity_provider.LowLiquidityAlertUseCase,
2023
ticker utils.Ticker,
2124
validationTimeout time.Duration,
2225
) *LiquidityCheckWatcher {
2326
watcherStopChannel := make(chan bool, 1)
2427
return &LiquidityCheckWatcher{
25-
checkLiquidityUseCase: checkLiquidityUseCase,
26-
watcherStopChannel: watcherStopChannel,
27-
ticker: ticker,
28-
validationTimeout: validationTimeout,
28+
checkLiquidityUseCase: checkLiquidityUseCase,
29+
lowLiquidityAlertUseCase: lowLiquidityAlertUseCase,
30+
watcherStopChannel: watcherStopChannel,
31+
ticker: ticker,
32+
validationTimeout: validationTimeout,
2933
}
3034
}
3135

@@ -46,6 +50,9 @@ watcherLoop:
4650
if err := watcher.checkLiquidityUseCase.Run(ctx); err != nil {
4751
log.Error("Error checking liquidity inside watcher: ", err)
4852
}
53+
if err := watcher.lowLiquidityAlertUseCase.Run(ctx); err != nil {
54+
log.Error("Error checking low liquidity inside watcher: ", err)
55+
}
4956
cancel()
5057
case <-watcher.watcherStopChannel:
5158
watcher.ticker.Stop()

internal/adapters/entrypoints/watcher/liquidity_check_test.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package watcher_test
22

33
import (
44
"context"
5+
"sync"
6+
"testing"
7+
"time"
8+
59
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/watcher"
610
"github.com/rsksmart/liquidity-provider-server/internal/entities"
711
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
@@ -12,22 +16,20 @@ import (
1216
"github.com/stretchr/testify/assert"
1317
"github.com/stretchr/testify/mock"
1418
"github.com/stretchr/testify/require"
15-
"sync"
16-
"testing"
17-
"time"
1819
)
1920

2021
func TestLiquidityCheckWatcher_Shutdown(t *testing.T) {
2122
createWatcherShutdownTest(t, func(ticker utils.Ticker) watcher.Watcher {
22-
return watcher.NewLiquidityCheckWatcher(nil, ticker, time.Duration(1))
23+
return watcher.NewLiquidityCheckWatcher(nil, nil, ticker, time.Duration(1))
2324
})
2425
}
2526

2627
func TestNewLiquidityCheckWatcher(t *testing.T) {
2728
ticker := &mocks.TickerMock{}
2829
providerMock := &mocks.ProviderMock{}
29-
useCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{}, &mocks.AlertSenderMock{}, test.AnyString)
30-
test.AssertNonZeroValues(t, watcher.NewLiquidityCheckWatcher(useCase, ticker, time.Duration(1)))
30+
checkLiquidityUseCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{}, &mocks.AlertSenderMock{}, test.AnyString)
31+
lowLiquidityUseCase := liquidity_provider.NewLowLiquidityAlertUseCase(providerMock, providerMock, &mocks.AlertSenderMock{}, test.AnyString, 3, 1)
32+
test.AssertNonZeroValues(t, watcher.NewLiquidityCheckWatcher(checkLiquidityUseCase, lowLiquidityUseCase, ticker, time.Duration(1)))
3133
}
3234

3335
func TestLiquidityCheckWatcher_Start(t *testing.T) {
@@ -38,10 +40,15 @@ func TestLiquidityCheckWatcher_Start(t *testing.T) {
3840
providerMock := &mocks.ProviderMock{}
3941
providerMock.On("HasPeginLiquidity", mock.Anything, mock.Anything).Return(nil)
4042
providerMock.On("HasPegoutLiquidity", mock.Anything, mock.Anything).Return(nil)
43+
providerMock.On("AvailablePeginLiquidity", mock.Anything).Return(entities.NewWei(0), nil)
44+
providerMock.On("AvailablePegoutLiquidity", mock.Anything).Return(entities.NewWei(0), nil)
4145
bridgeMock := &mocks.BridgeMock{}
4246
bridgeMock.On("GetMinimumLockTxValue").Return(entities.NewWei(5), nil)
43-
useCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{Bridge: bridgeMock}, &mocks.AlertSenderMock{}, test.AnyString)
44-
w := watcher.NewLiquidityCheckWatcher(useCase, ticker, time.Duration(1))
47+
alertSenderMock := &mocks.AlertSenderMock{}
48+
alertSenderMock.On("SendAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
49+
checkLiquidityUseCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{Bridge: bridgeMock}, alertSenderMock, test.AnyString)
50+
lowLiquidityUseCase := liquidity_provider.NewLowLiquidityAlertUseCase(providerMock, providerMock, alertSenderMock, test.AnyString, 3, 1)
51+
w := watcher.NewLiquidityCheckWatcher(checkLiquidityUseCase, lowLiquidityUseCase, ticker, time.Duration(1))
4552
wg := sync.WaitGroup{}
4653
wg.Add(2)
4754
closeChannel := make(chan bool)
@@ -68,10 +75,15 @@ func TestLiquidityCheckWatcher_Start_ErrorHandling(t *testing.T) {
6875
ticker.EXPECT().C().Return(tickerChannel)
6976
ticker.EXPECT().Stop().Return()
7077
providerMock := &mocks.ProviderMock{}
78+
providerMock.On("AvailablePeginLiquidity", mock.Anything).Return(entities.NewWei(0), nil)
79+
providerMock.On("AvailablePegoutLiquidity", mock.Anything).Return(entities.NewWei(0), nil)
7180
bridgeMock := &mocks.BridgeMock{}
7281
bridgeMock.On("GetMinimumLockTxValue").Return(nil, assert.AnError)
73-
useCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{Bridge: bridgeMock}, &mocks.AlertSenderMock{}, test.AnyString)
74-
w := watcher.NewLiquidityCheckWatcher(useCase, ticker, time.Duration(1))
82+
alertSenderMock := &mocks.AlertSenderMock{}
83+
alertSenderMock.On("SendAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
84+
checkLiquidityUseCase := liquidity_provider.NewCheckLiquidityUseCase(providerMock, providerMock, blockchain.RskContracts{Bridge: bridgeMock}, alertSenderMock, test.AnyString)
85+
lowLiquidityUseCase := liquidity_provider.NewLowLiquidityAlertUseCase(providerMock, providerMock, alertSenderMock, test.AnyString, 3, 1)
86+
w := watcher.NewLiquidityCheckWatcher(checkLiquidityUseCase, lowLiquidityUseCase, ticker, time.Duration(1))
7587
wg := sync.WaitGroup{}
7688
wg.Add(2)
7789
defer test.AssertLogContains(t, assert.AnError.Error())
@@ -90,6 +102,6 @@ func TestLiquidityCheckWatcher_Start_ErrorHandling(t *testing.T) {
90102
}
91103

92104
func TestLiquidityCheckWatcher_Prepare(t *testing.T) {
93-
w := watcher.NewLiquidityCheckWatcher(nil, &mocks.TickerMock{}, time.Duration(1))
105+
w := watcher.NewLiquidityCheckWatcher(nil, nil, &mocks.TickerMock{}, time.Duration(1))
94106
require.NoError(t, w.Prepare(context.Background()))
95107
}

internal/configuration/environment/environment.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,29 @@ type ManagementEnv struct {
190190
}
191191

192192
type ColdWalletEnv struct {
193-
BtcMinTransferFeeMultiplier uint64 `env:"BTC_MIN_TRANSFER_FEE_MULTIPLIER"`
194-
RbtcMinTransferFeeMultiplier uint64 `env:"RBTC_MIN_TRANSFER_FEE_MULTIPLIER"`
195-
ForceTransferAfterSeconds uint64 `env:"COLD_WALLET_FORCE_TRANSFER_AFTER_SECONDS"`
193+
BtcMinTransferFeeMultiplier uint64 `env:"BTC_MIN_TRANSFER_FEE_MULTIPLIER"`
194+
RbtcMinTransferFeeMultiplier uint64 `env:"RBTC_MIN_TRANSFER_FEE_MULTIPLIER"`
195+
ForceTransferAfterSeconds uint64 `env:"COLD_WALLET_FORCE_TRANSFER_AFTER_SECONDS"`
196+
HotWalletLowLiquidityWarning uint64 `env:"HOT_WALLET_LOW_LIQUIDITY_WARNING"`
197+
HotWalletLowLiquidityCritical uint64 `env:"HOT_WALLET_LOW_LIQUIDITY_CRITICAL"`
196198
}
197199

198200
func (env *ColdWalletEnv) FillWithDefaults() *ColdWalletEnv {
199201
defaults := ColdWalletEnv{
200-
BtcMinTransferFeeMultiplier: 5,
201-
RbtcMinTransferFeeMultiplier: 100,
202-
ForceTransferAfterSeconds: 1209600, // 2 weeks (14 days * 24 hours * 60 minutes * 60 seconds)
202+
BtcMinTransferFeeMultiplier: 5,
203+
RbtcMinTransferFeeMultiplier: 100,
204+
ForceTransferAfterSeconds: 1209600, // 2 weeks (14 days * 24 hours * 60 minutes * 60 seconds)
205+
HotWalletLowLiquidityWarning: 3,
206+
HotWalletLowLiquidityCritical: 1,
203207
}
204208
env.BtcMinTransferFeeMultiplier = utils.FirstNonZero(env.BtcMinTransferFeeMultiplier, defaults.BtcMinTransferFeeMultiplier)
205209
env.RbtcMinTransferFeeMultiplier = utils.FirstNonZero(env.RbtcMinTransferFeeMultiplier, defaults.RbtcMinTransferFeeMultiplier)
206210
env.ForceTransferAfterSeconds = utils.FirstNonZero(env.ForceTransferAfterSeconds, defaults.ForceTransferAfterSeconds)
211+
env.HotWalletLowLiquidityWarning = utils.FirstNonZero(env.HotWalletLowLiquidityWarning, defaults.HotWalletLowLiquidityWarning)
212+
env.HotWalletLowLiquidityCritical = utils.FirstNonZero(env.HotWalletLowLiquidityCritical, defaults.HotWalletLowLiquidityCritical)
213+
if env.HotWalletLowLiquidityCritical >= env.HotWalletLowLiquidityWarning {
214+
log.Fatal("HOT_WALLET_LOW_LIQUIDITY_CRITICAL must be less than HOT_WALLET_LOW_LIQUIDITY_WARNING")
215+
}
207216
return env
208217
}
209218

internal/configuration/registry/usecase.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type UseCaseRegistry struct {
8282
recommendedPeginUseCase *pegin.RecommendedPeginUseCase
8383
transferExcessToColdWalletUseCase *liquidity_provider.TransferExcessToColdWalletUseCase
8484
checkColdWalletAddressChangeUseCase *liquidity_provider.CheckColdWalletAddressChangeUseCase
85+
lowLiquidityAlertUseCase *liquidity_provider.LowLiquidityAlertUseCase
8586
}
8687

8788
// NewUseCaseRegistry
@@ -391,6 +392,17 @@ func NewUseCaseRegistry(
391392
messaging.AlertSender,
392393
env.Provider.AlertRecipientEmail,
393394
),
395+
lowLiquidityAlertUseCase: func() *liquidity_provider.LowLiquidityAlertUseCase {
396+
coldWallet := env.ColdWallet.FillWithDefaults()
397+
return liquidity_provider.NewLowLiquidityAlertUseCase(
398+
lpRegistry.LiquidityProvider,
399+
lpRegistry.LiquidityProvider,
400+
messaging.AlertSender,
401+
env.Provider.AlertRecipientEmail,
402+
coldWallet.HotWalletLowLiquidityWarning,
403+
coldWallet.HotWalletLowLiquidityCritical,
404+
)
405+
}(),
394406
checkColdWalletAddressChangeUseCase: liquidity_provider.NewCheckColdWalletAddressChangeUseCase(
395407
databaseRegistry.LiquidityProviderRepository,
396408
lpRegistry.LiquidityProvider,
@@ -578,3 +590,7 @@ func (registry *UseCaseRegistry) TransferExcessToColdWalletUseCase() *liquidity_
578590
func (registry *UseCaseRegistry) CheckColdWalletAddressChangeUseCase() *liquidity_provider.CheckColdWalletAddressChangeUseCase {
579591
return registry.checkColdWalletAddressChangeUseCase
580592
}
593+
594+
func (registry *UseCaseRegistry) LowLiquidityAlertUseCase() *liquidity_provider.LowLiquidityAlertUseCase {
595+
return registry.lowLiquidityAlertUseCase
596+
}

internal/configuration/registry/watcher.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func NewWatcherRegistry(
8989
),
9090
LiquidityCheckWatcher: watcher.NewLiquidityCheckWatcher(
9191
useCaseRegistry.liquidityCheckUseCase,
92+
useCaseRegistry.lowLiquidityAlertUseCase,
9293
tickers.LiquidityCheckTicker,
9394
timeouts.WatcherValidation.Seconds(),
9495
),

internal/entities/alerts/alerts.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import "context"
66
// Changing one of these constants impacts the external alerting system and there is not an automatic way
77
// to identify that error.
88
const (
9-
AlertSubjectPenalization = "LPS has been penalized"
10-
AlertSubjectPeginOutOfLiquidity = "PegIn: Out of liquidity"
11-
AlertSubjectPegoutOutOfLiquidity = "PegOut: Out of liquidity"
12-
AlertSubjectEclipseAttack = "Node Eclipse Detected"
13-
AlertSubjectColdWalletChange = "Cold wallet address changed"
14-
AlertSubjectHotToColdTransfer = "Hot to cold wallet transfer executed"
9+
AlertSubjectPenalization = "LPS has been penalized"
10+
AlertSubjectPeginOutOfLiquidity = "PegIn: Out of liquidity"
11+
AlertSubjectPegoutOutOfLiquidity = "PegOut: Out of liquidity"
12+
AlertSubjectEclipseAttack = "Node Eclipse Detected"
13+
AlertSubjectColdWalletChange = "Cold wallet address changed"
14+
AlertSubjectHotToColdTransfer = "Hot to cold wallet transfer executed"
15+
AlertSubjectHotWalletLowLiquidityWarning = "Hot wallet: Low liquidity, refill recommended"
16+
AlertSubjectHotWalletLowLiquidityCritical = "Hot wallet: Critical low liquidity, refill required"
1517
)
1618

1719
type AlertSender interface {

internal/entities/wei.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ func SatoshiToWei(x uint64) *Wei {
5656
return w
5757
}
5858

59+
func CoinToWei(x uint64) *Wei {
60+
coin := new(big.Int).SetUint64(x)
61+
w := new(Wei)
62+
w.AsBigInt().Mul(coin, bTenPowEighteen)
63+
return w
64+
}
65+
5966
func (w *Wei) Copy() *Wei {
6067
return NewBigWei(w.AsBigInt())
6168
}

internal/usecases/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const (
8282
RecommendedPeginId UseCaseId = "RecommendedPegin"
8383
TransferExcessToColdWalletId UseCaseId = "TransferExcessToColdWallet"
8484
CheckColdWalletAddressChangeId UseCaseId = "CheckColdWalletAddressChange"
85+
LowLiquidityAlertId UseCaseId = "LowLiquidityAlert"
8586
)
8687

8788
var (
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package liquidity_provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities"
8+
"github.com/rsksmart/liquidity-provider-server/internal/entities/alerts"
9+
"github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider"
10+
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
11+
log "github.com/sirupsen/logrus"
12+
)
13+
14+
type LowLiquidityAlertUseCase struct {
15+
peginProvider liquidity_provider.PeginLiquidityProvider
16+
pegoutProvider liquidity_provider.PegoutLiquidityProvider
17+
alertSender alerts.AlertSender
18+
recipient string
19+
warningThreshold uint64
20+
criticalThreshold uint64
21+
}
22+
23+
func NewLowLiquidityAlertUseCase(
24+
peginProvider liquidity_provider.PeginLiquidityProvider,
25+
pegoutProvider liquidity_provider.PegoutLiquidityProvider,
26+
alertSender alerts.AlertSender,
27+
recipient string,
28+
warningThreshold uint64,
29+
criticalThreshold uint64,
30+
) *LowLiquidityAlertUseCase {
31+
return &LowLiquidityAlertUseCase{
32+
peginProvider: peginProvider,
33+
pegoutProvider: pegoutProvider,
34+
alertSender: alertSender,
35+
recipient: recipient,
36+
warningThreshold: warningThreshold,
37+
criticalThreshold: criticalThreshold,
38+
}
39+
}
40+
41+
func (useCase *LowLiquidityAlertUseCase) Run(ctx context.Context) error {
42+
btcLiquidity, err := useCase.pegoutProvider.AvailablePegoutLiquidity(ctx)
43+
if err != nil {
44+
return usecases.WrapUseCaseError(usecases.LowLiquidityAlertId, err)
45+
}
46+
rbtcLiquidity, err := useCase.peginProvider.AvailablePeginLiquidity(ctx)
47+
if err != nil {
48+
return usecases.WrapUseCaseError(usecases.LowLiquidityAlertId, err)
49+
}
50+
51+
warningWei := entities.CoinToWei(useCase.warningThreshold)
52+
criticalWei := entities.CoinToWei(useCase.criticalThreshold)
53+
54+
useCase.checkAndAlert(ctx, "BTC", btcLiquidity, warningWei, criticalWei)
55+
useCase.checkAndAlert(ctx, "RBTC", rbtcLiquidity, warningWei, criticalWei)
56+
57+
return nil
58+
}
59+
60+
func (useCase *LowLiquidityAlertUseCase) checkAndAlert(
61+
ctx context.Context,
62+
network string,
63+
current *entities.Wei,
64+
warningThreshold, criticalThreshold *entities.Wei,
65+
) {
66+
if current.Cmp(criticalThreshold) < 0 {
67+
useCase.sendAlert(ctx, network, current, criticalThreshold, alerts.AlertSubjectHotWalletLowLiquidityCritical)
68+
return
69+
}
70+
if current.Cmp(warningThreshold) < 0 {
71+
useCase.sendAlert(ctx, network, current, warningThreshold, alerts.AlertSubjectHotWalletLowLiquidityWarning)
72+
}
73+
}
74+
75+
func (useCase *LowLiquidityAlertUseCase) sendAlert(
76+
ctx context.Context,
77+
network string,
78+
current, threshold *entities.Wei,
79+
subject string,
80+
) {
81+
body := fmt.Sprintf("Network: %s | Current: %s | Threshold: %s",
82+
network,
83+
current.ToRbtc().Text('f', 18),
84+
threshold.ToRbtc().Text('f', 18),
85+
)
86+
if err := useCase.alertSender.SendAlert(ctx, subject, body, []string{useCase.recipient}); err != nil {
87+
log.Error("Error sending low liquidity alert: ", err)
88+
}
89+
}

0 commit comments

Comments
 (0)