Skip to content

Commit de3529b

Browse files
authored
feat: signing api (#13)
* Allow coordinator to be set manually * Add manual coordinator test * Implement across message handler * Send signature to all relayers * Implement signature cache to store generated signatures * Speed up process coordination * Implement signature cache to store generated signatures * Add signature cache tests * Allow for coordinator to notify other relayers of an across message * Reduce tss timeout * Implement calculation of the unlock hash that matches on-chain implementation * Add across message handler tests * Update mocks to maintained version * Fix test race condition * Convert abi to constant * Use source chain id property from across data * Use message destination as across deposit source * Use coordinator from the host in message handler * Send across message result over the err chanel * Return api errors as json * Implement api serve function * Add signing handler tests * Ignore valid json encode errors * Remove print * Bump cache action * Use a custom big.Int type that decode big int from string * Add read timeout * Add custom lint configuration * Remove messy linters * Lint * Remove extra nolint * Remove linter errors
1 parent 7e7902b commit de3529b

31 files changed

+497
-84
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
go-version: ${{ matrix.go-version }}
2525
- name: Checkout code
2626
uses: actions/checkout@v2
27-
- uses: actions/cache@v2
27+
- uses: actions/cache@v4
2828
with:
2929
path: ~/go/pkg/mod
3030
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

.golangci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Refer to golangci-lint's example config file for more options and information:
2+
# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
3+
4+
run:
5+
timeout: 5m
6+
modules-download-mode: readonly
7+
exclude-dirs:
8+
- vendor
9+
- third_party
10+
11+
linters:
12+
enable:
13+
- errcheck
14+
- gosimple
15+
- goimports
16+
- govet
17+
- staticcheck
18+
- unused
19+
- ineffassign
20+
- bodyclose
21+
- gocognit
22+
- godox
23+
- gosec
24+
- misspell
25+
- nilerr
26+
- reassign
27+
- wastedassign
28+
- whitespace
29+
30+
issues:
31+
max-issues-per-linter: 0
32+
max-same-issues: 0

api/handlers/signing.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
across "github.com/sprintertech/sprinter-signing/chains/evm/message"
9+
"github.com/sygmaprotocol/sygma-core/relayer/message"
10+
)
11+
12+
type SigningBody struct {
13+
DepositId *BigInt `json:"depositId"`
14+
ChainId uint64 `json:"chainId"`
15+
}
16+
17+
type SigningHandler struct {
18+
msgChan chan []*message.Message
19+
chains map[uint64]struct{}
20+
}
21+
22+
func NewSigningHandler(msgChan chan []*message.Message, chains map[uint64]struct{}) *SigningHandler {
23+
return &SigningHandler{
24+
msgChan: msgChan,
25+
chains: chains,
26+
}
27+
}
28+
29+
// HandleSigning sends a message to the across message handler and returns status code 202
30+
// if the deposit has been accepted for the signing process
31+
func (h *SigningHandler) HandleSigning(w http.ResponseWriter, r *http.Request) {
32+
b := &SigningBody{}
33+
d := json.NewDecoder(r.Body)
34+
err := d.Decode(b)
35+
if err != nil {
36+
JSONError(w, fmt.Sprintf("invalid request body: %s", err), http.StatusBadRequest)
37+
return
38+
}
39+
40+
err = h.validate(b)
41+
if err != nil {
42+
JSONError(w, fmt.Sprintf("invalid request body: %s", err), http.StatusBadRequest)
43+
return
44+
}
45+
46+
errChn := make(chan error, 1)
47+
am := across.NewAcrossMessage(0, b.ChainId, across.AcrossData{
48+
DepositId: b.DepositId.Int,
49+
ErrChn: errChn,
50+
})
51+
h.msgChan <- []*message.Message{am}
52+
53+
err = <-errChn
54+
if err != nil {
55+
JSONError(w, fmt.Sprintf("Singing failed: %s", err), http.StatusInternalServerError)
56+
return
57+
}
58+
59+
w.WriteHeader(http.StatusAccepted)
60+
}
61+
62+
func (h *SigningHandler) validate(b *SigningBody) error {
63+
if b.DepositId == nil {
64+
return fmt.Errorf("missing field 'depositId'")
65+
}
66+
67+
if b.ChainId == 0 {
68+
return fmt.Errorf("missing field 'chainId'")
69+
}
70+
71+
_, ok := h.chains[b.ChainId]
72+
if !ok {
73+
return fmt.Errorf("chain '%d' not supported", b.ChainId)
74+
}
75+
76+
return nil
77+
}

