diff --git a/examples/create_grouped_orders.py b/examples/create_grouped_orders.py new file mode 100644 index 0000000..6dfa3a5 --- /dev/null +++ b/examples/create_grouped_orders.py @@ -0,0 +1,60 @@ +import asyncio +import lighter +import logging + +from lighter.signer_client import CreateOrderTxReq + +logging.basicConfig(level=logging.DEBUG) + +# The API_KEY_PRIVATE_KEY provided belongs to a dummy account registered on Testnet. +# It was generated using the setup_system.py script, and serves as an example. +BASE_URL = "https://testnet.zklighter.elliot.ai" +API_KEY_PRIVATE_KEY = "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b" +ACCOUNT_INDEX = 65 +API_KEY_INDEX = 3 + +async def main(): + client = lighter.SignerClient( + url=BASE_URL, + private_key=API_KEY_PRIVATE_KEY, + account_index=ACCOUNT_INDEX, + api_key_index=API_KEY_INDEX, + ) + + # Create a One-Cancels-the-Other grouped order with a take-profit and a stop-loss order + take_profit_order = CreateOrderTxReq( + MarketIndex=0, + ClientOrderIndex=0, + BaseAmount=1000, + Price=300000, + IsAsk=0, + Type=lighter.SignerClient.ORDER_TYPE_TAKE_PROFIT_LIMIT, + TimeInForce=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME, + ReduceOnly=1, + TriggerPrice=300000, + OrderExpiry=-1, + ) + + stop_loss_order = CreateOrderTxReq( + MarketIndex=0, + ClientOrderIndex=0, + BaseAmount=1000, + Price=500000, + IsAsk=0, + Type=lighter.SignerClient.ORDER_TYPE_STOP_LOSS_LIMIT, + TimeInForce=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME, + ReduceOnly=1, + TriggerPrice=500000, + OrderExpiry=-1, + ) + + transaction = await client.create_grouped_orders( + grouping_type=lighter.SignerClient.GROUPING_TYPE_ONE_CANCELS_THE_OTHER, + orders=[take_profit_order, stop_loss_order], + ) + + print("Create Grouped Order Tx:", transaction) + await client.close() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/lighter/signer_client.py b/lighter/signer_client.py index 20f7251..ca464a7 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -6,7 +6,7 @@ import logging import os import time -from typing import Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple from eth_account import Account from eth_account.messages import encode_defunct @@ -17,7 +17,7 @@ from lighter.models import TxHash from lighter import nonce_manager from lighter.models.resp_send_tx import RespSendTx -from lighter.transactions import CreateOrder, CancelOrder, Withdraw +from lighter.transactions import CreateOrder, CancelOrder, Withdraw, CreateGroupedOrders CODE_OK = 200 @@ -25,6 +25,19 @@ class ApiKeyResponse(ctypes.Structure): _fields_ = [("privateKey", ctypes.c_char_p), ("publicKey", ctypes.c_char_p), ("err", ctypes.c_char_p)] +class CreateOrderTxReq(ctypes.Structure): + _fields_ = [ + ("MarketIndex", ctypes.c_uint8), + ("ClientOrderIndex", ctypes.c_longlong), + ("BaseAmount", ctypes.c_longlong), + ("Price", ctypes.c_uint32), + ("IsAsk", ctypes.c_uint8), + ("Type", ctypes.c_uint8), + ("TimeInForce", ctypes.c_uint8), + ("ReduceOnly", ctypes.c_uint8), + ("TriggerPrice", ctypes.c_uint32), + ("OrderExpiry", ctypes.c_longlong), + ] class StrOrErr(ctypes.Structure): _fields_ = [("str", ctypes.c_char_p), ("err", ctypes.c_char_p)] @@ -126,6 +139,7 @@ class SignerClient: TX_TYPE_MINT_SHARES = 18 TX_TYPE_BURN_SHARES = 19 TX_TYPE_UPDATE_LEVERAGE = 20 + TX_TYPE_CREATE_GROUP_ORDER = 28 ORDER_TYPE_LIMIT = 0 ORDER_TYPE_MARKET = 1 @@ -152,6 +166,10 @@ class SignerClient: CROSS_MARGIN_MODE = 0 ISOLATED_MARGIN_MODE = 1 + GROUPING_TYPE_ONE_TRIGGERS_THE_OTHER = 1 + GROUPING_TYPE_ONE_CANCELS_THE_OTHER = 2 + GROUPING_TYPE_ONE_TRIGGERS_A_ONE_CANCELS_THE_OTHER = 3 + def __init__( self, url, @@ -354,6 +372,31 @@ def sign_create_order( return tx_info, error + def sign_create_grouped_orders( + self, + grouping_type: int, + orders: List[CreateOrderTxReq], + nonce=-1, + ): + arr_type = CreateOrderTxReq * len(orders) + orders_arr = arr_type(*orders) + + self.signer.SignCreateGroupedOrders.argtypes = [ + ctypes.c_uint8, + ctypes.POINTER(CreateOrderTxReq), + ctypes.c_int, + ctypes.c_longlong, + ] + self.signer.SignCreateGroupedOrders.restype = StrOrErr + + result = self.signer.SignCreateGroupedOrders( + grouping_type, orders_arr, len(orders), nonce + ) + + tx_info = result.str.decode("utf-8") if result.str else None + error = result.err.decode("utf-8") if result.err else None + return tx_info, error + def sign_cancel_order(self, market_index, order_index, nonce=-1): self.signer.SignCancelOrder.argtypes = [ ctypes.c_int, @@ -591,6 +634,27 @@ async def create_order( logging.debug(f"Create Order Send Tx Response: {api_response}") return CreateOrder.from_json(tx_info), api_response, None + @process_api_key_and_nonce + async def create_grouped_orders( + self, + grouping_type: int, + orders: List[CreateOrderTxReq], + nonce=-1, + api_key_index=-1, + ) -> (CreateGroupedOrders, TxHash, str): + tx_info, error = self.sign_create_grouped_orders( + grouping_type, + orders, + nonce, + ) + if error is not None: + return None, None, error + logging.debug(f"Create Grouped Orders Tx Info: {tx_info}") + + api_response = await self.send_tx(tx_type=self.TX_TYPE_CREATE_GROUP_ORDER, tx_info=tx_info) + logging.debug(f"Create Grouped Orders Send Tx Response: {api_response}") + return CreateGroupedOrders.from_json(tx_info), api_response, None + async def create_market_order( self, market_index, diff --git a/lighter/signers/signer-amd64.dll b/lighter/signers/signer-amd64.dll index fad37af..fdf817a 100644 Binary files a/lighter/signers/signer-amd64.dll and b/lighter/signers/signer-amd64.dll differ diff --git a/lighter/signers/signer-amd64.so b/lighter/signers/signer-amd64.so index 4818681..7e3a900 100644 Binary files a/lighter/signers/signer-amd64.so and b/lighter/signers/signer-amd64.so differ diff --git a/lighter/signers/signer-arm64.dylib b/lighter/signers/signer-arm64.dylib index e570d74..1080f8e 100644 Binary files a/lighter/signers/signer-arm64.dylib and b/lighter/signers/signer-arm64.dylib differ diff --git a/lighter/transactions/__init__.py b/lighter/transactions/__init__.py index 1650255..9cc7af3 100644 --- a/lighter/transactions/__init__.py +++ b/lighter/transactions/__init__.py @@ -1,3 +1,4 @@ from lighter.transactions.cancel_order import CancelOrder from lighter.transactions.create_order import CreateOrder +from lighter.transactions.create_grouped_orders import CreateGroupedOrders from lighter.transactions.withdraw import Withdraw diff --git a/lighter/transactions/create_grouped_orders.py b/lighter/transactions/create_grouped_orders.py new file mode 100644 index 0000000..f1dd0a8 --- /dev/null +++ b/lighter/transactions/create_grouped_orders.py @@ -0,0 +1,26 @@ +import json +from typing import Optional + +class CreateGroupedOrders: + def __init__(self): + self.account_index: Optional[int] = None + self.order_book_index: Optional[int] = None + self.grouping_type: Optional[int] = None + self.orders: Optional[list] = None + self.nonce: Optional[int] = None + self.sig: Optional[str] = None + + @classmethod + def from_json(cls, json_str: str) -> 'CreateGroupedOrders': + params = json.loads(json_str) + self = cls() + self.account_index = params.get('AccountIndex') + self.order_book_index = params.get('OrderBookIndex') + self.grouping_type = params.get('GroupingType') + self.orders = params.get('Orders') + self.nonce = params.get('Nonce') + self.sig = params.get('Sig') + return self + + def to_json(self) -> str: + return json.dumps(self.__dict__, default=str)