Files
2025-12-07 21:37:58 +08:00

732 lines
21 KiB
Go

//go:build js
// +build js
package main
import (
"fmt"
"strconv"
"syscall/js"
"time"
"github.com/elliottech/lighter-go/client"
"github.com/elliottech/lighter-go/client/http"
"github.com/elliottech/lighter-go/types"
"github.com/elliottech/lighter-go/types/txtypes"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func wrapErr(err error) js.Value {
if err != nil {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("%v", err)})
}
return js.ValueOf(map[string]interface{}{})
}
func messageToSign(info txtypes.TxInfo) string {
switch tx := info.(type) {
case *txtypes.L2ChangePubKeyTxInfo:
return tx.GetL1SignatureBody()
case *txtypes.L2TransferTxInfo:
return tx.GetL1SignatureBody()
default:
return ""
}
}
func convertTxInfoToJS(info txtypes.TxInfo, err error) js.Value {
if err != nil {
return wrapErr(err)
}
if info == nil {
return js.ValueOf(map[string]interface{}{"error": "nil response"})
}
txInfoStr, strErr := info.GetTxInfo()
if strErr != nil {
return wrapErr(strErr)
}
out := map[string]interface{}{
"txType": info.GetTxType(),
"txInfo": txInfoStr,
"txHash": info.GetTxHash(),
}
if msg := messageToSign(info); msg != "" {
out["messageToSign"] = msg
}
return js.ValueOf(out)
}
// safeInt safely extracts an int from a js.Value, handling undefined values
func safeInt(v js.Value, index int) (int64, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return int64(v.Int()), nil
}
// safeUint8 safely extracts a uint8 from a js.Value, handling undefined values
func safeUint8(v js.Value, index int) (uint8, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return uint8(v.Int()), nil
}
// safeUint32 safely extracts a uint32 from a js.Value, handling undefined values
func safeUint32(v js.Value, index int) (uint32, error) {
if v.Type() == js.TypeUndefined {
return 0, fmt.Errorf("argument %d is undefined", index)
}
return uint32(v.Int()), nil
}
func getClient(args []js.Value) (*client.TxClient, error) {
l := len(args)
if l < 2 {
return nil, fmt.Errorf("insufficient arguments: need at least 2 for apiKeyIndex and accountIndex")
}
// Check if the last two arguments are valid and extract safely
if args[l-2].Type() == js.TypeUndefined || args[l-1].Type() == js.TypeUndefined {
return nil, fmt.Errorf("apiKeyIndex or accountIndex is undefined")
}
apiKeyIndexVal, err := safeUint8(args[l-2], l-2)
if err != nil {
return nil, err
}
accountIndexVal, err := safeInt(args[l-1], l-1)
if err != nil {
return nil, err
}
return client.GetClient(apiKeyIndexVal, accountIndexVal)
}
// recoverPanic wraps a function execution with panic recovery
func recoverPanic(fn func() js.Value) (result js.Value) {
defer func() {
if r := recover(); r != nil {
result = wrapErr(fmt.Errorf("panic: %v", r))
}
}()
return fn()
}
func main() {
js.Global().Set("GenerateAPIKey", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 1 {
return js.ValueOf(map[string]interface{}{"error": "GenerateAPIKey expects 1 arg: seed"})
}
seed := args[0].String()
privateKey, publicKey, err := client.GenerateAPIKey(seed)
if err != nil {
return wrapErr(err)
}
return js.ValueOf(map[string]interface{}{"privateKey": privateKey, "publicKey": publicKey})
})
}))
js.Global().Set("CreateClient", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "CreateClient expects 5 args: url, privateKey, chainId, apiKeyIndex, accountIndex"})
}
url := args[0].String()
privateKey := args[1].String()
chainId := uint32(args[2].Int())
apiKeyIndex := uint8(args[3].Int())
accountIndex := int64(args[4].Int())
httpClient := http.NewClient(url)
_, err := client.CreateClient(httpClient, privateKey, chainId, apiKeyIndex, accountIndex)
if err != nil {
return wrapErr(err)
}
return wrapErr(nil)
})
}))
js.Global().Set("CheckClient", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 2 {
return js.ValueOf(map[string]interface{}{"error": "CheckClient expects 2 args: apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
err = c.Check()
if err != nil {
return wrapErr(err)
}
return wrapErr(nil)
})
}))
js.Global().Set("CreateAuthToken", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 3 {
return js.ValueOf(map[string]interface{}{"error": "CreateAuthToken expects 3 args: deadline, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
deadline := int64(args[0].Int())
if deadline == 0 {
deadline = time.Now().Add(time.Hour * 7).Unix()
}
token, err := c.GetAuthToken(time.Unix(deadline, 0))
if err != nil {
return wrapErr(err)
}
return js.ValueOf(map[string]interface{}{"authToken": token})
})
}))
js.Global().Set("SignChangePubKey", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 4 {
return js.ValueOf(map[string]interface{}{"error": "SignChangePubKey expects 4 args: pubKeyHex, nonce, apiKeyIndex, accountIndex"})
}
pubKeyHex := args[0].String()
nonce := int64(args[1].Int())
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
pubKeyBytes, err := hexutil.Decode(pubKeyHex)
if err != nil {
return wrapErr(err)
}
if len(pubKeyBytes) != 40 {
return js.ValueOf(map[string]interface{}{"error": "invalid pub key length. expected 40 but got " + strconv.Itoa(len(pubKeyBytes))})
}
var pubKey [40]byte
copy(pubKey[:], pubKeyBytes)
txInfo := &types.ChangePubKeyReq{
PubKey: pubKey,
}
ops := &types.TransactOpts{
Nonce: &nonce,
}
tx, err := c.GetChangePubKeyTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 13 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateOrder expects 13 args: marketIndex, clientOrderIndex, baseAmount, price, isAsk, orderType, timeInForce, reduceOnly, triggerPrice, orderExpiry, nonce, apiKeyIndex, accountIndex"})
}
// Validate all arguments are defined before accessing
for i := 0; i < 13; i++ {
if args[i].Type() == js.TypeUndefined {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("argument %d is undefined", i)})
}
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex, err := safeUint8(args[0], 0)
if err != nil {
return wrapErr(err)
}
clientOrderIndex, err := safeInt(args[1], 1)
if err != nil {
return wrapErr(err)
}
baseAmount, err := safeInt(args[2], 2)
if err != nil {
return wrapErr(err)
}
price, err := safeUint32(args[3], 3)
if err != nil {
return wrapErr(err)
}
isAsk, err := safeUint8(args[4], 4)
if err != nil {
return wrapErr(err)
}
orderType, err := safeUint8(args[5], 5)
if err != nil {
return wrapErr(err)
}
timeInForce, err := safeUint8(args[6], 6)
if err != nil {
return wrapErr(err)
}
reduceOnly, err := safeUint8(args[7], 7)
if err != nil {
return wrapErr(err)
}
triggerPrice, err := safeUint32(args[8], 8)
if err != nil {
return wrapErr(err)
}
orderExpiry, err := safeInt(args[9], 9)
if err != nil {
return wrapErr(err)
}
nonce, err := safeInt(args[10], 10)
if err != nil {
return wrapErr(err)
}
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli() // 28 days
}
txInfo := &types.CreateOrderTxReq{
MarketIndex: marketIndex,
ClientOrderIndex: clientOrderIndex,
BaseAmount: baseAmount,
Price: price,
IsAsk: isAsk,
Type: orderType,
TimeInForce: timeInForce,
ReduceOnly: reduceOnly,
TriggerPrice: triggerPrice,
OrderExpiry: orderExpiry,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreateOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCancelOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCancelOrder expects 5 args: marketIndex, orderIndex, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
orderIndex := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.CancelOrderTxReq{
MarketIndex: marketIndex,
Index: orderIndex,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCancelOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCancelAllOrders", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCancelAllOrders expects 5 args: timeInForce, time, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
timeInForce := uint8(args[0].Int())
timeVal := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.CancelAllOrdersTxReq{
TimeInForce: timeInForce,
Time: timeVal,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCancelAllOrdersTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignTransfer", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 7 {
return js.ValueOf(map[string]interface{}{"error": "SignTransfer expects 7 args: toAccount, usdcAmount, fee, memo, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
toAccount := int64(args[0].Int())
usdcAmount := int64(args[1].Int())
fee := int64(args[2].Int())
memoStr := args[3].String()
nonce := int64(args[4].Int())
var memoArr [32]byte
bs := []byte(memoStr)
if len(bs) != 32 {
return wrapErr(fmt.Errorf("memo expected to be 32 bytes long"))
}
for i := 0; i < 32; i++ {
memoArr[i] = bs[i]
}
txInfo := &types.TransferTxReq{
ToAccountIndex: toAccount,
USDCAmount: usdcAmount,
Fee: fee,
Memo: memoArr,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetTransferTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignWithdraw", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 4 {
return js.ValueOf(map[string]interface{}{"error": "SignWithdraw expects 4 args: usdcAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
usdcAmount := uint64(args[0].Int())
nonce := int64(args[1].Int())
txInfo := &types.WithdrawTxReq{
USDCAmount: usdcAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetWithdrawTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdateLeverage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdateLeverage expects 6 args: marketIndex, fraction, marginMode, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
fraction := uint16(args[1].Int())
marginMode := uint8(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.UpdateLeverageTxReq{
MarketIndex: marketIndex,
InitialMarginFraction: fraction,
MarginMode: marginMode,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdateLeverageTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignModifyOrder", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 8 {
return js.ValueOf(map[string]interface{}{"error": "SignModifyOrder expects 8 args: marketIndex, index, baseAmount, price, triggerPrice, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
index := int64(args[1].Int())
baseAmount := int64(args[2].Int())
price := uint32(args[3].Int())
triggerPrice := uint32(args[4].Int())
nonce := int64(args[5].Int())
txInfo := &types.ModifyOrderTxReq{
MarketIndex: marketIndex,
Index: index,
BaseAmount: baseAmount,
Price: price,
TriggerPrice: triggerPrice,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetModifyOrderTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateSubAccount", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 3 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateSubAccount expects 3 args: nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
nonce := int64(args[0].Int())
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreateSubAccountTransaction(ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreatePublicPool", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignCreatePublicPool expects 6 args: operatorFee, initialTotalShares, minOperatorShareRate, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
operatorFee := int64(args[0].Int())
initialTotalShares := int64(args[1].Int())
minOperatorShareRate := int64(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.CreatePublicPoolTxReq{
OperatorFee: operatorFee,
InitialTotalShares: initialTotalShares,
MinOperatorShareRate: minOperatorShareRate,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetCreatePublicPoolTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdatePublicPool", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 7 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdatePublicPool expects 7 args: publicPoolIndex, status, operatorFee, minOperatorShareRate, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := uint8(args[0].Int())
status := uint8(args[1].Int())
operatorFee := int64(args[2].Int())
minOperatorShareRate := int64(args[3].Int())
nonce := int64(args[4].Int())
txInfo := &types.UpdatePublicPoolTxReq{
PublicPoolIndex: int64(publicPoolIndex),
Status: status,
OperatorFee: operatorFee,
MinOperatorShareRate: minOperatorShareRate,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdatePublicPoolTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignMintShares", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignMintShares expects 5 args: publicPoolIndex, shareAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := int64(args[0].Int())
shareAmount := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.MintSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetMintSharesTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignBurnShares", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignBurnShares expects 5 args: publicPoolIndex, shareAmount, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
publicPoolIndex := int64(args[0].Int())
shareAmount := int64(args[1].Int())
nonce := int64(args[2].Int())
txInfo := &types.BurnSharesTxReq{
PublicPoolIndex: publicPoolIndex,
ShareAmount: shareAmount,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetBurnSharesTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignUpdateMargin", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 6 {
return js.ValueOf(map[string]interface{}{"error": "SignUpdateMargin expects 6 args: marketIndex, usdcAmount, direction, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
marketIndex := uint8(args[0].Int())
usdcAmount := int64(args[1].Int())
direction := uint8(args[2].Int())
nonce := int64(args[3].Int())
txInfo := &types.UpdateMarginTxReq{
MarketIndex: marketIndex,
USDCAmount: usdcAmount,
Direction: direction,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
tx, err := c.GetUpdateMarginTransaction(txInfo, ops)
return convertTxInfoToJS(tx, err)
})
}))
js.Global().Set("SignCreateGroupedOrders", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return recoverPanic(func() js.Value {
if len(args) < 5 {
return js.ValueOf(map[string]interface{}{"error": "SignCreateGroupedOrders expects 5 args: groupingType, orders array, nonce, apiKeyIndex, accountIndex"})
}
c, err := getClient(args)
if err != nil {
return wrapErr(err)
}
groupingType := uint8(args[0].Int())
// Parse orders array from JS
ordersArg := args[1]
if ordersArg.Type() != js.TypeObject {
return js.ValueOf(map[string]interface{}{"error": "orders must be an array"})
}
length := ordersArg.Length()
orders := make([]*types.CreateOrderTxReq, length)
for i := 0; i < length; i++ {
orderObj := ordersArg.Index(i)
if orderObj.Type() != js.TypeObject {
return js.ValueOf(map[string]interface{}{"error": fmt.Sprintf("order %d must be an object", i)})
}
orderExpiry := int64(orderObj.Get("OrderExpiry").Int())
if orderExpiry == -1 {
orderExpiry = time.Now().Add(time.Hour * 24 * 28).UnixMilli()
}
orders[i] = &types.CreateOrderTxReq{
MarketIndex: uint8(orderObj.Get("MarketIndex").Int()),
ClientOrderIndex: int64(orderObj.Get("ClientOrderIndex").Int()),
BaseAmount: int64(orderObj.Get("BaseAmount").Int()),
Price: uint32(orderObj.Get("Price").Int()),
IsAsk: uint8(orderObj.Get("IsAsk").Int()),
Type: uint8(orderObj.Get("Type").Int()),
TimeInForce: uint8(orderObj.Get("TimeInForce").Int()),
ReduceOnly: uint8(orderObj.Get("ReduceOnly").Int()),
TriggerPrice: uint32(orderObj.Get("TriggerPrice").Int()),
OrderExpiry: orderExpiry,
}
}
nonce := int64(args[2].Int())
req := &types.CreateGroupedOrdersTxReq{
GroupingType: groupingType,
Orders: orders,
}
ops := new(types.TransactOpts)
if nonce != -1 {
ops.Nonce = &nonce
}
txInfo, err := c.GetCreateGroupedOrdersTransaction(req, ops)
return convertTxInfoToJS(txInfo, err)
})
}))
select {}
}