Skip to content

Commit 4b4fdd3

Browse files
committed
test: add unit tests for the transfer excess to cold wallet feature
1 parent e9ac2f1 commit 4b4fdd3

File tree

7 files changed

+2677
-3
lines changed

7 files changed

+2677
-3
lines changed

internal/adapters/dataproviders/bitcoin/derivative_wallet_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,21 @@ func TestDerivativeWallet(t *testing.T) {
227227
})
228228
})
229229

230+
t.Run("Send", func(t *testing.T) {
231+
t.Run("Success", func(t *testing.T) { testSend(t, rskAccount, existingAddressInfo) })
232+
t.Run("Error handling on btc tx sending", func(t *testing.T) {
233+
cases := derivativeWalletSendErrorSetups(rskAccount)
234+
for _, testCase := range cases {
235+
client := &mocks.ClientAdapterMock{}
236+
client.On("GetWalletInfo").Return(&btcjson.GetWalletInfoResult{WalletName: bitcoin.DerivativeWalletId, Scanning: btcjson.ScanningOrFalse{Value: false}}, nil).Twice()
237+
client.On("GetAddressInfo", btcAddress).Return(existingAddressInfo, nil).Once()
238+
t.Run(testCase.description, func(t *testing.T) {
239+
testCase.setup(t, client)
240+
})
241+
}
242+
})
243+
})
244+
230245
t.Run("SendWithOpReturn", func(t *testing.T) {
231246
t.Run("Success", func(t *testing.T) { testSendWithOpReturn(t, rskAccount, existingAddressInfo) })
232247
t.Run("Error handling on btc tx sending", func(t *testing.T) {
@@ -468,6 +483,192 @@ func testEstimateFeesExtra(t *testing.T, rskAccount *account.RskAccount, address
468483
client.AssertExpectations(t)
469484
}
470485

486+
func testSend(t *testing.T, rskAccount *account.RskAccount, addressInfo *btcjson.GetAddressInfoResult) {
487+
client := &mocks.ClientAdapterMock{}
488+
value := entities.NewWei(600000000000000000)
489+
satoshis, _ := value.ToSatoshi().Float64()
490+
address, err := btcutil.DecodeAddress(testnetAddress, &chaincfg.TestNet3Params)
491+
require.NoError(t, err)
492+
client.On("GetWalletInfo").Return(&btcjson.GetWalletInfoResult{WalletName: bitcoin.DerivativeWalletId, Scanning: btcjson.ScanningOrFalse{Value: false}}, nil).Twice()
493+
client.On("GetAddressInfo", btcAddress).Return(addressInfo, nil).Once()
494+
client.On("CreateRawTransaction",
495+
([]btcjson.TransactionInput)(nil),
496+
mock.MatchedBy(func(outputs map[btcutil.Address]btcutil.Amount) bool {
497+
for k, v := range outputs {
498+
require.Equal(t, address, k)
499+
require.Equal(t, btcutil.Amount(satoshis), v)
500+
}
501+
return len(outputs) == 1
502+
}),
503+
(*int64)(nil),
504+
).Return(&wire.MsgTx{
505+
Version: 0,
506+
TxIn: nil,
507+
TxOut: []*wire.TxOut{{Value: int64(satoshis), PkScript: []byte(paymentScriptMock)}},
508+
LockTime: 0,
509+
}, nil).Once()
510+
tx := &wire.MsgTx{
511+
Version: 0,
512+
TxIn: nil,
513+
TxOut: []*wire.TxOut{{Value: int64(satoshis), PkScript: []byte(paymentScriptMock)}},
514+
LockTime: 0,
515+
}
516+
client.On("EstimateSmartFee", int64(1), &btcjson.EstimateModeEconomical).Return(&btcjson.EstimateSmartFeeResult{FeeRate: btcjson.Float64(feeRate), Blocks: 2}, nil).Once()
517+
client.On("FundRawTransaction", tx, btcjson.FundRawTransactionOpts{
518+
ChangeAddress: btcjson.String(btcAddress),
519+
ChangePosition: btcjson.Int(1),
520+
IncludeWatching: btcjson.Bool(true),
521+
LockUnspents: btcjson.Bool(true),
522+
FeeRate: btcjson.Float64(feeRate),
523+
Replaceable: btcjson.Bool(true),
524+
}, (*bool)(nil)).Return(&btcjson.FundRawTransactionResult{Transaction: tx, Fee: 50, ChangePosition: 1}, nil).Once()
525+
client.On("SendRawTransaction", tx, false).Return(chainhash.NewHashFromStr(testnetTestTxHash)).Once()
526+
client.On("SignRawTransactionWithKey", tx, mock.MatchedBy(func(pks []string) bool {
527+
return len(pks) == 1 && pks[0] != ""
528+
})).Return(tx, true, nil).Once()
529+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
530+
require.NoError(t, err)
531+
result, err := wallet.Send(testnetAddress, value)
532+
require.NoError(t, err)
533+
assert.NotEmpty(t, result.Hash)
534+
assert.NotNil(t, result.Fee)
535+
expectedFee := entities.SatoshiToWei(50)
536+
assert.Equal(t, expectedFee, result.Fee)
537+
client.AssertExpectations(t)
538+
}
539+
540+
// nolint:funlen
541+
func derivativeWalletSendErrorSetups(rskAccount *account.RskAccount) []struct {
542+
description string
543+
setup func(t *testing.T, client *mocks.ClientAdapterMock)
544+
} {
545+
rawTx := &wire.MsgTx{TxOut: []*wire.TxOut{{Value: int64(50000000), PkScript: []byte(paymentScriptMock)}}}
546+
return []struct {
547+
description string
548+
setup func(t *testing.T, client *mocks.ClientAdapterMock)
549+
}{
550+
{
551+
description: "error parsing address",
552+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
553+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
554+
require.NoError(t, err)
555+
result, err := wallet.Send(test.AnyString, entities.NewWei(1))
556+
require.Error(t, err)
557+
assert.Empty(t, result.Hash)
558+
assert.Nil(t, result.Fee)
559+
client.AssertExpectations(t)
560+
},
561+
},
562+
{
563+
description: "error creating raw tx",
564+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
565+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
566+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
567+
require.NoError(t, err)
568+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
569+
require.Error(t, err)
570+
assert.Empty(t, result.Hash)
571+
assert.Nil(t, result.Fee)
572+
client.AssertExpectations(t)
573+
},
574+
},
575+
{
576+
description: "error estimating fees",
577+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
578+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
579+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
580+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
581+
require.NoError(t, err)
582+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
583+
require.Error(t, err)
584+
assert.Empty(t, result.Hash)
585+
assert.Nil(t, result.Fee)
586+
client.AssertExpectations(t)
587+
},
588+
},
589+
{
590+
description: "error estimating fees (RPC error)",
591+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
592+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
593+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(&btcjson.EstimateSmartFeeResult{
594+
Errors: []string{assert.AnError.Error()},
595+
}, nil).Once()
596+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
597+
require.NoError(t, err)
598+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
599+
require.Error(t, err)
600+
assert.Empty(t, result.Hash)
601+
assert.Nil(t, result.Fee)
602+
client.AssertExpectations(t)
603+
},
604+
},
605+
{
606+
description: "error funding raw tx",
607+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
608+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
609+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(&btcjson.EstimateSmartFeeResult{FeeRate: btcjson.Float64(feeRate), Blocks: 1}, nil).Once()
610+
client.On("FundRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
611+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
612+
require.NoError(t, err)
613+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
614+
require.Error(t, err)
615+
assert.Empty(t, result.Hash)
616+
assert.Nil(t, result.Fee)
617+
client.AssertExpectations(t)
618+
},
619+
},
620+
{
621+
description: "error signing tx",
622+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
623+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
624+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(&btcjson.EstimateSmartFeeResult{FeeRate: btcjson.Float64(feeRate), Blocks: 1}, nil).Once()
625+
client.On("FundRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&btcjson.FundRawTransactionResult{Transaction: rawTx, Fee: 50, ChangePosition: 1}, nil).Once()
626+
client.On("SignRawTransactionWithKey", mock.Anything, mock.Anything).Return(nil, false, assert.AnError).Once()
627+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
628+
require.NoError(t, err)
629+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
630+
require.Error(t, err)
631+
assert.Empty(t, result.Hash)
632+
assert.Nil(t, result.Fee)
633+
client.AssertExpectations(t)
634+
},
635+
},
636+
{
637+
description: "error signing tx (incomplete signatures)",
638+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
639+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
640+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(&btcjson.EstimateSmartFeeResult{FeeRate: btcjson.Float64(feeRate), Blocks: 1}, nil).Once()
641+
client.On("FundRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&btcjson.FundRawTransactionResult{Transaction: rawTx, Fee: 50, ChangePosition: 1}, nil).Once()
642+
client.On("SignRawTransactionWithKey", mock.Anything, mock.Anything).Return(rawTx, false, nil).Once()
643+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
644+
require.NoError(t, err)
645+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
646+
require.Error(t, err)
647+
assert.Empty(t, result.Hash)
648+
assert.Nil(t, result.Fee)
649+
client.AssertExpectations(t)
650+
},
651+
},
652+
{
653+
description: "error sending tx",
654+
setup: func(t *testing.T, client *mocks.ClientAdapterMock) {
655+
client.On("CreateRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(rawTx, nil).Once()
656+
client.On("EstimateSmartFee", mock.Anything, mock.Anything).Return(&btcjson.EstimateSmartFeeResult{FeeRate: btcjson.Float64(feeRate), Blocks: 1}, nil).Once()
657+
client.On("FundRawTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&btcjson.FundRawTransactionResult{Transaction: rawTx, Fee: 50, ChangePosition: 1}, nil).Once()
658+
client.On("SignRawTransactionWithKey", mock.Anything, mock.Anything).Return(rawTx, true, nil).Once()
659+
client.On("SendRawTransaction", mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
660+
wallet, err := bitcoin.NewDerivativeWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.DerivativeWalletId), rskAccount)
661+
require.NoError(t, err)
662+
result, err := wallet.Send(testnetAddress, entities.NewWei(1))
663+
require.Error(t, err)
664+
assert.Empty(t, result.Hash)
665+
assert.Nil(t, result.Fee)
666+
client.AssertExpectations(t)
667+
},
668+
},
669+
}
670+
}
671+
471672
func testSendWithOpReturn(t *testing.T, rskAccount *account.RskAccount, addressInfo *btcjson.GetAddressInfoResult) {
472673
client := &mocks.ClientAdapterMock{}
473674
value := entities.NewWei(600000000000000000)

internal/adapters/dataproviders/bitcoin/watchonly_wallet_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@ func TestWatchOnlyWallet_Unlock(t *testing.T) {
296296
require.ErrorContains(t, err, "watch-only wallet does not support unlocking as it only has monitoring purposes")
297297
}
298298

299+
func TestWatchOnlyWallet_Send(t *testing.T) {
300+
client := &mocks.ClientAdapterMock{}
301+
client.On("GetWalletInfo").Return(&btcjson.GetWalletInfoResult{PrivateKeysEnabled: false}, nil).Once()
302+
wallet, err := bitcoin.NewWatchOnlyWallet(bitcoin.NewWalletConnection(&chaincfg.TestNet3Params, client, bitcoin.PeginWalletId))
303+
require.NoError(t, err)
304+
result, err := wallet.Send("address", nil)
305+
require.ErrorContains(t, err, "cannot send from a watch-only wallet")
306+
require.Empty(t, result.Hash)
307+
require.Nil(t, result.Fee)
308+
}
309+
299310
func TestWatchOnlyWallet_SendWithOpReturn(t *testing.T) {
300311
client := &mocks.ClientAdapterMock{}
301312
client.On("GetWalletInfo").Return(&btcjson.GetWalletInfoResult{PrivateKeysEnabled: false}, nil).Once()

internal/adapters/dataproviders/database/mongo/liquidity_provider_test.go

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package mongo_test
33
import (
44
"context"
55

6+
"testing"
7+
"time"
8+
69
"github.com/rsksmart/liquidity-provider-server/internal/adapters/dataproviders/database/mongo"
710
"github.com/rsksmart/liquidity-provider-server/internal/entities"
811
"github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider"
@@ -16,8 +19,6 @@ import (
1619
"go.mongodb.org/mongo-driver/bson/primitive"
1720
mongoDb "go.mongodb.org/mongo-driver/mongo"
1821
"go.mongodb.org/mongo-driver/mongo/options"
19-
"testing"
20-
"time"
2122
)
2223

2324
var peginTestConfig = &entities.Signed[liquidity_provider.PeginConfiguration]{
@@ -83,6 +84,20 @@ var testCredentials = &entities.Signed[liquidity_provider.HashedCredentials]{
8384
Hash: "credentials hash",
8485
}
8586

87+
var (
88+
lastBtcToColdWalletTransfer = time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
89+
lastRbtcToColdWalletTransfer = time.Date(2024, 1, 20, 14, 45, 0, 0, time.UTC)
90+
)
91+
92+
var testStateConfig = &entities.Signed[liquidity_provider.StateConfiguration]{
93+
Value: liquidity_provider.StateConfiguration{
94+
LastBtcToColdWalletTransfer: &lastBtcToColdWalletTransfer,
95+
LastRbtcToColdWalletTransfer: &lastRbtcToColdWalletTransfer,
96+
},
97+
Signature: "state signature",
98+
Hash: "state hash",
99+
}
100+
86101
func TestLpMongoRepository_GetPeginConfiguration(t *testing.T) {
87102
filter := bson.D{primitive.E{Key: "name", Value: mongo.ConfigurationName("pegin")}}
88103
log.SetLevel(log.DebugLevel)
@@ -328,3 +343,63 @@ func TestLpMongoRepository_UpsertCredentials(t *testing.T) {
328343
require.Error(t, err)
329344
})
330345
}
346+
347+
func TestLpMongoRepository_GetStateConfiguration(t *testing.T) {
348+
filter := bson.D{primitive.E{Key: "name", Value: mongo.ConfigurationName("state")}}
349+
log.SetLevel(log.DebugLevel)
350+
t.Run("state configuration read successfully", func(t *testing.T) {
351+
client, collection := getClientAndCollectionMocks(mongo.LiquidityProviderCollection)
352+
repo := mongo.NewLiquidityProviderRepository(mongo.NewConnection(client, time.Duration(1)))
353+
collection.On("FindOne", mock.Anything, filter).
354+
Return(mongoDb.NewSingleResultFromDocument(testStateConfig, nil, nil)).Once()
355+
defer test.AssertNoLog(t)()
356+
result, err := repo.GetStateConfiguration(context.Background())
357+
require.NoError(t, err)
358+
assert.Equal(t, testStateConfig, result)
359+
})
360+
t.Run("state configuration not found", func(t *testing.T) {
361+
client, collection := getClientAndCollectionMocks(mongo.LiquidityProviderCollection)
362+
repo := mongo.NewLiquidityProviderRepository(mongo.NewConnection(client, time.Duration(1)))
363+
collection.On("FindOne", mock.Anything, filter).
364+
Return(
365+
mongoDb.NewSingleResultFromDocument(entities.Signed[liquidity_provider.StateConfiguration]{}, mongoDb.ErrNoDocuments, nil),
366+
).Once()
367+
result, err := repo.GetStateConfiguration(context.Background())
368+
require.NoError(t, err)
369+
assert.Nil(t, result)
370+
})
371+
t.Run("Db error reading state configuration", func(t *testing.T) {
372+
client, collection := getClientAndCollectionMocks(mongo.LiquidityProviderCollection)
373+
repo := mongo.NewLiquidityProviderRepository(mongo.NewConnection(client, time.Duration(1)))
374+
collection.On("FindOne", mock.Anything, filter).Return(mongoDb.NewSingleResultFromDocument(nil, assert.AnError, nil)).Once()
375+
result, err := repo.GetStateConfiguration(context.Background())
376+
require.Error(t, err)
377+
assert.Nil(t, result)
378+
})
379+
}
380+
381+
func TestLpMongoRepository_UpsertStateConfiguration(t *testing.T) {
382+
log.SetLevel(log.DebugLevel)
383+
configName := mongo.ConfigurationName("state")
384+
filter := bson.D{primitive.E{Key: "name", Value: configName}}
385+
t.Run("state configuration upserted successfully", func(t *testing.T) {
386+
client, collection := getClientAndCollectionMocks(mongo.LiquidityProviderCollection)
387+
repo := mongo.NewLiquidityProviderRepository(mongo.NewConnection(client, time.Duration(1)))
388+
collection.On("ReplaceOne", mock.Anything, filter, mongo.StoredConfiguration[liquidity_provider.StateConfiguration]{
389+
Signed: *testStateConfig,
390+
Name: configName,
391+
}, options.Replace().SetUpsert(true)).
392+
Return(nil, nil).Once()
393+
defer test.AssertNoLog(t)()
394+
err := repo.UpsertStateConfiguration(context.Background(), *testStateConfig)
395+
require.NoError(t, err)
396+
})
397+
t.Run("Db error upserting state configuration", func(t *testing.T) {
398+
client, collection := getClientAndCollectionMocks(mongo.LiquidityProviderCollection)
399+
repo := mongo.NewLiquidityProviderRepository(mongo.NewConnection(client, time.Duration(1)))
400+
collection.On("ReplaceOne", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
401+
Return(nil, assert.AnError).Once()
402+
err := repo.UpsertStateConfiguration(context.Background(), *testStateConfig)
403+
require.Error(t, err)
404+
})
405+
}

internal/adapters/entrypoints/rest/routes/public_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ func TestGetPublicEndpoints(t *testing.T) {
3535
registryMock.EXPECT().GetServerInfoUseCase().Return(&liquidity_provider.ServerInfoUseCase{})
3636
registryMock.EXPECT().RecommendedPegoutUseCase().Return(&pegout.RecommendedPegoutUseCase{})
3737
registryMock.EXPECT().RecommendedPeginUseCase().Return(&pegin.RecommendedPeginUseCase{})
38-
registryMock.EXPECT().TransferExcessToColdWalletUseCase().Return(&liquidity_provider.TransferExcessToColdWalletUseCase{})
3938

4039
endpoints := routes.GetPublicEndpoints(registryMock)
4140
specBytes := test.ReadFile(t, "OpenApi.yml")

internal/adapters/entrypoints/rest/routes/routes_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func assertHasCsrfMiddleware(t *testing.T, router *mux.Router, endpoint routes.E
212212
assert.NotEqual(t, -1, i, "response does not have CSRF cookie")
213213
}
214214

215+
// nolint:funlen
215216
func setupRegistryMock(registryMock *mocks.UseCaseRegistryMock) {
216217
acceptQuoteUseCase := &pegin.AcceptQuoteUseCase{}
217218

0 commit comments

Comments
 (0)