api/handlers/signing_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package handlers_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"math/big"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/sprintertech/sprinter-signing/api/handlers"
13+
across "github.com/sprintertech/sprinter-signing/chains/evm/message"
14+
"github.com/stretchr/testify/suite"
15+
"github.com/sygmaprotocol/sygma-core/relayer/message"
16+
"go.uber.org/mock/gomock"
17+
)
18+
19+
type SigningHandlerTestSuite struct {
20+
suite.Suite
21+
22+
handler *handlers.SigningHandler
23+
msgChn chan []*message.Message
24+
}
25+
26+
func TestRunSigningHandlerTestSuite(t *testing.T) {
27+
suite.Run(t, new(SigningHandlerTestSuite))
28+
}
29+
30+
func (s *SigningHandlerTestSuite) SetupTest() {
31+
ctrl := gomock.NewController(s.T())
32+
defer ctrl.Finish()
33+
34+
chains := make(map[uint64]struct{})
35+
chains[1] = struct{}{}
36+
37+
s.msgChn = make(chan []*message.Message, 1)
38+
s.handler = handlers.NewSigningHandler(s.msgChn, chains)
39+
}
40+
41+
func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingDepositID() {
42+
input := handlers.SigningBody{
43+
ChainId: 1,
44+
}
45+
body, _ := json.Marshal(input)
46+
47+
req := httptest.NewRequest(http.MethodPost, "/signing", bytes.NewReader(body))
48+
req.Header.Set("Content-Type", "application/json")
49+
50+
recorder := httptest.NewRecorder()
51+
52+
go func() {
53+
msg := <-s.msgChn
54+
ad := msg[0].Data.(across.AcrossData)
55+
ad.ErrChn <- fmt.Errorf("error handling message")
56+
}()
57+
58+
s.handler.HandleSigning(recorder, req)
59+
60+
s.Equal(http.StatusBadRequest, recorder.Code)
61+
}
62+
63+
func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingChainID() {
64+
input := handlers.SigningBody{
65+
DepositId: &handlers.BigInt{big.NewInt(1000)},
66+
}
67+
body, _ := json.Marshal(input)
68+
69+
req := httptest.NewRequest(http.MethodPost, "/signing", bytes.NewReader(body))
70+
req.Header.Set("Content-Type", "application/json")
71+
72+
recorder := httptest.NewRecorder()
73+
74+
go func() {
75+
msg := <-s.msgChn
76+
ad := msg[0].Data.(across.AcrossData)
77+
ad.ErrChn <- fmt.Errorf("error handling message")
78+
}()
79+
80+
s.handler.HandleSigning(recorder, req)
81+
82+
s.Equal(http.StatusBadRequest, recorder.Code)
83+
}
84+
85+
func (s *SigningHandlerTestSuite) Test_HandleSigning_ChainNotSupported() {
86+
input := handlers.SigningBody{
87+
ChainId: 2,
88+
DepositId: &handlers.BigInt{big.NewInt(1000)},
89+
}
90+
body, _ := json.Marshal(input)
91+
92+
req := httptest.NewRequest(http.MethodPost, "/signing", bytes.NewReader(body))
93+
req.Header.Set("Content-Type", "application/json")
94+
95+
recorder := httptest.NewRecorder()
96+
97+
go func() {
98+
msg := <-s.msgChn
99+
ad := msg[0].Data.(across.AcrossData)
100+
ad.ErrChn <- fmt.Errorf("error handling message")
101+
}()
102+
103+
s.handler.HandleSigning(recorder, req)
104+
105+
s.Equal(http.StatusBadRequest, recorder.Code)
106+
}
107+
108+
func (s *SigningHandlerTestSuite) Test_HandleSigning_ErrorHandlingMessage() {
109+
input := handlers.SigningBody{
110+
ChainId: 1,
111+
DepositId: &handlers.BigInt{big.NewInt(1000)},
112+
}
113+
body, _ := json.Marshal(input)
114+
115+
req := httptest.NewRequest(http.MethodPost, "/signing", bytes.NewReader(body))
116+
req.Header.Set("Content-Type", "application/json")
117+
118+
recorder := httptest.NewRecorder()
119+
120+
go func() {
121+
msg := <-s.msgChn
122+
ad := msg[0].Data.(across.AcrossData)
123+
ad.ErrChn <- fmt.Errorf("error handling message")
124+
}()
125+
126+
s.handler.HandleSigning(recorder, req)
127+
128+
s.Equal(http.StatusInternalServerError, recorder.Code)
129+
}
130+
131+
func (s *SigningHandlerTestSuite) Test_HandleSigning_Success() {
132+
input := handlers.SigningBody{
133+
ChainId: 1,
134+
DepositId: &handlers.BigInt{big.NewInt(1000)},
135+
}
136+
body, _ := json.Marshal(input)
137+
138+
req := httptest.NewRequest(http.MethodPost, "/signing", bytes.NewReader(body))
139+
req.Header.Set("Content-Type", "application/json")
140+
141+
recorder := httptest.NewRecorder()
142+
143+
go func() {
144+
msg := <-s.msgChn
145+
ad := msg[0].Data.(across.AcrossData)
146+
ad.ErrChn <- nil
147+
}()
148+
149+
s.handler.HandleSigning(recorder, req)
150+
151+
s.Equal(http.StatusAccepted, recorder.Code)
152+
}

