Skip to content

Commit a3bf182

Browse files
authored
feat: unlock API (#73)
* Add unlock api endpoint * Add lifi unlock handler to app setup * Fix msg string
1 parent f517337 commit a3bf182

File tree

10 files changed

+572
-7
lines changed

10 files changed

+572
-7
lines changed

api/handlers/signing.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
AcrossProtocol ProtocolType = "across"
2121
MayanProtocol ProtocolType = "mayan"
2222
RhinestoneProtocol ProtocolType = "rhinestone"
23+
LifiProtocol ProtocolType = "lifi"
2324
)
2425

2526
type SigningBody struct {

api/handlers/unlock.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package handlers
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"fmt"
7+
"math/big"
8+
"net/http"
9+
"time"
10+
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/gorilla/mux"
13+
evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message"
14+
"github.com/sprintertech/sprinter-signing/tss/ecdsa/signing"
15+
"github.com/sygmaprotocol/sygma-core/relayer/message"
16+
)
17+
18+
const SIGNATURE_TIMEOUT = time.Second * 15
19+
20+
type UnlockResponse struct {
21+
Signature string `json:"signature"`
22+
ID string `json:"id"`
23+
}
24+
25+
type UnlockBody struct {
26+
ChainId uint64
27+
Protocol ProtocolType `json:"protocol"`
28+
OrderID string `json:"orderId"`
29+
Settler string `json:"settler"`
30+
}
31+
32+
type UnlockHandler struct {
33+
chains map[uint64]struct{}
34+
msgChan chan []*message.Message
35+
}
36+
37+
func NewUnlockHandler(msgChn chan []*message.Message, chains map[uint64]struct{}) *UnlockHandler {
38+
return &UnlockHandler{
39+
chains: chains,
40+
msgChan: msgChn,
41+
}
42+
}
43+
44+
// HandleSigning sends a message to the across message handler and returns status code 202
45+
// if the deposit has been accepted for the signing process
46+
func (h *UnlockHandler) HandleUnlock(w http.ResponseWriter, r *http.Request) {
47+
b := &UnlockBody{}
48+
d := json.NewDecoder(r.Body)
49+
err := d.Decode(b)
50+
if err != nil {
51+
JSONError(w, fmt.Errorf("invalid request body: %s", err), http.StatusBadRequest)
52+
return
53+
}
54+
55+
vars := mux.Vars(r)
56+
err = h.validate(b, vars)
57+
if err != nil {
58+
JSONError(w, fmt.Errorf("invalid request body: %s", err), http.StatusBadRequest)
59+
return
60+
}
61+
62+
sigChn := make(chan interface{}, 1)
63+
var m *message.Message
64+
switch b.Protocol {
65+
case LifiProtocol:
66+
{
67+
m = evmMessage.NewLifiUnlockMessage(0, b.ChainId, &evmMessage.LifiUnlockData{
68+
Source: 0,
69+
Destination: b.ChainId,
70+
SigChn: sigChn,
71+
OrderID: b.OrderID,
72+
Settler: common.HexToAddress(b.Settler),
73+
})
74+
}
75+
default:
76+
JSONError(w, fmt.Errorf("invalid protocol %s", b.Protocol), http.StatusBadRequest)
77+
return
78+
}
79+
h.msgChan <- []*message.Message{m}
80+
81+
for {
82+
select {
83+
case <-time.After(SIGNATURE_TIMEOUT):
84+
JSONError(w, fmt.Errorf("timeout"), http.StatusInternalServerError)
85+
return
86+
case sig := <-sigChn:
87+
{
88+
sig, ok := sig.(signing.EcdsaSignature)
89+
if !ok {
90+
JSONError(w, fmt.Errorf("invalid signature"), http.StatusInternalServerError)
91+
return
92+
}
93+
94+
data, _ := json.Marshal(UnlockResponse{
95+
Signature: hex.EncodeToString(sig.Signature),
96+
ID: sig.ID,
97+
})
98+
w.Header().Set("Content-Type", "application/json")
99+
w.WriteHeader(http.StatusOK)
100+
_, _ = w.Write(data)
101+
return
102+
}
103+
}
104+
}
105+
}
106+
107+
func (h *UnlockHandler) validate(b *UnlockBody, vars map[string]string) error {
108+
chainId, ok := new(big.Int).SetString(vars["chainId"], 10)
109+
if !ok {
110+
return fmt.Errorf("field 'chainId' invalid")
111+
}
112+
b.ChainId = chainId.Uint64()
113+
114+
if b.ChainId == 0 {
115+
return fmt.Errorf("missing field 'chainId'")
116+
}
117+
118+
_, ok = h.chains[b.ChainId]
119+
if !ok {
120+
return fmt.Errorf("chain '%d' not supported", b.ChainId)
121+
}
122+
123+
return nil
124+
}

