From 810ab988fd6d0dc938ced542f26dab7ba02497cc Mon Sep 17 00:00:00 2001 From: mark Date: Wed, 11 Mar 2026 18:23:41 +0000 Subject: [PATCH 1/4] Withdrawal and transfer to non user account --- tests/perpetual/test_withdrawal_object.py | 29 +++++++++++++ x10/perpetual/transfer_object.py | 2 + x10/perpetual/transfers.py | 48 ++++++++++++++++++++++ x10/perpetual/withdrawal_object.py | 22 ++++++---- x10/perpetual/withdrawals.py | 50 +++++++++++++++++++++++ 5 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 tests/perpetual/test_withdrawal_object.py diff --git a/tests/perpetual/test_withdrawal_object.py b/tests/perpetual/test_withdrawal_object.py new file mode 100644 index 0000000..8e1be4b --- /dev/null +++ b/tests/perpetual/test_withdrawal_object.py @@ -0,0 +1,29 @@ +import datetime + +from hamcrest import assert_that, equal_to +from eth_account import Account +from decimal import Decimal + +from hamcrest import equal_to + +from x10.perpetual.user_client.onboarding import get_l2_keys_from_l1_account +from x10.perpetual.withdrawals import Withdrawal + + +def test_withdrawal_object_generation(): + known_private_key = "50c8e358cc974aaaa6e460641e53f78bdc550fd372984aa78ef8fd27c751e6f4" + + l1_account = Account.from_key(known_private_key) + + payload = Withdrawal( + account_id=12, + target_wallet="0x1234", + amount=Decimal("1"), + expiration=datetime.datetime.fromtimestamp(1710176400,tz=datetime.timezone.utc), + asset_id="0x1", + ) + result = l1_account.sign_message(payload.to_signable_message("x10.exchange")).signature.hex() + assert_that( + result, + equal_to('f1d965cc6c3d020c103e2bd295a0416ab50b0f6c67b01312bf09ea883788df2027a7dad3666c7cca618063b9eda37854642c02dc7842679ccc07e4ea0d0ec0ec1c') + ) diff --git a/x10/perpetual/transfer_object.py b/x10/perpetual/transfer_object.py index 44c752a..4f03828 100644 --- a/x10/perpetual/transfer_object.py +++ b/x10/perpetual/transfer_object.py @@ -37,6 +37,7 @@ def create_transfer_object( config: EndpointConfig, stark_account: StarkPerpetualAccount, nonce: int | None = None, + signature: str | None = None, ) -> OnChainPerpetualTransferModel: expiration_timestamp = calc_expiration_timestamp() scaled_amount = amount.scaleb(config.collateral_decimals) @@ -77,4 +78,5 @@ def create_transfer_object( amount=amount, settlement=settlement, transferred_asset=config.collateral_asset_id, + signature=signature ) diff --git a/x10/perpetual/transfers.py b/x10/perpetual/transfers.py index 1466feb..9757a24 100644 --- a/x10/perpetual/transfers.py +++ b/x10/perpetual/transfers.py @@ -1,7 +1,11 @@ +from dataclasses import dataclass from decimal import Decimal from x10.perpetual.orders import SettlementSignatureModel from x10.utils.model import HexValue, X10BaseModel +from datetime import datetime, timezone +from eth_account.messages import SignableMessage, encode_typed_data + class StarkTransferSettlement(X10BaseModel): @@ -30,6 +34,7 @@ class OnChainPerpetualTransferModel(X10BaseModel): amount: Decimal settlement: StarkTransferSettlement transferred_asset: str + signature: str class TransferResponseModel(X10BaseModel): @@ -37,3 +42,46 @@ class TransferResponseModel(X10BaseModel): id: int | None = None hash_calculated: str | None = None stark_ex_representation: dict | None = None + +@dataclass +class Transfer: + source_account: int + target_account: int + asset_id: str + amount: Decimal + expiration: datetime + + def __post_init__(self): + self.expiration_string = self.expiration.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + def to_signable_message(self, signing_domain) -> SignableMessage: + asset = int(self.asset_id, 16) + domain = {"name": signing_domain} + + message = { + "sourceAccount": self.source_account, + "targetAccount": self.target_account, + "assetId": asset, + "amount": str(self.amount), + "expiration": self.expiration_string, + } + types = { + "EIP712Domain": [ + {"name": "name", "type": "string"} + ], + "Transfer": [ + {"name": "sourceAccount", "type": "int64"}, + {"name": "targetAccount", "type": "int64"}, + {"name": "assetId", "type": "int64"}, + {"name": "amount", "type": "string"}, + {"name": "expiration", "type": "string"} + ] + } + primary_type = "Transfer" + structured_data = { + "types": types, + "domain": domain, + "primaryType": primary_type, + "message": message, + } + return encode_typed_data(full_message=structured_data) \ No newline at end of file diff --git a/x10/perpetual/withdrawal_object.py b/x10/perpetual/withdrawal_object.py index 74a06cb..18f14ef 100644 --- a/x10/perpetual/withdrawal_object.py +++ b/x10/perpetual/withdrawal_object.py @@ -24,15 +24,17 @@ def calc_expiration_timestamp(): def create_withdrawal_object( - amount: Decimal, - recipient_stark_address: str, - stark_account: StarkPerpetualAccount, - config: EndpointConfig, - account_id: int, - chain_id: str, - description: str | None = None, - nonce: int | None = None, - quote_id: str | None = None, + amount: Decimal, + recipient_stark_address: str, + stark_account: StarkPerpetualAccount, + config: EndpointConfig, + account_id: int, + chain_id: str, + description: str | None = None, + nonce: int | None = None, + quote_id: str | None = None, + target_wallet: str | None = None, + signature: str | None = None, ) -> WithdrawalRequest: expiration_timestamp = calc_expiration_timestamp() scaled_amount = amount.scaleb(config.collateral_decimals) @@ -78,4 +80,6 @@ def create_withdrawal_object( chain_id=chain_id, quote_id=quote_id, asset="USD", + target_wallet=target_wallet, + signature=signature ) diff --git a/x10/perpetual/withdrawals.py b/x10/perpetual/withdrawals.py index 896659e..64d01c0 100644 --- a/x10/perpetual/withdrawals.py +++ b/x10/perpetual/withdrawals.py @@ -1,3 +1,7 @@ +from dataclasses import dataclass +from datetime import datetime, timezone +from eth_account.messages import SignableMessage, encode_typed_data + from decimal import Decimal from x10.utils.model import HexValue, SettlementSignatureModel, X10BaseModel @@ -25,3 +29,49 @@ class WithdrawalRequest(X10BaseModel): chain_id: str quote_id: str | None = None asset: str + target_wallet: str | None = None + signature: str | None = None + + +@dataclass +class Withdrawal: + account_id: int + target_wallet: str + asset_id: str + amount: Decimal + expiration: datetime + + def __post_init__(self): + self.expiration_string = self.expiration.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + def to_signable_message(self, signing_domain) -> SignableMessage: + domain = {"name": signing_domain} + asset = int(self.asset_id, 16) + message = { + "account": self.account_id, + "targetWallet": self.target_wallet, + "assetId": asset, + "amount": str(self.amount), + "expiration": self.expiration_string, + } + types = { + "EIP712Domain": [ + {"name": "name", "type": "string"} + ], + "Withdrawal": [ + {"name": "account", "type": "int64"}, + {"name": "targetWallet", "type": "string"}, + {"name": "assetId", "type": "int64"}, + {"name": "amount", "type": "string"}, + {"name": "expiration", "type": "string"} + ] + } + primary_type = "Withdrawal" + structured_data = { + "types": types, + "domain": domain, + "primaryType": primary_type, + "message": message, + } + return encode_typed_data(full_message=structured_data) + From 7db80c38ef02f74bede1540d83fbbeb4d12955f6 Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 19 Mar 2026 12:27:37 +0000 Subject: [PATCH 2/4] Crossuser transfer --- examples/06_crossuser_transfer.py | 42 +++++++++++++++++++ examples/06_transfer_between_user_accounts.py | 38 +++++++++++++++++ x10/perpetual/configuration.py | 2 +- .../trading_client/account_module.py | 16 ++++++- x10/perpetual/transfer_object.py | 3 +- x10/perpetual/transfers.py | 31 +++++++------- 6 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 examples/06_crossuser_transfer.py create mode 100644 examples/06_transfer_between_user_accounts.py diff --git a/examples/06_crossuser_transfer.py b/examples/06_crossuser_transfer.py new file mode 100644 index 0000000..7e70303 --- /dev/null +++ b/examples/06_crossuser_transfer.py @@ -0,0 +1,42 @@ +import logging.handlers +from asyncio import run +from decimal import Decimal + +from eth_account import Account +from eth_account.signers.local import LocalAccount + +from examples.init_env import init_env +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.trading_client import PerpetualTradingClient + +LOGGER = logging.getLogger() +ENDPOINT_CONFIG = TESTNET_CONFIG + + +# Bridged withdrawal example. Bridge disabled on sepolia, example works only on mainnet +async def run_example(): + env_config = init_env() + amount = 1 + to_vault = -1 + to_l2_key = "" + signing_account = Account.from_key("") + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + LOGGER.info("Sending transfer") + await trading_client.account.transfer( + to_vault=to_vault, + to_l2_key=to_l2_key, + amount=Decimal(amount), + signing_account=signing_account + ) + LOGGER.info("Transfer sent") + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/examples/06_transfer_between_user_accounts.py b/examples/06_transfer_between_user_accounts.py new file mode 100644 index 0000000..57e63da --- /dev/null +++ b/examples/06_transfer_between_user_accounts.py @@ -0,0 +1,38 @@ +import logging.handlers +from asyncio import run +from decimal import Decimal + +from examples.init_env import init_env +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.trading_client import PerpetualTradingClient + +LOGGER = logging.getLogger() +ENDPOINT_CONFIG = TESTNET_CONFIG + + +# Bridged withdrawal example. Bridge disabled on sepolia, example works only on mainnet +async def run_example(): + env_config = init_env() + amount = 1 + to_vault = -1 + to_l2_key = "" + + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + LOGGER.info("Sending transfer") + await trading_client.account.transfer( + to_vault=to_vault, + to_l2_key=to_l2_key, + amount=Decimal(amount), + ) + LOGGER.info("Transfer sent") + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/x10/perpetual/configuration.py b/x10/perpetual/configuration.py index a73f24d..00549a2 100644 --- a/x10/perpetual/configuration.py +++ b/x10/perpetual/configuration.py @@ -32,7 +32,7 @@ class EndpointConfig: signing_domain="starknet.sepolia.extended.exchange", collateral_asset_contract="0x31857064564ed0ff978e687456963cba09c2c6985d8f9300a1de4962fafa054", asset_operations_contract="", - collateral_asset_on_chain_id="0x1", + collateral_asset_on_chain_id="0x31857064564ed0ff978e687456963cba09c2c6985d8f9300a1de4962fafa054", collateral_decimals=6, collateral_asset_id="0x1", starknet_domain=StarknetDomain(name="Perpetuals", version="v0", chain_id="SN_SEPOLIA", revision="1"), diff --git a/x10/perpetual/trading_client/account_module.py b/x10/perpetual/trading_client/account_module.py index 78a698a..8ab26a4 100644 --- a/x10/perpetual/trading_client/account_module.py +++ b/x10/perpetual/trading_client/account_module.py @@ -1,6 +1,8 @@ from decimal import Decimal from typing import List, Optional +from eth_account.signers.local import LocalAccount + from x10.perpetual.accounts import AccountLeverage, AccountModel from x10.perpetual.assets import ( AssetOperationModel, @@ -16,7 +18,7 @@ from x10.perpetual.trades import AccountTradeModel, TradeType from x10.perpetual.trading_client.base_module import BaseModule from x10.perpetual.transfer_object import create_transfer_object -from x10.perpetual.transfers import TransferResponseModel +from x10.perpetual.transfers import TransferResponseModel, Transfer from x10.perpetual.withdrawal_object import create_withdrawal_object from x10.utils.http import ( WrappedApiResponse, @@ -215,6 +217,7 @@ async def transfer( to_l2_key: int | str, amount: Decimal, nonce: int | None = None, + signing_account: LocalAccount| None = None, ) -> WrappedApiResponse[TransferResponseModel]: from_vault = self._get_stark_account().vault url = self._get_url("/user/transfer/onchain") @@ -232,6 +235,17 @@ async def transfer( nonce=nonce, ) + if signing_account is not None: + message_to_sign = Transfer( + from_vault=from_vault, + to_vault=to_vault, + asset=request_model.transferred_asset, + amount=amount, + starknet_hash=request_model.transfer_hash, + ).to_signable_message(self._get_endpoint_config().signing_domain) + signature = '0x'+ signing_account.sign_message(message_to_sign).signature.hex() + request_model = request_model.model_copy(update={"signature": signature}) + return await send_post_request( await self.get_session(), url, diff --git a/x10/perpetual/transfer_object.py b/x10/perpetual/transfer_object.py index 4f03828..31c13cf 100644 --- a/x10/perpetual/transfer_object.py +++ b/x10/perpetual/transfer_object.py @@ -37,7 +37,6 @@ def create_transfer_object( config: EndpointConfig, stark_account: StarkPerpetualAccount, nonce: int | None = None, - signature: str | None = None, ) -> OnChainPerpetualTransferModel: expiration_timestamp = calc_expiration_timestamp() scaled_amount = amount.scaleb(config.collateral_decimals) @@ -77,6 +76,6 @@ def create_transfer_object( to_vault=to_vault, amount=amount, settlement=settlement, + transfer_hash= hex(transfer_hash), transferred_asset=config.collateral_asset_id, - signature=signature ) diff --git a/x10/perpetual/transfers.py b/x10/perpetual/transfers.py index 9757a24..9e507b7 100644 --- a/x10/perpetual/transfers.py +++ b/x10/perpetual/transfers.py @@ -34,7 +34,8 @@ class OnChainPerpetualTransferModel(X10BaseModel): amount: Decimal settlement: StarkTransferSettlement transferred_asset: str - signature: str + transfer_hash: str + signature: str | None = None class TransferResponseModel(X10BaseModel): @@ -45,36 +46,32 @@ class TransferResponseModel(X10BaseModel): @dataclass class Transfer: - source_account: int - target_account: int - asset_id: str + from_vault: int + to_vault: int + asset: str amount: Decimal - expiration: datetime - - def __post_init__(self): - self.expiration_string = self.expiration.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + starknet_hash: str def to_signable_message(self, signing_domain) -> SignableMessage: - asset = int(self.asset_id, 16) domain = {"name": signing_domain} message = { - "sourceAccount": self.source_account, - "targetAccount": self.target_account, - "assetId": asset, + "fromVault": self.from_vault, + "toVault": self.to_vault, + "asset": self.asset, "amount": str(self.amount), - "expiration": self.expiration_string, + "starknetHash": self.starknet_hash, } types = { "EIP712Domain": [ {"name": "name", "type": "string"} ], "Transfer": [ - {"name": "sourceAccount", "type": "int64"}, - {"name": "targetAccount", "type": "int64"}, - {"name": "assetId", "type": "int64"}, + {"name": "fromVault", "type": "int64"}, + {"name": "toVault", "type": "int64"}, + {"name": "asset", "type": "string"}, {"name": "amount", "type": "string"}, - {"name": "expiration", "type": "string"} + {"name": "starknetHash", "type": "string"} ] } primary_type = "Transfer" From 5448dd401dc79f3d7c5a6e2d8a87763059242020 Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 19 Mar 2026 12:27:50 +0000 Subject: [PATCH 3/4] Crossuser transfer --- ...ween_user_accounts.py => 07_transfer_between_user_accounts.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{06_transfer_between_user_accounts.py => 07_transfer_between_user_accounts.py} (100%) diff --git a/examples/06_transfer_between_user_accounts.py b/examples/07_transfer_between_user_accounts.py similarity index 100% rename from examples/06_transfer_between_user_accounts.py rename to examples/07_transfer_between_user_accounts.py From 7f8933c7367deb86edf3d6a3daf1b08291797e34 Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 19 Mar 2026 13:38:55 +0000 Subject: [PATCH 4/4] Crossuser withdrawal --- examples/06_crossuser_withdrawal.py | 57 +++++++++ examples/07_transfer_between_user_accounts.py | 4 +- ...r_transfer.py => 08_crossuser_transfer.py} | 4 +- .../trading_client/account_module.py | 112 +++++++++++------- x10/perpetual/withdrawal_object.py | 3 +- x10/perpetual/withdrawals.py | 17 ++- 6 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 examples/06_crossuser_withdrawal.py rename examples/{06_crossuser_transfer.py => 08_crossuser_transfer.py} (92%) diff --git a/examples/06_crossuser_withdrawal.py b/examples/06_crossuser_withdrawal.py new file mode 100644 index 0000000..5941287 --- /dev/null +++ b/examples/06_crossuser_withdrawal.py @@ -0,0 +1,57 @@ +import logging.handlers +from asyncio import run +from decimal import Decimal + +from eth_account import Account + +from examples.init_env import init_env +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import MAINNET_CONFIG, TESTNET_CONFIG +from x10.perpetual.trading_client import PerpetualTradingClient + +LOGGER = logging.getLogger() +ENDPOINT_CONFIG = MAINNET_CONFIG + + +# Bridged withdrawal example. Bridge disabled on sepolia, example works only on mainnet +async def run_example(): + env_config = init_env() + amount = 5 + target_chain = "ETH" + targetWallet = "" + signing_account = Account.from_key("") + + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + LOGGER.info("Getting quote") + quote = (await trading_client.account.get_bridge_quote(chain_in="STRK", + chain_out=target_chain, + amount=amount, + recipient=targetWallet + )).data + if quote.fee > Decimal(2): + LOGGER.info("Fee %s is too high", quote.fee) + return + LOGGER.info("Commiting quote") + await trading_client.account.commit_bridge_quote(quote.id) + LOGGER.info("Requesting withdrawal") + withdrawal_id = ( + await trading_client.account.withdraw( + amount=Decimal(amount), + chain_id=target_chain, + quote_id=quote.id, + target_wallet=targetWallet, + signing_account=signing_account + ) + ).data + + LOGGER.info("Withdrawal %s requested", withdrawal_id) + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/examples/07_transfer_between_user_accounts.py b/examples/07_transfer_between_user_accounts.py index 57e63da..d9c3383 100644 --- a/examples/07_transfer_between_user_accounts.py +++ b/examples/07_transfer_between_user_accounts.py @@ -4,11 +4,11 @@ from examples.init_env import init_env from x10.perpetual.accounts import StarkPerpetualAccount -from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.configuration import MAINNET_CONFIG from x10.perpetual.trading_client import PerpetualTradingClient LOGGER = logging.getLogger() -ENDPOINT_CONFIG = TESTNET_CONFIG +ENDPOINT_CONFIG = MAINNET_CONFIG # Bridged withdrawal example. Bridge disabled on sepolia, example works only on mainnet diff --git a/examples/06_crossuser_transfer.py b/examples/08_crossuser_transfer.py similarity index 92% rename from examples/06_crossuser_transfer.py rename to examples/08_crossuser_transfer.py index 7e70303..40a2615 100644 --- a/examples/06_crossuser_transfer.py +++ b/examples/08_crossuser_transfer.py @@ -7,11 +7,11 @@ from examples.init_env import init_env from x10.perpetual.accounts import StarkPerpetualAccount -from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.configuration import TESTNET_CONFIG, MAINNET_CONFIG from x10.perpetual.trading_client import PerpetualTradingClient LOGGER = logging.getLogger() -ENDPOINT_CONFIG = TESTNET_CONFIG +ENDPOINT_CONFIG = MAINNET_CONFIG # Bridged withdrawal example. Bridge disabled on sepolia, example works only on mainnet diff --git a/x10/perpetual/trading_client/account_module.py b/x10/perpetual/trading_client/account_module.py index 8ab26a4..9eaffd5 100644 --- a/x10/perpetual/trading_client/account_module.py +++ b/x10/perpetual/trading_client/account_module.py @@ -20,6 +20,7 @@ from x10.perpetual.transfer_object import create_transfer_object from x10.perpetual.transfers import TransferResponseModel, Transfer from x10.perpetual.withdrawal_object import create_withdrawal_object +from x10.perpetual.withdrawals import Withdrawal from x10.utils.http import ( WrappedApiResponse, send_get_request, @@ -47,7 +48,7 @@ async def get_balance(self) -> WrappedApiResponse[BalanceModel]: return await send_get_request(await self.get_session(), url, BalanceModel, api_key=self._get_api_key()) async def get_positions( - self, *, market_names: Optional[List[str]] = None, position_side: Optional[PositionSide] = None + self, *, market_names: Optional[List[str]] = None, position_side: Optional[PositionSide] = None ) -> WrappedApiResponse[List[PositionModel]]: """ https://api.docs.extended.exchange/#get-positions @@ -57,11 +58,11 @@ async def get_positions( return await send_get_request(await self.get_session(), url, List[PositionModel], api_key=self._get_api_key()) async def get_positions_history( - self, - market_names: Optional[List[str]] = None, - position_side: Optional[PositionSide] = None, - cursor: Optional[int] = None, - limit: Optional[int] = None, + self, + market_names: Optional[List[str]] = None, + position_side: Optional[PositionSide] = None, + cursor: Optional[int] = None, + limit: Optional[int] = None, ) -> WrappedApiResponse[List[PositionHistoryModel]]: """ https://api.docs.extended.exchange/#get-positions-history @@ -76,10 +77,10 @@ async def get_positions_history( ) async def get_open_orders( - self, - market_names: Optional[List[str]] = None, - order_type: Optional[OrderType] = None, - order_side: Optional[OrderSide] = None, + self, + market_names: Optional[List[str]] = None, + order_type: Optional[OrderType] = None, + order_side: Optional[OrderSide] = None, ) -> WrappedApiResponse[List[OpenOrderModel]]: """ https://api.docs.extended.exchange/#get-open-orders @@ -92,12 +93,12 @@ async def get_open_orders( return await send_get_request(await self.get_session(), url, List[OpenOrderModel], api_key=self._get_api_key()) async def get_orders_history( - self, - market_names: Optional[List[str]] = None, - order_type: Optional[OrderType] = None, - order_side: Optional[OrderSide] = None, - cursor: Optional[int] = None, - limit: Optional[int] = None, + self, + market_names: Optional[List[str]] = None, + order_type: Optional[OrderType] = None, + order_side: Optional[OrderSide] = None, + cursor: Optional[int] = None, + limit: Optional[int] = None, ) -> WrappedApiResponse[List[OpenOrderModel]]: """ https://api.docs.extended.exchange/#get-orders-history @@ -128,12 +129,12 @@ async def get_order_by_external_id(self, external_id: str) -> WrappedApiResponse return await send_get_request(await self.get_session(), url, list[OpenOrderModel], api_key=self._get_api_key()) async def get_trades( - self, - market_names: Optional[List[str]] = None, - trade_side: Optional[OrderSide] = None, - trade_type: Optional[TradeType] = None, - cursor: Optional[int] = None, - limit: Optional[int] = None, + self, + market_names: Optional[List[str]] = None, + trade_side: Optional[OrderSide] = None, + trade_type: Optional[TradeType] = None, + cursor: Optional[int] = None, + limit: Optional[int] = None, ) -> WrappedApiResponse[List[AccountTradeModel]]: """ https://api.docs.extended.exchange/#get-trades @@ -149,7 +150,7 @@ async def get_trades( ) async def get_fees( - self, *, market_names: Optional[List[str]] = None, builder_id: Optional[int] = None + self, *, market_names: Optional[List[str]] = None, builder_id: Optional[int] = None ) -> WrappedApiResponse[List[TradingFeeModel]]: """ https://api.docs.extended.exchange/#get-fees @@ -191,13 +192,15 @@ async def get_bridge_config(self) -> WrappedApiResponse[BridgesConfig]: url = self._get_url("/user/bridge/config") return await send_get_request(await self.get_session(), url, BridgesConfig, api_key=self._get_api_key()) - async def get_bridge_quote(self, chain_in: str, chain_out: str, amount: Decimal) -> WrappedApiResponse[Quote]: + async def get_bridge_quote(self, chain_in: str, chain_out: str, amount: Decimal, recipient: str | None = None) -> \ + WrappedApiResponse[Quote]: url = self._get_url( "/user/bridge/quote", query={ "chainIn": chain_in, "chainOut": chain_out, "amount": amount, + "recipient": recipient }, ) return await send_get_request(await self.get_session(), url, Quote, api_key=self._get_api_key()) @@ -212,12 +215,12 @@ async def commit_bridge_quote(self, id: str): await send_post_request(await self.get_session(), url, EmptyModel, api_key=self._get_api_key()) async def transfer( - self, - to_vault: int, - to_l2_key: int | str, - amount: Decimal, - nonce: int | None = None, - signing_account: LocalAccount| None = None, + self, + to_vault: int, + to_l2_key: int | str, + amount: Decimal, + nonce: int | None = None, + signing_account: LocalAccount | None = None, ) -> WrappedApiResponse[TransferResponseModel]: from_vault = self._get_stark_account().vault url = self._get_url("/user/transfer/onchain") @@ -239,11 +242,11 @@ async def transfer( message_to_sign = Transfer( from_vault=from_vault, to_vault=to_vault, - asset=request_model.transferred_asset, + asset="USD", amount=amount, starknet_hash=request_model.transfer_hash, ).to_signable_message(self._get_endpoint_config().signing_domain) - signature = '0x'+ signing_account.sign_message(message_to_sign).signature.hex() + signature = '0x' + signing_account.sign_message(message_to_sign).signature.hex() request_model = request_model.model_copy(update={"signature": signature}) return await send_post_request( @@ -255,13 +258,18 @@ async def transfer( ) async def withdraw( - self, - amount: Decimal, - chain_id: str = "STRK", - stark_address: str | None = None, - nonce: int | None = None, - quote_id: str | None = None, + self, + amount: Decimal, + chain_id: str = "STRK", + stark_address: str | None = None, + nonce: int | None = None, + quote_id: str | None = None, + target_wallet: str | None = None, + signing_account: LocalAccount | None = None, ) -> WrappedApiResponse[int]: + if target_wallet is not None: + target_wallet = target_wallet.lower() + url = self._get_url("/user/withdrawal") account = (await self.get_account()).data if account is None: @@ -297,7 +305,19 @@ async def withdraw( chain_id=chain_id, quote_id=quote_id, nonce=nonce, + target_wallet=target_wallet ) + if signing_account is not None: + message_to_sign = Withdrawal( + account_id=account.id, + target_wallet=target_wallet, + asset=request_model.asset, + amount=amount, + starknet_hash=request_model.withdrawal_hash, + ).to_signable_message(self._get_endpoint_config().signing_domain) + signature = '0x' + signing_account.sign_message(message_to_sign).signature.hex() + request_model = request_model.model_copy(update={"signature": signature}) + return await send_post_request( await self.get_session(), url, @@ -307,14 +327,14 @@ async def withdraw( ) async def asset_operations( - self, - id: Optional[int] = None, - operations_type: Optional[List[AssetOperationType]] = None, - operations_status: Optional[List[AssetOperationStatus]] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - cursor: Optional[int] = None, - limit: Optional[int] = None, + self, + id: Optional[int] = None, + operations_type: Optional[List[AssetOperationType]] = None, + operations_status: Optional[List[AssetOperationStatus]] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + cursor: Optional[int] = None, + limit: Optional[int] = None, ) -> WrappedApiResponse[List[AssetOperationModel]]: url = self._get_url( "/user/assetOperations", diff --git a/x10/perpetual/withdrawal_object.py b/x10/perpetual/withdrawal_object.py index 18f14ef..25483b2 100644 --- a/x10/perpetual/withdrawal_object.py +++ b/x10/perpetual/withdrawal_object.py @@ -81,5 +81,6 @@ def create_withdrawal_object( quote_id=quote_id, asset="USD", target_wallet=target_wallet, - signature=signature + signature=signature, + withdrawal_hash = hex(withdrawal_hash) ) diff --git a/x10/perpetual/withdrawals.py b/x10/perpetual/withdrawals.py index 64d01c0..d35c757 100644 --- a/x10/perpetual/withdrawals.py +++ b/x10/perpetual/withdrawals.py @@ -31,28 +31,25 @@ class WithdrawalRequest(X10BaseModel): asset: str target_wallet: str | None = None signature: str | None = None + withdrawal_hash: str | None = None @dataclass class Withdrawal: account_id: int target_wallet: str - asset_id: str + asset: str amount: Decimal - expiration: datetime - - def __post_init__(self): - self.expiration_string = self.expiration.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + starknet_hash: str def to_signable_message(self, signing_domain) -> SignableMessage: domain = {"name": signing_domain} - asset = int(self.asset_id, 16) message = { "account": self.account_id, "targetWallet": self.target_wallet, - "assetId": asset, + "asset": self.asset, "amount": str(self.amount), - "expiration": self.expiration_string, + "starknetHash": self.starknet_hash, } types = { "EIP712Domain": [ @@ -61,9 +58,9 @@ def to_signable_message(self, signing_domain) -> SignableMessage: "Withdrawal": [ {"name": "account", "type": "int64"}, {"name": "targetWallet", "type": "string"}, - {"name": "assetId", "type": "int64"}, + {"name": "asset", "type": "string"}, {"name": "amount", "type": "string"}, - {"name": "expiration", "type": "string"} + {"name": "starknetHash", "type": "string"} ] } primary_type = "Withdrawal"