Skip to content

Commit b768c23

Browse files
committed
feat: implement emergency pause feature
1 parent 0eb19fe commit b768c23

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1159
-66
lines changed

.mockery.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ packages:
4040
DefaultCredentialsProvider:
4141
github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/handlers:
4242
interfaces:
43+
ResignUseCase:
44+
AcceptQuoteUseCase:
4345
GetAssetsReportUseCase:
4446
PeginStatusUseCase:
4547
PegoutStatusUseCase:
@@ -64,6 +66,7 @@ packages:
6466
PegConfiguration:
6567
github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain:
6668
interfaces:
69+
Pausable:
6770
BitcoinWallet:
6871
RootstockRpcServer:
6972
PeginContract:

internal/adapters/entrypoints/rest/handlers/accept_pegin_authenticated_quote_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
78
"net/http"
89
"net/http/httptest"
910
"testing"
@@ -119,7 +120,7 @@ func TestAcceptPeginAuthenticatedQuoteHandlerValidationErrors(t *testing.T) {
119120
})
120121
}
121122

122-
// nolint:funlen
123+
// nolint:funlen,maintidx
123124
func TestAcceptPeginAuthenticatedQuoteHandlerErrorCases(t *testing.T) {
124125
t.Run("should return 400 on invalid quote hash format", func(t *testing.T) {
125126
reqBody := pkg.AcceptAuthenticatedQuoteRequest{
@@ -355,6 +356,36 @@ func TestAcceptPeginAuthenticatedQuoteHandlerErrorCases(t *testing.T) {
355356
assert.Contains(t, errorResponse, "message")
356357
assert.Equal(t, "unknown error", errorResponse["message"])
357358
})
359+
t.Run("should return 503 if contract is paused", func(t *testing.T) {
360+
quoteHash := "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
361+
signature := "validSignature123"
362+
reqBody := pkg.AcceptAuthenticatedQuoteRequest{
363+
QuoteHash: quoteHash,
364+
Signature: signature,
365+
}
366+
jsonBody, err := json.Marshal(reqBody)
367+
require.NoError(t, err)
368+
369+
request := httptest.NewRequest(http.MethodPost, "/pegin/acceptAuthenticatedQuote", bytes.NewBuffer(jsonBody))
370+
request.Header.Set("Content-Type", "application/json")
371+
recorder := httptest.NewRecorder()
372+
373+
mockUseCase := new(mocks.AcceptQuoteUseCaseMock)
374+
mockUseCase.EXPECT().Run(mock.Anything, mock.Anything, mock.Anything).Return(quote.AcceptedQuote{}, blockchain.ContractPausedError)
375+
376+
handlerFunc := handlers.NewAcceptPeginAuthenticatedQuoteHandler(mockUseCase)
377+
handlerFunc(recorder, request)
378+
379+
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code)
380+
381+
mockUseCase.AssertExpectations(t)
382+
383+
var errorResponse map[string]interface{}
384+
err = json.NewDecoder(recorder.Body).Decode(&errorResponse)
385+
require.NoError(t, err)
386+
assert.Contains(t, errorResponse, "message")
387+
assert.Equal(t, "protocol is paused", errorResponse["message"])
388+
})
358389
}
359390

360391
// nolint:funlen

internal/adapters/entrypoints/rest/handlers/accept_pegin_quote.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"context"
55
"errors"
6+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
67
"net/http"
78

89
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
@@ -50,6 +51,10 @@ func NewAcceptPeginQuoteHandler(useCase AcceptQuoteUseCase) http.HandlerFunc {
5051
jsonErr := rest.NewErrorResponseWithDetails("not enough liquidity", rest.DetailsFromError(err), true)
5152
rest.JsonErrorResponse(w, http.StatusConflict, jsonErr)
5253
return
54+
} else if errors.Is(err, blockchain.ContractPausedError) {
55+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
56+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
57+
return
5358
} else if err != nil {
5459
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
5560
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)

internal/adapters/entrypoints/rest/handlers/accept_pegin_quote_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
78
"net/http"
89
"net/http/httptest"
910
"testing"
@@ -55,7 +56,7 @@ func TestAcceptPeginQuoteHandlerHappyPath(t *testing.T) {
5556
mockUseCase.AssertExpectations(t)
5657
}
5758

58-
// nolint:funlen
59+
// nolint:funlen,maintidx
5960
func TestAcceptPeginQuoteHandlerErrorCases(t *testing.T) {
6061

6162
t.Run("should handle malformed JSON in request body", func(t *testing.T) {
@@ -309,5 +310,31 @@ func TestAcceptPeginQuoteHandlerErrorCases(t *testing.T) {
309310
assert.Contains(t, errorResponse, "message")
310311
assert.Equal(t, "unknown error", errorResponse["message"])
311312
})
313+
t.Run("should return 503 if contract is paused", func(t *testing.T) {
314+
quoteHash := "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
315+
reqBody := pkg.AcceptQuoteRequest{QuoteHash: quoteHash}
316+
jsonBody, err := json.Marshal(reqBody)
317+
require.NoError(t, err)
318+
319+
request := httptest.NewRequest(http.MethodPost, "/pegin/acceptQuote", bytes.NewBuffer(jsonBody))
320+
request.Header.Set("Content-Type", "application/json")
321+
recorder := httptest.NewRecorder()
322+
323+
mockUseCase := new(mocks.AcceptQuoteUseCaseMock)
324+
mockUseCase.EXPECT().Run(mock.Anything, mock.Anything, mock.Anything).Return(quote.AcceptedQuote{}, blockchain.ContractPausedError)
325+
326+
handlerFunc := handlers.NewAcceptPeginQuoteHandler(mockUseCase)
327+
handlerFunc(recorder, request)
328+
329+
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code)
330+
331+
mockUseCase.AssertExpectations(t)
332+
333+
var errorResponse map[string]interface{}
334+
err = json.NewDecoder(recorder.Body).Decode(&errorResponse)
335+
require.NoError(t, err)
336+
assert.Contains(t, errorResponse, "message")
337+
assert.Equal(t, "protocol is paused", errorResponse["message"])
338+
})
312339

313340
}

internal/adapters/entrypoints/rest/handlers/accept_pegout_authenticated_quote_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"errors"
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
78
"net/http"
89
"net/http/httptest"
910
"testing"
@@ -151,6 +152,7 @@ func TestAcceptPegoutAuthenticatedQuoteHandler_RequestErrors(t *testing.T) {
151152
})
152153
}
153154