api/handlers/unlock_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package handlers_test
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/gorilla/mux"
13+
"github.com/sprintertech/sprinter-signing/api/handlers"
14+
across "github.com/sprintertech/sprinter-signing/chains/evm/message"
15+
"github.com/sprintertech/sprinter-signing/tss/ecdsa/signing"
16+
"github.com/stretchr/testify/suite"
17+
"github.com/sygmaprotocol/sygma-core/relayer/message"
18+
)
19+
20+
type UnlockHandlerTestSuite struct {
21+
suite.Suite
22+
23+
chains map[uint64]struct{}
24+
}
25+
26+
func TestRunUnlockHandlerTestSuite(t *testing.T) {
27+
suite.Run(t, new(UnlockHandlerTestSuite))
28+
}
29+
30+
func (s *UnlockHandlerTestSuite) SetupTest() {
31+
chains := make(map[uint64]struct{})
32+
chains[1] = struct{}{}
33+
s.chains = chains
34+
}
35+
36+
func (s *UnlockHandlerTestSuite) Test_HandleUnlock_InvalidRequest() {
37+
msgChn := make(chan []*message.Message)
38+
handler := handlers.NewUnlockHandler(msgChn, s.chains)
39+
40+
input := handlers.UnlockBody{
41+
Protocol: "lifi",
42+
OrderID: "id",
43+
Settler: "settler",
44+
}
45+
body, _ := json.Marshal(input)
46+
47+
req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/unlocks", bytes.NewReader(body))
48+
req.Header.Set("Content-Type", "application/json")
49+
50+
recorder := httptest.NewRecorder()
51+
52+
go func() {
53+
msg := <-msgChn
54+
ad := msg[0].Data.(*across.LifiUnlockData)
55+
ad.SigChn <- signing.EcdsaSignature{}
56+
}()
57+
58+
handler.HandleUnlock(recorder, req)
59+
60+
s.Equal(http.StatusBadRequest, recorder.Code)
61+
}
62+
63+
func (s *UnlockHandlerTestSuite) Test_HandleUnlock_InvalidProtocol() {
64+
msgChn := make(chan []*message.Message)
65+
handler := handlers.NewUnlockHandler(msgChn, s.chains)
66+
67+
input := handlers.UnlockBody{
68+
Protocol: "across",
69+
OrderID: "id",
70+
Settler: "settler",
71+
}
72+
body, _ := json.Marshal(input)
73+
74+
req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/unlocks", bytes.NewReader(body))
75+
req.Header.Set("Content-Type", "application/json")
76+
req = mux.SetURLVars(req, map[string]string{
77+
"chainId": "1",
78+
})
79+
80+
recorder := httptest.NewRecorder()
81+
82+
go func() {
83+
msg := <-msgChn
84+
ad := msg[0].Data.(*across.LifiUnlockData)
85+
ad.SigChn <- signing.EcdsaSignature{}
86+
}()
87+
88+
handler.HandleUnlock(recorder, req)
89+
90+
s.Equal(http.StatusBadRequest, recorder.Code)
91+
}
92+
93+
func (s *UnlockHandlerTestSuite) Test_HandleUnlock_ValidRequest() {
94+
msgChn := make(chan []*message.Message)
95+
handler := handlers.NewUnlockHandler(msgChn, s.chains)
96+
97+
input := handlers.UnlockBody{
98+
Protocol: "lifi",
99+
OrderID: "id",
100+
Settler: "settler",
101+
}
102+
body, _ := json.Marshal(input)
103+
104+
req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/unlocks", bytes.NewReader(body))
105+
req.Header.Set("Content-Type", "application/json")
106+
req = mux.SetURLVars(req, map[string]string{
107+
"chainId": "1",
108+
})
109+
110+
recorder := httptest.NewRecorder()
111+
112+
sigBytes, _ := hex.DecodeString("abcd")
113+
go func() {
114+
msg := <-msgChn
115+
ad := msg[0].Data.(*across.LifiUnlockData)
116+
ad.SigChn <- signing.EcdsaSignature{
117+
Signature: sigBytes,
118+
ID: "id",
119+
}
120+
}()
121+
122+
handler.HandleUnlock(recorder, req)
123+
124+
s.Equal(http.StatusOK, recorder.Code)
125+
data, err := io.ReadAll(recorder.Body)
126+
s.Nil(err)
127+
128+
s.Equal(string(data), "{\"signature\":\"abcd\",\"id\":\"id\"}")
129+
}