api/handlers/util.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"math/big"
7+
"net/http"
8+
"strings"
9+
)
10+
11+
type BigInt struct {
12+
*big.Int
13+
}
14+
15+
func (b *BigInt) UnmarshalJSON(data []byte) error {
16+
if b.Int == nil {
17+
b.Int = new(big.Int)
18+
}
19+
20+
s := strings.Trim(string(data), "\"")
21+
_, ok := b.Int.SetString(s, 10)
22+
if !ok {
23+
return fmt.Errorf("failed to parse big.Int from %s", s)
24+
}
25+
26+
return nil
27+
}
28+
29+
func (b *BigInt) MarshalJSON() ([]byte, error) {
30+
return []byte(b.String()), nil
31+
}
32+
33+
func JSONError(w http.ResponseWriter, err interface{}, code int) {
34+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
35+
w.Header().Set("X-Content-Type-Options", "nosniff")
36+
w.WriteHeader(code)
37+
_ = json.NewEncoder(w).Encode(err)
38+
}

api/server.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
8+
"github.com/rs/zerolog/log"
9+
"github.com/sprintertech/sprinter-signing/api/handlers"
10+
)
11+
12+
func Serve(
13+
ctx context.Context,
14+
addr string,
15+
signingHandler *handlers.SigningHandler,
16+
) {
17+
mux := http.NewServeMux()
18+
mux.HandleFunc("POST /signing", signingHandler.HandleSigning)
19+
20+
server := &http.Server{
21+
Addr: addr,
22+
ReadTimeout: 10 * time.Second,
23+
}
24+
go func() {
25+
log.Info().Msgf("Starting server on %s", addr)
26+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
27+
panic(err)
28+
}
29+
}()
30+
31+
<-ctx.Done()
32+
shutdownCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
33+
defer cancel()
34+
35+
err := server.Shutdown(shutdownCtx)
36+
if err != nil {
37+
log.Err(err).Msgf("Error shutting down server")
38+
} else {
39+
log.Info().Msgf("Server shut down gracefully.")
40+
}
41+
}

chains/evm/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func NewEVMConfig(chainConfig map[string]interface{}) (*EVMConfig, error) {
7474
GeneralChainConfig: c.GeneralChainConfig,
7575
Admin: c.Admin,
7676
LiqudityPool: c.LiqudityPool,
77+
// nolint:gosec
7778
BlockRetryInterval: time.Duration(c.BlockRetryInterval) * time.Second,
7879
BlockConfirmations: big.NewInt(c.BlockConfirmations),
7980
BlockInterval: big.NewInt(c.BlockInterval),

0 commit comments

Comments
 (0)