155+
// nolint:funlen
154156
func TestAcceptPegoutAuthenticatedQuoteHandler_QuoteHashValidation(t *testing.T) {
155157
t.Run("should return 400 on invalid quote hash format - too short", func(t *testing.T) {
156158
reqBody := pkg.AcceptAuthenticatedQuoteRequest{
@@ -211,6 +213,36 @@ func TestAcceptPegoutAuthenticatedQuoteHandler_QuoteHashValidation(t *testing.T)
211213
assert.Contains(t, errorResponse, "message")
212214
assert.Contains(t, errorResponse["message"], "invalid quote hash")
213215
})
216+
t.Run("should return 503 if contract is paused", func(t *testing.T) {
217+
quoteHash := "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
218+
signature := "validSignature123"
219+
reqBody := pkg.AcceptAuthenticatedQuoteRequest{
220+
QuoteHash: quoteHash,
221+
Signature: signature,
222+
}
223+
jsonBody, err := json.Marshal(reqBody)
224+
require.NoError(t, err)
225+
226+
request := httptest.NewRequest(http.MethodPost, "/pegout/acceptAuthenticatedQuote", bytes.NewBuffer(jsonBody))
227+
request.Header.Set("Content-Type", "application/json")
228+
recorder := httptest.NewRecorder()
229+
230+
mockUseCase := new(mocks.AcceptQuoteUseCaseMock)
231+
mockUseCase.EXPECT().Run(mock.Anything, mock.Anything, mock.Anything).Return(quote.AcceptedQuote{}, blockchain.ContractPausedError)
232+
233+
handlerFunc := handlers.NewAcceptPegoutAuthenticatedQuoteHandler(mockUseCase)
234+
handlerFunc(recorder, request)
235+
236+
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code)
237+
238+
mockUseCase.AssertExpectations(t)
239+
240+
var errorResponse map[string]interface{}
241+
err = json.NewDecoder(recorder.Body).Decode(&errorResponse)
242+
require.NoError(t, err)
243+
assert.Contains(t, errorResponse, "message")
244+
assert.Equal(t, "protocol is paused", errorResponse["message"])
245+
})
214246
}
215247

216248
// nolint:funlen

internal/adapters/entrypoints/rest/handlers/accept_pegout_quote.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"context"
55
"errors"
6+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
67
"net/http"
78

89
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
@@ -50,6 +51,10 @@ func NewAcceptPegoutQuoteHandler(useCase AcceptPegoutQuoteUseCase) http.HandlerF
5051
jsonErr := rest.NewErrorResponseWithDetails("not enough liquidity", rest.DetailsFromError(err), true)
5152
rest.JsonErrorResponse(w, http.StatusConflict, jsonErr)
5253
return
54+
} else if errors.Is(err, blockchain.ContractPausedError) {
55+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
56+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
57+
return
5358
} else if err != nil {
5459
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
5560
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)

