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) +