mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
refactor: restructure Lighter SDK by removing deprecated Go files and enhancing Python documentation with new examples and models
This commit is contained in:
@@ -0,0 +1 @@
|
||||
api_key_config.json
|
||||
@@ -5,18 +5,152 @@
|
||||
- this will require you to enter your Ethereum private key
|
||||
- the eth private key will only be used in the Py SDK to sign a message
|
||||
- the eth private key is not required in order to trade on the platform
|
||||
- the eth private key is not passed to the binary
|
||||
- copy the output of the script and post it into `create_cancel_order.py`
|
||||
- the output should look like
|
||||
```
|
||||
BASE_URL = 'https://testnet.zklighter.elliot.ai'
|
||||
API_KEY_PRIVATE_KEY = '0xea5d2eca5be67eca056752eaf27b173518b8a5550117c09d2b58c7ea7d306cc4426f913ccf27ab19'
|
||||
ACCOUNT_INDEX = 595
|
||||
API_KEY_INDEX = 1
|
||||
```
|
||||
- start trading using
|
||||
- `create_cancel_order.py` has an example which created an order on testnet & cancels it
|
||||
- you'll need to set up both your account index, api key index & API Key private key
|
||||
- the eth private key is not passed to the binary
|
||||
- the API key config is saved in a local file `./api_key_config.json`
|
||||
|
||||
## Start trading on testnet
|
||||
- `create_modify_cancel_order_http.py`
|
||||
- creates an ask (sell) order for 0.1 ETH @ $4050
|
||||
- modified the order and increases the size to 0.11 ETH and increases the price to $4100
|
||||
- cancels the order
|
||||
- Note: all of these operations use the client order index of the order. You can use the order from the exchange as well
|
||||
|
||||
- `create_modify_cancel_order_ws.py`
|
||||
- same flow as `create_modify_cancel_order_http.py`
|
||||
- sends TXs over WS instead of HTTP
|
||||
|
||||
- `create_market_order_eth_buy.py`
|
||||
- creates a market buy order for 0.1 ETH @ market price
|
||||
- `create_market_order_eth_sell.py`
|
||||
- creates a market sell order for 0.1 ETH @ market price
|
||||
|
||||
- `create_grouped_ioc_with_attached_sl_tp.py`
|
||||
- creates an ask (sell) IoC order for 0.1 ETH
|
||||
- along w/ the order, it sets up a Stop Loss (SL) and a Take Profit (TP) order for the whole size of the order
|
||||
- the size of the SL/TP will be equal to the executed size of the order
|
||||
- the SL/TP orders are canceled when the sign of your position changes
|
||||
|
||||
- `create_position_tied_sl_tp.py`
|
||||
- creates a bid (buy) Stop Loss (SL) and a Take Profit (TP) to close your short position
|
||||
- the size of the orders will be for your whole position (because BaseAmount=0)
|
||||
- the orders will grow / shrink as you accumulate more position
|
||||
- the SL/TP orders are canceled when the sign of your position changes
|
||||
|
||||
## On SL/TP orders
|
||||
SL/TP orders need to be configured beyond just setting the trigger price. When the trigger price is set,
|
||||
the order will just be executed, like a normal order. This means that a market order, for example, might not have enough slippage! \
|
||||
Let's say that you have a 1 BTC long position, and the current price is $110'000. \
|
||||
You want to set up a take profit at $120'000
|
||||
- order should be an ask (sell) order, to close your position
|
||||
- the trigger price should be $120'000
|
||||
|
||||
What about the order types? Just as normal orders, SL/TP orders trigger an order, which can be:
|
||||
- market order
|
||||
- limit IOC / GTC
|
||||
|
||||
## Modify leverage / Margin Mode (Cross, Isolated) / Add Collateral to isolated-only positions
|
||||
- `margin_eth_20x_cross_http`
|
||||
- sets ETH market to 20x leverage and cross-margin mode, using HTTP
|
||||
- `margin_eth_50x_isolate_ws`
|
||||
- sets ETH market to 50x leverage and isolated margin mode, using HTTP
|
||||
- `margin_eth_add_collateral_http.py`
|
||||
- adds $10.5 USDC to the ETH position (must be opened and in isolated mode)
|
||||
- `margin_eth_remove_collateral_ws.py`
|
||||
- removes $5 USDC from the ETH position (must be opened and in isolated mode)
|
||||
|
||||
## Batch orders
|
||||
- `send_batch_tx_http.py`
|
||||
- sends multiple orders in a single HTTP request
|
||||
- `send_batch_tx_ws.py`
|
||||
- sends multiple orders in a single WS request`
|
||||
|
||||
Batch TXs will be executed back to back, without the possibility of other TXs interfering.
|
||||
|
||||
## Spot Trading
|
||||
To trade spot markets, you need to have spot USDC. USDC used in your perpetual account will be used as collateral for your cross-positions.
|
||||
USDC deposited in the spot account can only be used to buy spot assets.
|
||||
To transfer USDC between spot <> perp balance, or vice verse, check out
|
||||
- `spot_self_transfer_perp_spot.py`
|
||||
- `spot_self_transfer_spot_perp.py`
|
||||
|
||||
Order placement / trades work in the same way as for perpetual markets.
|
||||
The fee will be paid in the received asset for premium spot trades.
|
||||
This means that if you sell ETH, you'll receive less USDC, and if you BUY 1 ETH, you'll receive slightly less than 1 ETH.
|
||||
You can check out the following examples, which should work on spot ETH by changing the market index to 2048 instead of 0.
|
||||
- `create_modify_cancel_order_http.py`
|
||||
- `create_modify_cancel_order_ws.py`
|
||||
- `create_market_order_eth_buy.py`
|
||||
- `create_market_order_eth_sell.py`
|
||||
- `send_batch_tx_http.py`
|
||||
- `send_batch_tx_ws.py`
|
||||
|
||||
Trading setup is very similar to perpetual markets.
|
||||
The only difference is that you'll need to hold USDC / ETH before placing an order.
|
||||
For example, on perp markets you can place an order to short (sell) ETH without having to worry that much.
|
||||
The limitation there would be to have enough available collateral to cover the order.
|
||||
On spot orders, you need to have enough assets in your spot account to cover all open orders.
|
||||
If you want to place two orders, to buy 1000 USDC worth of ETH and 1000 USDC worth of ZK, you'll need to have at least 2000 available USDC.
|
||||
|
||||
You can get the order book details (including symbol and market index) as well as quote asset id (ETH) and base asset id (USDC)
|
||||
by following the example below:
|
||||
- `spot_get_order_books.py`
|
||||
|
||||
Note: you'll need the quote asset id and base asset id to check available balance.
|
||||
Available balance is not locked in open orders.
|
||||
|
||||
To keep track of your spot balance, you can use HTTP calls or a websocket subscription.
|
||||
Examples on how to do this can be found here:
|
||||
- `spot_get_account_assets_http.py`
|
||||
- `spot_get_account_assets_ws.py`
|
||||
|
||||
Moving money to / from subaccounts is possible for spot assets.
|
||||
For USDC, you can move directly from main perp balance to subaccount spot balance, for example.
|
||||
More details can be found in the following example:
|
||||
- `sub_account_create.py`
|
||||
- `sub_account_transfer_eth.py`
|
||||
- `sub_account_transfer_usdc.py`
|
||||
|
||||
## Public Pools
|
||||
Public pools behave just like subaccounts, except that anyone can join them.
|
||||
You can create / modify a public pool using the SDK. Check out the following example:
|
||||
- `public_pool_create_modify.py`
|
||||
|
||||
To create API keys for a public pool, you need to run the setup script but specify the `ACCOUNT_INDEX` to be the one of the public pool.
|
||||
After that, you can trade from the public as from any other account.
|
||||
|
||||
If you want to deposit / withdraw from a public pool, check the following example:
|
||||
- `public_pool_deposit.py`
|
||||
- `public_pool_withdraw.py`
|
||||
|
||||
To get information about pools, check:
|
||||
- `public_pool_info.py`
|
||||
|
||||
## Moving funds around
|
||||
- `withdraw_fast.py`
|
||||
- send USDC directly from Lighter to Arbitrum
|
||||
- `withdraw_normal.py`
|
||||
- send USDC/ETH from Lighter to Ethereum
|
||||
- `transfer.py`
|
||||
- generic example of how to transfer funds between accounts.
|
||||
- same functionality as `sub_account_transfer_eth` and `sub_account_transfer_usdc`
|
||||
|
||||
## Transfer Notes
|
||||
The `memo` field is a user message, and it has to be exactly 32 bytes long. In case of fast withdrawals, you need to specify the recipient in the memo.
|
||||
This is the case since the memo is part of the signature. This way, the recipient is verified.
|
||||
|
||||
When calling `client.transfer`, you pass the amount without needing to worry about the decimals.
|
||||
When calling `client.sign_transfer` on the other hand, you need to specify the decimals and pass an integer.
|
||||
|
||||
The `fee` field can be obtained by calling `info_api.transfer_fee_info(...)`. The field can be passed as it is.
|
||||
Transfers between subaccounts are free for all assets.
|
||||
|
||||
When sending assets, you can specify the source and destination routes.
|
||||
A route is either `perp` or `spot`. You can send USDC directly from your perp balance to another person's spot balance.
|
||||
If you receive USDC in your perp account, it will be instantly used as collateral for open positions.
|
||||
This also allows you to move USDC from your spot balance to your perp balance.
|
||||
Spot assets (like ETH) need to have both the from and to route set to `spot`.
|
||||
You can get all `asset_id`s by following the example below:
|
||||
- `spot_get_order_books.py`
|
||||
|
||||
## Setup steps for mainnet
|
||||
- deposit money on Lighter to create an account first
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lighter
|
||||
|
||||
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 servers as an example.
|
||||
# Alternatively, you can go to https://app.lighter.xyz/apikeys for mainnet api keys
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
API_KEY_PRIVATE_KEY = "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
|
||||
ACCOUNT_INDEX = 65
|
||||
API_KEY_INDEX = 1
|
||||
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
|
||||
|
||||
async def main():
|
||||
api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL))
|
||||
|
||||
client = lighter.SignerClient(
|
||||
url=BASE_URL,
|
||||
private_key=API_KEY_PRIVATE_KEY,
|
||||
account_index=ACCOUNT_INDEX,
|
||||
api_key_index=API_KEY_INDEX,
|
||||
)
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {trim_exception(err)}")
|
||||
return
|
||||
|
||||
# create order
|
||||
tx, tx_hash, err = await client.create_order(
|
||||
market_index=0,
|
||||
client_order_index=123,
|
||||
base_amount=100000,
|
||||
price=405000,
|
||||
is_ask=True,
|
||||
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
|
||||
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=0,
|
||||
trigger_price=0,
|
||||
)
|
||||
print(f"Create Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
auth, err = client.create_auth_token_with_expiry(lighter.SignerClient.DEFAULT_10_MIN_AUTH_EXPIRY)
|
||||
print(f"{auth=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
# cancel order
|
||||
tx, tx_hash, err = await client.cancel_order(
|
||||
market_index=0,
|
||||
order_index=123,
|
||||
)
|
||||
print(f"Cancel Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,69 @@
|
||||
import asyncio
|
||||
from lighter.signer_client import CreateOrderTxReq
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Sell some ETH at $2500
|
||||
# The size of the SL/TP orders will be equal to the size of the executed order
|
||||
|
||||
# set SL trigger price at 5000 and limit price at 5050
|
||||
# set TP trigger price at 1500 and limit price at 1550
|
||||
# Note: set the limit price to be higher than the SL/TP trigger price to ensure the order will be filled
|
||||
# If the mark price of ETH reaches 1500, there might be no one willing to sell you ETH at 1500, so trying to buy at 1550 would increase the fill rate
|
||||
|
||||
ioc_order = CreateOrderTxReq(
|
||||
MarketIndex=0,
|
||||
ClientOrderIndex=0,
|
||||
BaseAmount=1000, # 0.1 ETH
|
||||
Price=2500_00, # $2500
|
||||
IsAsk=1, # sell
|
||||
Type=client.ORDER_TYPE_LIMIT,
|
||||
TimeInForce=client.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
|
||||
ReduceOnly=0,
|
||||
TriggerPrice=0,
|
||||
OrderExpiry=0,
|
||||
)
|
||||
|
||||
# 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=0,
|
||||
Price=1550_00,
|
||||
IsAsk=0,
|
||||
Type=client.ORDER_TYPE_TAKE_PROFIT_LIMIT,
|
||||
TimeInForce=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
ReduceOnly=1,
|
||||
TriggerPrice=1500_00,
|
||||
OrderExpiry=-1,
|
||||
)
|
||||
|
||||
stop_loss_order = CreateOrderTxReq(
|
||||
MarketIndex=0,
|
||||
ClientOrderIndex=0,
|
||||
BaseAmount=0,
|
||||
Price=5050_00,
|
||||
IsAsk=0,
|
||||
Type=client.ORDER_TYPE_STOP_LOSS_LIMIT,
|
||||
TimeInForce=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
ReduceOnly=1,
|
||||
TriggerPrice=5000_00,
|
||||
OrderExpiry=-1,
|
||||
)
|
||||
|
||||
transaction = await client.create_grouped_orders(
|
||||
grouping_type=client.GROUPING_TYPE_ONE_TRIGGERS_A_ONE_CANCELS_THE_OTHER,
|
||||
orders=[ioc_order, take_profit_order, stop_loss_order],
|
||||
)
|
||||
|
||||
print("Create Grouped Order Tx:", transaction)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,39 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lighter
|
||||
|
||||
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
|
||||
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
tx = await client.create_market_order(
|
||||
market_index=0,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
avg_execution_price=170000, # $1700 -- worst acceptable price for the order
|
||||
is_ask=True,
|
||||
)
|
||||
print("Create Order Tx:", tx)
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,28 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
client.check_client()
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 0
|
||||
|
||||
tx, tx_hash, err = await client.create_market_order(
|
||||
market_index=market_index,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
avg_execution_price=4000_00, # $4000 -- worst acceptable price for the order
|
||||
is_ask=False,
|
||||
)
|
||||
print(f"Create Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,28 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
client.check_client()
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 0
|
||||
|
||||
tx, tx_hash, err = await client.create_market_order(
|
||||
market_index=market_index,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
avg_execution_price=1700_00, # $1700 -- worst acceptable price for the order
|
||||
is_ask=True,
|
||||
)
|
||||
print(f"Create Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,33 +1,21 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lighter
|
||||
|
||||
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 = "0xe0fa55e11d6b5575d54c0500bd2f3b240221ae90241e3b573f2307e27de20c04ea628de3f1936e56"
|
||||
ACCOUNT_INDEX = 22
|
||||
API_KEY_INDEX = 3
|
||||
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# tx = await client.create_market_order_limited_slippage(market_index=0, client_order_index=0, base_amount=30000000,
|
||||
# max_slippage=0.001, is_ask=True)
|
||||
tx = await client.create_market_order_if_slippage(market_index=0, client_order_index=0, base_amount=30000000,
|
||||
max_slippage=0.01, is_ask=True, ideal_price=300000)
|
||||
tx = await client.create_market_order_if_slippage(
|
||||
market_index=0, # ETH
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
max_slippage=0.01, # 1%
|
||||
is_ask=True,
|
||||
ideal_price=300000 # $3000
|
||||
)
|
||||
|
||||
print("Create Order Tx:", tx)
|
||||
await client.close()
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
client.check_client()
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 0
|
||||
|
||||
# create order
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
tx, tx_hash, err = await client.create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=123,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=4050_00, # $4050
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
print(f"Create Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
## modify order
|
||||
# use the same API key so the TX goes after the create order TX
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
tx, tx_hash, err = await client.modify_order(
|
||||
market_index=market_index,
|
||||
order_index=123,
|
||||
base_amount=1100, # 0.11 ETH
|
||||
price=4100_00, # $4100
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
print(f"Modify Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
## cancel order
|
||||
# use the same API key so the TX goes after the modify order TX
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
tx, tx_hash, err = await client.cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=123,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
print(f"Cancel Order {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,72 @@
|
||||
import websockets
|
||||
import asyncio
|
||||
from utils import default_example_setup, ws_send_tx
|
||||
|
||||
|
||||
# this example does the same thing as the create_modify_cancel_order.py example, but sends the TX over WS instead of HTTP
|
||||
async def main():
|
||||
client, api_client, ws_client_promise = default_example_setup()
|
||||
client.check_client()
|
||||
|
||||
# set up WS client and print a connected message
|
||||
ws_client: websockets.ClientConnection = await ws_client_promise
|
||||
print("Received:", await ws_client.recv())
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 0
|
||||
|
||||
# create order
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
tx_type, tx_info, tx_hash, err = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=123,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=4050_00, # $4050
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
await ws_send_tx(ws_client, tx_type, tx_info, tx_hash)
|
||||
|
||||
## modify order
|
||||
# use the same API key so the TX goes after the create order TX
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
tx_type, tx_info, tx_hash, err = client.sign_modify_order(
|
||||
market_index=market_index,
|
||||
order_index=123,
|
||||
base_amount=1100, # 0.11 ETH
|
||||
price=4100_00, # $4100
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
await ws_send_tx(ws_client, tx_type, tx_info, tx_hash)
|
||||
|
||||
## cancel order
|
||||
# use the same API key so the TX goes after the modify order TX
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
tx_type, tx_info, tx_hash, err = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=123,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
await ws_send_tx(ws_client, tx_type, tx_info, tx_hash)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
await ws_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,56 @@
|
||||
import asyncio
|
||||
from lighter.signer_client import CreateOrderTxReq
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Creates a position tied SL/TP pair
|
||||
# The SL/TP orders will close your whole position, even if you add/remove from it later on
|
||||
# if the positions reach 0 or switches from short -> long, the orders are canceled
|
||||
|
||||
# this particular example, sets the SL/TP for a short position
|
||||
# set SL trigger price at 5000 and limit price at 5050
|
||||
# set TP trigger price at 1500 and limit price at 1550
|
||||
# Note: set the limit price to be higher than the SL/TP trigger price to ensure the order will be filled
|
||||
# If the mark price of ETH reaches 1500, there might be no one willing to sell you ETH at 1500, so trying to buy at 1550 would increase the fill rate
|
||||
|
||||
# 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=0,
|
||||
Price=1550_00,
|
||||
IsAsk=0,
|
||||
Type=client.ORDER_TYPE_TAKE_PROFIT_LIMIT,
|
||||
TimeInForce=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
ReduceOnly=1,
|
||||
TriggerPrice=1500_00,
|
||||
OrderExpiry=-1,
|
||||
)
|
||||
|
||||
stop_loss_order = CreateOrderTxReq(
|
||||
MarketIndex=0,
|
||||
ClientOrderIndex=0,
|
||||
BaseAmount=0,
|
||||
Price=4050_00,
|
||||
IsAsk=0,
|
||||
Type=client.ORDER_TYPE_STOP_LOSS_LIMIT,
|
||||
TimeInForce=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
ReduceOnly=1,
|
||||
TriggerPrice=4000_00,
|
||||
OrderExpiry=-1,
|
||||
)
|
||||
|
||||
transaction = await client.create_grouped_orders(
|
||||
grouping_type=client.GROUPING_TYPE_ONE_CANCELS_THE_OTHER,
|
||||
orders=[take_profit_order, stop_loss_order],
|
||||
)
|
||||
|
||||
print("Create Grouped Order Tx:", transaction)
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,70 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lighter
|
||||
|
||||
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 servers as an example.
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
API_KEY_PRIVATE_KEY = "0xe0fa55e11d6b5575d54c0500bd2f3b240221ae90241e3b573f2307e27de20c04ea628de3f1936e56"
|
||||
ACCOUNT_INDEX = 22
|
||||
API_KEY_INDEX = 3
|
||||
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
tx = await client.create_tp_order(
|
||||
market_index=0,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
trigger_price=500000,
|
||||
price=500000,
|
||||
is_ask=False
|
||||
)
|
||||
print("Create Order Tx:", tx)
|
||||
|
||||
|
||||
tx = await client.create_sl_order(
|
||||
market_index=0,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
trigger_price=500000,
|
||||
price=500000,
|
||||
is_ask=False
|
||||
)
|
||||
print("Create Order Tx:", tx)
|
||||
|
||||
tx = await client.create_tp_limit_order(
|
||||
market_index=0,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
trigger_price=500000,
|
||||
price=500000,
|
||||
is_ask=False
|
||||
)
|
||||
|
||||
tx = await client.create_sl_limit_order(
|
||||
market_index=0,
|
||||
client_order_index=0,
|
||||
base_amount=1000, # 0.1 ETH
|
||||
trigger_price=500000,
|
||||
price=500000,
|
||||
is_ask=False
|
||||
)
|
||||
print("Create Order Tx:", tx)
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,47 +1,30 @@
|
||||
import time
|
||||
import asyncio
|
||||
import lighter
|
||||
|
||||
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
# use examples/system_setup.py or the apikeys page (for mainnet) to generate new api keys
|
||||
KEYS = {
|
||||
5: "API_PRIVATE_KEY_5",
|
||||
6: "API_PRIVATE_KEY_6",
|
||||
7: "API_PRIVATE_KEY_7",
|
||||
}
|
||||
ACCOUNT_INDEX = 100 # replace with your account_index
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client = lighter.SignerClient(
|
||||
url=BASE_URL,
|
||||
private_key=KEYS[5],
|
||||
account_index=ACCOUNT_INDEX,
|
||||
api_key_index=5,
|
||||
max_api_key_index=7,
|
||||
private_keys=KEYS,
|
||||
)
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
# create 20 orders. The client will use as many API keys as it was configured.
|
||||
|
||||
for i in range(20):
|
||||
res_tuple = await client.create_order(
|
||||
market_index=0,
|
||||
client_order_index=123 + i,
|
||||
base_amount=100000 + i,
|
||||
price=385000 + i,
|
||||
base_amount=1000 + i, # 0.1 ETH + dust
|
||||
price=3850_00 + i,
|
||||
is_ask=True,
|
||||
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
|
||||
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=0,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
)
|
||||
print(res_tuple)
|
||||
|
||||
await client.cancel_all_orders(time_in_force=client.CANCEL_ALL_TIF_IMMEDIATE, time=0)
|
||||
# wait for orders to be created
|
||||
time.sleep(1)
|
||||
await client.cancel_all_orders(time_in_force=client.CANCEL_ALL_TIF_IMMEDIATE, timestamp_ms=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -75,7 +75,11 @@ async def transaction_apis(client: lighter.ApiClient):
|
||||
# use with a valid sequence index
|
||||
# await print_api(transaction_instance.tx, by="sequence_index", value="5")
|
||||
await print_api(transaction_instance.txs, index=0, limit=2)
|
||||
|
||||
|
||||
async def funding_apis(client: lighter.ApiClient):
|
||||
logging.info("FUNDING APIS")
|
||||
account_instance = lighter.FundingApi(client)
|
||||
await print_api(account_instance.funding_rates)
|
||||
|
||||
async def main():
|
||||
client = lighter.ApiClient(configuration=lighter.Configuration(host="https://testnet.zklighter.elliot.ai"))
|
||||
@@ -84,6 +88,7 @@ async def main():
|
||||
await candlestick_apis(client)
|
||||
await order_apis(client)
|
||||
await transaction_apis(client)
|
||||
await funding_apis(client)
|
||||
await client.close()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Note: the HTTP method `update_leverage` receives `leverage` as the argument,
|
||||
# while the WS one that calls `sign_update_leverage` to get the TX to send it directly over WS
|
||||
# receives `fraction` as the argument, which is 10_000 / leverage
|
||||
# this was kept this way to not break backwards compatibility. Ideally, they would be consistent.
|
||||
|
||||
tx, tx_hash, err = await client.update_leverage(
|
||||
market_index=0,
|
||||
leverage=20,
|
||||
margin_mode=client.CROSS_MARGIN_MODE
|
||||
)
|
||||
|
||||
print(f"Update Leverage {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import websockets
|
||||
import asyncio
|
||||
from utils import default_example_setup, ws_send_tx
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, ws_client_promise = default_example_setup()
|
||||
|
||||
# set up WS client and print a connected message
|
||||
ws_client: websockets.ClientConnection = await ws_client_promise
|
||||
print("Received:", await ws_client.recv())
|
||||
|
||||
# Note: the HTTP method `update_leverage` receives `leverage` as the argument,
|
||||
# while the WS one that calls `sign_update_leverage` to get the TX to send it directly over WS
|
||||
# receives `fraction` as the argument, which is 10_000 / leverage
|
||||
# this was kept this way to not break backwards compatibility. Ideally, they would be consistent.
|
||||
|
||||
tx_type, tx_info, tx_hash, err = client.sign_update_leverage(
|
||||
market_index=0,
|
||||
fraction=10_000 // 50,
|
||||
margin_mode=client.ISOLATED_MARGIN_MODE
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
await ws_send_tx(ws_client, tx_type, tx_info, tx_hash)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
await ws_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Note: the HTTP method `update_margin` receives `usdc_amount` (float) as the argument,
|
||||
# while the WS one that calls `sign_update_margin` to get the TX to send it directly over WS
|
||||
# receives `usdc_amount` (int) as the argument, which is the float one * 1_000_000
|
||||
# this was kept this way to not break backwards compatibility. Ideally, they would be consistent.
|
||||
|
||||
tx, tx_hash, err = await client.update_margin(
|
||||
market_index=0,
|
||||
usdc_amount=10.5,
|
||||
direction=client.ISOLATED_MARGIN_ADD_COLLATERAL
|
||||
)
|
||||
|
||||
print(f"Update Margin {tx=} {tx_hash=} {err=}")
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
import websockets
|
||||
from utils import default_example_setup, ws_send_tx
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, ws_client_promise = default_example_setup()
|
||||
|
||||
# set up WS client and print a connected message
|
||||
ws_client: websockets.ClientConnection = await ws_client_promise
|
||||
print("Received:", await ws_client.recv())
|
||||
|
||||
# Note: the HTTP method `update_margin` receives `usdc_amount` (float) as the argument,
|
||||
# while the WS one that calls `sign_update_margin` to get the TX to send it directly over WS
|
||||
# receives `usdc_amount` (int) as the argument, which is the float one * 1_000_000
|
||||
# this was kept this way to not break backwards compatibility. Ideally, they would be consistent.
|
||||
|
||||
tx_type, tx_info, tx_hash, err = client.sign_update_margin(
|
||||
market_index=0,
|
||||
usdc_amount=5_000_000, # 5 USDC
|
||||
direction=client.ISOLATED_MARGIN_REMOVE_COLLATERAL
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
await ws_send_tx(ws_client, tx_type, tx_info, tx_hash)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import lighter
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
tx_api = lighter.TransactionApi(api_client)
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
auth, _ = client.create_auth_token_with_expiry()
|
||||
|
||||
# create a public pool
|
||||
tx_info, response, err = await client.create_public_pool(
|
||||
operator_fee=100000, # 10%
|
||||
initial_total_shares=1_000_000, # 1000 USDC
|
||||
min_operator_share_rate=100, # 1%
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f'failed to create public pool {err}')
|
||||
tx_hash = response.tx_hash
|
||||
print(f"✅ send create public pool tx. hash: {tx_hash}")
|
||||
|
||||
# fetch pool account index from tx hash
|
||||
pool_account_index = -1
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
try:
|
||||
response = await tx_api.tx(by="hash", value=tx_hash)
|
||||
event_info_j = json.loads(response.event_info)
|
||||
pool_account_index = event_info_j['a']
|
||||
except Exception as e:
|
||||
pass
|
||||
if pool_account_index != -1:
|
||||
break
|
||||
if pool_account_index == -1:
|
||||
raise Exception(f"failed to find pool account index for tx {tx_hash}")
|
||||
print(f"✅ pool account index: {pool_account_index}")
|
||||
|
||||
# Note: ❗️operator_fee can only decrease
|
||||
# modify pool metadata
|
||||
tx_info, response, err = await client.update_public_pool(
|
||||
public_pool_index=pool_account_index,
|
||||
status=0, # 0 is active | 1 is frozen
|
||||
operator_fee=50000, # 5%
|
||||
min_operator_share_rate=1000, # 10%
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f'failed to create update pool {err}')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
|
||||
from utils import default_example_setup
|
||||
|
||||
POOL_ACCOUNT_INDEX = 281474976710651
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
tx_info, response, err = await client.mint_shares(public_pool_index=POOL_ACCOUNT_INDEX, share_amount=10_000)
|
||||
if err is not None:
|
||||
raise Exception(f'failed to mint shares {err}')
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
import asyncio
|
||||
import lighter
|
||||
from utils import default_example_setup
|
||||
|
||||
POOL_ACCOUNT_INDEX = 281474976710651
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
account_api = lighter.AccountApi(api_client)
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
account = await account_api.account(by="index", value=str(client.account_index))
|
||||
|
||||
# Note: ❗️shares field does not return the shared you have in pools that you're the operator
|
||||
for pool in account.accounts[0].shares:
|
||||
pool_resp = await account_api.account(by="index", value=str(pool.public_pool_index))
|
||||
pool_account = pool_resp.accounts[0]
|
||||
|
||||
share_price = float(pool_account.total_asset_value) / float(pool_account.pool_info.total_shares)
|
||||
print(
|
||||
f"poolAccountId: {pool.public_pool_index} numShared: {pool.shares_amount} sharePrice: {share_price:.6f} value: {share_price * pool.shares_amount:.2f} pnl: {share_price * pool.shares_amount - float(pool.entry_usdc):.2f}")
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
POOL_ACCOUNT_INDEX = 281474976710651
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
auth, _ = client.create_auth_token_with_expiry()
|
||||
|
||||
tx_info, response, err = await client.burn_shares(public_pool_index=POOL_ACCOUNT_INDEX, share_amount=10_000)
|
||||
if err is not None:
|
||||
raise Exception(f'failed to mint shares {err}')
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1 @@
|
||||
*.json
|
||||
@@ -0,0 +1,168 @@
|
||||
# Read-Only Auth Token Pre-Generation
|
||||
|
||||
This example demonstrates how to pre-generate authentication tokens for read-only operations on the Lighter platform. By generating tokens ahead of time, you can avoid needing access to your API private keys during runtime for read-only queries.
|
||||
|
||||
## Overview
|
||||
|
||||
Authentication tokens on Lighter have a maximum expiry of 8 hours. This example allows you to:
|
||||
|
||||
1. Configure a dedicated API key (index 253) for all your accounts
|
||||
2. Pre-generate authentication tokens for future time periods
|
||||
3. Use these tokens for read-only operations without exposing your private keys
|
||||
|
||||
The tokens are generated at 6-hour intervals (aligned to Unix timestamp // 6 hours), with each token valid for 8 hours. This provides an overlap period ensuring continuous coverage.
|
||||
|
||||
## Setup
|
||||
|
||||
The setup script configures API key 253 for all accounts associated with your Ethereum private key.
|
||||
|
||||
### Running Setup
|
||||
|
||||
```bash
|
||||
cd examples/read-only-auth
|
||||
python3 setup.py config.json
|
||||
```
|
||||
|
||||
This will:
|
||||
- Query all accounts for your L1 address
|
||||
- Generate new API key pairs for each account
|
||||
- Change API key 253 to use the new keys
|
||||
- Output configuration in JSON format
|
||||
|
||||
### Configuration Variables
|
||||
|
||||
Edit the constants in `setup.py`:
|
||||
|
||||
```python
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
ETH_PRIVATE_KEY = "your_ethereum_private_key_here"
|
||||
API_KEY_INDEX = 253 # Using 253 as it's typically unused
|
||||
```
|
||||
|
||||
### Config Format
|
||||
|
||||
```json
|
||||
{
|
||||
"BASE_URL": "https://testnet.zklighter.elliot.ai",
|
||||
"ACCOUNTS": [
|
||||
{
|
||||
"api_key_private_key": "...",
|
||||
"account_index": 0,
|
||||
"api_key_index": 253
|
||||
},
|
||||
{
|
||||
"api_key_private_key": "...",
|
||||
"account_index": 1,
|
||||
"api_key_index": 253
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Generating Tokens
|
||||
|
||||
The generation script creates authentication tokens for future time periods.
|
||||
|
||||
### Running Generation
|
||||
|
||||
```bash
|
||||
NUM_DAYS=10 python3 generate.py config.json
|
||||
```
|
||||
|
||||
If no config file is specified, it defaults to `config.json`.
|
||||
|
||||
### Duration Configuration
|
||||
|
||||
You can specify the duration in days using the `NUM_DAYS` environment variable, as in the command above.
|
||||
If the value is not specified, it defaults to 28 days.
|
||||
|
||||
### Output Format
|
||||
|
||||
The script generates `auth-tokens.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"0": {
|
||||
"1697184000": "auth_token_string_1",
|
||||
"1697205600": "auth_token_string_2",
|
||||
"1697227200": "auth_token_string_3"
|
||||
},
|
||||
"1": {
|
||||
"1697184000": "auth_token_string_1",
|
||||
"1697205600": "auth_token_string_2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where:
|
||||
- First level key: account index
|
||||
- Second level key: Unix timestamp (aligned to 6-hour boundaries)
|
||||
- Value: authentication token
|
||||
|
||||
## Usage
|
||||
|
||||
### Looking Up Tokens
|
||||
|
||||
Check the `get_auth_token.py` script which prints the Auth Token that should be used **at this moment**, as this will be invalidated in at most 8 hours.
|
||||
|
||||
### Time Alignment
|
||||
|
||||
All timestamps are aligned to 6-hour boundaries:
|
||||
- Timestamps are divisible by 21600 seconds (6 hours)
|
||||
- Calculation: `unix_timestamp // (6 * 3600) * (6 * 3600)`
|
||||
- This ensures consistent token lookup across different systems
|
||||
|
||||
### Token Expiry
|
||||
|
||||
Each token is valid for 8 hours from its timestamp:
|
||||
- Token timestamp: aligned to 6-hour boundary
|
||||
- Valid until: timestamp + 8 hours
|
||||
- This provides 2 hours of overlap between consecutive tokens
|
||||
|
||||
## Security
|
||||
|
||||
### API Key 253
|
||||
|
||||
We use API key index 253 because:
|
||||
- It's the last available index [0-253]
|
||||
- It's not typically used by trading
|
||||
- Easy to remember for this specific use case
|
||||
- Easy to change and invalidate all tokens.
|
||||
|
||||
### Invalidating Tokens
|
||||
|
||||
To invalidate all existing tokens:
|
||||
|
||||
```bash
|
||||
python3 setup.py config.json
|
||||
```
|
||||
|
||||
Re-running the setup script generates new API keys for index 253, which invalidates all previously generated authentication tokens. This is useful if:
|
||||
- You suspect your tokens have been compromised
|
||||
- You want to rotate your tokens periodically
|
||||
- You need to revoke access immediately
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Store tokens securely**: The `auth-tokens.json` file contains sensitive data (read only, but still)
|
||||
2. **Dedicated API key**: Use API key 253 for read-only token generation, as it can be invalidated easely.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Account not found" error
|
||||
|
||||
Make sure your Ethereum private key corresponds to an account registered on the Lighter platform.
|
||||
|
||||
### "Failed to change API key" error
|
||||
|
||||
This could happen if:
|
||||
- The API key change transaction failed
|
||||
- Network connectivity issues
|
||||
- The account is not active
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- Tokens are specific to each account index
|
||||
- Each account has its own set of time-aligned tokens
|
||||
- The system uses the SignerClient's native `create_auth_token_with_expiry` method
|
||||
@@ -0,0 +1,103 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import lighter
|
||||
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
|
||||
|
||||
def create_auth_token_for_timestamp(signer_client, timestamp, expiry_hours):
|
||||
auth_token, error = signer_client.create_auth_token_with_expiry(expiry_hours * 3600, timestamp=timestamp)
|
||||
if error is not None:
|
||||
raise Exception(f"Failed to create auth token: {error}")
|
||||
return auth_token
|
||||
|
||||
|
||||
async def generate_tokens_for_account(account_info, base_url, duration_days):
|
||||
account_index = account_info["account_index"]
|
||||
api_key_private_key = account_info["api_key_private_key"]
|
||||
api_key_index = account_info["api_key_index"]
|
||||
|
||||
logging.info(f"Generating tokens for account {account_index}")
|
||||
|
||||
signer_client = lighter.SignerClient(
|
||||
url=base_url,
|
||||
private_key=api_key_private_key,
|
||||
account_index=account_index,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
current_time = int(time.time())
|
||||
interval_seconds = 6 * 3600
|
||||
start_timestamp = (current_time // interval_seconds) * interval_seconds
|
||||
|
||||
num_tokens = 4 * duration_days
|
||||
expiry_hours = 8
|
||||
|
||||
tokens = {}
|
||||
for i in range(num_tokens):
|
||||
timestamp = start_timestamp + (i * interval_seconds)
|
||||
try:
|
||||
auth_token = create_auth_token_for_timestamp(signer_client, timestamp, expiry_hours)
|
||||
tokens[str(timestamp)] = auth_token
|
||||
logging.debug(f"Generated token for timestamp {timestamp}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to generate token for timestamp {timestamp}: {e}")
|
||||
|
||||
await signer_client.close()
|
||||
|
||||
return account_index, tokens
|
||||
|
||||
|
||||
async def main():
|
||||
config_file = "config.json"
|
||||
if len(sys.argv) > 1:
|
||||
config_file = sys.argv[1]
|
||||
|
||||
try:
|
||||
with open(config_file, "r") as f:
|
||||
config = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logging.error(f"Config file '{config_file}' not found")
|
||||
logging.error("Run setup.py first: python3 setup.py > config.json")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"Invalid JSON in config file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
num_days = int(os.getenv("NUM_DAYS") or 28)
|
||||
base_url = config.get("BASE_URL")
|
||||
accounts = config.get("ACCOUNTS", [])
|
||||
duration_days = config.get("DURATION_IN_DAYS", num_days)
|
||||
|
||||
if not base_url:
|
||||
logging.error("BASE_URL not found in config")
|
||||
sys.exit(1)
|
||||
|
||||
if not accounts:
|
||||
logging.error("No accounts found in config")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"Generating tokens for {len(accounts)} account(s)")
|
||||
logging.info(f"Duration: {duration_days} days ({4 * duration_days} tokens per account)")
|
||||
|
||||
auth_tokens = {}
|
||||
for account_info in accounts:
|
||||
account_index, tokens = await generate_tokens_for_account(account_info, base_url, duration_days)
|
||||
auth_tokens[str(account_index)] = tokens
|
||||
|
||||
output_file = "auth-tokens.json"
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(auth_tokens, f, indent=2)
|
||||
|
||||
logging.info(f"Successfully generated tokens and saved to {output_file}")
|
||||
logging.info(f"Total accounts: {len(auth_tokens)}")
|
||||
for account_index, tokens in auth_tokens.items():
|
||||
logging.info(f" Account {account_index}: {len(tokens)} tokens")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,30 @@
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 1:
|
||||
logging.error("No account index specified")
|
||||
return
|
||||
|
||||
account_index = sys.argv[1]
|
||||
|
||||
# Load pre-generated tokens
|
||||
with open('auth-tokens.json') as f:
|
||||
auth_tokens = json.load(f)
|
||||
|
||||
# Get current aligned timestamp (6-hour boundary)
|
||||
current_timestamp = (int(time.time()) // (6 * 3600)) * (6 * 3600)
|
||||
|
||||
# Look up token for specific account
|
||||
auth_token = auth_tokens[account_index][str(current_timestamp)]
|
||||
|
||||
print(f"{auth_token=}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,113 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import eth_account
|
||||
import lighter
|
||||
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
|
||||
# use https://mainnet.zklighter.elliot.ai for mainnet
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
API_KEY_INDEX = 253
|
||||
|
||||
|
||||
async def setup_account(eth_private_key, account_index, base_url, api_key_index):
|
||||
private_key, public_key, err = lighter.create_api_key()
|
||||
if err is not None:
|
||||
return None, f"Failed to create API key for account {account_index}: {err}"
|
||||
|
||||
tx_client = lighter.SignerClient(
|
||||
url=base_url,
|
||||
private_key=private_key,
|
||||
account_index=account_index,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
response, err = await tx_client.change_api_key(
|
||||
eth_private_key=eth_private_key,
|
||||
new_pubkey=public_key,
|
||||
)
|
||||
if err is not None:
|
||||
await tx_client.close()
|
||||
return None, f"Failed to change API key for account {account_index}: {err}"
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
err = tx_client.check_client()
|
||||
if err is not None:
|
||||
await tx_client.close()
|
||||
return None, f"Failed to verify API key for account {account_index}: {err}"
|
||||
|
||||
await tx_client.close()
|
||||
|
||||
return {
|
||||
"api_key_private_key": private_key,
|
||||
"account_index": account_index,
|
||||
"api_key_index": api_key_index,
|
||||
}, None
|
||||
|
||||
|
||||
async def main():
|
||||
config_file = "config.json"
|
||||
if len(sys.argv) > 1:
|
||||
config_file = sys.argv[1]
|
||||
|
||||
api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL))
|
||||
eth_acc = eth_account.Account.from_key(ETH_PRIVATE_KEY)
|
||||
eth_address = eth_acc.address
|
||||
|
||||
try:
|
||||
response = await lighter.AccountApi(api_client).accounts_by_l1_address(
|
||||
l1_address=eth_address
|
||||
)
|
||||
except lighter.ApiException as e:
|
||||
if e.data.message == "account not found":
|
||||
print(f"error: account not found for {eth_address}", file=__import__('sys').stderr)
|
||||
await api_client.close()
|
||||
return
|
||||
else:
|
||||
await api_client.close()
|
||||
raise e
|
||||
|
||||
if len(response.sub_accounts) == 0:
|
||||
print(f"error: no accounts found for {eth_address}", file=__import__('sys').stderr)
|
||||
await api_client.close()
|
||||
return
|
||||
|
||||
logging.info(f"Found {len(response.sub_accounts)} account(s)")
|
||||
|
||||
# don't do this async
|
||||
accounts = []
|
||||
for sub_account in response.sub_accounts:
|
||||
logging.info(f"Setting up account index: {sub_account.index}")
|
||||
result, err = await setup_account(
|
||||
ETH_PRIVATE_KEY,
|
||||
sub_account.index,
|
||||
BASE_URL,
|
||||
API_KEY_INDEX,
|
||||
)
|
||||
|
||||
if err is not None:
|
||||
logging.error(err)
|
||||
else:
|
||||
accounts.append(result)
|
||||
|
||||
if not accounts:
|
||||
print("error: failed to setup any accounts", file=__import__('sys').stderr)
|
||||
await api_client.close()
|
||||
return
|
||||
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"BASE_URL": BASE_URL,
|
||||
"ACCOUNTS": accounts,
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
import time
|
||||
from utils import default_example_setup, trim_exception
|
||||
|
||||
|
||||
# this example does the same thing as the send_batch_tx_ws.py example, but sends the TX over HTTP instead of WS
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 0
|
||||
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
ask_tx_type, ask_tx_info, ask_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1001, # Unique identifier for this order
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=5000_00, # $5000
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing ask order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# intentionally pass api_key_index to the client.nonce_manager so it increases the nonce, without changing the API key.
|
||||
# in batch TXs, all TXs must come from the same API key.
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
bid_tx_type, bid_tx_info, bid_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1002, # Different unique identifier
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=1500_00, # $1500
|
||||
is_ask=False,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [ask_tx_type, bid_tx_type]
|
||||
tx_infos = [ask_tx_info, bid_tx_info]
|
||||
tx_hashes = [ask_tx_hash, bid_tx_hash]
|
||||
|
||||
try:
|
||||
response = await client.send_tx_batch(tx_types=tx_types, tx_infos=tx_infos)
|
||||
print(f"Batch transaction successful: {response} expected: {tx_hashes}")
|
||||
except Exception as e:
|
||||
print(f"Error sending batch transaction: {trim_exception(e)}")
|
||||
|
||||
# In case we want to see the changes in the UI, sleep a bit
|
||||
time.sleep(5)
|
||||
|
||||
# since this is a new batch, we can request a fresh API key
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
cancel_tx_type, cancel_tx_info, cancel_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1001, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# intentionally pass api_key_index to the client.nonce_manager so it increases the nonce, without changing the API key.
|
||||
# in batch TXs, all TXs must come from the same API key.
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
new_ask_tx_type, new_ask_tx_info, new_ask_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1003, # Different unique identifier
|
||||
base_amount=2000, # 0.2 ETH
|
||||
price=5500_00, # $5500
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [cancel_tx_type, new_ask_tx_type]
|
||||
tx_infos = [cancel_tx_info, new_ask_tx_info]
|
||||
tx_hashes = [cancel_tx_hash, new_ask_tx_hash]
|
||||
|
||||
try:
|
||||
response = await client.send_tx_batch(tx_types=tx_types, tx_infos=tx_infos)
|
||||
print(f"Batch transaction successful: {response} expected: {tx_hashes}")
|
||||
except Exception as e:
|
||||
print(f"Error sending batch transaction: {trim_exception(e)}")
|
||||
|
||||
# In case we want to see the changes in the UI, sleep a bit
|
||||
time.sleep(5)
|
||||
|
||||
# since this is a new batch, we can request a fresh API key
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
cancel_1_tx_type, cancel_1_tx_info, cancel_1_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1002, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (third batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
cancel_2_tx_type, cancel_2_tx_info, cancel_2_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1003, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (third batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [cancel_1_tx_type, cancel_2_tx_type]
|
||||
tx_infos = [cancel_1_tx_info, cancel_2_tx_info]
|
||||
tx_hashes = [cancel_1_tx_hash, cancel_2_tx_hash]
|
||||
|
||||
try:
|
||||
response = await client.send_tx_batch(tx_types=tx_types, tx_infos=tx_infos)
|
||||
print(f"Batch transaction successful: {response} expected: {tx_hashes}")
|
||||
except Exception as e:
|
||||
print(f"Error sending batch transaction: {trim_exception(e)}")
|
||||
|
||||
|
||||
# Clean up
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
# Run the async main function
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,150 @@
|
||||
import websockets
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from utils import default_example_setup, ws_send_batch_tx, trim_exception
|
||||
|
||||
|
||||
# this example does the same thing as the send_batch_tx_http.py example, but sends the TX over WS instead of HTTP
|
||||
async def main():
|
||||
client, api_client, ws_client_promise = default_example_setup()
|
||||
|
||||
# set up WS client and print a connected message
|
||||
ws_client: websockets.ClientConnection = await ws_client_promise
|
||||
print("Received:", await ws_client.recv())
|
||||
|
||||
# Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot.
|
||||
market_index = 2048
|
||||
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
ask_tx_type, ask_tx_info, ask_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1001, # Unique identifier for this order
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=5000_00, # $5000
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing ask order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# intentionally pass api_key_index to the client.nonce_manager so it increases the nonce, without changing the API key.
|
||||
# in batch TXs, all TXs must come from the same API key.
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
bid_tx_type, bid_tx_info, bid_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1002, # Different unique identifier
|
||||
base_amount=1000, # 0.1 ETH
|
||||
price=1500_00, # $1500
|
||||
is_ask=False,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [ask_tx_type, bid_tx_type]
|
||||
tx_infos = [ask_tx_info, bid_tx_info]
|
||||
tx_hashes = [ask_tx_hash, bid_tx_hash]
|
||||
|
||||
await ws_send_batch_tx(ws_client, tx_types, tx_infos, tx_hashes)
|
||||
|
||||
# In case we want to see the changes in the UI, sleep a bit
|
||||
time.sleep(5)
|
||||
|
||||
# since this is a new batch, we can request a fresh API key
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
cancel_tx_type, cancel_tx_info, cancel_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1001, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# intentionally pass api_key_index to the client.nonce_manager so it increases the nonce, without changing the API key.
|
||||
# in batch TXs, all TXs must come from the same API key.
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
new_ask_tx_type, new_ask_tx_info, new_ask_tx_hash, error = client.sign_create_order(
|
||||
market_index=market_index,
|
||||
client_order_index=1003, # Different unique identifier
|
||||
base_amount=2000, # 0.2 ETH
|
||||
price=5500_00, # $5500
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [cancel_tx_type, new_ask_tx_type]
|
||||
tx_infos = [cancel_tx_info, new_ask_tx_info]
|
||||
tx_hashes = [cancel_tx_hash, new_ask_tx_hash]
|
||||
|
||||
await ws_send_batch_tx(ws_client, tx_types, tx_infos, tx_hashes)
|
||||
|
||||
# In case we want to see the changes in the UI, sleep a bit
|
||||
time.sleep(5)
|
||||
|
||||
# since this is a new batch, we can request a fresh API key
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
cancel_1_tx_type, cancel_1_tx_info, cancel_1_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1002, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (third batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce(api_key_index)
|
||||
cancel_2_tx_type, cancel_2_tx_info, cancel_2_tx_hash, error = client.sign_cancel_order(
|
||||
market_index=market_index,
|
||||
order_index=1003, # the index of the order we want cancelled
|
||||
nonce=nonce,
|
||||
api_key_index=api_key_index,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (third batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = [cancel_1_tx_type, cancel_2_tx_type]
|
||||
tx_infos = [cancel_1_tx_info, cancel_2_tx_info]
|
||||
tx_hashes = [cancel_1_tx_hash, cancel_2_tx_hash]
|
||||
|
||||
await ws_send_batch_tx(ws_client, tx_types, tx_infos, tx_hashes)
|
||||
|
||||
|
||||
# Clean up
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
await ws_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,138 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lighter
|
||||
import json
|
||||
|
||||
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 servers as an example.
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
API_KEY_PRIVATE_KEY = "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
|
||||
ACCOUNT_INDEX = 65
|
||||
API_KEY_INDEX = 1
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize configuration and clients
|
||||
configuration = lighter.Configuration(BASE_URL)
|
||||
api_client = lighter.ApiClient(configuration)
|
||||
transaction_api = lighter.TransactionApi(api_client)
|
||||
|
||||
# Initialize signer client
|
||||
client = lighter.SignerClient(
|
||||
url=BASE_URL,
|
||||
private_key=API_KEY_PRIVATE_KEY,
|
||||
account_index=ACCOUNT_INDEX,
|
||||
api_key_index=API_KEY_INDEX
|
||||
)
|
||||
|
||||
# Check client connection
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {trim_exception(err)}")
|
||||
return
|
||||
|
||||
# use next nonce for getting nonces
|
||||
next_nonce = await transaction_api.next_nonce(account_index=ACCOUNT_INDEX, api_key_index=API_KEY_INDEX)
|
||||
nonce_value = next_nonce.nonce
|
||||
|
||||
ask_tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1001, # Unique identifier for this order
|
||||
base_amount=100000,
|
||||
price=280000,
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value
|
||||
)
|
||||
nonce_value += 1
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# Sign second order
|
||||
bid_tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1002, # Different unique identifier
|
||||
base_amount=200000,
|
||||
price=200000,
|
||||
is_ask=False,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value
|
||||
)
|
||||
nonce_value += 1
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (first batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = json.dumps([client.TX_TYPE_CREATE_ORDER, client.TX_TYPE_CREATE_ORDER])
|
||||
tx_infos = json.dumps([ask_tx_info, bid_tx_info])
|
||||
|
||||
try:
|
||||
tx_hashes = await transaction_api.send_tx_batch(tx_types=tx_types, tx_infos=tx_infos)
|
||||
print(f"Batch transaction successful: {tx_hashes}")
|
||||
except Exception as e:
|
||||
print(f"Error sending batch transaction: {trim_exception(e)}")
|
||||
|
||||
# In case we want to see the changes in the UI, sleep a bit
|
||||
import time
|
||||
time.sleep(5)
|
||||
|
||||
cancel_tx_info, error = client.sign_cancel_order(
|
||||
market_index=0,
|
||||
order_index=1001, # the index of the order we want cancelled
|
||||
nonce=nonce_value
|
||||
)
|
||||
nonce_value += 1
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
# Sign second order
|
||||
new_ask_tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1003, # Different unique identifier
|
||||
base_amount=300000,
|
||||
price=310000,
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value
|
||||
)
|
||||
nonce_value += 1
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (second batch): {trim_exception(error)}")
|
||||
return
|
||||
|
||||
tx_types = json.dumps([client.TX_TYPE_CANCEL_ORDER, client.TX_TYPE_CREATE_ORDER])
|
||||
tx_infos = json.dumps([cancel_tx_info, new_ask_tx_info])
|
||||
|
||||
try:
|
||||
tx_hashes = await transaction_api.send_tx_batch(tx_types=tx_types, tx_infos=tx_infos)
|
||||
print(f"Batch 2 transaction successful: {tx_hashes}")
|
||||
except Exception as e:
|
||||
print(f"Error sending batch transaction 2: {trim_exception(e)}")
|
||||
|
||||
# Clean up
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
# Run the async main function
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import lighter
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
account_api = lighter.AccountApi(api_client)
|
||||
response = await account_api.account(by="index", value=str(client.account_index))
|
||||
if len(response.accounts) == 0:
|
||||
raise "No account found"
|
||||
|
||||
account = response.accounts[0]
|
||||
# Note: cross-account value does not take into account isolated positions, but total does
|
||||
print("=== perp assets ===")
|
||||
print(f"total: {account.total_asset_value} available: {account.available_balance}")
|
||||
print(f"cross: {account.cross_asset_value} isolated: {float(account.total_asset_value) - float(account.cross_asset_value)}")
|
||||
|
||||
# Spot Assets
|
||||
print("=== spot assets ===")
|
||||
for asset in account.assets:
|
||||
print(f"{asset.symbol} total: {asset.balance} available: {float(asset.balance) - float(asset.locked_balance)}")
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import websockets
|
||||
|
||||
from lighter.models import WSAccountAssets
|
||||
from utils import default_example_setup, ws_subscribe, ws_ping
|
||||
|
||||
|
||||
async def consume_messages(ws):
|
||||
while True:
|
||||
msg_str = await ws.recv()
|
||||
if isinstance(msg_str, str):
|
||||
msg = json.loads(msg_str)
|
||||
else:
|
||||
raise msg_str
|
||||
|
||||
# handle ping here; if we receive, send pong
|
||||
if msg["type"] == "ping":
|
||||
await ws_ping(ws)
|
||||
continue
|
||||
|
||||
# handle account_all_assets updates -- just print stuff
|
||||
if msg["type"] == "subscribed/account_all_assets" or msg["type"] == "update/account_all_assets" :
|
||||
o = WSAccountAssets.from_dict(msg)
|
||||
for asset in o.assets.values():
|
||||
print(f"{asset.symbol} total: {asset.balance} available: {float(asset.balance) - float(asset.locked_balance)} accountId: {o.account_id}")
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, ws_client_promise = default_example_setup()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# set up WS client and print a connected message
|
||||
ws_client: websockets.ClientConnection = await ws_client_promise
|
||||
await ws_client.recv()
|
||||
|
||||
consume_task = asyncio.create_task(consume_messages(ws_client))
|
||||
|
||||
auth, _ = client.create_auth_token_with_expiry()
|
||||
await ws_subscribe(ws_client, f"account_all_assets/{client.account_index}", auth)
|
||||
|
||||
# wait a bit to print messages
|
||||
await asyncio.sleep(1000)
|
||||
|
||||
consume_task.cancel()
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
await ws_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import lighter
|
||||
from lighter import Asset
|
||||
from utils import default_example_setup
|
||||
|
||||
# This example shows how to fetch order books and assets details
|
||||
# This information should be enough to be able to trade on Lighter
|
||||
# Select the market ID accordingly to the symbol.
|
||||
# For spot markets, the order book contains base asset (ETH) and quote asset (USDC)
|
||||
# You can use these to keep track of your inventory
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
orders_api = lighter.OrderApi(api_client)
|
||||
response = await orders_api.order_books()
|
||||
response.order_books.sort(key=lambda x: x.market_id)
|
||||
|
||||
# fetch all assets
|
||||
assets_response = await orders_api.asset_details()
|
||||
|
||||
assets_dict: dict[int, Asset] = {}
|
||||
for asset in assets_response.asset_details:
|
||||
assets_dict[asset.asset_id] = asset
|
||||
|
||||
for order_book in response.order_books:
|
||||
if order_book.market_type == 'perp':
|
||||
print(f'symbol={order_book.symbol} id={order_book.market_id} type={order_book.market_type} sizeDecimals={order_book.supported_size_decimals} priceDecimals={order_book.supported_price_decimals}')
|
||||
else:
|
||||
print(f'symbol={order_book.symbol} id={order_book.market_id} type={order_book.market_type} sizeDecimals={order_book.supported_size_decimals} priceDecimals={order_book.supported_price_decimals} baseAssetId={order_book.base_asset_id} quoteAssetId={order_book.quote_asset_id}')
|
||||
b = assets_dict[order_book.base_asset_id]
|
||||
q = assets_dict[order_book.quote_asset_id]
|
||||
print(f' baseAsset: symbol={b.symbol} assetId={b.asset_id} decimals={b.decimals} price={b.index_price} min_withdraw={b.min_withdrawal_amount}')
|
||||
print(f' quoteAsset: symbol={q.symbol} assetId={q.asset_id} decimals={q.decimals} price={q.index_price} min_withdraw={q.min_withdrawal_amount}')
|
||||
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
# You can find more notes on transfers in the README.md file, under `Transfer Notes`
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
ETH_PRIVATE_KEY,
|
||||
to_account_index=client.account_index,
|
||||
asset_id=client.ASSET_ID_USDC,
|
||||
amount=1.234567, # decimals are added by sdk
|
||||
route_from=client.ROUTE_PERP,
|
||||
route_to=client.ROUTE_SPOT,
|
||||
fee=0,
|
||||
memo="0x" + "00" * 32,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
print(transfer_tx, response)
|
||||
|
||||
lev_tx, response, err = await client.update_leverage(4, client.CROSS_MARGIN_MODE, 3)
|
||||
print(lev_tx, response, err)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
# You can find more notes on transfers in the README.md file, under `Transfer Notes`
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
ETH_PRIVATE_KEY,
|
||||
to_account_index=client.account_index,
|
||||
asset_id=client.ASSET_ID_USDC,
|
||||
amount=1.234567, # decimals are added by sdk
|
||||
route_from=client.ROUTE_SPOT,
|
||||
route_to=client.ROUTE_PERP,
|
||||
fee=0,
|
||||
memo="0x" + "00" * 32,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
print(transfer_tx, response)
|
||||
|
||||
lev_tx, response, err = await client.update_leverage(4, client.CROSS_MARGIN_MODE, 3)
|
||||
print(lev_tx, response, err)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,17 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
tx_info, response, err = await client.create_sub_account()
|
||||
print(tx_info, response, err)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
TO_ACCOUNT_INDEX = 281474976710649
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
# You can find more notes on transfers in the README.md file, under `Transfer Notes`
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
ETH_PRIVATE_KEY,
|
||||
to_account_index=TO_ACCOUNT_INDEX,
|
||||
asset_id=client.ASSET_ID_ETH,
|
||||
amount=0.4, # decimals are added by sdk
|
||||
route_from=client.ROUTE_SPOT,
|
||||
route_to=client.ROUTE_SPOT,
|
||||
fee=0,
|
||||
memo="0x" + "00" * 32,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
print(transfer_tx, response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
TO_ACCOUNT_INDEX = 281474976710649
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
|
||||
# You can find more notes on transfers in the README.md file, under `Transfer Notes`
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
ETH_PRIVATE_KEY,
|
||||
to_account_index=TO_ACCOUNT_INDEX,
|
||||
asset_id=client.ASSET_ID_USDC,
|
||||
amount=100, # decimals are added by sdk
|
||||
route_from=client.ROUTE_PERP,
|
||||
route_to=client.ROUTE_SPOT,
|
||||
fee=0,
|
||||
memo="0x" + "00" * 32,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
print(transfer_tx, response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -3,15 +3,21 @@ import logging
|
||||
import time
|
||||
import eth_account
|
||||
import lighter
|
||||
from utils import save_api_key_config
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# this is a dummy private key which is registered on Testnet.
|
||||
# this is a dummy private key registered on Testnet.
|
||||
# It serves as a good example
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
API_KEY_INDEX = 3
|
||||
NUM_API_KEYS = 5
|
||||
|
||||
# If you set this to something other than None, the script will use that account index instead of using the master account index.
|
||||
# This is useful if you have multiple accounts on the same L1 address or are the owner of a public pool.
|
||||
# You need to use the private key associated to the master account or the owner of the public pool to change the API keys.
|
||||
ACCOUNT_INDEX = None
|
||||
|
||||
async def main():
|
||||
# verify that the account exists & fetch account index
|
||||
@@ -19,47 +25,58 @@ async def main():
|
||||
eth_acc = eth_account.Account.from_key(ETH_PRIVATE_KEY)
|
||||
eth_address = eth_acc.address
|
||||
|
||||
try:
|
||||
response = await lighter.AccountApi(api_client).accounts_by_l1_address(
|
||||
l1_address=eth_address
|
||||
)
|
||||
except lighter.ApiException as e:
|
||||
if e.data.message == "account not found":
|
||||
print(f"error: account not found for {eth_address}")
|
||||
return
|
||||
else:
|
||||
raise e
|
||||
|
||||
if len(response.sub_accounts) > 1:
|
||||
for sub_account in response.sub_accounts:
|
||||
print(f"found accountIndex: {sub_account.index}")
|
||||
|
||||
print("multiple accounts found, using the first one")
|
||||
account_index = response.sub_accounts[0].index
|
||||
if ACCOUNT_INDEX is not None:
|
||||
account_index = ACCOUNT_INDEX
|
||||
else:
|
||||
account_index = response.sub_accounts[0].index
|
||||
try:
|
||||
response = await lighter.AccountApi(api_client).accounts_by_l1_address(l1_address=eth_address)
|
||||
except lighter.ApiException as e:
|
||||
if e.data.message == "account not found":
|
||||
print(f"error: account not found for {eth_address}")
|
||||
return
|
||||
else:
|
||||
raise e
|
||||
|
||||
if len(response.sub_accounts) > 1:
|
||||
for sub_account in response.sub_accounts:
|
||||
print(f"found accountIndex: {sub_account.index}")
|
||||
|
||||
account = min(response.sub_accounts, key=lambda x: int(x.index))
|
||||
account_index = account.index
|
||||
print(f"multiple accounts found, using the master account {account_index}")
|
||||
else:
|
||||
account_index = response.sub_accounts[0].index
|
||||
|
||||
|
||||
# create a private/public key pair for the new API key
|
||||
# pass any string to be used as seed for create_api_key like
|
||||
# create_api_key("Hello world random seed to make things more secure")
|
||||
private_key, public_key, err = lighter.create_api_key()
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
private_keys = {}
|
||||
public_keys = []
|
||||
|
||||
for i in range(NUM_API_KEYS):
|
||||
private_key, public_key, err = lighter.create_api_key()
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
public_keys.append(public_key)
|
||||
private_keys[API_KEY_INDEX + i] = private_key
|
||||
|
||||
tx_client = lighter.SignerClient(
|
||||
url=BASE_URL,
|
||||
private_key=private_key,
|
||||
account_index=account_index,
|
||||
api_key_index=API_KEY_INDEX,
|
||||
api_private_keys=private_keys,
|
||||
)
|
||||
|
||||
# change the API key
|
||||
response, err = await tx_client.change_api_key(
|
||||
eth_private_key=ETH_PRIVATE_KEY,
|
||||
new_pubkey=public_key,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
# change all API keys
|
||||
for i in range(NUM_API_KEYS):
|
||||
response, err = await tx_client.change_api_key(
|
||||
eth_private_key=ETH_PRIVATE_KEY,
|
||||
new_pubkey=public_keys[i],
|
||||
api_key_index=API_KEY_INDEX + i
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
# wait some time so that we receive the new API key in the response
|
||||
time.sleep(10)
|
||||
@@ -69,14 +86,7 @@ async def main():
|
||||
if err is not None:
|
||||
raise Exception(err)
|
||||
|
||||
print(
|
||||
f"""
|
||||
BASE_URL = '{BASE_URL}'
|
||||
API_KEY_PRIVATE_KEY = '{private_key}'
|
||||
ACCOUNT_INDEX = {account_index}
|
||||
API_KEY_INDEX = {API_KEY_INDEX}
|
||||
"""
|
||||
)
|
||||
save_api_key_config(BASE_URL, account_index, private_keys)
|
||||
|
||||
await tx_client.close()
|
||||
await api_client.close()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
import lighter
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
TO_ACCOUNT_INDEX = 9
|
||||
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
info_api = lighter.InfoApi(api_client)
|
||||
|
||||
auth_token, err = client.create_auth_token_with_expiry()
|
||||
if err:
|
||||
raise Exception(f"Auth token failed: {err}")
|
||||
|
||||
fee_info = await info_api.transfer_fee_info(client.account_index, authorization=auth_token, auth=auth_token, to_account_index=TO_ACCOUNT_INDEX)
|
||||
|
||||
# You can find more notes on transfers in the README.md file, under `Transfer Notes`
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
eth_private_key=ETH_PRIVATE_KEY,
|
||||
to_account_index=TO_ACCOUNT_INDEX,
|
||||
asset_id=client.ASSET_ID_USDC,
|
||||
route_from=client.ROUTE_PERP,
|
||||
route_to=client.ROUTE_PERP,
|
||||
amount=5, # decimals are added by sdk
|
||||
fee=fee_info.transfer_fee_usdc,
|
||||
memo="0x" + "00" * 32,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
|
||||
print(transfer_tx, response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,47 +0,0 @@
|
||||
import asyncio
|
||||
import lighter
|
||||
|
||||
|
||||
from examples.secrets import BASE_URL, API_KEY_PRIVATE_KEY, ETH_PRIVATE_KEY
|
||||
|
||||
API_KEY_INDEX = 10
|
||||
TO_ACCOUNT_INDEX = 9
|
||||
ACCOUNT_INDEX = 10 # replace with your account_index
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL))
|
||||
info_api = lighter.InfoApi(api_client)
|
||||
order_api = lighter.OrderApi(api_client)
|
||||
|
||||
auth_token, _ = client.create_auth_token_with_expiry()
|
||||
fee_info = await info_api.transfer_fee_info(ACCOUNT_INDEX, authorization=auth_token, auth=auth_token, to_account_index=TO_ACCOUNT_INDEX)
|
||||
print(fee_info)
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {err}")
|
||||
return
|
||||
memo = "a"*32 # memo is a user message and it has to be exactly 32 bytes long
|
||||
transfer_tx, response, err = await client.transfer(
|
||||
ETH_PRIVATE_KEY,
|
||||
usdc_amount=100, # decimals are added by sdk
|
||||
to_account_index=TO_ACCOUNT_INDEX,
|
||||
fee=fee_info.transfer_fee_usdc,
|
||||
memo=memo,
|
||||
)
|
||||
if err != None:
|
||||
raise Exception(f"error transferring {err}")
|
||||
print(transfer_tx, response)
|
||||
|
||||
lev_tx, response, err = await client.update_leverage(4, client.CROSS_MARGIN_MODE, 3)
|
||||
print(lev_tx, response, err)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
from typing import Tuple, Optional
|
||||
import logging
|
||||
import json
|
||||
import websockets
|
||||
import lighter
|
||||
|
||||
|
||||
def trim_exception(e: Exception) -> str:
|
||||
return str(e).strip().split("\n")[-1]
|
||||
|
||||
|
||||
def save_api_key_config(base_url, account_index, private_keys, config_file="./api_key_config.json"):
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"baseUrl": base_url,
|
||||
"accountIndex": account_index,
|
||||
"privateKeys": private_keys,
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def get_api_key_config(config_file="./api_key_config.json"):
|
||||
with open(config_file) as f:
|
||||
cfg = json.load(f)
|
||||
|
||||
private_keys_original = cfg["privateKeys"]
|
||||
private_key = {}
|
||||
for key in private_keys_original.keys():
|
||||
private_key[int(key)] = private_keys_original[key]
|
||||
|
||||
return cfg["baseUrl"], cfg["accountIndex"], private_key
|
||||
|
||||
|
||||
def default_example_setup(config_file="./api_key_config.json") -> Optional[Tuple[lighter.SignerClient, lighter.ApiClient, websockets.connect]]:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
base_url, account_index, private_keys = get_api_key_config(config_file)
|
||||
api_client = lighter.ApiClient(configuration=lighter.Configuration(host=base_url))
|
||||
client = lighter.SignerClient(
|
||||
url=base_url,
|
||||
account_index=account_index,
|
||||
api_private_keys=private_keys,
|
||||
)
|
||||
|
||||
err = client.check_client()
|
||||
if err is not None:
|
||||
print(f"CheckClient error: {trim_exception(err)}")
|
||||
return
|
||||
|
||||
return client, api_client, websockets.connect(f"{base_url.replace('https', 'wss')}/stream")
|
||||
|
||||
|
||||
async def ws_ping(ws_client: websockets.ClientConnection):
|
||||
await ws_client.send(json.dumps({"type": "pong"}))
|
||||
|
||||
async def ws_subscribe(ws_client: websockets.ClientConnection, channel: str, auth: Optional[str] = None):
|
||||
if auth is None:
|
||||
await ws_client.send(json.dumps({"type": "subscribe", "channel": channel}))
|
||||
else:
|
||||
await ws_client.send(json.dumps({"type": "subscribe", "channel": channel, "auth": auth}))
|
||||
|
||||
async def ws_send_tx(ws_client: websockets.ClientConnection, tx_type, tx_info, tx_hash):
|
||||
# Note: you have the TX Hash from signing the TX
|
||||
# You can use this TX Hash to check the status of the TX later on
|
||||
# if the server generates a different hash, the signature will fail, so the hash will always be correct
|
||||
# because of this, the hash returned by the server will always be the same
|
||||
await ws_client.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "jsonapi/sendtx",
|
||||
"data": {
|
||||
"id": f"my_random_id_{12345678}", # optional helps id the response
|
||||
"tx_type": tx_type,
|
||||
"tx_info": json.loads(tx_info),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
print(f"expectedHash {tx_hash} response {await ws_client.recv()}")
|
||||
|
||||
|
||||
async def ws_send_batch_tx(ws_client: websockets.ClientConnection, tx_types, tx_infos, tx_hashes):
|
||||
# Note: you have the TX Hash from signing the TX
|
||||
# You can use this TX Hash to check the status of the TX later on
|
||||
# if the server generates a different hash, the signature will fail, so the hash will always be correct
|
||||
# because of this, the hash returned by the server will always be the same
|
||||
await ws_client.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "jsonapi/sendtxbatch",
|
||||
"data": {
|
||||
"id": f"my_random_id_{12345678}", # optional helps id the response
|
||||
"tx_types": json.dumps(tx_types),
|
||||
"tx_infos": json.dumps(tx_infos),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
print(f"expectedHash {tx_hashes} response {await ws_client.recv()}")
|
||||
@@ -0,0 +1,104 @@
|
||||
import asyncio
|
||||
import json
|
||||
import lighter
|
||||
from utils import default_example_setup
|
||||
|
||||
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
|
||||
WITHDRAW_ADDRESS = "0x0000..."
|
||||
AMOUNT_USDC = 5.0
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
auth_token, err = client.create_auth_token_with_expiry()
|
||||
if err:
|
||||
raise Exception(f"Auth token failed: {err}")
|
||||
|
||||
info_api = lighter.InfoApi(api_client)
|
||||
|
||||
try:
|
||||
# Get fast withdraw pool
|
||||
params = api_client.param_serialize(
|
||||
method='GET',
|
||||
resource_path='/api/v1/fastwithdraw/info',
|
||||
query_params=[('account_index', client.account_index)],
|
||||
header_params={'Authorization': auth_token}
|
||||
)
|
||||
response = await api_client.call_api(*params)
|
||||
await response.read()
|
||||
data = response.data
|
||||
assert data is not None
|
||||
|
||||
# get account to which to send money
|
||||
pool_info = json.loads(data.decode('utf-8'))
|
||||
if pool_info.get('code') != 200:
|
||||
raise Exception(f"Pool info failed: {pool_info.get('message')}")
|
||||
to_account = pool_info['to_account_index']
|
||||
print(f"Pool: {to_account}, Limit: {pool_info.get('withdraw_limit')}")
|
||||
|
||||
# get transfer fee
|
||||
fee_info = await info_api.transfer_fee_info(
|
||||
account_index=client.account_index,
|
||||
to_account_index=to_account,
|
||||
auth=auth_token
|
||||
)
|
||||
fee = fee_info.transfer_fee_usdc # this is already int
|
||||
|
||||
# Get Nonce & API key -- you can get this using HTTP call as well
|
||||
api_key_index, nonce = client.nonce_manager.next_nonce()
|
||||
|
||||
# Build memo (20-byte address + 12 zeros)
|
||||
addr_hex = WITHDRAW_ADDRESS.lower().removeprefix("0x")
|
||||
addr_bytes = bytes.fromhex(addr_hex)
|
||||
if len(addr_bytes) != 20:
|
||||
raise ValueError(f"Invalid address length: {len(addr_bytes)}")
|
||||
memo_list = list(addr_bytes + b"\x00" * 12)
|
||||
memo_hex = ''.join(format(b, '02x') for b in memo_list)
|
||||
|
||||
# create TX
|
||||
tx_type, tx_info_str, tx_hash, err = client.sign_transfer(
|
||||
eth_private_key=ETH_PRIVATE_KEY,
|
||||
to_account_index=to_account,
|
||||
asset_id=client.ASSET_ID_USDC,
|
||||
route_from=client.ROUTE_PERP,
|
||||
route_to=client.ROUTE_PERP,
|
||||
usdc_amount=int(AMOUNT_USDC) * 10 ** 6,
|
||||
fee=fee,
|
||||
memo=memo_hex,
|
||||
api_key_index=api_key_index,
|
||||
nonce=nonce
|
||||
)
|
||||
if err:
|
||||
raise Exception(f"L2 signing failed: {err}")
|
||||
|
||||
# Submit
|
||||
params = api_client.param_serialize(
|
||||
method='POST',
|
||||
resource_path='/api/v1/fastwithdraw',
|
||||
post_params=[
|
||||
('tx_info', tx_info_str),
|
||||
('to_address', WITHDRAW_ADDRESS)
|
||||
],
|
||||
header_params={
|
||||
'Authorization': auth_token,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
)
|
||||
response = await api_client.call_api(*params)
|
||||
await response.read()
|
||||
data = response.data
|
||||
assert data is not None
|
||||
result = json.loads(data.decode('utf-8'))
|
||||
|
||||
if result.get('code') == 200:
|
||||
print(f"✓ Success! TX: {result.get('tx_hash')}")
|
||||
else:
|
||||
raise Exception(f"Failed: {result.get('message')}")
|
||||
|
||||
finally:
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
from utils import default_example_setup
|
||||
|
||||
AMOUNT = 5.0
|
||||
|
||||
async def main():
|
||||
client, api_client, _ = default_example_setup()
|
||||
|
||||
# Note: There is no limit or fee for normal withdrawal
|
||||
withdraw_tx, response, err = await client.withdraw(
|
||||
asset_id=client.ASSET_ID_USDC, # change this to `client.ASSET_ID_ETH` to withdraw ETH. Also, change route_type to spot
|
||||
route_type=client.ROUTE_PERP, # change this to `client.ROUTE_SPOT` to withdraw from spot balance
|
||||
amount=AMOUNT,
|
||||
)
|
||||
if err is not None:
|
||||
raise Exception(f"error withdrawing {err}")
|
||||
|
||||
print(withdraw_tx, response)
|
||||
|
||||
await client.close()
|
||||
await api_client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,101 +0,0 @@
|
||||
import lighter
|
||||
import json
|
||||
import websockets
|
||||
import asyncio
|
||||
|
||||
# The API_KEY_PRIVATE_KEY provided belongs to a dummy account registered on Testnet.
|
||||
# It was generated using the setup_system.py script, and servers as an example.
|
||||
# Alternatively, you can go to https://app.lighter.xyz/apikeys for mainnet api keys
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
API_KEY_PRIVATE_KEY = (
|
||||
"0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
|
||||
)
|
||||
ACCOUNT_INDEX = 65
|
||||
API_KEY_INDEX = 1
|
||||
|
||||
|
||||
async def ws_flow(tx_types, tx_infos):
|
||||
async with websockets.connect(f"{BASE_URL.replace('https', 'wss')}/stream") as ws:
|
||||
msg = await ws.recv()
|
||||
print("Received:", msg)
|
||||
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "jsonapi/sendtxbatch",
|
||||
"data": {
|
||||
"id": f"my_random_batch_id_{12345678}", # optional, helps id the response
|
||||
"tx_types": json.dumps(tx_types),
|
||||
"tx_infos": json.dumps(tx_infos),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
print("Response:", await ws.recv())
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
configuration = lighter.Configuration(BASE_URL)
|
||||
api_client = lighter.ApiClient(configuration)
|
||||
transaction_api = lighter.TransactionApi(api_client)
|
||||
|
||||
# use next nonce for getting nonces
|
||||
next_nonce = await transaction_api.next_nonce(
|
||||
account_index=ACCOUNT_INDEX, api_key_index=API_KEY_INDEX
|
||||
)
|
||||
nonce_value = next_nonce.nonce
|
||||
|
||||
ask_tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1001, # Unique identifier for this order
|
||||
base_amount=100000,
|
||||
price=280000,
|
||||
is_ask=True,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value,
|
||||
)
|
||||
nonce_value += 1
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing first order (first batch): {error}")
|
||||
return
|
||||
|
||||
# Sign second order
|
||||
bid_tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1002, # Different unique identifier
|
||||
base_amount=200000,
|
||||
price=200000,
|
||||
is_ask=False,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
print(f"Error signing second order (first batch): {error}")
|
||||
return
|
||||
|
||||
tx_types = [
|
||||
lighter.SignerClient.TX_TYPE_CREATE_ORDER,
|
||||
lighter.SignerClient.TX_TYPE_CREATE_ORDER,
|
||||
]
|
||||
tx_infos = [ask_tx_info, bid_tx_info]
|
||||
|
||||
await ws_flow(tx_types, tx_infos)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,74 +0,0 @@
|
||||
import lighter
|
||||
import json
|
||||
import websockets
|
||||
import asyncio
|
||||
|
||||
# The API_KEY_PRIVATE_KEY provided belongs to a dummy account registered on Testnet.
|
||||
# It was generated using the setup_system.py script, and servers as an example.
|
||||
# Alternatively, you can go to https://app.lighter.xyz/apikeys for mainnet api keys
|
||||
BASE_URL = "https://testnet.zklighter.elliot.ai"
|
||||
API_KEY_PRIVATE_KEY = (
|
||||
"0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
|
||||
)
|
||||
ACCOUNT_INDEX = 65
|
||||
API_KEY_INDEX = 3
|
||||
|
||||
|
||||
async def ws_flow(tx_type, tx_info):
|
||||
async with websockets.connect(f"{BASE_URL.replace('https', 'wss')}/stream") as ws:
|
||||
msg = await ws.recv()
|
||||
print("Received:", msg)
|
||||
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "jsonapi/sendtx",
|
||||
"data": {
|
||||
"id": f"my_random_id_{12345678}", # optional, helps id the response
|
||||
"tx_type": tx_type,
|
||||
"tx_info": json.loads(tx_info),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
print("Response:", await ws.recv())
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
configuration = lighter.Configuration(BASE_URL)
|
||||
api_client = lighter.ApiClient(configuration)
|
||||
transaction_api = lighter.TransactionApi(api_client)
|
||||
|
||||
next_nonce = await transaction_api.next_nonce(
|
||||
account_index=ACCOUNT_INDEX, api_key_index=API_KEY_INDEX
|
||||
)
|
||||
nonce_value = next_nonce.nonce
|
||||
|
||||
tx_info, error = client.sign_create_order(
|
||||
market_index=0,
|
||||
client_order_index=1002, # Different unique identifier
|
||||
base_amount=200000,
|
||||
price=200000,
|
||||
is_ask=False,
|
||||
order_type=client.ORDER_TYPE_LIMIT,
|
||||
time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
||||
reduce_only=False,
|
||||
trigger_price=0,
|
||||
nonce=nonce_value,
|
||||
)
|
||||
if error is not None:
|
||||
print(f"Error signing order: {error}")
|
||||
return
|
||||
|
||||
await ws_flow(lighter.SignerClient.TX_TYPE_CREATE_ORDER, tx_info)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user