internal/adapters/entrypoints/rest/handlers/add_pegin_collateral.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
66
"github.com/rsksmart/liquidity-provider-server/internal/entities"
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
78
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
89
"github.com/rsksmart/liquidity-provider-server/internal/usecases/pegin"
910
"github.com/rsksmart/liquidity-provider-server/pkg"
@@ -31,6 +32,10 @@ func NewAddPeginCollateralHandler(useCase *pegin.AddCollateralUseCase) http.Hand
3132
jsonErr := rest.NewErrorResponseWithDetails("not enough for minimum collateral", rest.DetailsFromError(err), false)
3233
rest.JsonErrorResponse(w, http.StatusConflict, jsonErr)
3334
return
35+
} else if errors.Is(err, blockchain.ContractPausedError) {
36+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
37+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
38+
return
3439
} else if err != nil {
3540
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
3641
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)

internal/adapters/entrypoints/rest/handlers/add_pegout_collateral.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
66
"github.com/rsksmart/liquidity-provider-server/internal/entities"
7+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
78
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
89
"github.com/rsksmart/liquidity-provider-server/internal/usecases/pegout"
910
"github.com/rsksmart/liquidity-provider-server/pkg"
@@ -31,6 +32,10 @@ func NewAddPegoutCollateralHandler(useCase *pegout.AddCollateralUseCase) http.Ha
3132
jsonErr := rest.NewErrorResponseWithDetails("not enough for minimum collateral", rest.DetailsFromError(err), false)
3233
rest.JsonErrorResponse(w, http.StatusConflict, jsonErr)
3334
return
35+
} else if errors.Is(err, blockchain.ContractPausedError) {
36+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
37+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
38+
return
3439
} else if err != nil {
3540
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
3641
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)

internal/adapters/entrypoints/rest/handlers/common.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"errors"
5+
"github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain"
56
"net/http"
67

78
"github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest"
@@ -52,6 +53,9 @@ func HandleAcceptQuoteError(w http.ResponseWriter, err error) {
5253
case errors.Is(err, liquidity_provider.TamperedTrustedAccountError):
5354
jsonErr := rest.NewErrorResponseWithDetails("error fetching trusted account", rest.DetailsFromError(err), true)
5455
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)
56+
case errors.Is(err, blockchain.ContractPausedError):
57+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
58+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
5559
default:
5660
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
5761
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)

internal/adapters/entrypoints/rest/handlers/get_pegin_quote.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ func NewGetPeginQuoteHandler(useCase *pegin.GetQuoteUseCase) http.HandlerFunc {
4444
)
4545

4646
result, err = useCase.Run(req.Context(), peginRequest)
47-
if errors.Is(err, blockchain.BtcAddressNotSupportedError) ||
48-
errors.Is(err, blockchain.BtcAddressInvalidNetworkError) ||
49-
errors.Is(err, usecases.RskAddressNotSupportedError) ||
50-
errors.Is(err, usecases.TxBelowMinimumError) ||
51-
errors.Is(err, liquidity_provider.AmountOutOfRangeError) {
47+
if isGetPeginQuoteBadRequest(err) {
5248
jsonErr := rest.NewErrorResponseWithDetails("invalid request", rest.DetailsFromError(err), true)
5349
rest.JsonErrorResponse(w, http.StatusBadRequest, jsonErr)
5450
return
51+
} else if errors.Is(err, blockchain.ContractPausedError) {
52+
jsonErr := rest.NewErrorResponseWithDetails("protocol is paused", rest.DetailsFromError(err), true)
53+
rest.JsonErrorResponse(w, http.StatusServiceUnavailable, jsonErr)
54+
return
5555
} else if err != nil {
5656
jsonErr := rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)
5757
rest.JsonErrorResponse(w, http.StatusInternalServerError, jsonErr)
@@ -65,3 +65,11 @@ func NewGetPeginQuoteHandler(useCase *pegin.GetQuoteUseCase) http.HandlerFunc {
6565
rest.JsonResponseWithBody(w, http.StatusOK, &responseBody)
6666
}
6767
}
68+
69+
func isGetPeginQuoteBadRequest(err error) bool {
70+
return errors.Is(err, blockchain.BtcAddressNotSupportedError) ||
71+
errors.Is(err, blockchain.BtcAddressInvalidNetworkError) ||
72+
errors.Is(err, usecases.RskAddressNotSupportedError) ||
73+
errors.Is(err, usecases.TxBelowMinimumError) ||
74+
errors.Is(err, liquidity_provider.AmountOutOfRangeError)
75+
}

0 commit comments

Comments
 (0)