3118 lines
147 KiB
Python
3118 lines
147 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
from ccxt.base.exchange import Exchange
|
|
from ccxt.abstract.lighter import ImplicitAPI
|
|
from ccxt.base.types import Account, Any, Balances, Currencies, Currency, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction, TransferEntry
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class lighter(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(lighter, self).describe(), {
|
|
'id': 'lighter',
|
|
'name': 'Lighter',
|
|
'countries': [],
|
|
'version': 'v1',
|
|
'rateLimit': 1000, # 60 requests per minute - normal account
|
|
'certified': False,
|
|
'pro': True,
|
|
'dex': True,
|
|
'quoteJsonNumbers': False,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': False,
|
|
'margin': False,
|
|
'swap': True,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': True,
|
|
'borrowCrossMargin': False,
|
|
'borrowIsolatedMargin': False,
|
|
'borrowMargin': False,
|
|
'cancelAllOrders': True,
|
|
'cancelAllOrdersAfter': True,
|
|
'cancelOrder': True,
|
|
'cancelOrders': False,
|
|
'cancelOrdersForSymbols': False,
|
|
'closeAllPositions': False,
|
|
'closePosition': False,
|
|
'createMarketBuyOrderWithCost': False,
|
|
'createMarketOrderWithCost': False,
|
|
'createMarketSellOrderWithCost': False,
|
|
'createOrder': True,
|
|
'createOrders': False,
|
|
'createPostOnlyOrder': False,
|
|
'createReduceOnlyOrder': False,
|
|
'createStopOrder': False,
|
|
'createTriggerOrder': False,
|
|
'editOrder': True,
|
|
'fetchAccounts': True,
|
|
'fetchAllGreeks': False,
|
|
'fetchBalance': True,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRate': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': False,
|
|
'fetchBorrowRates': False,
|
|
'fetchBorrowRatesPerSymbol': False,
|
|
'fetchCanceledAndClosedOrders': False,
|
|
'fetchCanceledOrders': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': False,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDeposits': True,
|
|
'fetchDepositWithdrawFee': False,
|
|
'fetchDepositWithdrawFees': False,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': True,
|
|
'fetchGreeks': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchLedger': False,
|
|
'fetchLeverage': False,
|
|
'fetchLeverageTiers': False,
|
|
'fetchLiquidations': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyLiquidations': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterest': False,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenInterests': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOption': False,
|
|
'fetchOptionChain': False,
|
|
'fetchOrder': False,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': False,
|
|
'fetchOrderTrades': False,
|
|
'fetchPosition': True,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': True,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchStatus': True,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTime': True,
|
|
'fetchTrades': False,
|
|
'fetchTradingFee': False,
|
|
'fetchTradingFees': False,
|
|
'fetchTransfer': False,
|
|
'fetchTransfers': True,
|
|
'fetchVolatilityHistory': False,
|
|
'fetchWithdrawal': False,
|
|
'fetchWithdrawals': True,
|
|
'reduceMargin': True,
|
|
'repayCrossMargin': False,
|
|
'repayIsolatedMargin': False,
|
|
'sandbox': True,
|
|
'setLeverage': True,
|
|
'setMargin': True,
|
|
'setMarginMode': True,
|
|
'setPositionMode': False,
|
|
'transfer': True,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'1h': '1h',
|
|
'4h': '4h',
|
|
'12h': '12h',
|
|
'1d': '1d',
|
|
'1w': '1w',
|
|
},
|
|
'hostname': 'zklighter.elliot.ai',
|
|
'urls': {
|
|
'logo': 'https://github.com/user-attachments/assets/ff1aaf96-bffb-4545-a750-5eba716e75d0',
|
|
'api': {
|
|
'root': 'https://mainnet.{hostname}',
|
|
'public': 'https://mainnet.{hostname}',
|
|
'private': 'https://mainnet.{hostname}',
|
|
},
|
|
'test': {
|
|
'root': 'https://testnet.{hostname}',
|
|
'public': 'https://testnet.{hostname}',
|
|
'private': 'https://testnet.{hostname}',
|
|
},
|
|
'www': 'https://lighter.xyz/',
|
|
'doc': 'https://apidocs.lighter.xyz/',
|
|
'fees': 'https://docs.lighter.xyz/perpetual-futures/fees',
|
|
'referral': {
|
|
'url': 'app.lighter.xyz/?referral=715955W9',
|
|
'discount': 0.1, # user gets 10% of the points
|
|
},
|
|
},
|
|
'api': {
|
|
'root': {
|
|
'get': {
|
|
# root
|
|
'': 1, # status
|
|
'info': 1,
|
|
},
|
|
},
|
|
'public': {
|
|
'get': {
|
|
# account
|
|
'account': 1,
|
|
'accountsByL1Address': 1,
|
|
'apikeys': 1,
|
|
# order
|
|
'exchangeStats': 1,
|
|
'assetDetails': 1,
|
|
'orderBookDetails': 1,
|
|
'orderBookOrders': 1,
|
|
'orderBooks': 1,
|
|
'recentTrades': 1,
|
|
# transaction
|
|
'blockTxs': 1,
|
|
'nextNonce': 1,
|
|
'tx': 1,
|
|
'txFromL1TxHash': 1,
|
|
'txs': 1,
|
|
# announcement
|
|
'announcement': 1,
|
|
# block
|
|
'block': 1,
|
|
'blocks': 1,
|
|
'currentHeight': 1,
|
|
# candlestick
|
|
'candles': 1,
|
|
'fundings': 1,
|
|
# bridge
|
|
'fastbridge/info': 1,
|
|
# funding
|
|
'funding-rates': 1,
|
|
# info
|
|
'withdrawalDelay': 1,
|
|
},
|
|
'post': {
|
|
# transaction
|
|
'sendTx': 1,
|
|
'sendTxBatch': 1,
|
|
},
|
|
},
|
|
'private': {
|
|
'get': {
|
|
# account
|
|
'accountLimits': 1,
|
|
'accountMetadata': 1,
|
|
'pnl': 1,
|
|
'l1Metadata': 1,
|
|
'liquidations': 1,
|
|
'positionFunding': 1,
|
|
'publicPoolsMetadata': 1,
|
|
# order
|
|
'accountActiveOrders': 1,
|
|
'accountInactiveOrders': 1,
|
|
'export': 1,
|
|
'trades': 1,
|
|
# transaction
|
|
'accountTxs': 1,
|
|
'deposit/history': 1,
|
|
'transfer/history': 1,
|
|
'withdraw/history': 1,
|
|
# referral
|
|
'referral/points': 1,
|
|
# info
|
|
'transferFeeInfo': 1,
|
|
},
|
|
'post': {
|
|
# account
|
|
'changeAccountTier': 1,
|
|
# notification
|
|
'notification/ack': 1,
|
|
},
|
|
},
|
|
},
|
|
'httpExceptions': {
|
|
},
|
|
'exceptions': {
|
|
'exact': {
|
|
'21146': ExchangeError, # system account cannot be an integrator
|
|
'21500': ExchangeError, # transaction not found
|
|
'21501': ExchangeError, # invalid tx info
|
|
'21502': ExchangeError, # marshal tx failed
|
|
'21503': ExchangeError, # marshal event failed
|
|
'21504': ExchangeError, # fail to l1 signature
|
|
'21505': ExchangeError, # unsupported tx type
|
|
'21506': ExchangeError, # too many pending txs. Please try again later
|
|
'21507': ExchangeError, # account is below maintenance margin, can't execute transaction
|
|
'21508': ExchangeError, # account is below initial margin, can't execute transaction
|
|
'21511': ExchangeError, # invalid tx type for account
|
|
'21512': ExchangeError, # invalid l1 request id
|
|
'21600': InvalidOrder, # given order is not an active limit order
|
|
'21601': InvalidOrder, # order book is full
|
|
'21602': InvalidOrder, # invalid market index
|
|
'21603': InvalidOrder, # invalid min amounts for market
|
|
'21604': InvalidOrder, # invalid margin fractions for market
|
|
'21605': InvalidOrder, # invalid market status
|
|
'21606': InvalidOrder, # market already exist for given index
|
|
'21607': InvalidOrder, # invalid market fees
|
|
'21608': InvalidOrder, # invalid quote multiplier
|
|
'21611': InvalidOrder, # invalid interest rate
|
|
'21612': InvalidOrder, # invalid open interest
|
|
'21613': InvalidOrder, # invalid margin mode
|
|
'21614': InvalidOrder, # no position found
|
|
'21700': InvalidOrder, # invalid order index
|
|
'21701': InvalidOrder, # invalid base amount
|
|
'21702': InvalidOrder, # invalid price
|
|
'21703': InvalidOrder, # invalid isAsk
|
|
'21704': InvalidOrder, # invalid OrderType
|
|
'21705': InvalidOrder, # invalid OrderTimeInForce
|
|
'21706': InvalidOrder, # invalid order base or quote amount
|
|
'21707': InvalidOrder, # account is not owner of the order
|
|
'21708': InvalidOrder, # order is empty
|
|
'21709': InvalidOrder, # order is inactive
|
|
'21710': InvalidOrder, # unsupported order type
|
|
'21711': InvalidOrder, # invalid expiry
|
|
'21712': InvalidOrder, # account has a queued cancel all orders request
|
|
'21713': InvalidOrder, # invalid cancel all time in force
|
|
'21714': InvalidOrder, # invalid cancel all time
|
|
'21715': InvalidOrder, # given order is not an active order
|
|
'21716': InvalidOrder, # order is not expired
|
|
'21717': InvalidOrder, # maximum active limit order count reached
|
|
'21718': InvalidOrder, # maximum active limit order count per market reached
|
|
'21719': InvalidOrder, # maximum pending order count reached
|
|
'21720': InvalidOrder, # maximum pending order count per market reached
|
|
'21721': InvalidOrder, # maximum twap order count reached
|
|
'21722': InvalidOrder, # maximum conditional order count reached
|
|
'21723': InvalidOrder, # invalid account health
|
|
'21724': InvalidOrder, # invalid liquidation size
|
|
'21725': InvalidOrder, # invalid liquidation price
|
|
'21726': InvalidOrder, # insurance fund cannot be partially liquidated
|
|
'21727': InvalidOrder, # invalid client order index
|
|
'21728': InvalidOrder, # client order index already exists
|
|
'21729': InvalidOrder, # invalid order trigger price
|
|
'21730': InvalidOrder, # order status is not pending
|
|
'21731': InvalidOrder, # order can not be triggered
|
|
'21732': InvalidOrder, # reduce only increases position
|
|
'21733': InvalidOrder, # order price flagged accidental price
|
|
'21734': InvalidOrder, # limit order price is too far from the mark price
|
|
'21735': InvalidOrder, # SL/TP order price is too far from the trigger price
|
|
'21736': InvalidOrder, # invalid order trigger status
|
|
'21737': InvalidOrder, # invalid order status
|
|
'21738': InvalidOrder, # invalid reduce only direction
|
|
'21739': InvalidOrder, # not enough margin to create the order
|
|
'21740': InvalidOrder, # invalid reduce only mode
|
|
'21901': InvalidOrder, # deleverage against itself
|
|
'21902': InvalidOrder, # deleverage does not match liquidation status
|
|
'21903': InvalidOrder, # deleverage with open orders
|
|
'21904': InvalidOrder, # invalid deleverage size
|
|
'21905': InvalidOrder, # invalid deleverage price
|
|
'21906': InvalidOrder, # invalid deleverage side
|
|
'23000': RateLimitExceeded, # Too Many Requests!
|
|
'23001': RateLimitExceeded, # Too Many Subscriptions!
|
|
'23002': RateLimitExceeded, # Too Many Different Accounts!
|
|
'23003': RateLimitExceeded, # Too Many Connections!
|
|
},
|
|
'broad': {
|
|
},
|
|
},
|
|
'fees': {
|
|
'taker': 0,
|
|
'maker': 0,
|
|
},
|
|
'requiredCredentials': {
|
|
'apiKey': False,
|
|
'secret': False,
|
|
'walletAddress': False,
|
|
'privateKey': True,
|
|
'password': False,
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'commonCurrencies': {},
|
|
'options': {
|
|
'defaultType': 'swap',
|
|
'builderFee': True,
|
|
'chainId': 304,
|
|
'accountIndex': None,
|
|
'apiKeyIndex': None,
|
|
'lighterPrivateKey': None,
|
|
'wasmExecPath': None, # [JS Only] users should set the path to wasm_exec.js. It can be downloaded here https://github.com/ccxt/lighter-wasm
|
|
'libraryPath': None, # users should set the path to the lighter signing library. It can be downloaded here https://github.com/elliottech/lighter-python/tree/main/lighter/signers, GO users don't need it
|
|
'integratorAccountIndex': 718718,
|
|
'integratorMakerFee': 1000,
|
|
'integratorTakerFee': 1000,
|
|
'authDeadlineExpiry': 28800, # 8h validity for auth tokens
|
|
'authDeadlineMinimumRemaining': 60,
|
|
},
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'leverage': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'marketBuyByCost': False,
|
|
'selfTradePrevention': False,
|
|
'trailing': False,
|
|
'iceberg': False,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
def load_account(self, chainId, privateKey, apiKeyIndex: str, accountIndex: str, params={}):
|
|
self.init_auth_object(accountIndex, apiKeyIndex)
|
|
cachedAuths = self.safe_dict(self.options['auths'][accountIndex], apiKeyIndex)
|
|
signer = self.safe_value(cachedAuths, 'signer')
|
|
if signer is not None:
|
|
return signer
|
|
libraryPath = None
|
|
libraryPath, params = self.handle_option_and_params(params, 'loadAccount', 'libraryPath')
|
|
lighterPrivateKeyIsSet = (privateKey is not None) and (privateKey != '')
|
|
if lighterPrivateKeyIsSet and (libraryPath is not None) and (apiKeyIndex is not None) and (accountIndex is not None):
|
|
# load lighter library, and create lighter client
|
|
signer = self.load_lighter_library(libraryPath, chainId, privateKey, self.parse_to_int(apiKeyIndex), self.parse_to_int(accountIndex), True)
|
|
self.options['auths'][accountIndex][apiKeyIndex]['signer'] = signer
|
|
return signer
|
|
privateKeyIsSet = (self.privateKey is not None) and (self.privateKey != '')
|
|
if privateKeyIsSet and (apiKeyIndex is not None) and (accountIndex is not None):
|
|
if len(self.privateKey) > 66:
|
|
raise NotSupported(self.id + ' after the latest update(v4.5.50), CCXT now expects the l1 private key to be provided in the credentials. Please check for more details: https://github.com/ccxt/ccxt/wiki/FAQ#how-to-use-the-lighter-exchange-in-ccxt')
|
|
# load lighter library without creating lighter client
|
|
signer = self.load_lighter_library(libraryPath, chainId, '', self.parse_to_int(apiKeyIndex), self.parse_to_int(accountIndex), False)
|
|
self.options['auths'][accountIndex][apiKeyIndex]['signer'] = signer
|
|
res = self.change_api_key()
|
|
self.handle_builder_fee_approval(self.parse_to_int(accountIndex), self.parse_to_int(apiKeyIndex))
|
|
return res
|
|
return signer
|
|
|
|
def init_auth_object(self, strAccountIndex: str, strApiKeyIndex: str):
|
|
if not ('auths' in self.options):
|
|
self.options['auths'] = {}
|
|
if not (strAccountIndex in self.options['auths']):
|
|
self.options['auths'][strAccountIndex] = {}
|
|
if not (strApiKeyIndex in self.options['auths'][strAccountIndex]):
|
|
self.options['auths'][strAccountIndex][strApiKeyIndex] = {
|
|
'signer': None,
|
|
'lighterPrivateKey': None,
|
|
'deadline': None,
|
|
'token': None,
|
|
}
|
|
|
|
def get_lighter_private_key(self, strAccountIndex: str, strApiKeyIndex: str):
|
|
if not ('auths' in self.options):
|
|
return None
|
|
if not (strAccountIndex in self.options['auths']):
|
|
return None
|
|
if not (strApiKeyIndex in self.options['auths'][strAccountIndex]):
|
|
return None
|
|
if not ('lighterPrivateKey' in self.options['auths'][strAccountIndex][strApiKeyIndex]):
|
|
return None
|
|
return self.options['auths'][strAccountIndex][strApiKeyIndex]['lighterPrivateKey']
|
|
|
|
def pre_load_lighter_library(self, params={}):
|
|
"""
|
|
if the required credentials are available in options, it will pre-load the lighter Signer to avoid delaying sensitive calls like createOrder the first time they're executed
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns boolean: True if the signer was loaded, False otherwise
|
|
"""
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'loadAccount', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'loadAccount', 'accountIndex', 'account_index')
|
|
if accountIndex is None:
|
|
raise ArgumentsRequired(self.id + ' requires accountIndex or account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.init_auth_object(strAccountIndex, strApiKeyIndex)
|
|
signer = self.safe_dict(self.options['auths'][strAccountIndex][strApiKeyIndex], 'signer')
|
|
if signer is not None:
|
|
return True
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex)
|
|
self.handle_builder_fee_approval(accountIndex, apiKeyIndex)
|
|
return(signer is not None)
|
|
|
|
def handle_api_key_index(self, params: object, methodName1: str, optionName1: str, optionName2: str, defaultValue=None) -> List[Any]:
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_option_and_params_2(params, methodName1, optionName1, optionName2, defaultValue)
|
|
if (apiKeyIndex is None) or (apiKeyIndex < 4) or (apiKeyIndex > 254):
|
|
# apiKeyIndex = self.rand_number(2)
|
|
apiKeyIndex = 254
|
|
self.options['apiKeyIndex'] = apiKeyIndex # default to a value to avoid overriding other keys
|
|
return [self.parse_to_int(apiKeyIndex), params]
|
|
|
|
def handle_account_index(self, params: object, methodName1: str, optionName1: str, optionName2: str, defaultValue=None) -> List[Any]:
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_option_and_params_2(params, methodName1, optionName1, optionName2, defaultValue)
|
|
if accountIndex is None:
|
|
walletAddress = self.walletAddress
|
|
if self.privateKey is not None:
|
|
if len(self.privateKey) > 66:
|
|
raise NotSupported(self.id + ' after the latest update(v4.5.50), CCXT now expects the l1 private key to be provided in the credentials. Please check for more details: https://github.com/ccxt/ccxt/wiki/FAQ#how-to-use-the-lighter-exchange-in-ccxt')
|
|
walletAddress = self.eth_get_address_from_private_key(self.privateKey)
|
|
if walletAddress is None or walletAddress == '':
|
|
raise ArgumentsRequired(self.id + ' ' + methodName1 + '() requires an ' + optionName1 + '/' + optionName2 + ' parameter or walletAddress to fetch accountIndex. Alternatively set privateKey in credentials to enable automatic walletAddress detection.')
|
|
res = self.publicGetAccountsByL1Address({'l1_address': walletAddress})
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "l1_address": "0xaaaabbbb....ccccdddd",
|
|
# "sub_accounts": [
|
|
# {
|
|
# "code": 0,
|
|
# "account_type": 0,
|
|
# "index": 666666,
|
|
# "l1_address": "0xaaaabbbb....ccccdddd",
|
|
# "cancel_all_time": 0,
|
|
# "total_order_count": 0,
|
|
# "total_isolated_order_count": 0,
|
|
# "pending_order_count": 0,
|
|
# "available_balance": "",
|
|
# "status": 0,
|
|
# "collateral": "40",
|
|
# "transaction_time": 0,
|
|
# "account_trading_mode": 0
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
subAccounts = self.safe_list(res, 'sub_accounts')
|
|
if isinstance(subAccounts, list):
|
|
account = self.safe_dict(subAccounts, 0)
|
|
if account is None:
|
|
raise ArgumentsRequired(self.id + ' ' + methodName1 + '() requires an ' + optionName1 + ' or ' + optionName2 + ' parameter')
|
|
accountIndex = account['index']
|
|
self.options['accountIndex'] = accountIndex
|
|
return [self.parse_to_int(accountIndex), params]
|
|
|
|
def create_sub_account(self, name: str, params={}):
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'createSubAccount', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'createSubAccount', 'accountIndex', 'account_index')
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
txType, txInfo = self.lighter_sign_create_sub_account(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
return self.publicPostSendTx(request)
|
|
|
|
def create_auth(self, params={}):
|
|
# don't omit [accountIndex, apiKeyIndex], request may need them
|
|
apiKeyIndex = self.safe_string_2(params, 'apiKeyIndex', 'api_key_index')
|
|
if apiKeyIndex is None:
|
|
res = self.handle_option_and_params_2({}, 'createAuth', 'apiKeyIndex', 'api_key_index')
|
|
apiKeyIndex = self.safe_string(res, 0)
|
|
accountIndex = self.safe_string_2(params, 'accountIndex', 'account_index')
|
|
if accountIndex is None:
|
|
res = self.handle_option_and_params_2({}, 'createAuth', 'accountIndex', 'account_index')
|
|
accountIndex = self.safe_string(res, 0)
|
|
auths = self.safe_dict(self.options, 'auths')
|
|
accountAuths = self.safe_dict(auths, accountIndex)
|
|
cachedAuth = self.safe_dict(accountAuths, apiKeyIndex)
|
|
cachedDeadline = self.safe_integer(cachedAuth, 'deadline')
|
|
if cachedDeadline is not None:
|
|
minimumDeadline = self.seconds() + self.safe_integer(self.options, 'authDeadlineMinimumRemaining')
|
|
if cachedDeadline >= minimumDeadline:
|
|
return self.safe_string(cachedAuth, 'token')
|
|
deadline = self.seconds() + self.safe_integer(self.options, 'authDeadlineExpiry')
|
|
request = {
|
|
'deadline': deadline,
|
|
'api_key_index': self.parse_to_int(apiKeyIndex),
|
|
'account_index': self.parse_to_int(accountIndex),
|
|
}
|
|
token = self.lighter_create_auth_token(self.options['auths'][accountIndex][apiKeyIndex]['signer'], request)
|
|
self.options['auths'][accountIndex][apiKeyIndex]['deadline'] = deadline
|
|
self.options['auths'][accountIndex][apiKeyIndex]['token'] = token
|
|
return token
|
|
|
|
def pow(self, n: str, m: str):
|
|
r = Precise.string_mul(n, '1')
|
|
c = self.parse_to_int(m)
|
|
if c < 0:
|
|
raise BadRequest(self.id + ' pow() requires m > 0.')
|
|
if c == 0:
|
|
return '1'
|
|
if c > 100:
|
|
raise BadRequest(self.id + ' pow() requires m < 100.')
|
|
for i in range(1, c):
|
|
r = Precise.string_mul(r, n)
|
|
return r
|
|
|
|
def hash_message(self, message: str):
|
|
binaryMessage = self.encode(message)
|
|
binaryMessageLength = self.binary_length(binaryMessage)
|
|
x19 = self.base16_to_binary('19')
|
|
newline = self.base16_to_binary('0a')
|
|
prefix = self.binary_concat(x19, self.encode('Ethereum Signed Message:'), newline, self.encode(self.number_to_string(binaryMessageLength)))
|
|
return '0x' + self.hash(self.binary_concat(prefix, binaryMessage), 'keccak', 'hex')
|
|
|
|
def sign_hash(self, hash, privateKey):
|
|
self.check_required_credentials()
|
|
signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None)
|
|
r = signature['r']
|
|
s = signature['s']
|
|
v = self.int_to_base16(self.sum(27, signature['v']))
|
|
return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v
|
|
|
|
def sign_l1_and_prepare_tx_info(self, txInfo, message, privateKey):
|
|
hashMessage = self.hash_message(message)
|
|
signature = self.sign_hash(hashMessage, privateKey)
|
|
decTxInfo = self.parse_json(txInfo)
|
|
decTxInfo['L1Sig'] = signature
|
|
return self.json(decTxInfo)
|
|
|
|
def handle_builder_fee_approval(self, accountIndex: float, apiKeyIndex: float):
|
|
buildFee = self.safe_bool(self.options, 'builderFee', True)
|
|
if not buildFee:
|
|
return False
|
|
approvedBuilderFee = self.safe_bool(self.options, 'approvedBuilderFee', False)
|
|
if approvedBuilderFee:
|
|
return True
|
|
try:
|
|
builder = self.safe_integer(self.options, 'integratorAccountIndex', 718718)
|
|
takerFeeRate = self.safe_integer(self.options, 'integratorTakerFee', 1000)
|
|
makerFeeRate = self.safe_integer(self.options, 'integratorMakerFee', 1000)
|
|
self.approve_builder_fee(builder, takerFeeRate, makerFeeRate, accountIndex, apiKeyIndex)
|
|
self.options['approvedBuilderFee'] = True
|
|
except Exception as e:
|
|
self.options['builderFee'] = False
|
|
return True
|
|
|
|
def approve_builder_fee(self, builder: float, takerFeeRate: float, makerFeeRate: float, accountIndex: float, apiKeyIndex: float, params: object = {}):
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, self.extend(params, {'skipNonce': False}))
|
|
expiry = self.milliseconds() + 365 * 864000
|
|
signRaw = {
|
|
'integrator_account_index': builder,
|
|
'integrator_taker_fee': takerFeeRate,
|
|
'integrator_maker_fee': makerFeeRate,
|
|
'approval_expiry': expiry,
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo, messageToSign = self.lighter_sign_approve_integrator(signer, self.extend(signRaw, params))
|
|
newTxInfo = self.sign_l1_and_prepare_tx_info(txInfo, messageToSign, self.privateKey)
|
|
request = {
|
|
'tx_type': txType,
|
|
'tx_info': newTxInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return response
|
|
|
|
def change_api_key(self, params: object = {}):
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'changeApiKey', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'changeApiKey', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signerNotLoad = self.options['auths'][strAccountIndex][strApiKeyIndex]['signer']
|
|
privateKey, publicKey = self.lighter_generate_api_key(signerNotLoad)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, self.extend(params, {'skipNonce': False}))
|
|
signRaw = {
|
|
'pubkey': self.encode(publicKey),
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
# create lighter client
|
|
signer = self.lighter_create_client(signerNotLoad, self.options['chainId'], privateKey, apiKeyIndex, accountIndex)
|
|
txType, txInfo, messageToSign = self.lighter_sign_change_pubkey(signer, self.extend(signRaw, params))
|
|
newTxInfo = self.sign_l1_and_prepare_tx_info(txInfo, messageToSign, self.privateKey)
|
|
request = {
|
|
'tx_type': txType,
|
|
'tx_info': newTxInfo,
|
|
}
|
|
self.publicPostSendTx(request)
|
|
self.options['auths'][strAccountIndex][strApiKeyIndex]['lighterPrivateKey'] = privateKey
|
|
self.options['auths'][strAccountIndex][strApiKeyIndex]['signer'] = signer # reassign signer in go
|
|
self.handle_builder_fee_approval(accountIndex, apiKeyIndex)
|
|
return signer
|
|
|
|
def set_sandbox_mode(self, enable: bool):
|
|
super(lighter, self).set_sandbox_mode(enable)
|
|
self.options['sandboxMode'] = enable
|
|
self.options['chainId'] = 300 if enable else 304
|
|
|
|
def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> List[Any]:
|
|
"""
|
|
@ignore
|
|
helper function to build the request
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much you want to trade in units of the base currency
|
|
:param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.nonce]: nonce for the account
|
|
:param int [params.apiKeyIndex]: apiKeyIndex
|
|
:param int [params.accountIndex]: accountIndex
|
|
:param int [params.orderExpiry]: orderExpiry
|
|
:returns any[]: request to be sent to the exchange
|
|
"""
|
|
if price is None:
|
|
raise ArgumentsRequired(self.id + ' createOrder() requires a price argument')
|
|
reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only', False) # default False
|
|
orderType = type.upper()
|
|
market = self.market(symbol)
|
|
orderSide = side.upper()
|
|
request: dict = {
|
|
'market_index': self.parse_to_int(market['id']),
|
|
}
|
|
nonce = None
|
|
apiKeyIndex = None
|
|
accountIndex = None
|
|
orderExpiry = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'createOrder', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex, params = self.handle_option_and_params_2(params, 'createOrder', 'accountIndex', 'account_index')
|
|
nonce, params = self.handle_option_and_params(params, 'createOrder', 'nonce')
|
|
orderExpiry, params = self.handle_option_and_params(params, 'createOrder', 'orderExpiry', 0)
|
|
if nonce is not None:
|
|
request['nonce'] = nonce
|
|
request['api_key_index'] = apiKeyIndex
|
|
request['account_index'] = self.parse_to_int(accountIndex)
|
|
triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice')
|
|
stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice)
|
|
takeProfitPrice = self.safe_value(params, 'takeProfitPrice')
|
|
stopLoss = self.safe_value(params, 'stopLoss')
|
|
takeProfit = self.safe_value(params, 'takeProfit')
|
|
hasStopLoss = (stopLoss is not None)
|
|
hasTakeProfit = (takeProfit is not None)
|
|
isConditional = (stopLossPrice or takeProfitPrice)
|
|
isMarketOrder = (orderType == 'MARKET')
|
|
timeInForce = self.safe_string_lower(params, 'timeInForce', 'gtt')
|
|
postOnly = self.is_post_only(isMarketOrder, None, params)
|
|
params = self.omit(params, ['stopLoss', 'takeProfit', 'timeInForce'])
|
|
orderTypeNum = None
|
|
timeInForceNum = None
|
|
if isMarketOrder:
|
|
orderTypeNum = 1
|
|
timeInForceNum = 0
|
|
else:
|
|
orderTypeNum = 0
|
|
if orderSide == 'BUY':
|
|
request['is_ask'] = 0
|
|
else:
|
|
request['is_ask'] = 1
|
|
if postOnly:
|
|
timeInForceNum = 2
|
|
else:
|
|
if not isMarketOrder:
|
|
if timeInForce == 'ioc':
|
|
timeInForceNum = 0
|
|
orderExpiry = 0
|
|
elif timeInForce == 'gtt':
|
|
timeInForceNum = 1
|
|
orderExpiry = -1
|
|
marketInfo = self.safe_dict(market, 'info')
|
|
amountStr = None
|
|
priceStr = self.price_to_precision(symbol, price)
|
|
amountScale = self.pow('10', marketInfo['size_decimals'])
|
|
priceScale = self.pow('10', marketInfo['price_decimals'])
|
|
triggerPriceStr = '0' # default is 0
|
|
defaultClientOrderId = self.rand_number(9) # c# only support int32 2147483647.
|
|
clientOrderId = self.safe_integer_2(params, 'client_order_index', 'clientOrderId', defaultClientOrderId)
|
|
params = self.omit(params, ['reduceOnly', 'reduce_only', 'timeInForce', 'postOnly', 'nonce', 'apiKeyIndex', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'client_order_index', 'clientOrderId'])
|
|
if isConditional:
|
|
amountStr = self.number_to_string(amount)
|
|
if stopLossPrice is not None:
|
|
if isMarketOrder:
|
|
orderTypeNum = 2
|
|
else:
|
|
orderTypeNum = 3
|
|
triggerPriceStr = self.price_to_precision(symbol, stopLossPrice)
|
|
elif takeProfitPrice is not None:
|
|
if isMarketOrder:
|
|
orderTypeNum = 4
|
|
else:
|
|
orderTypeNum = 5
|
|
triggerPriceStr = self.price_to_precision(symbol, takeProfitPrice)
|
|
else:
|
|
amountStr = self.amount_to_precision(symbol, amount)
|
|
request['order_expiry'] = orderExpiry
|
|
request['order_type'] = orderTypeNum
|
|
request['time_in_force'] = timeInForceNum
|
|
request['reduce_only'] = 1 if (reduceOnly) else 0
|
|
request['client_order_index'] = clientOrderId
|
|
request['base_amount'] = self.parse_to_int(Precise.string_mul(amountStr, amountScale))
|
|
request['avg_execution_price'] = self.parse_to_int(Precise.string_mul(priceStr, priceScale))
|
|
request['trigger_price'] = self.parse_to_int(Precise.string_mul(triggerPriceStr, priceScale))
|
|
if self.safe_bool(self.options, 'builderFee', True):
|
|
request['integrator_account_index'] = self.options['integratorAccountIndex']
|
|
request['integrator_taker_fee'] = self.options['integratorTakerFee']
|
|
request['integrator_maker_fee'] = self.options['integratorMakerFee']
|
|
orders = []
|
|
orders.append(self.extend(request, params))
|
|
if hasStopLoss or hasTakeProfit:
|
|
# group order
|
|
orders[0]['client_order_index'] = 0 # client order index should be 0
|
|
triggerOrderSide = ''
|
|
if side == 'BUY':
|
|
triggerOrderSide = 'sell'
|
|
else:
|
|
triggerOrderSide = 'buy'
|
|
stopLossOrderTriggerPrice = self.safe_number_n(stopLoss, ['triggerPrice', 'stopPrice'])
|
|
stopLossOrderType = self.safe_string(stopLoss, 'type', 'limit')
|
|
stopLossOrderLimitPrice = self.safe_number_n(stopLoss, ['price', 'stopLossPrice'], stopLossOrderTriggerPrice)
|
|
takeProfitOrderTriggerPrice = self.safe_number_n(takeProfit, ['triggerPrice', 'stopPrice'])
|
|
takeProfitOrderType = self.safe_string(takeProfit, 'type', 'limit')
|
|
takeProfitOrderLimitPrice = self.safe_number_n(takeProfit, ['price', 'takeProfitPrice'], takeProfitOrderTriggerPrice)
|
|
# amount should be 0 for child orders
|
|
if stopLoss is not None:
|
|
orderObj = self.create_order_request(symbol, stopLossOrderType, triggerOrderSide, 0, stopLossOrderLimitPrice, self.extend(params, {
|
|
'stopLossPrice': stopLossOrderTriggerPrice,
|
|
'reduceOnly': True,
|
|
}))[0]
|
|
orderObj['client_order_index'] = 0
|
|
orders.append(orderObj)
|
|
if takeProfit is not None:
|
|
orderObj = self.create_order_request(symbol, takeProfitOrderType, triggerOrderSide, 0, takeProfitOrderLimitPrice, self.extend(params, {
|
|
'takeProfitPrice': takeProfitOrderTriggerPrice,
|
|
'reduceOnly': True,
|
|
}))[0]
|
|
orderObj['client_order_index'] = 0
|
|
orders.append(orderObj)
|
|
return orders
|
|
|
|
def fetch_nonce(self, accountIndex, apiKeyIndex, params={}):
|
|
if (accountIndex is None) or (apiKeyIndex is None):
|
|
raise ArgumentsRequired(self.id + ' fetchNonce() requires accountIndex and apiKeyIndex.')
|
|
if 'nonce' in params:
|
|
return self.safe_integer(params, 'nonce')
|
|
nonceInOptions = self.safe_integer(self.options, 'nonce')
|
|
if nonceInOptions is not None:
|
|
return nonceInOptions
|
|
# avoid skipNonce for l1 operations
|
|
skipNonce = True
|
|
skipNonce, params = self.handle_option_and_params(params, 'fetchNonce', 'skipNonce', True)
|
|
if skipNonce:
|
|
return self.milliseconds()
|
|
response = self.publicGetNextNonce({'account_index': accountIndex, 'api_key_index': apiKeyIndex})
|
|
return self.safe_integer(response, 'nonce')
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.timeInForce]: 'GTT' or 'IOC', default is 'GTT'
|
|
:param int [params.clientOrderId]: client order id, should be unique for each order, default is a random number
|
|
:param str [params.triggerPrice]: trigger price for stop loss or take profit orders, in units of the quote currency
|
|
:param boolean [params.reduceOnly]: whether the order is reduce only, default False
|
|
:param int [params.nonce]: nonce for the account
|
|
:param int [params.apiKeyIndex]: apiKeyIndex
|
|
:param int [params.accountIndex]: accountIndex
|
|
:param int [params.orderExpiry]: orderExpiry
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'createOrder', 'accountIndex', 'account_index')
|
|
params['accountIndex'] = accountIndex
|
|
market = self.market(symbol)
|
|
groupingType = None
|
|
groupingType, params = self.handle_option_and_params(params, 'createOrder', 'groupingType', 3) # default GROUPING_TYPE_ONE_TRIGGERS_A_ONE_CANCELS_THE_OTHER
|
|
orderRequests = self.create_order_request(symbol, type, side, amount, price, params)
|
|
# for php
|
|
totalOrderRequests = len(orderRequests)
|
|
apiKeyIndex = None
|
|
order = None
|
|
if totalOrderRequests > 0:
|
|
order = orderRequests[0]
|
|
apiKeyIndex = order['api_key_index']
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
# the nonce could be updated
|
|
if self.safe_integer(order, 'nonce') is None:
|
|
order['nonce'] = self.fetch_nonce(accountIndex, apiKeyIndex)
|
|
txType = None
|
|
txInfo = None
|
|
if totalOrderRequests < 2:
|
|
txType, txInfo = self.lighter_sign_create_order(signer, order)
|
|
else:
|
|
signingPayload = {
|
|
'grouping_type': groupingType,
|
|
'orders': orderRequests,
|
|
'nonce': order['nonce'],
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
if self.safe_bool(self.options, 'builderFee', True):
|
|
signingPayload['integrator_account_index'] = order['integrator_account_index']
|
|
signingPayload['integrator_taker_fee'] = order['integrator_taker_fee']
|
|
signingPayload['integrator_maker_fee'] = order['integrator_maker_fee']
|
|
txType, txInfo = self.lighter_sign_create_grouped_orders(signer, signingPayload)
|
|
request = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "message": "{\"ratelimit\": \"didn't use volume quota\"}",
|
|
# "tx_hash": "txhash",
|
|
# "predicted_execution_time_ms": 1766088500120
|
|
# }
|
|
#
|
|
return self.parse_order(self.deep_extend(response, order), market)
|
|
|
|
def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order:
|
|
"""
|
|
cancels an order and places a new order
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of the currency you want to trade in units of the base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'editOrder', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'editOrder', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
market = self.market(symbol)
|
|
marketInfo = self.safe_dict(market, 'info')
|
|
amountScale = self.pow('10', marketInfo['size_decimals'])
|
|
priceScale = self.pow('10', marketInfo['price_decimals'])
|
|
triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice'])
|
|
params = self.omit(params, ['stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice'])
|
|
amountStr = None
|
|
priceStr = self.price_to_precision(symbol, price)
|
|
triggerPriceStr = '0' # default is 0
|
|
if triggerPrice is not None:
|
|
amountStr = self.number_to_string(amount)
|
|
triggerPriceStr = self.price_to_precision(symbol, triggerPrice)
|
|
else:
|
|
amountStr = self.amount_to_precision(symbol, amount)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'market_index': self.parse_to_int(market['id']),
|
|
'index': self.parse_to_int(id),
|
|
'base_amount': self.parse_to_int(Precise.string_mul(amountStr, amountScale)),
|
|
'price': self.parse_to_int(Precise.string_mul(priceStr, priceScale)),
|
|
'trigger_price': self.parse_to_int(Precise.string_mul(triggerPriceStr, priceScale)),
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
'integrator_account_index': self.options['integratorAccountIndex'],
|
|
'integrator_taker_fee': self.options['integratorTakerFee'],
|
|
'integrator_maker_fee': self.options['integratorMakerFee'],
|
|
}
|
|
txType, txInfo = self.lighter_sign_modify_order(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_order(response, market)
|
|
|
|
def fetch_status(self, params={}):
|
|
"""
|
|
the latest known information on the availability of the exchange API
|
|
|
|
https://apidocs.lighter.xyz/reference/status
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `status structure <https://docs.ccxt.com/?id=exchange-status-structure>`
|
|
"""
|
|
response = self.rootGet(params)
|
|
#
|
|
# {
|
|
# "status": "1",
|
|
# "network_id": "1",
|
|
# "timestamp": "1717777777"
|
|
# }
|
|
#
|
|
status = self.safe_string(response, 'status')
|
|
return {
|
|
'status': 'ok' if (status == '200') else 'error', # if there's no Errors, status = 'ok'
|
|
'updated': None,
|
|
'eta': None,
|
|
'url': None,
|
|
'info': response,
|
|
}
|
|
|
|
def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
|
|
https://apidocs.lighter.xyz/reference/status
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
response = self.rootGet(params)
|
|
#
|
|
# {
|
|
# "status": "1",
|
|
# "network_id": "1",
|
|
# "timestamp": "1717777777"
|
|
# }
|
|
#
|
|
return self.safe_timestamp(response, 'timestamp')
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for lighter
|
|
|
|
https://apidocs.lighter.xyz/reference/orderbookdetails
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = self.publicGetOrderBookDetails(params)
|
|
#
|
|
# {
|
|
# "code": "200",
|
|
# "message": "string",
|
|
# "order_book_details": [
|
|
# {
|
|
# "symbol": "ETH",
|
|
# "market_id": 0,
|
|
# "market_type": "perp",
|
|
# "base_asset_id": 0,
|
|
# "quote_asset_id": 0,
|
|
# "status": "active",
|
|
# "taker_fee": "0.0001",
|
|
# "maker_fee": "0.0000",
|
|
# "liquidation_fee": "0.01",
|
|
# "min_base_amount": "0.01",
|
|
# "min_quote_amount": "0.1",
|
|
# "supported_size_decimals": "4",
|
|
# "supported_price_decimals": "4",
|
|
# "supported_quote_decimals": "4",
|
|
# "order_quote_limit": "281474976.710655",
|
|
# "size_decimals": "4",
|
|
# "price_decimals": "4",
|
|
# "quote_multiplier": "10000",
|
|
# "default_initial_margin_fraction": "100",
|
|
# "min_initial_margin_fraction": "100",
|
|
# "maintenance_margin_fraction": "50",
|
|
# "closeout_margin_fraction": "100",
|
|
# "last_trade_price": "3024.66",
|
|
# "daily_trades_count": "68",
|
|
# "daily_base_token_volume": "235.25",
|
|
# "daily_quote_token_volume": "93566.25",
|
|
# "daily_price_low": "3014.66",
|
|
# "daily_price_high": "3024.66",
|
|
# "daily_price_change": "3.66",
|
|
# "open_interest": "93.0",
|
|
# "daily_chart": "{1640995200:3024.66}",
|
|
# "market_config": {
|
|
# "market_margin_mode": 0,
|
|
# "insurance_fund_account_index": 281474976710655,
|
|
# "liquidation_mode": 0,
|
|
# "force_reduce_only": False,
|
|
# "funding_fee_discounts_enabled": True,
|
|
# "trading_hours": "",
|
|
# "hidden": True
|
|
# },
|
|
# "strategy_index": 0
|
|
# }
|
|
# ],
|
|
# "spot_order_book_details": [
|
|
# {
|
|
# "symbol": "ETH/USDC",
|
|
# "market_id": 2048,
|
|
# "market_type": "spot",
|
|
# "base_asset_id": 1,
|
|
# "quote_asset_id": 3,
|
|
# "status": "active",
|
|
# "taker_fee": "0.0000",
|
|
# "maker_fee": "0.0000",
|
|
# "liquidation_fee": "0.0000",
|
|
# "min_base_amount": "0.0001",
|
|
# "min_quote_amount": "0.000001",
|
|
# "order_quote_limit": "2500000.000000",
|
|
# "supported_size_decimals": 4,
|
|
# "supported_price_decimals": 2,
|
|
# "supported_quote_decimals": 6,
|
|
# "size_decimals": 4,
|
|
# "price_decimals": 2,
|
|
# "last_trade_price": 2731.79,
|
|
# "daily_trades_count": 126993,
|
|
# "daily_base_token_volume": 1203.0962,
|
|
# "daily_quote_token_volume": 3516374.947553,
|
|
# "daily_price_low": 2717.47,
|
|
# "daily_price_high": 3044.21,
|
|
# "daily_price_change": -10.2389493724579,
|
|
# "daily_chart": "{1640995200:3024.66}"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
spotMarkets = self.safe_list(response, 'spot_order_book_details', [])
|
|
swapMarkets = self.safe_list(response, 'order_book_details', [])
|
|
markets = self.array_concat(spotMarkets, swapMarkets)
|
|
result = []
|
|
for i in range(0, len(markets)):
|
|
market = markets[i]
|
|
id = self.safe_string(market, 'market_id')
|
|
type = self.safe_string(market, 'market_type')
|
|
type = 'swap' if (type == 'perp') else type
|
|
baseId = self.safe_string(market, 'symbol')
|
|
if baseId is not None and baseId.find('/') != -1:
|
|
baseId = baseId.split('/')[0]
|
|
quoteId = 'USDC'
|
|
settleId = 'USDC' if (type == 'swap') else None
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
settle = self.safe_currency_code(settleId)
|
|
symbol = base + '/' + quote
|
|
if settle is not None:
|
|
symbol = symbol + ':' + settle
|
|
amountDecimals = self.safe_string_2(market, 'size_decimals', 'supported_size_decimals')
|
|
priceDecimals = self.safe_string_2(market, 'price_decimals', 'supported_price_decimals')
|
|
amountPrecision = None if (amountDecimals is None) else self.parse_number(self.parse_precision(amountDecimals))
|
|
pricePrecision = None if (priceDecimals is None) else self.parse_number(self.parse_precision(priceDecimals))
|
|
quoteMultiplier = self.safe_number(market, 'quote_multiplier')
|
|
result.append({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': settle,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': settleId,
|
|
'type': type,
|
|
'spot': type == 'spot',
|
|
'margin': False,
|
|
'swap': type == 'swap',
|
|
'future': False,
|
|
'option': False,
|
|
'active': self.safe_string(market, 'status') == 'active',
|
|
'contract': type == 'swap',
|
|
'linear': True if (type == 'swap') else None,
|
|
'inverse': False if (type == 'swap') else None,
|
|
'taker': self.safe_number(market, 'taker_fee'),
|
|
'maker': self.safe_number(market, 'maker_fee'),
|
|
'contractSize': quoteMultiplier,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': amountPrecision,
|
|
'price': pricePrecision,
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'amount': {
|
|
'min': self.safe_number(market, 'min_base_amount'),
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'min_quote_amount'),
|
|
'max': self.safe_number(market, 'order_quote_limit'),
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
return result
|
|
|
|
def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://apidocs.lighter.xyz/reference/assetdetails
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
response = self.publicGetAssetDetails(params)
|
|
if self.check_required_credentials(False):
|
|
self.pre_load_lighter_library()
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "asset_details": [
|
|
# {
|
|
# "asset_id": 3,
|
|
# "symbol": "USDC",
|
|
# "l1_decimals": 6,
|
|
# "decimals": 6,
|
|
# "min_transfer_amount": "1.000000",
|
|
# "min_withdrawal_amount": "1.000000",
|
|
# "margin_mode": "enabled",
|
|
# "index_price": "1.000000",
|
|
# "l1_address": "0x95Fd23d5110f9D89A4b0B7d63D78F5B5Ea5074D1"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'asset_details', [])
|
|
result: dict = {}
|
|
for i in range(0, len(data)):
|
|
entry = data[i]
|
|
id = self.safe_string(entry, 'asset_id')
|
|
code = self.safe_currency_code(self.safe_string(entry, 'symbol'))
|
|
decimals = self.safe_string(entry, 'decimals')
|
|
isUSDC = (code == 'USDC')
|
|
depositMin = None
|
|
withdrawMin = None
|
|
if isUSDC:
|
|
depositMin = self.safe_number(entry, 'min_transfer_amount')
|
|
withdrawMin = self.safe_number(entry, 'min_withdrawal_amount')
|
|
result[code] = self.safe_currency_structure({
|
|
'id': id,
|
|
'name': code,
|
|
'code': code,
|
|
'precision': self.parse_number('1e-' + decimals),
|
|
'active': True,
|
|
'fee': None,
|
|
'networks': {},
|
|
'deposit': isUSDC,
|
|
'withdraw': isUSDC,
|
|
'type': 'crypto',
|
|
'limits': {
|
|
'deposit': {
|
|
'min': depositMin,
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': withdrawMin,
|
|
'max': None,
|
|
},
|
|
},
|
|
'info': entry,
|
|
})
|
|
return result
|
|
|
|
def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://apidocs.lighter.xyz/reference/orderbookorders
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a symbol argument')
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market_id': market['id'],
|
|
'limit': 100,
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 100)
|
|
response = self.publicGetOrderBookOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "total_asks": 1,
|
|
# "asks": [
|
|
# {
|
|
# "order_index": 281475565888172,
|
|
# "order_id": "281475565888172",
|
|
# "owner_account_index": 134436,
|
|
# "initial_base_amount": "0.2000",
|
|
# "remaining_base_amount": "0.2000",
|
|
# "price": "3430.00",
|
|
# "order_expiry": 1765419046808
|
|
# }
|
|
# ],
|
|
# "total_bids": 1,
|
|
# "bids": [
|
|
# {
|
|
# "order_index": 562949401225099,
|
|
# "order_id": "562949401225099",
|
|
# "owner_account_index": 314236,
|
|
# "initial_base_amount": "1.7361",
|
|
# "remaining_base_amount": "1.3237",
|
|
# "price": "3429.80",
|
|
# "order_expiry": 1765419047587
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'remaining_base_amount')
|
|
return result
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# fetchTicker, fetchTickers
|
|
# {
|
|
# "symbol": "ETH",
|
|
# "market_id": 0,
|
|
# "status": "active",
|
|
# "taker_fee": "0.0000",
|
|
# "maker_fee": "0.0000",
|
|
# "liquidation_fee": "1.0000",
|
|
# "min_base_amount": "0.0050",
|
|
# "min_quote_amount": "10.000000",
|
|
# "order_quote_limit": "",
|
|
# "supported_size_decimals": 4,
|
|
# "supported_price_decimals": 2,
|
|
# "supported_quote_decimals": 6,
|
|
# "size_decimals": 4,
|
|
# "price_decimals": 2,
|
|
# "quote_multiplier": 1,
|
|
# "default_initial_margin_fraction": 500,
|
|
# "min_initial_margin_fraction": 200,
|
|
# "maintenance_margin_fraction": 120,
|
|
# "closeout_margin_fraction": 80,
|
|
# "last_trade_price": 3550.69,
|
|
# "daily_trades_count": 1197349,
|
|
# "daily_base_token_volume": 481297.3509,
|
|
# "daily_quote_token_volume": 1671431095.263844,
|
|
# "daily_price_low": 3402.41,
|
|
# "daily_price_high": 3571.45,
|
|
# "daily_price_change": 0.5294300840859545,
|
|
# "open_interest": 39559.3278,
|
|
# "daily_chart": {},
|
|
# "market_config": {
|
|
# "market_margin_mode": 0,
|
|
# "insurance_fund_account_index": 281474976710654,
|
|
# "liquidation_mode": 0,
|
|
# "force_reduce_only": False,
|
|
# "trading_hours": ""
|
|
# }
|
|
# }
|
|
#
|
|
# watchTicker, watchTickers
|
|
# {
|
|
# "market_id": 0,
|
|
# "index_price": "3015.56",
|
|
# "mark_price": "3013.91",
|
|
# "open_interest": "122736286.659423",
|
|
# "open_interest_limit": "72057594037927936.000000",
|
|
# "funding_clamp_small": "0.0500",
|
|
# "funding_clamp_big": "4.0000",
|
|
# "last_trade_price": "3013.13",
|
|
# "current_funding_rate": "0.0012",
|
|
# "funding_rate": "0.0012",
|
|
# "funding_timestamp": 1763532000004,
|
|
# "daily_base_token_volume": 643235.2763,
|
|
# "daily_quote_token_volume": 1983505435.673896,
|
|
# "daily_price_low": 2977.42,
|
|
# "daily_price_high": 3170.81,
|
|
# "daily_price_change": -0.3061987051035322
|
|
# }
|
|
#
|
|
marketId = self.safe_string(ticker, 'market_id')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
last = self.safe_string(ticker, 'last_trade_price')
|
|
high = self.safe_string(ticker, 'daily_price_high')
|
|
low = self.safe_string(ticker, 'daily_price_low')
|
|
baseVolume = self.safe_string(ticker, 'daily_base_token_volume')
|
|
quoteVolume = self.safe_string(ticker, 'daily_quote_token_volume')
|
|
change = self.safe_string(ticker, 'daily_price_change')
|
|
openInterest = self.safe_string(ticker, 'open_interest')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'high': high,
|
|
'low': low,
|
|
'bid': None,
|
|
'bidVolume': None,
|
|
'ask': None,
|
|
'askVolume': None,
|
|
'vwap': None,
|
|
'open': None,
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': change,
|
|
'average': None,
|
|
'baseVolume': baseVolume,
|
|
'quoteVolume': quoteVolume,
|
|
'markPrice': self.safe_string(ticker, 'mark_price'),
|
|
'indexPrice': self.safe_string(ticker, 'index_price'),
|
|
'openInterest': openInterest,
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
|
|
https://apidocs.lighter.xyz/reference/orderbookdetails
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchTicker() requires a symbol argument')
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market_id': market['id'],
|
|
}
|
|
response = self.publicGetOrderBookDetails(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "order_book_details": [
|
|
# {
|
|
# "symbol": "ETH",
|
|
# "market_id": 0,
|
|
# "status": "active",
|
|
# "taker_fee": "0.0000",
|
|
# "maker_fee": "0.0000",
|
|
# "liquidation_fee": "1.0000",
|
|
# "min_base_amount": "0.0050",
|
|
# "min_quote_amount": "10.000000",
|
|
# "order_quote_limit": "",
|
|
# "supported_size_decimals": 4,
|
|
# "supported_price_decimals": 2,
|
|
# "supported_quote_decimals": 6,
|
|
# "size_decimals": 4,
|
|
# "price_decimals": 2,
|
|
# "quote_multiplier": 1,
|
|
# "default_initial_margin_fraction": 500,
|
|
# "min_initial_margin_fraction": 200,
|
|
# "maintenance_margin_fraction": 120,
|
|
# "closeout_margin_fraction": 80,
|
|
# "last_trade_price": 3550.69,
|
|
# "daily_trades_count": 1197349,
|
|
# "daily_base_token_volume": 481297.3509,
|
|
# "daily_quote_token_volume": 1671431095.263844,
|
|
# "daily_price_low": 3402.41,
|
|
# "daily_price_high": 3571.45,
|
|
# "daily_price_change": 0.5294300840859545,
|
|
# "open_interest": 39559.3278,
|
|
# "daily_chart": {},
|
|
# "market_config": {
|
|
# "market_margin_mode": 0,
|
|
# "insurance_fund_account_index": 281474976710655,
|
|
# "liquidation_mode": 0,
|
|
# "force_reduce_only": False,
|
|
# "trading_hours": ""
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
spotTickers = self.safe_list(response, 'spot_order_book_details', [])
|
|
swapTickers = self.safe_list(response, 'order_book_details', [])
|
|
tickers = self.array_concat(spotTickers, swapTickers)
|
|
first = self.safe_dict(tickers, 0, {})
|
|
return self.parse_ticker(first, market)
|
|
|
|
def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
|
|
|
|
https://apidocs.lighter.xyz/reference/orderbookdetails
|
|
|
|
:param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
response = self.publicGetOrderBookDetails(params)
|
|
spotTickers = self.safe_list(response, 'spot_order_book_details', [])
|
|
swapTickers = self.safe_list(response, 'order_book_details', [])
|
|
tickers = self.array_concat(spotTickers, swapTickers)
|
|
return self.parse_tickers(tickers, symbols)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "t": 1767700500000,
|
|
# "o": 3236.86,
|
|
# "h": 3237.78,
|
|
# "l": 3235.36,
|
|
# "c": 3235.39,
|
|
# "v": 55.1632,
|
|
# "V": 178530.793575,
|
|
# "i": 779870452,
|
|
# "C": "string",
|
|
# "H": "string",
|
|
# "L": "string",
|
|
# "O": "string"
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_integer(ohlcv, 't'),
|
|
self.safe_number(ohlcv, 'o'),
|
|
self.safe_number(ohlcv, 'h'),
|
|
self.safe_number(ohlcv, 'l'),
|
|
self.safe_number(ohlcv, 'c'),
|
|
self.safe_number(ohlcv, 'v'),
|
|
]
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://apidocs.lighter.xyz/reference/candles
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest candle to fetch
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a symbol argument')
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
until = self.safe_integer(params, 'until')
|
|
params = self.omit(params, ['until'])
|
|
now = self.milliseconds()
|
|
startTs = None
|
|
endTs = None
|
|
if since is not None:
|
|
startTs = since
|
|
if until is not None:
|
|
endTs = until
|
|
elif limit is not None:
|
|
duration = self.parse_timeframe(timeframe)
|
|
endTs = self.sum(since, duration * limit * 1000)
|
|
else:
|
|
endTs = now
|
|
else:
|
|
endTs = until if (until is not None) else now
|
|
defaultLimit = 100
|
|
if limit is not None:
|
|
startTs = endTs - self.parse_timeframe(timeframe) * 1000 * limit
|
|
else:
|
|
startTs = endTs - self.parse_timeframe(timeframe) * 1000 * defaultLimit
|
|
request: dict = {
|
|
'market_id': market['id'],
|
|
'count_back': 0,
|
|
'resolution': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
'start_timestamp': startTs,
|
|
'end_timestamp': endTs,
|
|
}
|
|
response = self.publicGetCandles(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "r": "1m",
|
|
# "c": [
|
|
# {
|
|
# "t": 1767700500000,
|
|
# "o": 3236.86,
|
|
# "h": 3237.78,
|
|
# "l": 3235.36,
|
|
# "c": 3235.39,
|
|
# "v": 55.1632,
|
|
# "V": 178530.793575,
|
|
# "i": 779870452,
|
|
# "C": "string",
|
|
# "H": "string",
|
|
# "L": "string",
|
|
# "O": "string"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
ohlcvs = self.safe_list(response, 'c', [])
|
|
return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit)
|
|
|
|
def parse_funding_rate(self, contract, market: Market = None) -> FundingRate:
|
|
#
|
|
# {
|
|
# "market_id": 0,
|
|
# "exchange": "lighter",
|
|
# "symbol": "ETH",
|
|
# "rate": 0.00009599999999999999
|
|
# }
|
|
#
|
|
marketId = self.safe_string(contract, 'market_id')
|
|
return {
|
|
'info': contract,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'markPrice': None,
|
|
'indexPrice': None,
|
|
'interestRate': None,
|
|
'estimatedSettlePrice': None,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'fundingRate': self.safe_number(contract, 'rate'),
|
|
'fundingTimestamp': None,
|
|
'fundingDatetime': None,
|
|
'nextFundingRate': None,
|
|
'nextFundingTimestamp': None,
|
|
'nextFundingDatetime': None,
|
|
'previousFundingRate': None,
|
|
'previousFundingTimestamp': None,
|
|
'previousFundingDatetime': None,
|
|
'interval': None,
|
|
}
|
|
|
|
def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates:
|
|
"""
|
|
fetch the current funding rate for multiple symbols
|
|
|
|
https://apidocs.lighter.xyz/reference/funding-rates
|
|
|
|
:param str[] [symbols]: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-structure>`
|
|
"""
|
|
self.load_markets()
|
|
response = self.publicGetFundingRates(self.extend(params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "funding_rates": [
|
|
# {
|
|
# "market_id": 0,
|
|
# "exchange": "lighter",
|
|
# "symbol": "ETH",
|
|
# "rate": 0.00009599999999999999
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'funding_rates', [])
|
|
result = []
|
|
for i in range(0, len(data)):
|
|
exchange = self.safe_string(data[i], 'exchange')
|
|
if exchange == 'lighter':
|
|
result.append(data[i])
|
|
return self.parse_funding_rates(result, symbols)
|
|
|
|
def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://apidocs.lighter.xyz/reference/account-1
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.by]: fetch balance by 'index' or 'l1_address', defaults to 'index'
|
|
:param str [params.value]: fetch balance value, account index or l1 address
|
|
:param str [params.type]: 'spot', 'swap', default is 'swap'
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
|
|
"""
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchBalance', 'accountIndex', 'account_index')
|
|
defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot')
|
|
type = self.safe_string(params, 'type', defaultType)
|
|
request: dict = {
|
|
'by': self.safe_string(params, 'by', 'index'),
|
|
'value': accountIndex,
|
|
}
|
|
response = self.publicGetAccount(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": "200",
|
|
# "total": "1",
|
|
# "accounts": [
|
|
# {
|
|
# "code": "0",
|
|
# "account_type": "0",
|
|
# "index": "1077",
|
|
# "l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "cancel_all_time": "0",
|
|
# "total_order_count": "1",
|
|
# "total_isolated_order_count": "0",
|
|
# "pending_order_count": "0",
|
|
# "available_balance": "7996.489834",
|
|
# "status": "1",
|
|
# "collateral": "9000.000000",
|
|
# "account_index": "1077",
|
|
# "name": "",
|
|
# "description": "",
|
|
# "can_invite": True,
|
|
# "referral_points_percentage": "",
|
|
# "positions": [],
|
|
# "assets": [
|
|
# {
|
|
# "symbol": "ETH",
|
|
# "asset_id": "1",
|
|
# "balance": "3.00000000",
|
|
# "locked_balance": "0.00000000"
|
|
# },
|
|
# {
|
|
# "symbol": "USDC",
|
|
# "asset_id": "3",
|
|
# "balance": "1000.000000",
|
|
# "locked_balance": "0.000000"
|
|
# }
|
|
# ],
|
|
# "total_asset_value": "9536.789088",
|
|
# "cross_asset_value": "9536.789088",
|
|
# "shares": []
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result: dict = {'info': response}
|
|
accounts = self.safe_list(response, 'accounts', [])
|
|
for i in range(0, len(accounts)):
|
|
account = accounts[i]
|
|
if type == 'spot':
|
|
assets = self.safe_list(account, 'assets', [])
|
|
for j in range(0, len(assets)):
|
|
asset = assets[j]
|
|
codeId = self.safe_string(asset, 'symbol')
|
|
code = self.safe_currency_code(codeId)
|
|
balance = self.safe_dict(result, code, self.account())
|
|
balance['total'] = Precise.string_add(balance['total'], self.safe_string(asset, 'balance'))
|
|
balance['used'] = Precise.string_add(balance['used'], self.safe_string(asset, 'locked_balance'))
|
|
result[code] = balance
|
|
else:
|
|
perpBalance = self.safe_dict(result, 'USDC', self.account())
|
|
perpUSDCTotal = self.safe_string(account, 'collateral')
|
|
perpUSDCFree = self.safe_string(account, 'available_balance')
|
|
perpBalance['total'] = Precise.string_add(perpBalance['total'], perpUSDCTotal)
|
|
perpBalance['free'] = Precise.string_add(perpBalance['free'], perpUSDCFree)
|
|
result['USDC'] = perpBalance
|
|
return self.safe_balance(result)
|
|
|
|
def fetch_position(self, symbol: str, params={}):
|
|
"""
|
|
fetch data on an open position
|
|
|
|
https://apidocs.lighter.xyz/reference/account-1
|
|
|
|
:param str symbol: unified market symbol of the market the position is held in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.by]: fetch balance by 'index' or 'l1_address', defaults to 'index'
|
|
:param str [params.value]: fetch balance value, account index or l1 address
|
|
:returns dict: a `position structure <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
positions = self.fetch_positions([symbol], params)
|
|
return self.safe_dict(positions, 0, {})
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://apidocs.lighter.xyz/reference/account-1
|
|
|
|
:param str[] [symbols]: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.by]: fetch balance by 'index' or 'l1_address', defaults to 'index'
|
|
:param str [params.value]: fetch balance value, account index or l1 address
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchPositions', 'accountIndex', 'account_index')
|
|
request: dict = {
|
|
'by': self.safe_string(params, 'by', 'index'),
|
|
'value': accountIndex,
|
|
}
|
|
response = self.publicGetAccount(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "total": 2,
|
|
# "accounts": [
|
|
# {
|
|
# "code": 0,
|
|
# "account_type": 0,
|
|
# "index": 1077,
|
|
# "l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "cancel_all_time": 0,
|
|
# "total_order_count": 0,
|
|
# "total_isolated_order_count": 0,
|
|
# "pending_order_count": 0,
|
|
# "available_balance": "12582.743947",
|
|
# "status": 1,
|
|
# "collateral": "9100.242706",
|
|
# "account_index": 1077,
|
|
# "name": "",
|
|
# "description": "",
|
|
# "can_invite": True,
|
|
# "referral_points_percentage": "",
|
|
# "positions": [
|
|
# {
|
|
# "market_id": 0,
|
|
# "symbol": "ETH",
|
|
# "initial_margin_fraction": "5.00",
|
|
# "open_order_count": 0,
|
|
# "pending_order_count": 0,
|
|
# "position_tied_order_count": 0,
|
|
# "sign": 1,
|
|
# "position": "18.0193",
|
|
# "avg_entry_price": "2669.84",
|
|
# "position_value": "54306.566340",
|
|
# "unrealized_pnl": "6197.829558",
|
|
# "realized_pnl": "0.000000",
|
|
# "liquidation_price": "2191.1107231380406",
|
|
# "margin_mode": 0,
|
|
# "allocated_margin": "0.000000"
|
|
# }
|
|
# ],
|
|
# "assets": [],
|
|
# "total_asset_value": "15298.072264000002",
|
|
# "cross_asset_value": "15298.072264000002",
|
|
# "shares": []
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
allPositions = []
|
|
accounts = self.safe_list(response, 'accounts', [])
|
|
for i in range(0, len(accounts)):
|
|
account = accounts[i]
|
|
positions = self.safe_list(account, 'positions', [])
|
|
for j in range(0, len(positions)):
|
|
allPositions.append(positions[j])
|
|
return self.parse_positions(allPositions, symbols)
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# {
|
|
# "market_id": 0,
|
|
# "symbol": "ETH",
|
|
# "initial_margin_fraction": "5.00",
|
|
# "open_order_count": 0,
|
|
# "pending_order_count": 0,
|
|
# "position_tied_order_count": 0,
|
|
# "sign": 1,
|
|
# "position": "18.0193",
|
|
# "avg_entry_price": "2669.84",
|
|
# "position_value": "54306.566340",
|
|
# "unrealized_pnl": "6197.829558",
|
|
# "realized_pnl": "0.000000",
|
|
# "liquidation_price": "2191.1107231380406",
|
|
# "margin_mode": 0,
|
|
# "allocated_margin": "0.000000"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 'market_id')
|
|
market = self.safe_market(marketId, market)
|
|
sign = self.safe_integer(position, 'sign')
|
|
side = None
|
|
if sign is not None:
|
|
side = 'long' if (sign == 1) else 'short'
|
|
marginModeId = self.safe_integer(position, 'margin_mode')
|
|
marginMode = None
|
|
if marginModeId is not None:
|
|
marginMode = 'cross' if (marginModeId == 0) else 'isolated'
|
|
imfStr = self.safe_string(position, 'initial_margin_fraction')
|
|
leverage = None
|
|
if imfStr is not None:
|
|
imf = self.parse_to_int(imfStr)
|
|
if imf > 0:
|
|
leverage = 100 / imf
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': market['symbol'],
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'isolated': (marginMode == 'isolated'),
|
|
'hedged': None,
|
|
'side': side,
|
|
'contracts': self.safe_number(position, 'position'),
|
|
'contractSize': None,
|
|
'entryPrice': self.safe_number(position, 'avg_entry_price'),
|
|
'markPrice': None,
|
|
'notional': self.safe_number(position, 'position_value'),
|
|
'leverage': leverage,
|
|
'collateral': self.safe_number(position, 'allocated_margin'),
|
|
'initialMargin': None,
|
|
'maintenanceMargin': None,
|
|
'initialMarginPercentage': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'),
|
|
'liquidationPrice': self.safe_number(position, 'liquidation_price'),
|
|
'marginMode': marginMode,
|
|
'percentage': None,
|
|
})
|
|
|
|
def fetch_accounts(self, params={}) -> List[Account]:
|
|
"""
|
|
fetch all the accounts associated with a profile
|
|
|
|
https://apidocs.lighter.xyz/reference/account-1
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.by]: fetch balance by 'index' or 'l1_address', defaults to 'index'
|
|
:param str [params.value]: fetch balance value, account index or l1 address
|
|
:returns dict: a dictionary of `account structures <https://docs.ccxt.com/?id=accounts-structure>` indexed by the account type
|
|
"""
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchAccounts', 'accountIndex', 'account_index')
|
|
request: dict = {
|
|
'by': self.safe_string(params, 'by', 'index'),
|
|
'value': accountIndex,
|
|
}
|
|
response = self.publicGetAccount(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": "200",
|
|
# "total": "1",
|
|
# "accounts": [
|
|
# {
|
|
# "code": "0",
|
|
# "account_type": "0",
|
|
# "index": "1077",
|
|
# "l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "cancel_all_time": "0",
|
|
# "total_order_count": "1",
|
|
# "total_isolated_order_count": "0",
|
|
# "pending_order_count": "0",
|
|
# "available_balance": "7996.489834",
|
|
# "status": "1",
|
|
# "collateral": "9000.000000",
|
|
# "account_index": "1077",
|
|
# "name": "",
|
|
# "description": "",
|
|
# "can_invite": True,
|
|
# "referral_points_percentage": "",
|
|
# "positions": [],
|
|
# "assets": [],
|
|
# "total_asset_value": "9536.789088",
|
|
# "cross_asset_value": "9536.789088",
|
|
# "shares": []
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
accounts = self.safe_list(response, 'accounts', [])
|
|
return self.parse_accounts(accounts, params)
|
|
|
|
def parse_account(self, account):
|
|
#
|
|
# {
|
|
# "code": "0",
|
|
# "account_type": "0",
|
|
# "index": "1077",
|
|
# "l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "cancel_all_time": "0",
|
|
# "total_order_count": "1",
|
|
# "total_isolated_order_count": "0",
|
|
# "pending_order_count": "0",
|
|
# "available_balance": "7996.489834",
|
|
# "status": "1",
|
|
# "collateral": "9000.000000",
|
|
# "account_index": "1077",
|
|
# "name": "",
|
|
# "description": "",
|
|
# "can_invite": True,
|
|
# "referral_points_percentage": "",
|
|
# "positions": [],
|
|
# "assets": [],
|
|
# "total_asset_value": "9536.789088",
|
|
# "cross_asset_value": "9536.789088",
|
|
# "shares": []
|
|
# }
|
|
#
|
|
accountType = self.safe_string(account, 'account_type')
|
|
return {
|
|
'id': self.safe_string(account, 'account_index'),
|
|
'type': 'main' if (accountType == '0') else 'subaccount',
|
|
'code': None,
|
|
'info': account,
|
|
}
|
|
|
|
def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://apidocs.lighter.xyz/reference/accountactiveorders
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument')
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchOpenOrders', 'accountIndex', 'account_index')
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchOpenOrders', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market_id': market['id'],
|
|
'account_index': accountIndex,
|
|
}
|
|
response = self.privateGetAccountActiveOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "orders": [
|
|
# {
|
|
# "order_index": 281474977354074,
|
|
# "client_order_index": 0,
|
|
# "order_id": "281474977354074",
|
|
# "client_order_id": "0",
|
|
# "market_index": 0,
|
|
# "owner_account_index": 1077,
|
|
# "initial_base_amount": "36.0386",
|
|
# "price": "2221.60",
|
|
# "nonce": 643418,
|
|
# "remaining_base_amount": "0.0000",
|
|
# "is_ask": True,
|
|
# "base_size": 0,
|
|
# "base_price": 222160,
|
|
# "filled_base_amount": "0.0000",
|
|
# "filled_quote_amount": "0.000000",
|
|
# "side": "",
|
|
# "type": "market",
|
|
# "time_in_force": "immediate-or-cancel",
|
|
# "reduce_only": False,
|
|
# "trigger_price": "0.00",
|
|
# "order_expiry": 0,
|
|
# "status": "canceled-margin-not-allowed",
|
|
# "trigger_status": "na",
|
|
# "trigger_time": 0,
|
|
# "parent_order_index": 0,
|
|
# "parent_order_id": "0",
|
|
# "to_trigger_order_id_0": "0",
|
|
# "to_trigger_order_id_1": "0",
|
|
# "to_cancel_order_id_0": "0",
|
|
# "block_height": 102202,
|
|
# "timestamp": 1766387932,
|
|
# "created_at": 1766387932,
|
|
# "updated_at": 1766387932
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'orders', [])
|
|
return self.parse_orders(data, market, since, limit)
|
|
|
|
def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently closed orders
|
|
|
|
https://apidocs.lighter.xyz/reference/accountinactiveorders
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument')
|
|
self.load_markets()
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchClosedOrders', 'accountIndex', 'account_index')
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchClosedOrders', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market_id': market['id'],
|
|
'account_index': accountIndex,
|
|
'limit': 100, # required, max 100
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 100)
|
|
response = self.privateGetAccountInactiveOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "orders": [
|
|
# {
|
|
# "order_index": 281474977354074,
|
|
# "client_order_index": 0,
|
|
# "order_id": "281474977354074",
|
|
# "client_order_id": "0",
|
|
# "market_index": 0,
|
|
# "owner_account_index": 1077,
|
|
# "initial_base_amount": "36.0386",
|
|
# "price": "2221.60",
|
|
# "nonce": 643418,
|
|
# "remaining_base_amount": "0.0000",
|
|
# "is_ask": True,
|
|
# "base_size": 0,
|
|
# "base_price": 222160,
|
|
# "filled_base_amount": "0.0000",
|
|
# "filled_quote_amount": "0.000000",
|
|
# "side": "",
|
|
# "type": "market",
|
|
# "time_in_force": "immediate-or-cancel",
|
|
# "reduce_only": False,
|
|
# "trigger_price": "0.00",
|
|
# "order_expiry": 0,
|
|
# "status": "canceled-margin-not-allowed",
|
|
# "trigger_status": "na",
|
|
# "trigger_time": 0,
|
|
# "parent_order_index": 0,
|
|
# "parent_order_id": "0",
|
|
# "to_trigger_order_id_0": "0",
|
|
# "to_trigger_order_id_1": "0",
|
|
# "to_cancel_order_id_0": "0",
|
|
# "block_height": 102202,
|
|
# "timestamp": 1766387932,
|
|
# "created_at": 1766387932,
|
|
# "updated_at": 1766387932
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'orders', [])
|
|
return self.parse_orders(data, market, since, limit)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# {
|
|
# "order_index": 281474977354074,
|
|
# "client_order_index": 0,
|
|
# "order_id": "281474977354074",
|
|
# "client_order_id": "0",
|
|
# "market_index": 0,
|
|
# "owner_account_index": 1077,
|
|
# "initial_base_amount": "36.0386",
|
|
# "price": "2221.60",
|
|
# "nonce": 643418,
|
|
# "remaining_base_amount": "0.0000",
|
|
# "is_ask": True,
|
|
# "base_size": 0,
|
|
# "base_price": 222160,
|
|
# "filled_base_amount": "0.0000",
|
|
# "filled_quote_amount": "0.000000",
|
|
# "side": "",
|
|
# "type": "market",
|
|
# "time_in_force": "immediate-or-cancel",
|
|
# "reduce_only": False,
|
|
# "trigger_price": "0.00",
|
|
# "order_expiry": 0,
|
|
# "status": "canceled-margin-not-allowed",
|
|
# "trigger_status": "na",
|
|
# "trigger_time": 0,
|
|
# "parent_order_index": 0,
|
|
# "parent_order_id": "0",
|
|
# "to_trigger_order_id_0": "0",
|
|
# "to_trigger_order_id_1": "0",
|
|
# "to_cancel_order_id_0": "0",
|
|
# "block_height": 102202,
|
|
# "timestamp": 1766387932,
|
|
# "created_at": 1766387932,
|
|
# "updated_at": 1766387932
|
|
# }
|
|
#
|
|
marketId = self.safe_string(order, 'market_index')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_timestamp(order, 'timestamp')
|
|
isAsk = self.safe_bool(order, 'is_ask')
|
|
if isAsk is None:
|
|
isAskAsInteger = self.safe_integer(order, 'is_ask')
|
|
if isAskAsInteger is not None:
|
|
isAsk = isAskAsInteger == 1
|
|
side = None
|
|
if isAsk is not None:
|
|
side = 'sell' if isAsk else 'buy'
|
|
type = self.safe_string(order, 'type')
|
|
if type is None:
|
|
typeAsInteger = self.safe_integer(order, 'order_type')
|
|
type = self.parse_order_type_integer(typeAsInteger)
|
|
triggerPrice = self.parse_number(self.omit_zero(self.safe_string(order, 'trigger_price')))
|
|
stopLossPrice = None
|
|
takeProfitPrice = None
|
|
if type is not None:
|
|
if type.find('stop-loss') >= 0:
|
|
stopLossPrice = triggerPrice
|
|
if type.find('take-profit') >= 0:
|
|
takeProfitPrice = triggerPrice
|
|
# Try to parse to integer first, because parsing an integer to a string wouldn't result in None
|
|
tif = None
|
|
tifAsInteger = self.safe_integer(order, 'time_in_force')
|
|
if tifAsInteger is not None:
|
|
tif = self.parse_order_time_in_force_integer(tifAsInteger)
|
|
else:
|
|
tif = self.safe_string(order, 'time_in_force')
|
|
reduceOnly = self.safe_bool(order, 'reduce_only')
|
|
if reduceOnly is None:
|
|
reduceOnlyAsInteger = self.safe_integer(order, 'reduce_only')
|
|
if reduceOnlyAsInteger is not None:
|
|
reduceOnly = reduceOnlyAsInteger == 1
|
|
status = self.safe_string(order, 'status')
|
|
return self.safe_order({
|
|
'info': order,
|
|
'id': self.safe_string(order, 'order_id'),
|
|
'clientOrderId': self.omit_zero(self.safe_string_2(order, 'client_order_id', 'client_order_index')),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'lastUpdateTimestamp': self.safe_timestamp(order, 'updated_at'),
|
|
'symbol': market['symbol'],
|
|
'type': self.parse_order_type(type),
|
|
'timeInForce': self.parse_order_time_in_force(tif),
|
|
'postOnly': tif == 'post-only',
|
|
'reduceOnly': reduceOnly,
|
|
'side': side,
|
|
'price': self.safe_string(order, 'price'),
|
|
'triggerPrice': triggerPrice,
|
|
'stopLossPrice': stopLossPrice,
|
|
'takeProfitPrice': takeProfitPrice,
|
|
'amount': self.safe_string(order, 'initial_base_amount'),
|
|
'cost': self.safe_string(order, 'filled_quote_amount'),
|
|
'average': None,
|
|
'filled': self.safe_string(order, 'filled_base_amount'),
|
|
'remaining': self.safe_string(order, 'remaining_base_amount'),
|
|
'status': self.parse_order_status(status),
|
|
'fee': None,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'in-progress': 'open',
|
|
'pending': 'open',
|
|
'open': 'open',
|
|
'filled': 'closed',
|
|
'canceled': 'canceled',
|
|
'canceled-post-only': 'canceled',
|
|
'canceled-reduce-only': 'canceled',
|
|
'canceled-position-not-allowed': 'rejected',
|
|
'canceled-margin-not-allowed': 'rejected',
|
|
'canceled-too-much-slippage': 'canceled',
|
|
'canceled-not-enough-liquidity': 'canceled',
|
|
'canceled-self-trade': 'canceled',
|
|
'canceled-expired': 'expired',
|
|
'canceled-oco': 'canceled',
|
|
'canceled-child': 'canceled',
|
|
'canceled-liquidation': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order_type(self, type):
|
|
types: dict = {
|
|
'limit': 'limit',
|
|
'market': 'market',
|
|
'stop-loss': 'market',
|
|
'stop-loss-limit': 'limit',
|
|
'take-profit': 'market',
|
|
'take-profit-limit': 'limit',
|
|
'twap': 'twap',
|
|
'twap-sub': 'twap',
|
|
'liquidation': 'market',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_order_type_integer(self, typeInteger):
|
|
if typeInteger is None:
|
|
return None
|
|
types: dict = {
|
|
'0': 'limit',
|
|
'1': 'market',
|
|
'2': 'stop-loss',
|
|
'3': 'stop-loss-limit',
|
|
'4': 'take-profit',
|
|
'5': 'take-profit-limit',
|
|
'6': 'twap',
|
|
'7': 'twap-sub',
|
|
'8': 'liquidation',
|
|
}
|
|
return self.safe_string(types, str(typeInteger))
|
|
|
|
def parse_order_time_in_force(self, tif):
|
|
timeInForces: dict = {
|
|
'immediate-or-cancel': 'IOC',
|
|
'good-till-time': 'GTC',
|
|
'post-only': 'PO',
|
|
'Unknown': None,
|
|
}
|
|
return self.safe_string(timeInForces, tif, tif)
|
|
|
|
def parse_order_time_in_force_integer(self, tifInteger):
|
|
timeInForces: dict = {
|
|
'0': 'immediate-or-cancel',
|
|
'1': 'good-till-time',
|
|
'2': 'post-only',
|
|
}
|
|
return self.safe_string(timeInForces, str(tifInteger))
|
|
|
|
def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
transfer currency internally between wallets on the same account
|
|
:param str code: unified currency code
|
|
:param float amount: amount to transfer
|
|
:param str fromAccount: account to transfer from(spot, perp)
|
|
:param str toAccount: account to transfer to(spot, perp)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.toAccountIndex]: to account index, defaults to fromAccountIndex
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:param str [params.memo]: hex encoding memo
|
|
:returns dict: a `transfer structure <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'transfer', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'transfer', 'accountIndex', 'account_index')
|
|
toAccountIndex = None
|
|
toAccountIndex, params = self.handle_option_and_params_2(params, 'transfer', 'toAccountIndex', 'to_account_index', accountIndex)
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
currency = self.currency(code)
|
|
if currency['code'] == 'USDC':
|
|
amount = self.parse_to_int(Precise.string_mul(self.pow('10', '6'), self.currency_to_precision(code, amount)))
|
|
elif currency['code'] == 'ETH':
|
|
amount = self.parse_to_int(Precise.string_mul(self.pow('10', '8'), self.currency_to_precision(code, amount)))
|
|
else:
|
|
raise ExchangeError(self.id + ' transfer() only supports USDC and ETH transfers')
|
|
fromRouteType = 0 if (fromAccount == 'perp') else 1 # 0: perp, 1: spot
|
|
toRouteType = 0 if (toAccount == 'perp') else 1
|
|
memo = self.safe_string(params, 'memo', '0x000000000000000000000000000000')
|
|
params = self.omit(params, ['memo'])
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'to_account_index': toAccountIndex,
|
|
'asset_index': self.parse_to_int(currency['id']),
|
|
'from_route_type': fromRouteType,
|
|
'to_route_type': toRouteType,
|
|
'amount': amount,
|
|
'usdc_fee': 0,
|
|
'memo': memo,
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_transfer(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_transfer(response)
|
|
|
|
def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]:
|
|
"""
|
|
fetch a history of internal transfers made on an account
|
|
|
|
https://apidocs.lighter.xyz/reference/transfer_history
|
|
|
|
:param str code: unified currency code of the currency transferred
|
|
:param int [since]: the earliest time in ms to fetch transfers for
|
|
:param int [limit]: the maximum number of transfers structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict[]: a list of `transfer structures <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_cursor('fetchTransfers', code, since, limit, params, 'cursor', 'cursor', None, 50)
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchTransfers', 'accountIndex', 'account_index')
|
|
request: dict = {
|
|
'account_index': accountIndex,
|
|
}
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchTransfers', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.privateGetTransferHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "transfers": [
|
|
# {
|
|
# "id": "3085014",
|
|
# "asset_id": 3,
|
|
# "amount": "11.000000",
|
|
# "fee": "0.000000",
|
|
# "timestamp": 1766387292752,
|
|
# "type": "L2TransferOutflow",
|
|
# "from_l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "to_l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "from_account_index": 1077,
|
|
# "to_account_index": 281474976710608,
|
|
# "from_route": "spot",
|
|
# "to_route": "spot",
|
|
# "tx_hash": "d8e96178273d0938f9ede556edffc0aab8def9ec70c46a65791905291a2f5792af18625406102c80"
|
|
# }
|
|
# ],
|
|
# "cursor": "eyJpbmRleCI6MzA4NDkxNX0="
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'transfers', [])
|
|
cursor = self.safe_string(response, 'cursor')
|
|
first = self.safe_dict(rows, 0)
|
|
if (first is not None) and (cursor is not None):
|
|
rows[0]['cursor'] = cursor
|
|
return self.parse_transfers(rows, currency, since, limit, params)
|
|
|
|
def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
|
|
#
|
|
# {
|
|
# "id": "3085014",
|
|
# "asset_id": 3,
|
|
# "amount": "11.000000",
|
|
# "fee": "0.000000",
|
|
# "timestamp": 1766387292752,
|
|
# "type": "L2TransferOutflow",
|
|
# "from_l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "to_l1_address": "0x15f43D1f2DeE81424aFd891943262aa90F22cc2A",
|
|
# "from_account_index": 1077,
|
|
# "to_account_index": 281474976710608,
|
|
# "from_route": "spot",
|
|
# "to_route": "spot",
|
|
# "tx_hash": "d8e96178273d0938f9ede556edffc0aab8def9ec70c46a65791905291a2f5792af18625406102c80"
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(transfer, 'asset_id')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
timestamp = self.safe_integer(transfer, 'timestamp')
|
|
fromAccount = self.safe_dict(transfer, 'from', {})
|
|
toAccount = self.safe_dict(transfer, 'to', {})
|
|
return {
|
|
'id': self.safe_string(transfer, 'id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'currency': code,
|
|
'amount': self.safe_number(transfer, 'amount'),
|
|
'fromAccount': self.safe_string(fromAccount, 'from_account_index'),
|
|
'toAccount': self.safe_string(toAccount, 'to_account_index'),
|
|
'status': None,
|
|
'info': transfer,
|
|
}
|
|
|
|
def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all deposits made to an account
|
|
|
|
https://apidocs.lighter.xyz/reference/deposit_history
|
|
|
|
:param str [code]: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch deposits for
|
|
:param int [limit]: the maximum number of deposits structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.address]: l1_address
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'cursor', 'cursor', None, 50)
|
|
address = None
|
|
address, params = self.handle_option_and_params_2(params, 'fetchDeposits', 'address', 'l1_address')
|
|
if address is None:
|
|
raise ArgumentsRequired(self.id + ' fetchDeposits() requires an address parameter')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchDeposits', 'accountIndex', 'account_index')
|
|
request: dict = {
|
|
'account_index': accountIndex,
|
|
'l1_address': address,
|
|
}
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchDeposits', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['coin'] = currency['id']
|
|
response = self.privateGetDepositHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "deposits": [
|
|
# {
|
|
# "id": "2901843",
|
|
# "asset_id": 5,
|
|
# "amount": "100000.0",
|
|
# "timestamp": 1766112729741,
|
|
# "status": "completed",
|
|
# "l1_tx_hash": "0xa24d83d58e1fd72b2a44a12d1ec766fb061fa0b806de2fed940b5d8ecd50744d"
|
|
# }
|
|
# ],
|
|
# "cursor": "eyJpbmRleCI6MjkwMTg0MH0="
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'deposits', [])
|
|
cursor = self.safe_string(response, 'cursor')
|
|
first = self.safe_dict(data, 0)
|
|
if (first is not None) and (cursor is not None):
|
|
data[0]['cursor'] = cursor
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all withdrawals made from an account
|
|
|
|
https://apidocs.lighter.xyz/reference/withdraw_history
|
|
|
|
:param str [code]: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch withdrawals for
|
|
:param int [limit]: the maximum number of withdrawals structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'cursor', 'cursor', None, 50)
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchWithdrawals', 'accountIndex', 'account_index')
|
|
self.load_markets()
|
|
request: dict = {
|
|
'account_index': accountIndex,
|
|
}
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchWithdrawals', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['coin'] = currency['id']
|
|
response = self.privateGetWithdrawHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": "200",
|
|
# "message": "string",
|
|
# "withdraws": [
|
|
# {
|
|
# "id": "string",
|
|
# "amount": "0.1",
|
|
# "timestamp": "1640995200",
|
|
# "status": "failed",
|
|
# "type": "secure",
|
|
# "l1_tx_hash": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
|
# }
|
|
# ],
|
|
# "cursor": "string"
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'withdraws', [])
|
|
cursor = self.safe_string(response, 'cursor')
|
|
first = self.safe_dict(data, 0)
|
|
if (first is not None) and (cursor is not None):
|
|
data[0]['cursor'] = cursor
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# fetchDeposits
|
|
# {
|
|
# "id": "2901843",
|
|
# "asset_id": 5,
|
|
# "amount": "100000.0",
|
|
# "timestamp": 1766112729741,
|
|
# "status": "completed",
|
|
# "l1_tx_hash": "0xa24d83d58e1fd72b2a44a12d1ec766fb061fa0b806de2fed940b5d8ecd50744d",
|
|
# }
|
|
#
|
|
# fetchWithdrawals
|
|
# {
|
|
# "id": "string",
|
|
# "amount": "0.1",
|
|
# "timestamp": "1640995200",
|
|
# "status": "failed",
|
|
# "type": "secure",
|
|
# "l1_tx_hash": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
|
# }
|
|
#
|
|
type = self.safe_string(transaction, 'type')
|
|
if type is None:
|
|
type = 'deposit'
|
|
else:
|
|
type = 'withdrawal'
|
|
timestamp = self.safe_integer(transaction, 'timestamp')
|
|
status = self.safe_string(transaction, 'status')
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string(transaction, 'id'),
|
|
'txid': self.safe_string(transaction, 'l1_tx_hash'),
|
|
'type': type,
|
|
'currency': self.safe_currency_code(self.safe_string(transaction, 'asset_id'), currency),
|
|
'network': None,
|
|
'amount': self.safe_number(transaction, 'amount'),
|
|
'status': self.parse_transaction_status(status),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'address': None,
|
|
'addressFrom': None,
|
|
'addressTo': None,
|
|
'tag': None,
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': None,
|
|
'comment': None,
|
|
'fee': None,
|
|
'internal': None,
|
|
}
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
'failed': 'failed',
|
|
'pending': 'pending',
|
|
'completed': 'ok',
|
|
'claimable': 'ok',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str [tag]:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:param int [params.routeType]: wallet type, 0: perp, 1: spot, default is 0
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'withdraw', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'withdraw', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
currency = self.currency(code)
|
|
if currency['code'] == 'USDC':
|
|
amount = self.parse_to_int(Precise.string_mul(self.pow('10', '6'), self.currency_to_precision(code, amount)))
|
|
elif currency['code'] == 'ETH':
|
|
amount = self.parse_to_int(Precise.string_mul(self.pow('10', '8'), self.currency_to_precision(code, amount)))
|
|
else:
|
|
raise ExchangeError(self.id + ' withdraw() only supports USDC and ETH transfers')
|
|
routeType = self.safe_integer(params, 'routeType', 0) # 0: perp, 1: spot
|
|
params = self.omit(params, 'routeType')
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'asset_index': self.parse_to_int(currency['id']),
|
|
'route_type': routeType,
|
|
'amount': amount,
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_withdraw(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_transaction(response)
|
|
|
|
def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://apidocs.lighter.xyz/reference/trades
|
|
|
|
:param str [symbol]: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:param int [params.until]: timestamp in ms of the latest trade to fetch
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'next_cursor', 'cursor', None, 50)
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'fetchMyTrades', 'accountIndex', 'account_index')
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'fetchMyTrades', 'apiKeyIndex', 'api_key_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
request: dict = {
|
|
'sort_by': 'timestamp',
|
|
'limit': 100,
|
|
'account_index': accountIndex,
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 100)
|
|
until = None
|
|
until, params = self.handle_option_and_params_2(params, 'fetchMyTrades', 'until', 'from')
|
|
if until is not None:
|
|
request['from'] = until
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['market_id'] = market['id']
|
|
response = self.privateGetTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": 200,
|
|
# "trades": [
|
|
# {
|
|
# "trade_id": 17609,
|
|
# "tx_hash": "99ffeaa3899fbaa51043840ddf762fd18c182a33b5125092105bee57af11fab04edf5fd90e969abd",
|
|
# "type": "trade",
|
|
# "market_id": 0,
|
|
# "size": "10.2304",
|
|
# "price": "2958.75",
|
|
# "usd_amount": "30269.196000",
|
|
# "ask_id": 281474977339869,
|
|
# "bid_id": 562949952870533,
|
|
# "ask_client_id": 0,
|
|
# "bid_client_id": 0,
|
|
# "ask_account_id": 20,
|
|
# "bid_account_id": 1077,
|
|
# "is_maker_ask": True,
|
|
# "block_height": 102070,
|
|
# "timestamp": 1766386112741,
|
|
# "taker_position_size_before": "0.0000",
|
|
# "taker_entry_quote_before": "0.000000",
|
|
# "taker_position_sign_changed": True,
|
|
# "maker_position_size_before": "-1856.8547",
|
|
# "maker_entry_quote_before": "5491685.069325",
|
|
# "maker_initial_margin_fraction_before": 500
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'trades', [])
|
|
for i in range(0, len(data)):
|
|
data[i]['account_index'] = accountIndex
|
|
nextCursor = self.safe_string(response, 'next_cursor')
|
|
first = self.safe_dict(data, 0)
|
|
if (first is not None) and (nextCursor is not None):
|
|
data[0]['next_cursor'] = nextCursor
|
|
return self.parse_trades(data, market, since, limit, params)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# {
|
|
# "trade_id": 17609,
|
|
# "tx_hash": "99ffeaa3899fbaa51043840ddf762fd18c182a33b5125092105bee57af11fab04edf5fd90e969abd",
|
|
# "type": "trade",
|
|
# "market_id": 0,
|
|
# "size": "10.2304",
|
|
# "price": "2958.75",
|
|
# "usd_amount": "30269.196000",
|
|
# "ask_id": 281474977339869,
|
|
# "bid_id": 562949952870533,
|
|
# "ask_client_id": 0,
|
|
# "bid_client_id": 0,
|
|
# "ask_account_id": 20,
|
|
# "bid_account_id": 1077,
|
|
# "is_maker_ask": True,
|
|
# "block_height": 102070,
|
|
# "timestamp": 1766386112741,
|
|
# "taker_position_size_before": "0.0000",
|
|
# "taker_entry_quote_before": "0.000000",
|
|
# "taker_position_sign_changed": True,
|
|
# "maker_position_size_before": "-1856.8547",
|
|
# "maker_entry_quote_before": "5491685.069325",
|
|
# "maker_initial_margin_fraction_before": 500
|
|
# }
|
|
#
|
|
marketId = self.safe_string(trade, 'market_id')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer(trade, 'timestamp')
|
|
accountIndex = self.safe_string(trade, 'account_index')
|
|
askAccountId = self.safe_string(trade, 'ask_account_id')
|
|
bidAccountId = self.safe_string(trade, 'bid_account_id')
|
|
isMakerAsk = self.safe_bool(trade, 'is_maker_ask')
|
|
side = None
|
|
orderId = None
|
|
if accountIndex is not None:
|
|
if accountIndex == askAccountId:
|
|
side = 'sell'
|
|
orderId = self.safe_string(trade, 'ask_id')
|
|
elif accountIndex == bidAccountId:
|
|
side = 'buy'
|
|
orderId = self.safe_string(trade, 'bid_id')
|
|
takerOrMaker = None
|
|
if side is not None and isMakerAsk is not None:
|
|
isMaker = isMakerAsk if (side == 'sell') else not isMakerAsk
|
|
takerOrMaker = 'maker' if isMaker else 'taker'
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'id': self.safe_string(trade, 'trade_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'order': orderId,
|
|
'type': self.safe_string(trade, 'type'),
|
|
'side': side,
|
|
'takerOrMaker': takerOrMaker,
|
|
'price': self.safe_string(trade, 'price'),
|
|
'amount': self.safe_string(trade, 'size'),
|
|
'cost': self.safe_string(trade, 'usd_amount'),
|
|
'fee': None,
|
|
}, market)
|
|
|
|
def set_leverage(self, leverage: int, symbol: Str = None, params={}):
|
|
"""
|
|
set the level of leverage for a market
|
|
:param float leverage: the rate of leverage
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:param str [params.marginMode]: margin mode, 'cross' or 'isolated'
|
|
:returns dict: response from the exchange
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument')
|
|
marginMode = None
|
|
marginMode, params = self.handle_option_and_params_2(params, 'setLeverage', 'marginMode', 'margin_mode')
|
|
if marginMode is None:
|
|
raise ArgumentsRequired(self.id + ' setLeverage() requires an marginMode parameter')
|
|
return self.modify_leverage_and_margin_mode(leverage, marginMode, symbol, params)
|
|
|
|
def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}):
|
|
"""
|
|
set margin mode to 'cross' or 'isolated'
|
|
:param str marginMode: 'cross' or 'isolated'
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:param int [params.leverage]: required leverage
|
|
:returns dict: response from the exchange
|
|
"""
|
|
if marginMode is None:
|
|
raise ArgumentsRequired(self.id + ' setMarginMode() requires an marginMode parameter')
|
|
leverage = None
|
|
leverage, params = self.handle_option_and_params(params, 'setMarginMode', 'leverage', 'leverage')
|
|
if leverage is None:
|
|
raise ArgumentsRequired(self.id + ' setMarginMode() requires an leverage parameter')
|
|
return self.modify_leverage_and_margin_mode(leverage, marginMode, symbol, params)
|
|
|
|
def modify_leverage_and_margin_mode(self, leverage: int, marginMode: str, symbol: Str = None, params={}):
|
|
self.load_markets()
|
|
if (marginMode != 'cross') and (marginMode != 'isolated'):
|
|
raise BadRequest(self.id + ' modifyLeverageAndMarginMode() requires a marginMode parameter that must be either cross or isolated')
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'modifyLeverageAndMarginMode', 'apiKeyIndex', 'api_key_index')
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' modifyLeverageAndMarginMode() requires a symbol argument')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'modifyLeverageAndMarginMode', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
market = self.market(symbol)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'market_index': self.parse_to_int(market['id']),
|
|
'initial_margin_fraction': self.parse_to_int(10000 / leverage),
|
|
'margin_mode': 0 if (marginMode == 'cross') else 1, # 0: CROSS, 1: ISOLATED
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_update_leverage(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
return self.publicPostSendTx(request)
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'cancelOrder', 'apiKeyIndex', 'api_key_index')
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
|
|
market = self.market(symbol)
|
|
clientOrderId = self.safe_string_2(params, 'client_order_index', 'clientOrderId')
|
|
params = self.omit(params, ['client_order_index', 'clientOrderId'])
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'cancelOrder', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'market_index': self.parse_to_int(market['id']),
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
if clientOrderId is not None:
|
|
signRaw['order_index'] = self.parse_to_int(clientOrderId)
|
|
elif id is not None:
|
|
signRaw['order_index'] = self.parse_to_int(id)
|
|
else:
|
|
raise ArgumentsRequired(self.id + ' cancelOrder requires order id or client order id')
|
|
txType, txInfo = self.lighter_sign_cancel_order(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_order(response, market)
|
|
|
|
def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders
|
|
:param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'cancelAllOrders', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'cancelAllOrders', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'time_in_force': 0, # 0: IMMEDIATE 1: SCHEDULED 2: ABORT
|
|
'time': 0, # if time_in_force is not IMMEDIATE, set the timestamp_ms here
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_cancel_all_orders(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_orders([response])
|
|
|
|
def cancel_all_orders_after(self, timeout: Int, params={}):
|
|
"""
|
|
dead man's switch, cancel all orders after the given timeout
|
|
:param number timeout: time in milliseconds, 0 represents cancel the timer
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: the api result
|
|
"""
|
|
self.load_markets()
|
|
if (timeout < 300000) or (timeout > 1296000000):
|
|
raise BadRequest(self.id + ' timeout should be between 5 minutes and 15 days.')
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'cancelOrder', 'apiKeyIndex', 'api_key_index')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'cancelAllOrdersAfter', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'time_in_force': 1, # 0: IMMEDIATE 1: SCHEDULED 2: ABORT
|
|
'time': self.milliseconds() + timeout, # if time_in_force is not IMMEDIATE, set the timestamp_ms here
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_cancel_all_orders(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return response
|
|
|
|
def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
|
|
"""
|
|
add margin
|
|
:param str symbol: unified market symbol
|
|
:param float amount: amount of margin to add
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `margin structure <https://docs.ccxt.com/?id=add-margin-structure>`
|
|
"""
|
|
request: dict = {
|
|
'direction': 1,
|
|
}
|
|
return self.set_margin(symbol, amount, self.extend(request, params))
|
|
|
|
def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
|
|
"""
|
|
remove margin from a position
|
|
:param str symbol: unified market symbol
|
|
:param float amount: the amount of margin to remove
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `margin structure <https://docs.ccxt.com/?id=reduce-margin-structure>`
|
|
"""
|
|
request: dict = {
|
|
'direction': 0,
|
|
}
|
|
return self.set_margin(symbol, amount, self.extend(request, params))
|
|
|
|
def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
|
|
"""
|
|
Either adds or reduces margin in an isolated position in order to set the margin to a specific value
|
|
:param str symbol: unified market symbol of the market to set margin in
|
|
:param float amount: the amount to set the margin to
|
|
:param dict [params]: parameters specific to the bingx api endpoint
|
|
:param str [params.accountIndex]: account index
|
|
:param str [params.apiKeyIndex]: api key index
|
|
:returns dict: A `margin structure <https://docs.ccxt.com/?id=add-margin-structure>`
|
|
"""
|
|
self.load_markets()
|
|
apiKeyIndex = None
|
|
apiKeyIndex, params = self.handle_api_key_index(params, 'setMargin', 'apiKeyIndex', 'api_key_index')
|
|
direction = self.safe_integer(params, 'direction') # 1 increase margin 0 decrease margin
|
|
if direction is None:
|
|
raise ArgumentsRequired(self.id + ' setMargin() requires a direction parameter either 1(increase margin) or 0(decrease margin)')
|
|
if not self.in_array(direction, [0, 1]):
|
|
raise ArgumentsRequired(self.id + ' setMargin() requires a direction parameter either 1(increase margin) or 0(decrease margin)')
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' setMargin() requires a symbol argument')
|
|
accountIndex = None
|
|
accountIndex, params = self.handle_account_index(params, 'setMargin', 'accountIndex', 'account_index')
|
|
strAccountIndex = self.number_to_string(accountIndex)
|
|
strApiKeyIndex = self.number_to_string(apiKeyIndex)
|
|
signer = self.load_account(self.options['chainId'], self.get_lighter_private_key(strAccountIndex, strApiKeyIndex), strApiKeyIndex, strAccountIndex, params)
|
|
market = self.market(symbol)
|
|
nonce = self.fetch_nonce(accountIndex, apiKeyIndex, params)
|
|
signRaw: dict = {
|
|
'market_index': self.parse_to_int(market['id']),
|
|
'usdc_amount': self.parse_to_int(Precise.string_mul(self.pow('10', '6'), self.currency_to_precision('USDC', amount))),
|
|
'direction': direction,
|
|
'nonce': nonce,
|
|
'api_key_index': apiKeyIndex,
|
|
'account_index': accountIndex,
|
|
}
|
|
txType, txInfo = self.lighter_sign_update_margin(signer, self.extend(signRaw, params))
|
|
request: dict = {
|
|
'tx_type': txType,
|
|
'tx_info': txInfo,
|
|
}
|
|
response = self.publicPostSendTx(request)
|
|
return self.parse_margin_modification(response, market)
|
|
|
|
def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification:
|
|
timestamp = self.safe_integer(data, 'predicted_execution_time_ms')
|
|
return {
|
|
'info': data,
|
|
'symbol': self.safe_string(market, 'symbol'),
|
|
'type': None,
|
|
'marginMode': None,
|
|
'amount': None,
|
|
'total': None,
|
|
'code': 'USDC',
|
|
'status': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
}
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
url = None
|
|
if api == 'root':
|
|
url = self.implode_hostname(self.urls['api']['public'])
|
|
else:
|
|
url = self.implode_hostname(self.urls['api'][api]) + '/api/' + self.version + '/' + path
|
|
if api == 'private':
|
|
headers = {
|
|
'Authorization': self.create_auth(params),
|
|
}
|
|
if params:
|
|
if method == 'POST':
|
|
headers = {
|
|
'Content-Type': 'multipart/form-data',
|
|
}
|
|
body = params
|
|
else:
|
|
url += '?' + self.rawencode(params)
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if not response:
|
|
return None # fallback to default error handler
|
|
#
|
|
# {
|
|
# "code": "200",
|
|
# "message": "string"
|
|
# }
|
|
#
|
|
code = self.safe_string(response, 'code')
|
|
message = self.safe_string(response, 'msg')
|
|
if code is not None and code != '0' and code != '200':
|
|
feedback = self.id + ' ' + body
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
raise ExchangeError(feedback) # unknown message
|
|
return None
|