mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
313 lines
11 KiB
Python
313 lines
11 KiB
Python
import binascii
|
|
import hashlib
|
|
import time
|
|
import uuid
|
|
from typing import Dict, Any, Optional, Tuple, List, Union
|
|
|
|
import requests
|
|
from Crypto.Hash import keccak
|
|
|
|
from .signing_adapter import SigningAdapter
|
|
|
|
# Import field prime for modular arithmetic
|
|
try:
|
|
from ..crypto.constants import FIELD_PRIME
|
|
except ImportError:
|
|
# Fallback if crypto module is not available
|
|
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
|
|
|
# Constants
|
|
LIMIT_ORDER_WITH_FEE_TYPE = 3
|
|
|
|
|
|
class L2Signature:
|
|
"""Represents a signature for L2 operations."""
|
|
|
|
def __init__(self, r: str, s: str, v: str = ""):
|
|
self.r = r
|
|
self.s = s
|
|
self.v = v
|
|
|
|
|
|
class Client:
|
|
"""Base client with common functionality."""
|
|
|
|
def __init__(self, base_url: str, account_id: int, stark_pri_key: str, signing_adapter: Optional[SigningAdapter] = None):
|
|
"""
|
|
Initialize the internal client.
|
|
|
|
Args:
|
|
base_url: Base URL for API endpoints
|
|
account_id: Account ID for authentication
|
|
stark_pri_key: Stark private key for signing
|
|
signing_adapter: Optional signing adapter to use for cryptographic operations
|
|
"""
|
|
self.http_client = requests.Session()
|
|
self.http_client.headers.update({
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json"
|
|
})
|
|
self.base_url = base_url
|
|
self.account_id = account_id
|
|
self.stark_pri_key = stark_pri_key
|
|
|
|
# Use the provided signing adapter (required)
|
|
if signing_adapter is None:
|
|
raise ValueError("signing_adapter is required")
|
|
self.signing_adapter = signing_adapter
|
|
|
|
def get_account_id(self) -> int:
|
|
"""Get the account ID."""
|
|
return self.account_id
|
|
|
|
def get_stark_pri_key(self) -> str:
|
|
"""Get the stark private key."""
|
|
return self.stark_pri_key
|
|
|
|
def sign(self, message_hash: bytes) -> L2Signature:
|
|
"""
|
|
Sign a message hash using the client's Stark private key.
|
|
|
|
Args:
|
|
message_hash: The hash of the message to sign
|
|
|
|
Returns:
|
|
L2Signature: The signature components
|
|
|
|
Raises:
|
|
ValueError: If the stark private key is not set or invalid
|
|
"""
|
|
private_key = self.get_stark_pri_key()
|
|
if not private_key:
|
|
raise ValueError("stark private key not set")
|
|
|
|
# Sign the message using the signing adapter
|
|
try:
|
|
r, s = self.signing_adapter.sign(message_hash, private_key)
|
|
return L2Signature(r=r, s=s, v="")
|
|
except Exception as e:
|
|
raise ValueError(f"failed to sign message: {str(e)}")
|
|
|
|
def generate_uuid(self) -> str:
|
|
"""Generate a UUID for client order IDs."""
|
|
return str(uuid.uuid4())
|
|
|
|
def calc_nonce(self, client_order_id: str) -> int:
|
|
"""
|
|
Calculate a nonce from a client order ID.
|
|
|
|
Args:
|
|
client_order_id: The client order ID
|
|
|
|
Returns:
|
|
int: The calculated nonce
|
|
"""
|
|
# Use SHA256 like the Go SDK (not Keccak256)
|
|
h = hashlib.sha256()
|
|
h.update(client_order_id.encode())
|
|
hash_hex = h.hexdigest()
|
|
return int(hash_hex[:8], 16)
|
|
|
|
def calc_limit_order_hash(
|
|
self,
|
|
synthetic_asset_id: str,
|
|
collateral_asset_id: str,
|
|
fee_asset_id: str,
|
|
is_buy: bool,
|
|
amount_synthetic: int,
|
|
amount_collateral: int,
|
|
amount_fee: int,
|
|
nonce: int,
|
|
account_id: int,
|
|
expire_time: int
|
|
) -> bytes:
|
|
"""
|
|
Calculate the hash for a limit order using StarkEx protocol.
|
|
|
|
Args:
|
|
synthetic_asset_id: The synthetic asset ID (hex string)
|
|
collateral_asset_id: The collateral asset ID (hex string)
|
|
fee_asset_id: The fee asset ID (hex string)
|
|
is_buy: Whether the order is a buy order
|
|
amount_synthetic: The synthetic amount
|
|
amount_collateral: The collateral amount
|
|
amount_fee: The fee amount
|
|
nonce: The nonce
|
|
account_id: The account ID (position ID)
|
|
expire_time: The expiration time
|
|
|
|
Returns:
|
|
bytes: The calculated hash
|
|
"""
|
|
# Remove 0x prefix if present
|
|
if synthetic_asset_id.startswith('0x'):
|
|
synthetic_asset_id = synthetic_asset_id[2:]
|
|
if collateral_asset_id.startswith('0x'):
|
|
collateral_asset_id = collateral_asset_id[2:]
|
|
if fee_asset_id.startswith('0x'):
|
|
fee_asset_id = fee_asset_id[2:]
|
|
|
|
# Convert hex strings to integers and ensure they're within the field
|
|
asset_id_synthetic = int(synthetic_asset_id, 16) % FIELD_PRIME
|
|
asset_id_collateral = int(collateral_asset_id, 16) % FIELD_PRIME
|
|
asset_id_fee = int(fee_asset_id, 16) % FIELD_PRIME
|
|
|
|
# Determine buy/sell assets based on order direction
|
|
if is_buy:
|
|
asset_id_sell = asset_id_collateral
|
|
asset_id_buy = asset_id_synthetic
|
|
amount_sell = amount_collateral
|
|
amount_buy = amount_synthetic
|
|
else:
|
|
asset_id_sell = asset_id_synthetic
|
|
asset_id_buy = asset_id_collateral
|
|
amount_sell = amount_synthetic
|
|
amount_buy = amount_collateral
|
|
|
|
# Use the signing adapter to calculate the Pedersen hash
|
|
# First hash: hash(asset_id_sell, asset_id_buy)
|
|
msg = self.signing_adapter.pedersen_hash([asset_id_sell, asset_id_buy])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Second hash: hash(msg, asset_id_fee)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, asset_id_fee])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Pack message 0
|
|
# packed_message0 = amount_sell * 2^64 + amount_buy * 2^64 + max_amount_fee * 2^32 + nonce
|
|
packed_message0 = amount_sell
|
|
packed_message0 = (packed_message0 << 64) + amount_buy
|
|
packed_message0 = (packed_message0 << 64) + amount_fee
|
|
packed_message0 = (packed_message0 << 32) + nonce
|
|
packed_message0 = packed_message0 % FIELD_PRIME # Ensure within field
|
|
|
|
# Third hash: hash(msg, packed_message0)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message0])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Pack message 1
|
|
# packed_message1 = LIMIT_ORDER_WITH_FEES * 2^64 + position_id * 2^64 + position_id * 2^64 + position_id * 2^32 + expiration_timestamp * 2^17
|
|
packed_message1 = LIMIT_ORDER_WITH_FEE_TYPE
|
|
packed_message1 = (packed_message1 << 64) + account_id
|
|
packed_message1 = (packed_message1 << 64) + account_id
|
|
packed_message1 = (packed_message1 << 64) + account_id
|
|
packed_message1 = (packed_message1 << 32) + expire_time
|
|
packed_message1 = packed_message1 << 17 # Padding
|
|
packed_message1 = packed_message1 % FIELD_PRIME # Ensure within field
|
|
|
|
# Final hash: hash(msg, packed_message1)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message1])
|
|
|
|
return msg
|
|
|
|
def calc_transfer_hash(
|
|
self,
|
|
asset_id: int,
|
|
asset_id_fee: int,
|
|
receiver_public_key: int,
|
|
sender_position_id: int,
|
|
receiver_position_id: int,
|
|
fee_position_id: int,
|
|
nonce: int,
|
|
amount: int,
|
|
max_amount_fee: int,
|
|
expiration_timestamp: int
|
|
) -> bytes:
|
|
"""
|
|
Calculate the hash for a transfer using StarkEx protocol.
|
|
|
|
Args:
|
|
asset_id: The asset ID
|
|
asset_id_fee: The fee asset ID
|
|
receiver_public_key: The receiver's public key
|
|
sender_position_id: The sender's position ID
|
|
receiver_position_id: The receiver's position ID
|
|
fee_position_id: The fee position ID
|
|
nonce: The nonce
|
|
amount: The transfer amount
|
|
max_amount_fee: The maximum fee amount
|
|
expiration_timestamp: The expiration timestamp
|
|
|
|
Returns:
|
|
bytes: The calculated hash
|
|
"""
|
|
# First hash: hash(asset_id, asset_id_fee)
|
|
msg = self.signing_adapter.pedersen_hash([asset_id, asset_id_fee])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Second hash: hash(msg, receiver_public_key)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, receiver_public_key])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Pack message 0
|
|
# packed_msg0 = sender_position_id * 2^64 + receiver_position_id * 2^64 + fee_position_id * 2^32 + nonce
|
|
packed_msg0 = sender_position_id
|
|
packed_msg0 = (packed_msg0 << 64) + receiver_position_id
|
|
packed_msg0 = (packed_msg0 << 64) + fee_position_id
|
|
packed_msg0 = (packed_msg0 << 32) + nonce
|
|
packed_msg0 = packed_msg0 % FIELD_PRIME # Ensure within field
|
|
|
|
# Third hash: hash(msg, packed_msg0)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg0])
|
|
msg_int = int.from_bytes(msg, byteorder='big')
|
|
|
|
# Pack message 1
|
|
# packed_msg1 = 4 * 2^64 + amount * 2^64 + max_amount_fee * 2^32 + expiration_timestamp * 2^81
|
|
packed_msg1 = 4 # Transfer type
|
|
packed_msg1 = (packed_msg1 << 64) + amount
|
|
packed_msg1 = (packed_msg1 << 64) + max_amount_fee
|
|
packed_msg1 = (packed_msg1 << 32) + expiration_timestamp
|
|
packed_msg1 = packed_msg1 << 81 # Padding
|
|
packed_msg1 = packed_msg1 % FIELD_PRIME # Ensure within field
|
|
|
|
# Final hash: hash(msg, packed_msg1)
|
|
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg1])
|
|
|
|
return msg
|
|
|
|
def get_value(self, data: Union[Dict[str, Any], List[Any], str, int, float, None]) -> str:
|
|
"""
|
|
Convert a value to a string representation for signing.
|
|
This function recursively processes dictionaries, lists, and primitive types.
|
|
|
|
Args:
|
|
data: The value to convert
|
|
|
|
Returns:
|
|
str: The string representation
|
|
"""
|
|
if data is None:
|
|
return ""
|
|
|
|
if isinstance(data, str):
|
|
return data
|
|
|
|
if isinstance(data, bool):
|
|
# Convert boolean to lowercase string to match Go SDK
|
|
return str(data).lower()
|
|
|
|
if isinstance(data, (int, float)):
|
|
return str(data)
|
|
|
|
if isinstance(data, list):
|
|
if len(data) == 0:
|
|
return ""
|
|
values = [self.get_value(item) for item in data]
|
|
return "&".join(values)
|
|
|
|
if isinstance(data, dict):
|
|
# Convert all values to strings and sort by keys
|
|
sorted_map = {}
|
|
for key, val in data.items():
|
|
sorted_map[key] = self.get_value(val)
|
|
|
|
# Get sorted keys
|
|
keys = sorted(sorted_map.keys())
|
|
|
|
# Build key=value pairs
|
|
pairs = [f"{key}={sorted_map[key]}" for key in keys]
|
|
return "&".join(pairs)
|
|
|
|
# Handle other types by converting to string
|
|
return str(data)
|