api/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ func Serve(
1414
ctx context.Context,
1515
addr string,
1616
signingHandler *handlers.SigningHandler,
17+
unlockHandler *handlers.UnlockHandler,
1718
statusHandler *handlers.StatusHandler,
1819
confirmationsHandler *handlers.ConfirmationsHandler,
1920
) {
2021
r := mux.NewRouter()
22+
r.HandleFunc("/v1/chains/{chainId:[0-9]+}/unlocks", unlockHandler.HandleUnlock).Methods("POST")
2123
r.HandleFunc("/v1/chains/{chainId:[0-9]+}/signatures", signingHandler.HandleSigning).Methods("POST")
2224
r.HandleFunc("/v1/chains/{chainId:[0-9]+}/signatures/{depositId}", statusHandler.HandleRequest).Methods("GET")
2325
r.HandleFunc("/v1/chains/{chainId:[0-9]+}/confirmations", confirmationsHandler.HandleRequest).Methods("GET")

app/app.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ func Run() error {
269269
confirmationsPerChain[*c.GeneralChainConfig.Id] = c.ConfirmationsByValue
270270
}
271271

272+
lifiUnlockMh := evmMessage.NewLifiUnlockHandler(
273+
*c.GeneralChainConfig.Id,
274+
repayerAddresses,
275+
coordinator,
276+
host,
277+
communication,
278+
keyshareStore,
279+
)
280+
mh.RegisterMessageHandler(evmMessage.LifiUnlockMessage, lifiUnlockMh)
281+
272282
var startBlock *big.Int
273283
var listener *coreListener.EVMListener
274284
eventHandlers := make([]coreListener.EventHandler, 0)
@@ -316,7 +326,14 @@ func Run() error {
316326
signingHandler := handlers.NewSigningHandler(msgChan, supportedChains)
317327
statusHandler := handlers.NewStatusHandler(signatureCache, supportedChains)
318328
confirmationsHandler := handlers.NewConfirmationsHandler(confirmationsPerChain)
319-
go api.Serve(ctx, configuration.RelayerConfig.ApiAddr, signingHandler, statusHandler, confirmationsHandler)
329+
unlockHandler := handlers.NewUnlockHandler(msgChan, supportedChains)
330+
go api.Serve(
331+
ctx,
332+
configuration.RelayerConfig.ApiAddr,
333+
signingHandler,
334+
unlockHandler,
335+
statusHandler,
336+
confirmationsHandler)
320337

321338
sig := <-sysErr
322339
log.Info().Msgf("terminating got ` [%v] signal", sig)

chains/evm/message/message.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import (
1414
)
1515

1616
const (
17-
AcrossMessage = "AcrossMessage"
18-
MayanMessage = "MayanMessage"
17+
AcrossMessage = "AcrossMessage"
18+
MayanMessage = "MayanMessage"
19+
LifiUnlockMessage = "LifiUnlockMessage"
1920

2021
DOMAIN_NAME = "LiquidityPool"
2122
VERSION = "1.0.0"
@@ -95,6 +96,27 @@ func NewRhinestoneMessage(source, destination uint64, rhinestoneData *Rhinestone
9596
}
9697
}
9798

99+
type LifiUnlockData struct {
100+
SigChn chan interface{} `json:"-"`
101+
102+
OrderID string
103+
Settler common.Address
104+
105+
Coordinator peer.ID
106+
Source uint64
107+
Destination uint64
108+
}
109+
110+
func NewLifiUnlockMessage(source, destination uint64, lifiData *LifiUnlockData) *message.Message {
111+
return &message.Message{
112+
Source: source,
113+
Destination: destination,
114+
Data: lifiData,
115+
Type: LifiUnlockMessage,
116+
Timestamp: time.Now(),
117+
}
118+
}
119+
98120
// unlockHash calculates the hash that has to signed and submitted on-chain to the liquidity
99121
// pool contract.
100122
func unlockHash(

0 commit comments

Comments
 (0)