support lighter spot

This commit is contained in:
discountry
2025-12-07 21:37:58 +08:00
parent 5f79b134f2
commit 180f47b6e0
89 changed files with 11921 additions and 124 deletions
@@ -0,0 +1,116 @@
package client
import (
"fmt"
"strings"
"sync"
curve "github.com/elliottech/poseidon_crypto/curve/ecgfp5"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// SharedClientManager holds the global txClient and backupTxClients
// This will be managed by both sharedlib and wasm builds
// Supports multiple accounts and API keys with thread safety
var (
txClientMu sync.Mutex
defaultTxClient *TxClient
defaultClientPerAccount = make(map[int64]*TxClient)
allTxClients map[int64]map[uint8]*TxClient // accountIndex -> apiKeyIndex -> client
)
// GenerateAPIKey generates a new API key pair from a seed
func GenerateAPIKey(seed string) (string, string, error) {
var seedP *string
if seed != "" {
seedP = &seed
}
key := curve.SampleScalar(seedP)
publicKeyStr := hexutil.Encode(schnorr.SchnorrPkFromSk(key).ToLittleEndianBytes())
privateKeyStr := hexutil.Encode(key.ToLittleEndianBytes())
return privateKeyStr, publicKeyStr, nil
}
// GetClient retrieves a client for specific account and API key
// If apiKeyIndex==255 && accountIndex==-1, returns default client
func GetClient(apiKeyIndex uint8, accountIndex int64) (*TxClient, error) {
txClientMu.Lock()
defer txClientMu.Unlock()
if apiKeyIndex == 255 && accountIndex != -1 {
client := defaultClientPerAccount[accountIndex]
if client != nil {
return client, nil
}
}
// Special case: return default client
if apiKeyIndex == 255 && accountIndex == -1 {
if defaultTxClient == nil {
return nil, fmt.Errorf("client is not created, call CreateClient() first")
}
return defaultTxClient, nil
}
// Look up client in double map
var c *TxClient
if allTxClients[accountIndex] != nil {
c = allTxClients[accountIndex][apiKeyIndex]
}
if c == nil {
return nil, fmt.Errorf("client is not created for apiKeyIndex: %v accountIndex: %v", apiKeyIndex, accountIndex)
}
return c, nil
}
// CreateClient creates a new TxClient and stores it
// httpClientFactory is a function that creates an HTTP client from a URL string
func CreateClient(httpClient MinimalHTTPClient, privateKey string, chainId uint32, apiKeyIndex uint8, accountIndex int64) (*TxClient, error) {
if accountIndex <= 0 {
return nil, fmt.Errorf("invalid account index")
}
txClientInstance, err := NewTxClient(httpClient, privateKey, accountIndex, apiKeyIndex, chainId)
if err != nil {
return nil, fmt.Errorf("error occurred when creating TxClient. err: %v", err)
}
txClientMu.Lock()
if allTxClients == nil {
allTxClients = make(map[int64]map[uint8]*TxClient)
}
if allTxClients[accountIndex] == nil {
allTxClients[accountIndex] = make(map[uint8]*TxClient)
}
allTxClients[accountIndex][apiKeyIndex] = txClientInstance
// Update default client (most recently created becomes default)
defaultTxClient = txClientInstance
defaultClientPerAccount[accountIndex] = txClientInstance
txClientMu.Unlock()
return txClientInstance, nil
}
// Check validates that the client exists and the API key matches the one on the server
func (c *TxClient) Check() error {
// check that the API key registered on Lighter matches this one
publicKey, err := c.HTTP().GetApiKey(c.accountIndex, c.apiKeyIndex)
if err != nil {
return fmt.Errorf("failed to get Api Keys. err: %v", err)
}
pubKeyBytes := c.GetKeyManager().PubKeyBytes()
pubKeyStr := hexutil.Encode(pubKeyBytes[:])
pubKeyStr = strings.Replace(pubKeyStr, "0x", "", 1)
if publicKey != pubKeyStr {
return fmt.Errorf("private key does not match the one on Lighter. ownPubKey: %s response: %+v", pubKeyStr, publicKey)
}
return nil
}
@@ -0,0 +1,6 @@
# HTTP
The HTTP module is a simple implementation that aims to provide just 2 things:
- `GetNextNonce` so that users can send transactions w/out calling managing nonces on their side
- `GetApiKey` so that users can call `CheckClient` from other sources, which makes sure that the client was configured correctly.
Other usages, like sending trades, fetching open orders or any WebSocket operations should happen outside the core SDK.
@@ -0,0 +1,45 @@
package http
import (
"crypto/tls"
"net"
"net/http"
"time"
core "github.com/elliottech/lighter-go/client"
)
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: false},
}
httpClient = &http.Client{
Timeout: time.Second * 30,
Transport: transport,
}
)
var _ core.MinimalHTTPClient = (*client)(nil)
type client struct {
endpoint string
}
func NewClient(baseUrl string) core.MinimalHTTPClient {
if baseUrl == "" {
return nil
}
return &client{
endpoint: baseUrl,
}
}
@@ -0,0 +1,27 @@
package http
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"`
}
@@ -0,0 +1,75 @@
package http
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
)
func (c *client) 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 *client) 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 *client) 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 *client) GetApiKey(accountIndex int64, apiKeyIndex uint8) (string, error) {
result := &AccountApiKeys{}
err := c.getAndParseL2HTTPResponse("api/v1/apikeys", map[string]any{"account_index": accountIndex, "api_key_index": apiKeyIndex}, result)
if err != nil {
return "", err
}
if len(result.ApiKeys) == 0 {
return "", fmt.Errorf("no api keys returned")
}
return result.ApiKeys[0].PublicKey, nil
}
@@ -0,0 +1,6 @@
package client
type MinimalHTTPClient interface {
GetNextNonce(accountIndex int64, apiKeyIndex uint8) (int64, error)
GetApiKey(accountIndex int64, apiKeyIndex uint8) (string, error)
}
@@ -0,0 +1,104 @@
package client
import (
"encoding/hex"
"fmt"
"time"
"github.com/elliottech/lighter-go/signer"
"github.com/elliottech/lighter-go/types"
)
var (
// DefaultExpireTime is a public var, so it can be changed directly in the SDK if required.
// The encouraged behaviour is the manually specify the TX deadline in types.TransactOpts.ExpiredAt
DefaultExpireTime = time.Minute*10 - time.Second // we need to give a second margin, to eliminate millisecond differences
)
type TxClient struct {
apiClient MinimalHTTPClient
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 MinimalHTTPClient, 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
}
// FullFillDefaultOps returns a usable TransactOpts object if none was provided.
// This should not the be case for sharedlib, except for the nonce, which is optional.
// Still, the behaviour is implemented, so it can be extended easily by extending the code GO SDK.
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 || *ops.Nonce == -1 {
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) GetChainId() uint32 {
return c.chainId
}
func (c *TxClient) GetKeyManager() signer.KeyManager {
return c.keyManager
}
func (c *TxClient) GetAccountIndex() int64 {
return c.accountIndex
}
func (c *TxClient) GetApiKeyIndex() uint8 {
return c.apiKeyIndex
}
func (c *TxClient) HTTP() MinimalHTTPClient {
return c.apiClient
}
@@ -0,0 +1,209 @@
package client
import (
"fmt"
"time"
schnorr "github.com/elliottech/poseidon_crypto/signature/schnorr"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
)
func (c *TxClient) GetAuthToken(deadline time.Time) (string, error) {
return types.ConstructAuthToken(c.keyManager, deadline, &types.TransactOpts{
ApiKeyIndex: &c.apiKeyIndex,
FromAccountIndex: &c.accountIndex,
})
}
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) GetCreateGroupedOrdersTransaction(tx *types.CreateGroupedOrdersTxReq, ops *types.TransactOpts) (*txtypes.L2CreateGroupedOrdersTxInfo, error) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructL2CreateGroupedOrdersTx(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) {
ops, err := c.FullFillDefaultOps(ops)
if err != nil {
return nil, err
}
txInfo, err := types.ConstructUpdateMarginTx(c.keyManager, c.chainId, tx, ops)
if err != nil {
return nil, err
}
return txInfo, nil
}