diff --git a/Makefile b/Makefile index d1c5f081..72b61c96 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ genmocks: mockgen -source=./chains/evm/message/confirmations.go -destination=./chains/evm/message/mock/confirmations.go mockgen -source=./api/handlers/signing.go -destination=./api/handlers/mock/signing.go mockgen -package mock_message -destination=./chains/evm/message/mock/pricing.go github.com/sprintertech/lifi-solver/pkg/pricing OrderPricer + mockgen -source=./chains/lighter/message/lighter.go -destination=./chains/lighter/message/mock/lighter.go diff --git a/api/handlers/signing.go b/api/handlers/signing.go index becd3d2b..597bb480 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message" + lighterMessage "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/sygmaprotocol/sygma-core/relayer/message" ) @@ -20,8 +21,9 @@ const ( AcrossProtocol ProtocolType = "across" MayanProtocol ProtocolType = "mayan" RhinestoneProtocol ProtocolType = "rhinestone" - LifiEscrow ProtocolType = "lifi-escrow" + LifiEscrowProtocol ProtocolType = "lifi-escrow" LifiProtocol ProtocolType = "lifi" + LighterProtocol ProtocolType = "lighter" ) type SigningBody struct { @@ -113,7 +115,7 @@ func (h *SigningHandler) HandleSigning(w http.ResponseWriter, r *http.Request) { BorrowAmount: b.BorrowAmount.Int, }) } - case LifiEscrow: + case LifiEscrowProtocol: { m = evmMessage.NewLifiEscrowData(0, b.ChainId, &evmMessage.LifiEscrowData{ OrderID: b.DepositId, @@ -126,6 +128,19 @@ func (h *SigningHandler) HandleSigning(w http.ResponseWriter, r *http.Request) { BorrowAmount: b.BorrowAmount.Int, }) } + case LighterProtocol: + { + m = lighterMessage.NewLighterMessage(0, b.ChainId, &lighterMessage.LighterData{ + OrderHash: b.DepositId, + Nonce: b.Nonce.Int, + LiquidityPool: common.HexToAddress(b.LiquidityPool), + Caller: common.HexToAddress(b.Caller), + BorrowAmount: b.BorrowAmount.Int, + ErrChn: errChn, + Source: 0, + Destination: b.ChainId, + }) + } default: JSONError(w, fmt.Errorf("invalid protocol %s", b.Protocol), http.StatusBadRequest) return diff --git a/api/handlers/signing_test.go b/api/handlers/signing_test.go index dca2b179..edf49ca9 100644 --- a/api/handlers/signing_test.go +++ b/api/handlers/signing_test.go @@ -16,6 +16,7 @@ import ( "github.com/sprintertech/sprinter-signing/api/handlers" mock_handlers "github.com/sprintertech/sprinter-signing/api/handlers/mock" across "github.com/sprintertech/sprinter-signing/chains/evm/message" + lighter "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/stretchr/testify/suite" "github.com/sygmaprotocol/sygma-core/relayer/message" "go.uber.org/mock/gomock" @@ -394,6 +395,40 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_LifiSuccess() { s.Equal(http.StatusAccepted, recorder.Code) } +func (s *SigningHandlerTestSuite) Test_HandleSigning_LighterSuccess() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains) + + input := handlers.SigningBody{ + DepositId: "depositID", + Protocol: "lighter", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + } + body, _ := json.Marshal(input) + + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + + go func() { + msg := <-msgChn + ad := msg[0].Data.(*lighter.LighterData) + ad.ErrChn <- nil + }() + + handler.HandleSigning(recorder, req) + + s.Equal(http.StatusAccepted, recorder.Code) +} + type StatusHandlerTestSuite struct { suite.Suite diff --git a/app/app.go b/app/app.go index 681495c1..95280e2f 100644 --- a/app/app.go +++ b/app/app.go @@ -32,6 +32,9 @@ import ( "github.com/sprintertech/sprinter-signing/chains/evm/calls/events" evmListener "github.com/sprintertech/sprinter-signing/chains/evm/listener" evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message" + "github.com/sprintertech/sprinter-signing/chains/lighter" + lighterMessage "github.com/sprintertech/sprinter-signing/chains/lighter/message" + "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/comm/elector" "github.com/sprintertech/sprinter-signing/comm/p2p" "github.com/sprintertech/sprinter-signing/config" @@ -42,6 +45,7 @@ import ( "github.com/sprintertech/sprinter-signing/price" "github.com/sprintertech/sprinter-signing/protocol/across" "github.com/sprintertech/sprinter-signing/protocol/lifi" + lighterAPI "github.com/sprintertech/sprinter-signing/protocol/lighter" "github.com/sprintertech/sprinter-signing/protocol/mayan" "github.com/sprintertech/sprinter-signing/topology" "github.com/sprintertech/sprinter-signing/tss" @@ -254,7 +258,7 @@ func Run() error { sigChn) go acrossMh.Listen(ctx) - mh.RegisterMessageHandler(evmMessage.AcrossMessage, acrossMh) + mh.RegisterMessageHandler(message.MessageType(comm.AcrossMsg.String()), acrossMh) supportedChains[*c.GeneralChainConfig.Id] = struct{}{} confirmationsPerChain[*c.GeneralChainConfig.Id] = c.ConfirmationsByValue } @@ -278,7 +282,7 @@ func Run() error { sigChn) go mayanMh.Listen(ctx) - mh.RegisterMessageHandler(evmMessage.MayanMessage, mayanMh) + mh.RegisterMessageHandler(message.MessageType(comm.MayanMsg.String()), mayanMh) supportedChains[*c.GeneralChainConfig.Id] = struct{}{} confirmationsPerChain[*c.GeneralChainConfig.Id] = c.ConfirmationsByValue } @@ -291,7 +295,7 @@ func Run() error { resolver := token.NewTokenResolver(solverConfig, usdPricer) orderPricer := pricing.NewStandardPricer(resolver) lifiApi := lifi.NewLifiAPI() - lifiValidator := validation.NewLifiEscrowOrderValidator(solverConfig, orderPricer) + lifiValidator := validation.NewLifiEscrowOrderValidator(solverConfig, orderPricer, resolver) lifiMh := evmMessage.NewLifiEscrowMessageHandler( *c.GeneralChainConfig.Id, @@ -309,7 +313,7 @@ func Run() error { sigChn, ) go lifiMh.Listen(ctx) - mh.RegisterMessageHandler(evmMessage.LifiEscrowMessage, lifiMh) + mh.RegisterMessageHandler(message.MessageType(comm.LifiEscrowMsg.String()), lifiMh) supportedChains[*c.GeneralChainConfig.Id] = struct{}{} confirmationsPerChain[*c.GeneralChainConfig.Id] = c.ConfirmationsByValue } @@ -323,7 +327,7 @@ func Run() error { keyshareStore, ) go lifiUnlockMh.Listen(ctx) - mh.RegisterMessageHandler(evmMessage.LifiUnlockMessage, lifiUnlockMh) + mh.RegisterMessageHandler(message.MessageType(comm.LifiUnlockMsg.String()), lifiUnlockMh) var startBlock *big.Int var listener *coreListener.EVMListener @@ -349,6 +353,24 @@ func Run() error { } } + lighterConfig, err := lighter.NewLighterConfig(*solverConfig) + panicOnError(err) + lighterAPI := lighterAPI.NewLighterAPI() + lighterMessageHandler := lighterMessage.NewLighterMessageHandler( + lighterConfig.WithdrawalAddress, + lighterConfig.UsdcAddress, + lighterConfig.RepaymentAddress, + lighterAPI, + coordinator, + host, + communication, + keyshareStore, + sigChn, + ) + go lighterMessageHandler.Listen(ctx) + lighterChain := lighter.NewLighterChain(lighterMessageHandler) + domains[lighter.LIGHTER_DOMAIN_ID] = lighterChain + go jobs.StartCommunicationHealthCheckJob(host, configuration.RelayerConfig.MpcConfig.CommHealthCheckInterval, sygmaMetrics) r := relayer.NewRelayer(domains, sygmaMetrics) diff --git a/chains/evm/calls/consts/lighter.go b/chains/evm/calls/consts/lighter.go new file mode 100644 index 00000000..c1af3a62 --- /dev/null +++ b/chains/evm/calls/consts/lighter.go @@ -0,0 +1,19 @@ +package consts + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +var LighterABI, _ = abi.JSON(strings.NewReader(`[{ + "name": "withdraw", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [ + {"name": "txHash", "type": "bytes32"}, + {"name": "toAddress", "type": "address"}, + {"name": "amount", "type": "uint256"} + ], + "outputs": [] +}]`)) diff --git a/chains/evm/message/across.go b/chains/evm/message/across.go index 6777be8e..89138a5d 100644 --- a/chains/evm/message/across.go +++ b/chains/evm/message/across.go @@ -14,6 +14,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog/log" "github.com/sprintertech/sprinter-signing/chains/evm/calls/events" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/tss" "github.com/sprintertech/sprinter-signing/tss/ecdsa/signing" @@ -141,7 +142,7 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop return nil, err } - unlockHash, err := borrowUnlockHash( + unlockHash, err := signature.BorrowUnlockHash( calldata, d.OutputAmount, common.BytesToAddress(d.OutputToken[12:]), diff --git a/chains/evm/message/lifiEscrow.go b/chains/evm/message/lifiEscrow.go index 841b9b16..fafed71f 100644 --- a/chains/evm/message/lifiEscrow.go +++ b/chains/evm/message/lifiEscrow.go @@ -13,6 +13,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog/log" "github.com/sprintertech/sprinter-signing/chains/evm/calls/consts" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/config" "github.com/sprintertech/sprinter-signing/tss" @@ -150,7 +151,7 @@ func (h *LifiEscrowMessageHandler) HandleMessage(m *message.Message) (*proposal. big.NewInt(order.Order.FillDeadline.Unix()).Uint64(), ) - unlockHash, err := borrowUnlockHash( + unlockHash, err := signature.BorrowUnlockHash( calldata, data.BorrowAmount, borrowToken, diff --git a/chains/evm/message/mayan.go b/chains/evm/message/mayan.go index 7e51ae71..84756f87 100644 --- a/chains/evm/message/mayan.go +++ b/chains/evm/message/mayan.go @@ -12,6 +12,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog/log" "github.com/sprintertech/sprinter-signing/chains/evm/calls/contracts" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/config" "github.com/sprintertech/sprinter-signing/protocol/mayan" @@ -160,7 +161,7 @@ func (h *MayanMessageHandler) HandleMessage(m *message.Message) (*proposal.Propo data.ErrChn <- nil - unlockHash, err := borrowUnlockHash( + unlockHash, err := signature.BorrowUnlockHash( calldataBytes, data.BorrowAmount, destinationBorrowToken.Address, diff --git a/chains/evm/message/message.go b/chains/evm/message/message.go index dca7671d..8453b8a4 100644 --- a/chains/evm/message/message.go +++ b/chains/evm/message/message.go @@ -1,26 +1,16 @@ package message import ( - "fmt" "math/big" "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/libp2p/go-libp2p/core/peer" + "github.com/sprintertech/sprinter-signing/comm" "github.com/sygmaprotocol/sygma-core/relayer/message" ) const ( - AcrossMessage = "AcrossMessage" - MayanMessage = "MayanMessage" - LifiEscrowMessage = "LifiEscrowMessage" - LifiUnlockMessage = "LifiUnlockMessage" - - DOMAIN_NAME = "LiquidityPool" - VERSION = "1.0.0" TIMEOUT = 10 * time.Minute BLOCK_RANGE = 1000 ) @@ -44,7 +34,7 @@ func NewAcrossMessage(source, destination uint64, acrossData *AcrossData) *messa Source: source, Destination: destination, Data: acrossData, - Type: AcrossMessage, + Type: message.MessageType(comm.AcrossMsg.String()), Timestamp: time.Now(), } } @@ -69,7 +59,7 @@ func NewMayanMessage(source, destination uint64, mayanData *MayanData) *message. Source: source, Destination: destination, Data: mayanData, - Type: MayanMessage, + Type: message.MessageType(comm.MayanMsg.String()), Timestamp: time.Now(), } } @@ -92,7 +82,7 @@ func NewRhinestoneMessage(source, destination uint64, rhinestoneData *Rhinestone Source: source, Destination: destination, Data: rhinestoneData, - Type: MayanMessage, + Type: message.MessageType(comm.RhinestoneMsg.String()), Timestamp: time.Now(), } } @@ -116,7 +106,7 @@ func NewLifiEscrowData(source, destination uint64, lifiData *LifiEscrowData) *me Source: source, Destination: destination, Data: lifiData, - Type: LifiEscrowMessage, + Type: message.MessageType(comm.LifiEscrowMsg.String()), Timestamp: time.Now(), } } @@ -137,146 +127,7 @@ func NewLifiUnlockMessage(source, destination uint64, lifiData *LifiUnlockData) Source: source, Destination: destination, Data: lifiData, - Type: LifiUnlockMessage, + Type: message.MessageType(comm.LifiUnlockMsg.String()), Timestamp: time.Now(), } } - -// borrowUnlockHash calculates the hash that has to be signed and submitted on-chain to the liquidity -// pool contract. -func borrowUnlockHash( - calldata []byte, - outputAmount *big.Int, - outputToken common.Address, - destinationChainId *big.Int, - target common.Address, - deadline uint64, - caller common.Address, - liquidityPool common.Address, - nonce *big.Int, -) ([]byte, error) { - msg := apitypes.TypedDataMessage{ - "caller": caller.Hex(), - "borrowToken": outputToken.Hex(), - "amount": outputAmount, - "target": target.Hex(), - "targetCallData": calldata, - "nonce": nonce, - "deadline": new(big.Int).SetUint64(deadline), - } - - chainId := math.HexOrDecimal256(*destinationChainId) - typedData := apitypes.TypedData{ - Types: apitypes.Types{ - "EIP712Domain": []apitypes.Type{ - {Name: "name", Type: "string"}, - {Name: "version", Type: "string"}, - {Name: "chainId", Type: "uint256"}, - {Name: "verifyingContract", Type: "address"}, - }, - "Borrow": []apitypes.Type{ - {Name: "caller", Type: "address"}, - {Name: "borrowToken", Type: "address"}, - {Name: "amount", Type: "uint256"}, - {Name: "target", Type: "address"}, - {Name: "targetCallData", Type: "bytes"}, - {Name: "nonce", Type: "uint256"}, - {Name: "deadline", Type: "uint256"}, - }, - }, - PrimaryType: "Borrow", - Domain: apitypes.TypedDataDomain{ - Name: DOMAIN_NAME, - ChainId: &chainId, - Version: VERSION, - VerifyingContract: liquidityPool.Hex(), - }, - Message: msg, - } - - domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - return []byte{}, err - } - - messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - return []byte{}, err - } - - rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(messageHash))) - return crypto.Keccak256(rawData), nil -} - -/* -// borrowManyUnlockHash calculates the hash that has to be signed and submitted on-chain to the liquidity -// pool contract. -func borrowManyUnlockHash( - calldata []byte, - outputAmounts []*big.Int, - outputTokens []common.Address, - destinationChainId *big.Int, - target common.Address, - deadline uint64, - caller common.Address, - liquidityPool common.Address, - nonce *big.Int, -) ([]byte, error) { - hexOutputTokens := make([]string, len(outputTokens)) - for i, token := range outputTokens { - hexOutputTokens[i] = token.Hex() - } - - msg := apitypes.TypedDataMessage{ - "caller": caller.Hex(), - "borrowTokens": hexOutputTokens, - "amounts": outputAmounts, - "target": target.Hex(), - "targetCallData": calldata, - "nonce": nonce, - "deadline": new(big.Int).SetUint64(deadline), - } - - chainId := math.HexOrDecimal256(*destinationChainId) - typedData := apitypes.TypedData{ - Types: apitypes.Types{ - "EIP712Domain": []apitypes.Type{ - {Name: "name", Type: "string"}, - {Name: "version", Type: "string"}, - {Name: "chainId", Type: "uint256"}, - {Name: "verifyingContract", Type: "address"}, - }, - "BorrowMany": []apitypes.Type{ - {Name: "caller", Type: "address"}, - {Name: "borrowTokens", Type: "address[]"}, - {Name: "amounts", Type: "uint256[]"}, - {Name: "target", Type: "address"}, - {Name: "targetCallData", Type: "bytes"}, - {Name: "nonce", Type: "uint256"}, - {Name: "deadline", Type: "uint256"}, - }, - }, - PrimaryType: "BorrowMany", - Domain: apitypes.TypedDataDomain{ - Name: DOMAIN_NAME, - ChainId: &chainId, - Version: VERSION, - VerifyingContract: liquidityPool.Hex(), - }, - Message: msg, - } - - domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - return []byte{}, err - } - - messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - return []byte{}, err - } - - rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(messageHash))) - return crypto.Keccak256(rawData), nil -} -*/ diff --git a/chains/evm/message/rhinestone.go b/chains/evm/message/rhinestone.go index 756234d1..7ab83e70 100644 --- a/chains/evm/message/rhinestone.go +++ b/chains/evm/message/rhinestone.go @@ -13,6 +13,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog/log" "github.com/sprintertech/sprinter-signing/chains/evm/calls/contracts" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/config" "github.com/sprintertech/sprinter-signing/protocol/rhinestone" @@ -109,7 +110,7 @@ func (h *RhinestoneMessageHandler) HandleMessage(m *message.Message) (*proposal. } data.ErrChn <- nil - unlockHash, err := borrowUnlockHash( + unlockHash, err := signature.BorrowUnlockHash( calldata, data.BorrowAmount, borrowToken, diff --git a/chains/evm/message/unlock.go b/chains/evm/message/unlock.go index 6a8d2e54..2bc385f7 100644 --- a/chains/evm/message/unlock.go +++ b/chains/evm/message/unlock.go @@ -13,6 +13,7 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog/log" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/tss" "github.com/sprintertech/sprinter-signing/tss/ecdsa/signing" @@ -157,9 +158,9 @@ func (h *LifiUnlockHandler) lifiUnlockHash(data *LifiUnlockData) ([]byte, error) }, PrimaryType: "AllowOpen", Domain: apitypes.TypedDataDomain{ - Name: DOMAIN_NAME, + Name: signature.DOMAIN_NAME, ChainId: &chainId, - Version: VERSION, + Version: signature.VERSION, VerifyingContract: data.Settler.Hex(), }, Message: msg, diff --git a/chains/evm/signature/hash.go b/chains/evm/signature/hash.go new file mode 100644 index 00000000..f7bc0e97 --- /dev/null +++ b/chains/evm/signature/hash.go @@ -0,0 +1,153 @@ +package signature + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +const ( + DOMAIN_NAME = "LiquidityPool" + VERSION = "1.0.0" +) + +// BorrowUnlockHash calculates the hash that has to be signed and submitted on-chain to the liquidity +// pool contract. +func BorrowUnlockHash( + calldata []byte, + outputAmount *big.Int, + outputToken common.Address, + destinationChainId *big.Int, + target common.Address, + deadline uint64, + caller common.Address, + liquidityPool common.Address, + nonce *big.Int, +) ([]byte, error) { + msg := apitypes.TypedDataMessage{ + "caller": caller.Hex(), + "borrowToken": outputToken.Hex(), + "amount": outputAmount, + "target": target.Hex(), + "targetCallData": calldata, + "nonce": nonce, + "deadline": new(big.Int).SetUint64(deadline), + } + + chainId := math.HexOrDecimal256(*destinationChainId) + typedData := apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + "Borrow": []apitypes.Type{ + {Name: "caller", Type: "address"}, + {Name: "borrowToken", Type: "address"}, + {Name: "amount", Type: "uint256"}, + {Name: "target", Type: "address"}, + {Name: "targetCallData", Type: "bytes"}, + {Name: "nonce", Type: "uint256"}, + {Name: "deadline", Type: "uint256"}, + }, + }, + PrimaryType: "Borrow", + Domain: apitypes.TypedDataDomain{ + Name: DOMAIN_NAME, + ChainId: &chainId, + Version: VERSION, + VerifyingContract: liquidityPool.Hex(), + }, + Message: msg, + } + + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return []byte{}, err + } + + messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return []byte{}, err + } + + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(messageHash))) + return crypto.Keccak256(rawData), nil +} + +// BorrowManyUnlockHash calculates the hash that has to be signed and submitted on-chain to the liquidity +// pool contract. +func BorrowManyUnlockHash( + calldata []byte, + outputAmounts []*big.Int, + outputTokens []common.Address, + destinationChainId *big.Int, + target common.Address, + deadline uint64, + caller common.Address, + liquidityPool common.Address, + nonce *big.Int, +) ([]byte, error) { + hexOutputTokens := make([]string, len(outputTokens)) + for i, token := range outputTokens { + hexOutputTokens[i] = token.Hex() + } + + msg := apitypes.TypedDataMessage{ + "caller": caller.Hex(), + "borrowTokens": hexOutputTokens, + "amounts": outputAmounts, + "target": target.Hex(), + "targetCallData": calldata, + "nonce": nonce, + "deadline": new(big.Int).SetUint64(deadline), + } + + chainId := math.HexOrDecimal256(*destinationChainId) + typedData := apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + "BorrowMany": []apitypes.Type{ + {Name: "caller", Type: "address"}, + {Name: "borrowTokens", Type: "address[]"}, + {Name: "amounts", Type: "uint256[]"}, + {Name: "target", Type: "address"}, + {Name: "targetCallData", Type: "bytes"}, + {Name: "nonce", Type: "uint256"}, + {Name: "deadline", Type: "uint256"}, + }, + }, + PrimaryType: "BorrowMany", + Domain: apitypes.TypedDataDomain{ + Name: DOMAIN_NAME, + ChainId: &chainId, + Version: VERSION, + VerifyingContract: liquidityPool.Hex(), + }, + Message: msg, + } + + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return []byte{}, err + } + + messageHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return []byte{}, err + } + + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(messageHash))) + return crypto.Keccak256(rawData), nil +} diff --git a/chains/lighter/chain.go b/chains/lighter/chain.go new file mode 100644 index 00000000..768a0806 --- /dev/null +++ b/chains/lighter/chain.go @@ -0,0 +1,42 @@ +package lighter + +import ( + "context" + + "github.com/sygmaprotocol/sygma-core/relayer/message" + "github.com/sygmaprotocol/sygma-core/relayer/proposal" +) + +const ( + LIGHTER_DOMAIN_ID uint64 = 1701984817 // lower 32bits of sha256 hash of lighter caip ("lighter:1") +) + +type MessageHandler interface { + HandleMessage(m *message.Message) (*proposal.Proposal, error) +} + +type LighterChain struct { + messageHandler MessageHandler + domainID uint64 +} + +func NewLighterChain(messageHandler MessageHandler) *LighterChain { + return &LighterChain{ + messageHandler: messageHandler, + domainID: LIGHTER_DOMAIN_ID, + } +} + +func (c *LighterChain) PollEvents(_ context.Context) {} + +func (c *LighterChain) ReceiveMessage(m *message.Message) (*proposal.Proposal, error) { + return c.messageHandler.HandleMessage(m) +} + +func (c *LighterChain) Write(_ []*proposal.Proposal) error { + return nil +} + +func (c *LighterChain) DomainID() uint64 { + return c.domainID +} diff --git a/chains/lighter/config.go b/chains/lighter/config.go new file mode 100644 index 00000000..a7404eb0 --- /dev/null +++ b/chains/lighter/config.go @@ -0,0 +1,45 @@ +package lighter + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + solverConfig "github.com/sprintertech/solver-config/go/config" +) + +var ( + ARBITRUM_CHAIN_ID = big.NewInt(42161) + LIGHTER_CAIP = "lighter:1" + ARBITRUM_CAIP = "eip155:42161" + USDC = "usdc" +) + +type LighterConfig struct { + WithdrawalAddress common.Address + UsdcAddress common.Address + RepaymentAddress string +} + +func NewLighterConfig(solverConfig solverConfig.SolverConfig) (*LighterConfig, error) { + arbitrumConfig, ok := solverConfig.Chains[ARBITRUM_CAIP] + if !ok { + return nil, fmt.Errorf("no solver config for id %s", ARBITRUM_CAIP) + } + + usdcConfig, ok := arbitrumConfig.Tokens[USDC] + if !ok { + return nil, fmt.Errorf("usdc not configured") + } + + withdrawalAddress, ok := solverConfig.ProtocolsMetadata.Lighter.FastWithdrawalContract[ARBITRUM_CAIP] + if !ok { + return nil, fmt.Errorf("withdrawal address not configured") + } + + return &LighterConfig{ + WithdrawalAddress: common.HexToAddress(withdrawalAddress), + RepaymentAddress: solverConfig.ProtocolsMetadata.Lighter.RepaymentAddress, + UsdcAddress: common.HexToAddress(usdcConfig.Address), + }, nil +} diff --git a/chains/lighter/config_test.go b/chains/lighter/config_test.go new file mode 100644 index 00000000..f843e17b --- /dev/null +++ b/chains/lighter/config_test.go @@ -0,0 +1,97 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package lighter_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + solverConfig "github.com/sprintertech/solver-config/go/config" + "github.com/sprintertech/sprinter-signing/chains/lighter" + "github.com/stretchr/testify/suite" +) + +type NewLighterConfigTestSuite struct { + suite.Suite +} + +func TestRunNewLighterConfigTestSuite(t *testing.T) { + suite.Run(t, new(NewLighterConfigTestSuite)) +} + +func (s *NewLighterConfigTestSuite) Test_ArbitrumNotConfigured() { + solverChains := make(map[string]solverConfig.Chain) + _, err := lighter.NewLighterConfig(solverConfig.SolverConfig{ + Chains: solverChains, + ProtocolsMetadata: solverConfig.ProtocolsMetadata{}, + }) + + s.NotNil(err) +} + +func (s *NewLighterConfigTestSuite) Test_UsdcNotConfigured() { + solverChains := make(map[string]solverConfig.Chain) + solverChains["eip155:42161"] = solverConfig.Chain{} + + _, err := lighter.NewLighterConfig(solverConfig.SolverConfig{ + Chains: solverChains, + ProtocolsMetadata: solverConfig.ProtocolsMetadata{}, + }) + + s.NotNil(err) +} + +func (s *NewLighterConfigTestSuite) Test_WithdrawalAddressNotConfigured() { + tokens := make(map[string]solverConfig.Token) + tokens["usdc"] = solverConfig.Token{ + Address: "address", + Decimals: 6, + } + + solverChains := make(map[string]solverConfig.Chain) + solverChains["eip155:42161"] = solverConfig.Chain{ + Tokens: tokens, + } + + _, err := lighter.NewLighterConfig(solverConfig.SolverConfig{ + Chains: solverChains, + ProtocolsMetadata: solverConfig.ProtocolsMetadata{ + Lighter: &solverConfig.Lighter{}, + }, + }) + + s.NotNil(err) +} + +func (s *NewLighterConfigTestSuite) Test_ValidConfig() { + tokens := make(map[string]solverConfig.Token) + tokens["usdc"] = solverConfig.Token{ + Address: "usdc", + Decimals: 6, + } + + solverChains := make(map[string]solverConfig.Chain) + solverChains["eip155:42161"] = solverConfig.Chain{ + Tokens: tokens, + } + + config, err := lighter.NewLighterConfig(solverConfig.SolverConfig{ + Chains: solverChains, + ProtocolsMetadata: solverConfig.ProtocolsMetadata{ + Lighter: &solverConfig.Lighter{ + RepaymentAddress: "3", + FastWithdrawalContract: map[string]string{ + "eip155:42161": "withdrawal", + }, + }, + }, + }) + + s.Nil(err) + s.Equal(config, &lighter.LighterConfig{ + WithdrawalAddress: common.HexToAddress("withdrawal"), + UsdcAddress: common.HexToAddress("usdc"), + RepaymentAddress: "3", + }) +} diff --git a/chains/lighter/message/lighter.go b/chains/lighter/message/lighter.go new file mode 100644 index 00000000..363374f9 --- /dev/null +++ b/chains/lighter/message/lighter.go @@ -0,0 +1,207 @@ +package message + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "strconv" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/rs/zerolog/log" + "github.com/sprintertech/sprinter-signing/chains/evm/calls/consts" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" + "github.com/sprintertech/sprinter-signing/comm" + "github.com/sprintertech/sprinter-signing/protocol/lighter" + "github.com/sprintertech/sprinter-signing/tss" + "github.com/sprintertech/sprinter-signing/tss/ecdsa/signing" + "github.com/sygmaprotocol/sygma-core/relayer/message" + "github.com/sygmaprotocol/sygma-core/relayer/proposal" +) + +var ( + ARBITRUM_CHAIN_ID = big.NewInt(42161) + FILL_DEADLINE = time.Minute * 5 +) + +type Coordinator interface { + Execute(ctx context.Context, tssProcesses []tss.TssProcess, resultChn chan interface{}, coordinator peer.ID) error +} + +type TxFetcher interface { + GetTx(hash string) (*lighter.LighterTx, error) +} + +type LighterMessageHandler struct { + coordinator Coordinator + host host.Host + comm comm.Communication + fetcher signing.SaveDataFetcher + sigChn chan any + + lighterAddress common.Address + usdcAddress common.Address + repaymentAccount string + txFetcher TxFetcher +} + +func NewLighterMessageHandler( + lighterAddress common.Address, + usdcAddress common.Address, + repaymentAccount string, + txFetcher TxFetcher, + coordinator Coordinator, + host host.Host, + comm comm.Communication, + fetcher signing.SaveDataFetcher, + sigChn chan any, +) *LighterMessageHandler { + return &LighterMessageHandler{ + txFetcher: txFetcher, + usdcAddress: usdcAddress, + repaymentAccount: repaymentAccount, + lighterAddress: lighterAddress, + coordinator: coordinator, + host: host, + comm: comm, + fetcher: fetcher, + sigChn: sigChn, + } +} + +// HandleMessage finds the Mayan deposit with the according deposit ID and starts +// the MPC signature process for it. The result will be saved into the signature +// cache through the result channel. +func (h *LighterMessageHandler) HandleMessage(m *message.Message) (*proposal.Proposal, error) { + data := m.Data.(*LighterData) + + err := h.notify(data) + if err != nil { + log.Warn().Msgf("Failed to notify relayers because of %s", err) + } + + tx, err := h.txFetcher.GetTx(data.OrderHash) + if err != nil { + data.ErrChn <- err + return nil, err + } + + if err = h.verifyWithdrawal(tx, data.BorrowAmount); err != nil { + data.ErrChn <- err + return nil, err + } + + data.ErrChn <- nil + + calldata, err := h.calldata(tx, data.BorrowAmount) + if err != nil { + return nil, err + } + + unlockHash, err := signature.BorrowUnlockHash( + calldata, + new(big.Int).SetUint64(tx.Transfer.USDCAmount), + h.usdcAddress, + ARBITRUM_CHAIN_ID, + h.lighterAddress, + //nolint:gosec + uint64(time.Now().Add(FILL_DEADLINE).Unix()), + data.Caller, + data.LiquidityPool, + data.Nonce) + if err != nil { + data.ErrChn <- err + return nil, err + } + + sessionID := fmt.Sprintf("lighter-%s", "") + signing, err := signing.NewSigning( + new(big.Int).SetBytes(unlockHash), + sessionID, + sessionID, + h.host, + h.comm, + h.fetcher) + if err != nil { + return nil, err + } + + err = h.coordinator.Execute(context.Background(), []tss.TssProcess{signing}, h.sigChn, data.Coordinator) + if err != nil { + return nil, err + } + return nil, nil +} + +func (h *LighterMessageHandler) verifyWithdrawal(tx *lighter.LighterTx, borrowAmount *big.Int) error { + if tx.Type != lighter.TxTypeL2Transfer { + return errors.New("invalid transaction type") + } + + if strconv.Itoa(tx.Transfer.ToAccountIndex) != h.repaymentAccount { + return errors.New("transfer account index invalid") + } + + if borrowAmount.Cmp(new(big.Int).SetUint64(tx.Transfer.USDCAmount)) != -1 { + return errors.New("borrow amount higher transfer amount") + } + + return nil +} + +func (h *LighterMessageHandler) calldata(tx *lighter.LighterTx, borrowAmount *big.Int) ([]byte, error) { + return consts.LighterABI.Pack( + "withdraw", + common.HexToHash(tx.Hash), + common.HexToAddress(tx.L1Address), + borrowAmount) +} + +func (h *LighterMessageHandler) Listen(ctx context.Context) { + msgChn := make(chan *comm.WrappedMessage) + subID := h.comm.Subscribe(comm.LighterSessionID, comm.LighterMsg, msgChn) + + for { + select { + case wMsg := <-msgChn: + { + d := &LighterData{} + err := json.Unmarshal(wMsg.Payload, d) + if err != nil { + log.Warn().Msgf("Failed unmarshaling Mayan message: %s", err) + continue + } + + d.ErrChn = make(chan error, 1) + msg := NewLighterMessage(d.Source, d.Destination, d) + _, err = h.HandleMessage(msg) + if err != nil { + log.Err(err).Msgf("Failed handling Mayan message %+v because of: %s", msg, err) + } + } + case <-ctx.Done(): + { + h.comm.UnSubscribe(subID) + return + } + } + } +} + +func (h *LighterMessageHandler) notify(data *LighterData) error { + if data.Coordinator != peer.ID("") { + return nil + } + + data.Coordinator = h.host.ID() + msgBytes, err := json.Marshal(data) + if err != nil { + return err + } + + return h.comm.Broadcast(h.host.Peerstore().Peers(), msgBytes, comm.LighterMsg, comm.LighterSessionID) +} diff --git a/chains/lighter/message/lighter_test.go b/chains/lighter/message/lighter_test.go new file mode 100644 index 00000000..20b5f5ca --- /dev/null +++ b/chains/lighter/message/lighter_test.go @@ -0,0 +1,271 @@ +package message_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/sprintertech/sprinter-signing/chains/lighter/message" + mock_message "github.com/sprintertech/sprinter-signing/chains/lighter/message/mock" + "github.com/sprintertech/sprinter-signing/comm" + mock_communication "github.com/sprintertech/sprinter-signing/comm/mock" + mock_host "github.com/sprintertech/sprinter-signing/comm/p2p/mock/host" + "github.com/sprintertech/sprinter-signing/keyshare" + "github.com/sprintertech/sprinter-signing/protocol/lighter" + mock_tss "github.com/sprintertech/sprinter-signing/tss/ecdsa/common/mock" + "github.com/stretchr/testify/suite" + coreMessage "github.com/sygmaprotocol/sygma-core/relayer/message" + "go.uber.org/mock/gomock" +) + +type LighterMessageHandlerTestSuite struct { + suite.Suite + + mockCommunication *mock_communication.MockCommunication + mockCoordinator *mock_message.MockCoordinator + mockHost *mock_host.MockHost + mockFetcher *mock_tss.MockSaveDataFetcher + mockTxFetcher *mock_message.MockTxFetcher + + handler *message.LighterMessageHandler + sigChn chan interface{} +} + +func TestRunLighterMessageHandlerTestSuite(t *testing.T) { + suite.Run(t, new(LighterMessageHandlerTestSuite)) +} + +func (s *LighterMessageHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + + s.mockCommunication = mock_communication.NewMockCommunication(ctrl) + s.mockCoordinator = mock_message.NewMockCoordinator(ctrl) + s.mockHost = mock_host.NewMockHost(ctrl) + s.mockHost.EXPECT().ID().Return(peer.ID("")).AnyTimes() + + s.mockFetcher = mock_tss.NewMockSaveDataFetcher(ctrl) + s.mockFetcher.EXPECT().UnlockKeyshare().AnyTimes() + s.mockFetcher.EXPECT().LockKeyshare().AnyTimes() + s.mockFetcher.EXPECT().GetKeyshare().AnyTimes().Return(keyshare.ECDSAKeyshare{}, nil) + + s.mockTxFetcher = mock_message.NewMockTxFetcher(ctrl) + + s.sigChn = make(chan interface{}, 1) + + s.handler = message.NewLighterMessageHandler( + common.Address{}, + common.Address{}, + "3", + s.mockTxFetcher, + s.mockCoordinator, + s.mockHost, + s.mockCommunication, + s.mockFetcher, + s.sigChn, + ) +} + +func (s *LighterMessageHandlerTestSuite) Test_HandleMessage_ValidMessage() { + s.mockCommunication.EXPECT().Broadcast( + gomock.Any(), + gomock.Any(), + comm.LighterMsg, + "lighter", + ).Return(nil) + p, _ := pstoremem.NewPeerstore() + s.mockHost.EXPECT().Peerstore().Return(p) + + errChn := make(chan error, 1) + ad := &message.LighterData{ + ErrChn: errChn, + Nonce: big.NewInt(101), + LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"), + Caller: common.HexToAddress("0xde526bA5d1ad94cC59D7A79d99A59F607d31A657"), + OrderHash: "orderHash", + BorrowAmount: big.NewInt(1900000), + } + + s.mockCoordinator.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + s.mockTxFetcher.EXPECT().GetTx(ad.OrderHash).Return(&lighter.LighterTx{ + Type: lighter.TxTypeL2Transfer, + Transfer: &lighter.Transfer{ + USDCAmount: 2000000, + ToAccountIndex: 3, + }, + }, nil) + + m := &coreMessage.Message{ + Data: ad, + Source: 0, + Destination: 10, + } + prop, err := s.handler.HandleMessage(m) + + s.Nil(prop) + s.Nil(err) + + err = <-errChn + s.Nil(err) +} + +func (s *LighterMessageHandlerTestSuite) Test_HandleMessage_InvalidTxType() { + s.mockCommunication.EXPECT().Broadcast( + gomock.Any(), + gomock.Any(), + comm.LighterMsg, + "lighter", + ).Return(nil) + p, _ := pstoremem.NewPeerstore() + s.mockHost.EXPECT().Peerstore().Return(p) + + errChn := make(chan error, 1) + ad := &message.LighterData{ + ErrChn: errChn, + Nonce: big.NewInt(101), + LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"), + Caller: common.HexToAddress("0xde526bA5d1ad94cC59D7A79d99A59F607d31A657"), + OrderHash: "orderHash", + BorrowAmount: big.NewInt(1900000), + } + s.mockTxFetcher.EXPECT().GetTx(ad.OrderHash).Return(&lighter.LighterTx{ + Type: lighter.TxTypeL2Withdraw, + Transfer: &lighter.Transfer{ + USDCAmount: 2000000, + ToAccountIndex: 3, + }, + }, nil) + + m := &coreMessage.Message{ + Data: ad, + Source: 0, + Destination: 10, + } + prop, err := s.handler.HandleMessage(m) + + s.Nil(prop) + s.NotNil(err) + + err = <-errChn + s.NotNil(err) +} + +func (s *LighterMessageHandlerTestSuite) Test_HandleMessage_InvalidAccount() { + s.mockCommunication.EXPECT().Broadcast( + gomock.Any(), + gomock.Any(), + comm.LighterMsg, + "lighter", + ).Return(nil) + p, _ := pstoremem.NewPeerstore() + s.mockHost.EXPECT().Peerstore().Return(p) + + errChn := make(chan error, 1) + ad := &message.LighterData{ + ErrChn: errChn, + Nonce: big.NewInt(101), + LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"), + Caller: common.HexToAddress("0xde526bA5d1ad94cC59D7A79d99A59F607d31A657"), + OrderHash: "orderHash", + BorrowAmount: big.NewInt(1900000), + } + s.mockTxFetcher.EXPECT().GetTx(ad.OrderHash).Return(&lighter.LighterTx{ + Type: lighter.TxTypeL2Transfer, + Transfer: &lighter.Transfer{ + USDCAmount: 2000000, + ToAccountIndex: 5, + }, + }, nil) + + m := &coreMessage.Message{ + Data: ad, + Source: 0, + Destination: 10, + } + prop, err := s.handler.HandleMessage(m) + + s.Nil(prop) + s.NotNil(err) + + err = <-errChn + s.NotNil(err) +} + +func (s *LighterMessageHandlerTestSuite) Test_HandleMessage_MissingTx() { + s.mockCommunication.EXPECT().Broadcast( + gomock.Any(), + gomock.Any(), + comm.LighterMsg, + "lighter", + ).Return(nil) + p, _ := pstoremem.NewPeerstore() + s.mockHost.EXPECT().Peerstore().Return(p) + + errChn := make(chan error, 1) + ad := &message.LighterData{ + ErrChn: errChn, + Nonce: big.NewInt(101), + LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"), + Caller: common.HexToAddress("0xde526bA5d1ad94cC59D7A79d99A59F607d31A657"), + OrderHash: "orderHash", + BorrowAmount: big.NewInt(1900000), + } + s.mockTxFetcher.EXPECT().GetTx(ad.OrderHash).Return(nil, fmt.Errorf("not found")) + + m := &coreMessage.Message{ + Data: ad, + Source: 0, + Destination: 10, + } + prop, err := s.handler.HandleMessage(m) + + s.Nil(prop) + s.NotNil(err) + + err = <-errChn + s.NotNil(err) +} + +func (s *LighterMessageHandlerTestSuite) Test_HandleMessage_BorrowAmountTooHigh() { + s.mockCommunication.EXPECT().Broadcast( + gomock.Any(), + gomock.Any(), + comm.LighterMsg, + "lighter", + ).Return(nil) + p, _ := pstoremem.NewPeerstore() + s.mockHost.EXPECT().Peerstore().Return(p) + + errChn := make(chan error, 1) + ad := &message.LighterData{ + ErrChn: errChn, + Nonce: big.NewInt(101), + LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"), + Caller: common.HexToAddress("0xde526bA5d1ad94cC59D7A79d99A59F607d31A657"), + OrderHash: "orderHash", + BorrowAmount: big.NewInt(2000000), + } + + s.mockTxFetcher.EXPECT().GetTx(ad.OrderHash).Return(&lighter.LighterTx{ + Type: lighter.TxTypeL2Transfer, + Transfer: &lighter.Transfer{ + USDCAmount: 2000000, + ToAccountIndex: 3, + }, + }, nil) + + m := &coreMessage.Message{ + Data: ad, + Source: 0, + Destination: 10, + } + prop, err := s.handler.HandleMessage(m) + + s.Nil(prop) + s.NotNil(err) + + err = <-errChn + s.NotNil(err) +} diff --git a/chains/lighter/message/message.go b/chains/lighter/message/message.go new file mode 100644 index 00000000..38da4708 --- /dev/null +++ b/chains/lighter/message/message.go @@ -0,0 +1,36 @@ +package message + +import ( + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/sprintertech/sprinter-signing/comm" + "github.com/sygmaprotocol/sygma-core/relayer/message" +) + +type LighterData struct { + ErrChn chan error `json:"-"` + + OrderHash string + Coordinator peer.ID + LiquidityPool common.Address + Caller common.Address + DepositTxHash string + Calldata string + Nonce *big.Int + BorrowAmount *big.Int + Source uint64 + Destination uint64 +} + +func NewLighterMessage(source, destination uint64, lighterData *LighterData) *message.Message { + return &message.Message{ + Source: source, + Destination: destination, + Data: lighterData, + Type: message.MessageType(comm.LighterMsg.String()), + Timestamp: time.Now(), + } +} diff --git a/chains/lighter/message/mock/lighter.go b/chains/lighter/message/mock/lighter.go new file mode 100644 index 00000000..1e8dd935 --- /dev/null +++ b/chains/lighter/message/mock/lighter.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/lighter/message/lighter.go +// +// Generated by this command: +// +// mockgen -source=./chains/lighter/message/lighter.go -destination=./chains/lighter/message/mock/lighter.go +// + +// Package mock_message is a generated GoMock package. +package mock_message + +import ( + context "context" + reflect "reflect" + + peer "github.com/libp2p/go-libp2p/core/peer" + lighter "github.com/sprintertech/sprinter-signing/protocol/lighter" + tss "github.com/sprintertech/sprinter-signing/tss" + gomock "go.uber.org/mock/gomock" +) + +// MockCoordinator is a mock of Coordinator interface. +type MockCoordinator struct { + ctrl *gomock.Controller + recorder *MockCoordinatorMockRecorder + isgomock struct{} +} + +// MockCoordinatorMockRecorder is the mock recorder for MockCoordinator. +type MockCoordinatorMockRecorder struct { + mock *MockCoordinator +} + +// NewMockCoordinator creates a new mock instance. +func NewMockCoordinator(ctrl *gomock.Controller) *MockCoordinator { + mock := &MockCoordinator{ctrl: ctrl} + mock.recorder = &MockCoordinatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCoordinator) EXPECT() *MockCoordinatorMockRecorder { + return m.recorder +} + +// Execute mocks base method. +func (m *MockCoordinator) Execute(ctx context.Context, tssProcesses []tss.TssProcess, resultChn chan any, coordinator peer.ID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", ctx, tssProcesses, resultChn, coordinator) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute. +func (mr *MockCoordinatorMockRecorder) Execute(ctx, tssProcesses, resultChn, coordinator any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCoordinator)(nil).Execute), ctx, tssProcesses, resultChn, coordinator) +} + +// MockTxFetcher is a mock of TxFetcher interface. +type MockTxFetcher struct { + ctrl *gomock.Controller + recorder *MockTxFetcherMockRecorder + isgomock struct{} +} + +// MockTxFetcherMockRecorder is the mock recorder for MockTxFetcher. +type MockTxFetcherMockRecorder struct { + mock *MockTxFetcher +} + +// NewMockTxFetcher creates a new mock instance. +func NewMockTxFetcher(ctrl *gomock.Controller) *MockTxFetcher { + mock := &MockTxFetcher{ctrl: ctrl} + mock.recorder = &MockTxFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTxFetcher) EXPECT() *MockTxFetcherMockRecorder { + return m.recorder +} + +// GetTx mocks base method. +func (m *MockTxFetcher) GetTx(hash string) (*lighter.LighterTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTx", hash) + ret0, _ := ret[0].(*lighter.LighterTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTx indicates an expected call of GetTx. +func (mr *MockTxFetcherMockRecorder) GetTx(hash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTx", reflect.TypeOf((*MockTxFetcher)(nil).GetTx), hash) +} diff --git a/comm/messages.go b/comm/messages.go index 704efbb2..dbc81f80 100644 --- a/comm/messages.go +++ b/comm/messages.go @@ -39,6 +39,8 @@ const ( AcrossMsg // MayanMsg message type is used for the process coordinator to share mayan data MayanMsg + // LighterMsg message type is used for the process coordinator to share lighter data + LighterMsg // LifiEscrowMsg message type is used for the process coordinator to share lifi data LifiEscrowMsg // Rhinestone message type is used for the process coordinator to share rhinestone data @@ -55,6 +57,7 @@ const ( MayanSessionID = "mayan" LifiEscrowSessionID = "lifi-escrow" RhinestoneSessionID = "rhinestone" + LighterSessionID = "lighter" LifiUnlockSessionID = "lifi-unlock" ) @@ -97,6 +100,8 @@ func (msgType MessageType) String() string { return "LifiEscrowMsg" case LifiUnlockMsg: return "LifiUnlockMsg" + case LighterMsg: + return "LighterMsg" default: return "UnknownMsg" } diff --git a/go.mod b/go.mod index 8c82ab49..71f5366b 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( github.com/rs/zerolog v1.25.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.9.0 - github.com/sprintertech/lifi-solver v0.0.0-20251010142703-ff6f20f8b6eb - github.com/sprintertech/solver-config/go v0.0.0-20251003113310-77bd6669a2ef + github.com/sprintertech/lifi-solver v0.0.0-20251023154209-4cf9ac1ab166 + github.com/sprintertech/solver-config/go v0.0.0-20251027142430-7f32bdd5da1e github.com/stretchr/testify v1.10.0 github.com/sygmaprotocol/sygma-core v0.0.0-20250304150334-bd39ac4f7b82 go.opentelemetry.io/otel v1.16.0 diff --git a/go.sum b/go.sum index 4fafe83a..54f7d362 100644 --- a/go.sum +++ b/go.sum @@ -491,11 +491,15 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -942,10 +946,12 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/sprintertech/lifi-solver v0.0.0-20251010142703-ff6f20f8b6eb h1:d87LBM/B3nMgkhSbZVAaYa+oecI3h/PCwQuw3h6A1sE= -github.com/sprintertech/lifi-solver v0.0.0-20251010142703-ff6f20f8b6eb/go.mod h1:EbAH3JJxioBVuHvyjaP5zTh6uPmumCpbE93WEjwpRm0= -github.com/sprintertech/solver-config/go v0.0.0-20251003113310-77bd6669a2ef h1:WPyHT8WM/f5rRFmzxeuTvuI6MCqM7wvTeBsW/B7y3lk= -github.com/sprintertech/solver-config/go v0.0.0-20251003113310-77bd6669a2ef/go.mod h1:MrIGW6M815PSYKtWSeOd1Z7eiSeOIk/uA/6E2PhlQVQ= +github.com/sprintertech/lifi-solver v0.0.0-20251023154209-4cf9ac1ab166 h1:mCcKOxfLs+5tFGD5ZyVVMeqlUuNzhHttul05x27zWFo= +github.com/sprintertech/lifi-solver v0.0.0-20251023154209-4cf9ac1ab166/go.mod h1:EbAH3JJxioBVuHvyjaP5zTh6uPmumCpbE93WEjwpRm0= +github.com/sprintertech/solver-config/go v0.0.0-20251024140304-ee77ffefd608 h1:J5RxJUhqMGfoIUb8XnSjDBlUMH658nM9NIjXft+DjkA= +github.com/sprintertech/solver-config/go v0.0.0-20251024140304-ee77ffefd608/go.mod h1:MrIGW6M815PSYKtWSeOd1Z7eiSeOIk/uA/6E2PhlQVQ= +github.com/sprintertech/solver-config/go v0.0.0-20251027142430-7f32bdd5da1e h1:5sSP6GbqCT/ApxxZmUtav6GHy5Ke98zh5oqQxewhJd4= +github.com/sprintertech/solver-config/go v0.0.0-20251027142430-7f32bdd5da1e/go.mod h1:MrIGW6M815PSYKtWSeOd1Z7eiSeOIk/uA/6E2PhlQVQ= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/protocol/lighter/api.go b/protocol/lighter/api.go new file mode 100644 index 00000000..db5c03d1 --- /dev/null +++ b/protocol/lighter/api.go @@ -0,0 +1,93 @@ +package lighter + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + LIGHTER_URL = "https://mainnet.zklighter.elliot.ai/api" +) + +type TxType uint64 + +const ( + TxTypeL2Transfer TxType = 12 + TxTypeL2Withdraw TxType = 13 +) + +type Transfer struct { + USDCAmount uint64 + FromAccountIndex uint64 + ToAccountIndex int + Fee uint64 +} + +type LighterTx struct { + Code uint64 `json:"code"` + Hash string `json:"hash"` + Type TxType `json:"type"` + Info string `json:"info"` + L1Address string `json:"l1_address"` + Transfer *Transfer +} + +func (tx *LighterTx) UnmarshalJSON(data []byte) error { + type t LighterTx + if err := json.Unmarshal(data, (*t)(tx)); err != nil { + return err + } + + if tx.Type == TxTypeL2Transfer { + var t *Transfer + if err := json.Unmarshal([]byte(tx.Info), &t); err != nil { + return err + } + tx.Transfer = t + } + + return nil +} + +type LighterAPI struct { + HTTPClient *http.Client +} + +func NewLighterAPI() *LighterAPI { + return &LighterAPI{ + HTTPClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// GetTx fetches transaction from the lighter API +func (a *LighterAPI) GetTx(hash string) (*LighterTx, error) { + url := fmt.Sprintf("%s/v1/tx?by=hash&value=%s", LIGHTER_URL, hash) + resp, err := a.HTTPClient.Get(url) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d, %s", resp.StatusCode, url) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + fmt.Println(string(body)) + + s := new(LighterTx) + if err := json.Unmarshal(body, s); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + return s, nil +} diff --git a/protocol/lighter/api_test.go b/protocol/lighter/api_test.go new file mode 100644 index 00000000..5906b780 --- /dev/null +++ b/protocol/lighter/api_test.go @@ -0,0 +1,115 @@ +//nolint:gocognit +package lighter_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "reflect" + "testing" + + "github.com/sprintertech/sprinter-signing/protocol/lighter" + "github.com/sprintertech/sprinter-signing/protocol/lighter/mock" +) + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func Test_LighterAPI_GetTx(t *testing.T) { + tests := []struct { + name string + id string + mockResponse []byte + statusCode int + mockError error + wantResult []byte + wantErr bool + }{ + { + name: "successful response", + id: "testhash", + mockResponse: []byte(mock.LighterMockResponse), + statusCode: http.StatusOK, + wantResult: []byte(mock.ExpectedLighterResponse), + }, + { + name: "HTTP error", + id: "errorhash", + mockError: errors.New("connection refused"), + wantErr: true, + }, + { + name: "non-200 status", + id: "badstatus", + mockResponse: []byte("Not found"), + statusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "invalid JSON", + id: "badjson", + mockResponse: []byte("{invalid"), + statusCode: http.StatusOK, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := lighter.NewLighterAPI() + client.HTTPClient.Transport = roundTripperFunc(func(req *http.Request) (*http.Response, error) { + expectedURL := fmt.Sprintf("%s/v1/tx?by=hash&value=%s", lighter.LIGHTER_URL, tc.id) + if req.URL.String() != expectedURL { + return nil, fmt.Errorf("unexpected URL: got %s, want %s", req.URL.String(), expectedURL) + } + + if tc.mockError != nil { + return nil, tc.mockError + } + + return &http.Response{ + StatusCode: tc.statusCode, + Body: io.NopCloser(bytes.NewReader(tc.mockResponse)), + Header: make(http.Header), + }, nil + }) + + got, err := client.GetTx(tc.id) + + if tc.wantErr { + if err == nil { + t.Errorf("expected error got %v", err) + } + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tc.wantResult != nil { + if got == nil { + t.Fatal("expected non-nil result, got nil") + } + + var want *lighter.LighterTx + err = json.Unmarshal(tc.wantResult, &want) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %+v, got %+v", tc.wantResult, got) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("expected %+v, got %+v", tc.wantResult, got) + } + } else if got != nil { + t.Errorf("expected nil result, got %+v", got) + } + }) + } +} diff --git a/protocol/lighter/mock/mockData.go b/protocol/lighter/mock/mockData.go new file mode 100644 index 00000000..53aae39c --- /dev/null +++ b/protocol/lighter/mock/mockData.go @@ -0,0 +1,8 @@ +//nolint:gosec +package mock + +const LighterMockResponse = ` +{"code":200,"hash":"1e16eea4ad2ea1db483e7a0aeea2149fcafd96da3b9e7ecab5e95506f002182d1809858336e2af22","type":12,"info":"{\"FromAccountIndex\":283390,\"ApiKeyIndex\":0,\"ToAccountIndex\":3,\"USDCAmount\":2000000,\"Fee\":3000000,\"Memo\":[238,123,250,212,202,237,62,98,106,248,169,199,213,3,76,213,137,238,73,144,0,0,0,0,0,0,0,0,0,0,0,0],\"ExpiredAt\":1761149674469,\"Nonce\":2,\"Sig\":\"crr/TWDu2CzHpkOtGaTIow70UcgsdfChqSYurrEoFO/K+f9nZHvpOzNVXtZQBf7JhhVyMK9dDbzgt3nUK7z9N/GdbH/NakIGHzVod+9ULhQ=\",\"L1Sig\":\"0x320bdc9a593130a7e25103a572458188dd4e2efdb18c5d862c7e54da10d780fe751b444a259195baa4b5a7e3dc600dfa2227ec077a09919f3f97a736475eea471c\"}","event_info":"{\"f\":283390,\"t\":3,\"u\":2000000,\"fee\":3000000,\"ae\":\"\"}","status":3,"transaction_index":16,"l1_address":"0xEe7BfAD4caEd3e626AF8a9C7d5034CD589EE4990","account_index":283390,"nonce":2,"expire_at":1761149674469,"block_height":70687079,"queued_at":1761149084285,"sequence_index":16778685698,"parent_hash":"","committed_at":0,"verified_at":0,"executed_at":1761149084292} +` + +const ExpectedLighterResponse = `{"code":200,"hash":"1e16eea4ad2ea1db483e7a0aeea2149fcafd96da3b9e7ecab5e95506f002182d1809858336e2af22","type":12,"info":"{\"FromAccountIndex\":283390,\"ApiKeyIndex\":0,\"ToAccountIndex\":3,\"USDCAmount\":2000000,\"Fee\":3000000,\"Memo\":[238,123,250,212,202,237,62,98,106,248,169,199,213,3,76,213,137,238,73,144,0,0,0,0,0,0,0,0,0,0,0,0],\"ExpiredAt\":1761149674469,\"Nonce\":2,\"Sig\":\"crr/TWDu2CzHpkOtGaTIow70UcgsdfChqSYurrEoFO/K+f9nZHvpOzNVXtZQBf7JhhVyMK9dDbzgt3nUK7z9N/GdbH/NakIGHzVod+9ULhQ=\",\"L1Sig\":\"0x320bdc9a593130a7e25103a572458188dd4e2efdb18c5d862c7e54da10d780fe751b444a259195baa4b5a7e3dc600dfa2227ec077a09919f3f97a736475eea471c\"}","event_info":"{\"f\":283390,\"t\":3,\"u\":2000000,\"fee\":3000000,\"ae\":\"\"}","status":3,"transaction_index":16,"l1_address":"0xEe7BfAD4caEd3e626AF8a9C7d5034CD589EE4990","account_index":283390,"nonce":2,"expire_at":1761149674469,"block_height":70687079,"queued_at":1761149084285,"sequence_index":16778685698,"parent_hash":"","committed_at":0,"verified_at":0,"executed_at":1761149084292}`