Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions lighter/signer_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ctypes
from decimal import Decimal
from fractions import Fraction
from functools import wraps
import inspect
Expand Down Expand Up @@ -264,6 +265,13 @@ def trim_exc(exception_body: str):
return exception_body.strip().split("\n")[-1]


def to_ticks(amount, scale) -> int:
# Scale through the amount's decimal representation: int(2.01 * 1e6) is 2009999,
# because 2.01 * 1e6 == 2009999.9999999998 in binary floating point.
# Precision finer than one tick is still truncated, as before.
return int(Decimal(str(amount)) * Decimal(scale))


def process_api_key_and_nonce(func):
@wraps(func)
async def wrapper(self, *args, **kwargs):
Expand Down Expand Up @@ -938,7 +946,7 @@ async def create_market_order_quote_amount(
nonce: int = DEFAULT_NONCE,
api_key_index: int = DEFAULT_API_KEY_INDEX,
):
quote_amount = int(quote_amount * 1e6)
quote_amount = to_ticks(quote_amount, self.USDC_TICKER_SCALE)
ob_orders = await self.order_api.order_book_orders(market_index, 100)
if ideal_price is None:
logging.debug(
Expand Down Expand Up @@ -1235,7 +1243,7 @@ async def create_sl_limit_order(
@process_api_key_and_nonce
async def withdraw(self, asset_id: int, route_type: int, amount: float, skip_nonce : int = SKIP_NONCE_OFF, nonce: int = DEFAULT_NONCE, api_key_index: int = DEFAULT_API_KEY_INDEX) -> Union[Tuple[Withdraw, RespSendTx, None], Tuple[None, None, str]]:
if asset_id in self.ASSET_TO_TICKER_SCALE:
amount = int(amount * self.ASSET_TO_TICKER_SCALE[asset_id])
amount = to_ticks(amount, self.ASSET_TO_TICKER_SCALE[asset_id])
else:
raise ValueError(f"Unsupported asset id: {asset_id}")

Expand Down Expand Up @@ -1350,7 +1358,7 @@ async def approve_integrator(
@process_api_key_and_nonce
async def transfer(self, eth_private_key: str, to_account_index: int, asset_id: int, route_from: int, route_to: int, amount: float, fee: int, memo: str, skip_nonce : int = SKIP_NONCE_OFF, nonce: int = DEFAULT_NONCE, api_key_index: int = DEFAULT_API_KEY_INDEX):
if asset_id in self.ASSET_TO_TICKER_SCALE:
amount = int(amount * self.ASSET_TO_TICKER_SCALE[asset_id])
amount = to_ticks(amount, self.ASSET_TO_TICKER_SCALE[asset_id])
else:
raise ValueError(f"Unsupported asset id: {asset_id}")

Expand All @@ -1366,7 +1374,7 @@ async def transfer(self, eth_private_key: str, to_account_index: int, asset_id:
@process_api_key_and_nonce
async def transfer_same_master_account(self, to_account_index: int, asset_id: int, route_from: int, route_to: int, amount: float, fee: int, memo: str, skip_nonce : int = SKIP_NONCE_OFF, nonce: int = DEFAULT_NONCE, api_key_index: int = DEFAULT_API_KEY_INDEX):
if asset_id in self.ASSET_TO_TICKER_SCALE:
amount = int(amount * self.ASSET_TO_TICKER_SCALE[asset_id])
amount = to_ticks(amount, self.ASSET_TO_TICKER_SCALE[asset_id])
else:
raise ValueError(f"Unsupported asset id: {asset_id}")

Expand Down Expand Up @@ -1468,7 +1476,7 @@ async def update_leverage(self, market_index, margin_mode, leverage, skip_nonce

@process_api_key_and_nonce
async def update_margin(self, market_index: int, usdc_amount: float, direction: int, skip_nonce : int = SKIP_NONCE_OFF, nonce: int = DEFAULT_NONCE, api_key_index: int = DEFAULT_API_KEY_INDEX):
usdc_amount = int(usdc_amount * self.USDC_TICKER_SCALE)
usdc_amount = to_ticks(usdc_amount, self.USDC_TICKER_SCALE)
tx_type, tx_info, tx_hash, error = self.sign_update_margin(market_index, usdc_amount, direction, skip_nonce, nonce, api_key_index)

if error is not None:
Expand Down
37 changes: 37 additions & 0 deletions test/test_amount_scaling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import types
import unittest

from lighter.signer_client import SignerClient


def _capture_withdraw_amount(asset_id, amount):
captured = {}

def sign_withdraw(asset_index, route_type, amount, skip_nonce, nonce, api_key_index):
captured["amount"] = amount
return None, None, None, "stop"

fake = types.SimpleNamespace(
ASSET_TO_TICKER_SCALE=SignerClient.ASSET_TO_TICKER_SCALE,
sign_withdraw=sign_withdraw,
)
asyncio.run(SignerClient.withdraw(fake, asset_id, SignerClient.ROUTE_PERP, amount, nonce=1, api_key_index=0))
return captured["amount"]


class TestAmountScaling(unittest.TestCase):
def test_withdraw_usdc_amount_is_exact(self):
# 2.01 * 1e6 == 2009999.9999999998 in binary floating point
self.assertEqual(_capture_withdraw_amount(SignerClient.ASSET_ID_USDC, 2.01), 2_010_000)

def test_withdraw_eth_amount_is_exact(self):
# 0.29 * 1e8 == 28999999.999999996 in binary floating point
self.assertEqual(_capture_withdraw_amount(SignerClient.ASSET_ID_ETH, 0.29), 29_000_000)

def test_sub_tick_precision_is_still_truncated(self):
self.assertEqual(_capture_withdraw_amount(SignerClient.ASSET_ID_USDC, 1.0000009), 1_000_000)


if __name__ == "__main__":
unittest.main()