feat: 添加 Lighter 适配器及相关功能,支持 trailing stops 和新的交易逻辑

This commit is contained in:
discountry
2025-09-30 22:08:05 +08:00
parent e83bdfccf9
commit c8b0ab1c8e
101 changed files with 90621 additions and 3 deletions
@@ -0,0 +1,49 @@
package client
import (
"crypto/tls"
"net"
"net/http"
"time"
)
var (
dialer = &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 60 * time.Second,
}
transport = &http.Transport{
DialContext: dialer.DialContext,
MaxConnsPerHost: 1000,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient = &http.Client{
Timeout: time.Second * 30,
Transport: transport,
}
)
type HTTPClient struct {
endpoint string
channelName string
fatFingerProtection bool
}
func NewHTTPClient(baseUrl string) *HTTPClient {
if baseUrl == "" {
return nil
}
return &HTTPClient{
endpoint: baseUrl,
channelName: "",
fatFingerProtection: true,
}
}
func (c *HTTPClient) SetFatFingerProtection(enabled bool) {
c.fatFingerProtection = enabled
}
@@ -0,0 +1,128 @@
package client
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/elliottech/lighter-go/types/txtypes"
)
func (c *HTTPClient) parseResultStatus(respBody []byte) error {
resultStatus := &ResultCode{}
if err := json.Unmarshal(respBody, resultStatus); err != nil {
return err
}
if resultStatus.Code != CodeOK {
return errors.New(resultStatus.Message)
}
return nil
}
func (c *HTTPClient) getAndParseL2HTTPResponse(path string, params map[string]any, result interface{}) error {
u, err := url.Parse(c.endpoint)
if err != nil {
return err
}
u.Path = path
q := u.Query()
for k, v := range params {
q.Set(k, fmt.Sprintf("%v", v))
}
u.RawQuery = q.Encode()
resp, err := httpClient.Get(u.String())
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(body))
}
if err = c.parseResultStatus(body); err != nil {
return err
}
if err := json.Unmarshal(body, result); err != nil {
return err
}
return nil
}
func (c *HTTPClient) GetNextNonce(accountIndex int64, apiKeyIndex uint8) (int64, error) {
result := &NextNonce{}
err := c.getAndParseL2HTTPResponse("api/v1/nextNonce", map[string]any{"account_index": accountIndex, "api_key_index": apiKeyIndex}, result)
if err != nil {
return -1, err
}
return result.Nonce, nil
}
func (c *HTTPClient) GetApiKey(accountIndex int64, apiKeyIndex uint8) (*AccountApiKeys, error) {
result := &AccountApiKeys{}
err := c.getAndParseL2HTTPResponse("api/v1/apikeys", map[string]any{"account_index": accountIndex, "api_key_index": apiKeyIndex}, result)
if err != nil {
return nil, err
}
return result, nil
}
func (c *HTTPClient) SendRawTx(tx txtypes.TxInfo) (string, error) {
txType := tx.GetTxType()
txInfo, err := tx.GetTxInfo()
if err != nil {
return "", err
}
data := url.Values{"tx_type": {strconv.Itoa(int(txType))}, "tx_info": {txInfo}}
if c.fatFingerProtection == false {
data.Add("price_protection", "false")
}
req, _ := http.NewRequest("POST", c.endpoint+"/api/v1/sendTx", strings.NewReader(data.Encode()))
req.Header.Set("Channel-Name", c.channelName)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(string(body))
}
if err = c.parseResultStatus(body); err != nil {
return "", err
}
res := &TxHash{}
if err := json.Unmarshal(body, res); err != nil {
return "", err
}
return res.TxHash, nil
}
func (c *HTTPClient) GetTransferFeeInfo(accountIndex, toAccountIndex int64, auth string) (*TransferFeeInfo, error) {
result := &TransferFeeInfo{}
err := c.getAndParseL2HTTPResponse("api/v1/transferFeeInfo", map[string]any{
"account_index": accountIndex,
"to_account_index": toAccountIndex,
"auth": auth,
}, result)
if err != nil {
return nil, err
}
return result, nil
}
@@ -0,0 +1,37 @@
package client
const (
CodeOK = 200
)
type ResultCode struct {
Code int32 `json:"code,example=200"`
Message string `json:"message,omitempty"`
}
type NextNonce struct {
ResultCode
Nonce int64 `json:"nonce,example=722"`
}
type ApiKey struct {
AccountIndex int64 `json:"account_index,example=3"`
ApiKeyIndex uint8 `json:"api_key_index,example=0"`
Nonce int64 `json:"nonce,example=722"`
PublicKey string `json:"public_key"`
}
type AccountApiKeys struct {
ResultCode
ApiKeys []*ApiKey `json:"api_keys"`
}
type TxHash struct {
ResultCode
TxHash string `json:"tx_hash,example=0x70997970C51812dc3A010C7d01b50e0d17dc79C8"`
}
type TransferFeeInfo struct {
ResultCode
TransferFee int64 `json:"transfer_fee_usdc"`
}
@@ -0,0 +1,109 @@
package client
import (
"encoding/hex"
"fmt"
"time"
"github.com/elliottech/lighter-go/signer"
"github.com/elliottech/lighter-go/types"
)
const (
defaultExpireTime = time.Minute*10 - time.Second // we need to give a second margin, to eliminate millisecond differences
)
type TxClient struct {
apiClient *HTTPClient
chainId uint32
keyManager signer.KeyManager
accountIndex int64
apiKeyIndex uint8
}
// NewTxClient is linked to a specific (account, apiKey) pair
// apiKeyPrivateKey should be hex-encoded bytes generated using `hexutil.Encode(TxClient.GetKeyManager().PrvKeyBytes())`
func NewTxClient(apiClient *HTTPClient, apiKeyPrivateKey string, accountIndex int64, apiKeyIndex uint8, chainId uint32) (*TxClient, error) {
// remove 0x from private key, if any, and parse to bytes
if len(apiKeyPrivateKey) < 2 {
return nil, fmt.Errorf("empty private key")
}
if apiKeyPrivateKey[:2] == "0x" {
apiKeyPrivateKey = apiKeyPrivateKey[2:]
}
b, err := hex.DecodeString(apiKeyPrivateKey)
if err != nil {
return nil, err
}
keyManager, err := signer.NewKeyManager(b)
if err != nil {
return nil, err
}
return &TxClient{
apiClient: apiClient,
apiKeyIndex: apiKeyIndex,
accountIndex: accountIndex,
chainId: chainId,
keyManager: keyManager,
}, nil
}
func (c *TxClient) FullFillDefaultOps(ops *types.TransactOpts) (*types.TransactOpts, error) {
if ops == nil {
ops = new(types.TransactOpts)
}
if ops.ExpiredAt == 0 {
ops.ExpiredAt = time.Now().Add(defaultExpireTime).UnixMilli()
}
if ops.FromAccountIndex == nil {
ops.FromAccountIndex = &c.accountIndex
}
if ops.ApiKeyIndex == nil {
ops.ApiKeyIndex = &c.apiKeyIndex
}
if ops.Nonce == nil {
if c.apiClient == nil {
return nil, fmt.Errorf("nonce was not provided & HTTPClient is nil. Either provide the nonce or enable HTTPClient to get the nonce from Lighter")
}
nonce, err := c.apiClient.GetNextNonce(*ops.FromAccountIndex, *ops.ApiKeyIndex)
if err != nil {
return nil, err
}
ops.Nonce = &nonce
}
return ops, nil
}
func (c *TxClient) GetAccountIndex() int64 {
return c.accountIndex
}
func (c *TxClient) GetApiKeyIndex() uint8 {
return c.apiKeyIndex
}
func (c *TxClient) GetKeyManager() signer.KeyManager {
return c.keyManager
}
func (c *TxClient) GetAuthToken(deadline time.Time) (string, error) {
if time.Until(deadline) > (7 * time.Hour) {
return "", fmt.Errorf("deadline should be within 7 hours")
}
return types.ConstructAuthToken(c.keyManager, deadline, &types.TransactOpts{
ApiKeyIndex: &c.apiKeyIndex,
FromAccountIndex: &c.accountIndex,
})
}
func (c *TxClient) HTTP() *HTTPClient {
return c.apiClient
}
func (c *TxClient) SwitchAPIKey(apiKey uint8) {
c.apiKeyIndex = apiKey
}
@@ -0,0 +1,192 @@
package client
import (
"fmt"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
)
func (c *TxClient) GetChangePubKeyTransaction(tx *types.ChangePubKeyReq, ops *types.TransactOpts) (*txtypes.L2ChangePubKeyTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructChangePubKeyTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
pk := c.keyManager.PubKeyBytes()
msgHash, _ := txInfo.Hash(c.chainId)
if err := schnorr.Validate(pk[:], msgHash, txInfo.Sig); err != nil {
return nil, fmt.Errorf("failed to validate signature. error: %v", err)
}
return txInfo, nil
}
func (c *TxClient) GetCreateSubAccountTransaction(ops *types.TransactOpts) (*txtypes.L2CreateSubAccountTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreateSubAccountTx(c.keyManager, c.chainId, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCreatePublicPoolTransaction(tx *types.CreatePublicPoolTxReq, ops *types.TransactOpts) (*txtypes.L2CreatePublicPoolTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreatePublicPoolTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdatePublicPoolTransaction(tx *types.UpdatePublicPoolTxReq, ops *types.TransactOpts) (*txtypes.L2UpdatePublicPoolTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdatePublicPoolTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetTransferTransaction(tx *types.TransferTxReq, ops *types.TransactOpts) (*txtypes.L2TransferTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructTransferTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetWithdrawTransaction(tx *types.WithdrawTxReq, ops *types.TransactOpts) (*txtypes.L2WithdrawTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructWithdrawTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCreateOrderTransaction(tx *types.CreateOrderTxReq, ops *types.TransactOpts) (*txtypes.L2CreateOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructCreateOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCancelOrderTransaction(tx *types.CancelOrderTxReq, ops *types.TransactOpts) (*txtypes.L2CancelOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CancelOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetModifyOrderTransaction(tx *types.ModifyOrderTxReq, ops *types.TransactOpts) (*txtypes.L2ModifyOrderTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2ModifyOrderTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetCancelAllOrdersTransaction(tx *types.CancelAllOrdersTxReq, ops *types.TransactOpts) (*txtypes.L2CancelAllOrdersTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CancelAllOrdersTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetMintSharesTransaction(tx *types.MintSharesTxReq, ops *types.TransactOpts) (*txtypes.L2MintSharesTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructMintSharesTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetBurnSharesTransaction(tx *types.BurnSharesTxReq, ops *types.TransactOpts) (*txtypes.L2BurnSharesTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructBurnSharesTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdateLeverageTransaction(tx *types.UpdateLeverageTxReq, ops *types.TransactOpts) (*txtypes.L2UpdateLeverageTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdateLeverageTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}
func (c *TxClient) GetUpdateMarginTransaction(tx *types.UpdateMarginTxReq, ops *types.TransactOpts) (*txtypes.L2UpdateMarginTxInfo, error) {
if c.keyManager == nil {
return nil, fmt.Errorf("key manager is nil")
}
if ops == nil {
ops = new(types.TransactOpts)
}
txInfo, err := types.ConstructUpdateMarginTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}