From 7e27cc779844b10ce3e16dedec746a1b392095bd Mon Sep 17 00:00:00 2001 From: mozluk <160273088+mozluk@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:36:44 +0300 Subject: [PATCH] fix(python-sdk): add robust input validation and timeout bounding to REST requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation This PR hardens the `python-sdk` repository by introducing comprehensive input validation bounds and explicit network request timeouts, as identified during the Pacific-Fi security audit[cite: 29]. Previously, decimal amounts, addresses, and leverage parameters lacked strict format and range checks, and outgoing REST requests lacked execution timeouts[cite: 29]. ## Modifications * **Shared Input Validation (`common/validate.py`, `tests/test_validate.py`)**: * Implemented strict decimal string parsing and exact base-unit conversion (`to_base_units`) to prevent floating-point precision loss and silent token rounding[cite: 29]. * Added validation guards for leverage ranges (1–50x) and base58 Solana address lengths[cite: 29]. * Added and executed a comprehensive unit test suite covering validation logic[cite: 29]. * **Outbound Request Bounding (`rest/*`)**: * Enforced explicit execution timeouts on all `requests.get` and `requests.post` calls to prevent hanging connections[cite: 29]. * Migrated query string arguments to safe parameter passing (`params=`)[cite: 29]. ## Checklist - [x] Format your code according to the Contributor Guide. - [x] Add unit tests as outlined in the Contributor Guide. - [x] Update documentation as needed, including docstrings or example tutorials. ```[cite: 29] --- common/validate.py | 113 ++++ rest/api_agent_keys.py | 279 ++++----- rest/api_agent_keys_detailed.py | 702 +++++++++++----------- rest/api_config_keys.py | 347 +++++------ rest/batch_orders.py | 226 +++---- rest/cancel_all_orders.py | 131 ++-- rest/cancel_order.py | 133 ++-- rest/cancel_twap_order.py | 133 ++-- rest/create_lake.py | 133 ++-- rest/create_limit_order.py | 145 ++--- rest/create_market_order.py | 143 ++--- rest/create_position_tpsl.py | 161 ++--- rest/create_subaccount.py | 277 ++++----- rest/create_subaccount_hardware.py | 177 +++--- rest/create_twap_order.py | 147 ++--- rest/deposit.py | 184 +++--- rest/get_open_twap_order.py | 60 +- rest/get_twap_order_history.py | 59 +- rest/get_twap_order_history_by_id.py | 61 +- rest/lake_deposit.py | 137 ++--- rest/lake_withdraw.py | 137 ++--- rest/list_subaccounts.py | 153 ++--- rest/transfer_subaccount_fund.py | 143 ++--- rest/transfer_subaccount_fund_hardware.py | 143 ++--- rest/update_leverage.py | 139 +++-- tests/test_validate.py | 158 +++++ 26 files changed, 2475 insertions(+), 2146 deletions(-) create mode 100644 common/validate.py create mode 100644 tests/test_validate.py diff --git a/common/validate.py b/common/validate.py new file mode 100644 index 0000000..89569c7 --- /dev/null +++ b/common/validate.py @@ -0,0 +1,113 @@ +"""Bounds and format checks for the values these examples sign. + +Every script here signs a payload and submits it without any confirmation step, so a +mistyped literal becomes a real order, a real leverage change, or a real transfer. The +checks below are cheap local guards against the mistakes that are expensive and +irreversible rather than merely wrong: an extra zero on an amount, a leverage value +copied from a different venue's scale, an address with a typo that still base58-decodes. + +Amounts are handled as ``Decimal`` from a *string*, never as ``float``. A USDC amount +has six decimals, and binary floating point cannot represent most decimal fractions +exactly: ``0.1 + 0.2`` is ``0.30000000000000004``, and ``int(round(4200.69 * 1_000_000))`` +depends on how the literal happened to round. The API accepts amounts as strings for +this reason, and signatures are computed over the exact string, so keeping the value in +decimal form all the way through is both safer and required for the signature to match +what the user intended. +""" + +from decimal import Decimal, InvalidOperation + +import base58 + +# USDC on Solana has six decimal places. A value with more precision than the token +# supports would be silently truncated somewhere downstream. +USDC_DECIMALS = 6 +USDC_SCALE = Decimal(10) ** USDC_DECIMALS + +# Pacifica's documented leverage range. Checked so a value from another venue's scale +# (or a stray extra digit) is rejected locally instead of being signed and sent. +MIN_LEVERAGE = 1 +MAX_LEVERAGE = 50 + +PUBLIC_KEY_BYTES = 32 + + +def check_amount(value, *, name="amount", minimum="0", maximum=None): + """Validate a decimal amount given as a string and return it as ``Decimal``. + + ``value`` must be a string (or ``Decimal``); passing a ``float`` is rejected rather + than coerced, because by the time a float reaches this function the precision loss + has already happened and there is nothing useful left to check. + """ + if isinstance(value, float): + raise TypeError( + f"{name} was passed as a float ({value!r}); pass it as a string such as " + f'"{value:.6f}" so the decimal value is exact.' + ) + try: + amount = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise ValueError(f"{name}={value!r} is not a valid decimal number.") from exc + + if not amount.is_finite(): + raise ValueError(f"{name}={value!r} is not a finite number.") + if amount <= Decimal(minimum): + raise ValueError(f"{name} must be greater than {minimum}; got {amount}.") + if maximum is not None and amount > Decimal(maximum): + raise ValueError( + f"{name}={amount} exceeds the configured ceiling of {maximum}. Raise the " + "ceiling deliberately if this is intended." + ) + if -amount.as_tuple().exponent > USDC_DECIMALS: + raise ValueError( + f"{name}={amount} has more than {USDC_DECIMALS} decimal places, which is " + "more precision than the token supports." + ) + return amount + + +def to_base_units(amount): + """Convert a validated ``Decimal`` amount to integer base units (6 decimals). + + The multiplication is exact because both operands are ``Decimal``. ``check_amount`` + has already rejected anything with more than six decimal places, so the result has + no fractional part to round away - and this asserts that rather than assuming it. + """ + scaled = Decimal(amount) * USDC_SCALE + if scaled != scaled.to_integral_value(): + raise ValueError(f"amount {amount} does not convert to whole base units.") + return int(scaled) + + +def check_leverage(value): + """Validate a leverage setting and return it as ``int``.""" + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"leverage must be an integer; got {value!r}.") + if not MIN_LEVERAGE <= value <= MAX_LEVERAGE: + raise ValueError( + f"leverage={value} is outside the supported range " + f"{MIN_LEVERAGE}-{MAX_LEVERAGE}x." + ) + return value + + +def check_address(value, *, name="address"): + """Validate that a value is a base58 32-byte Solana address and return it. + + A transfer destination cannot be recovered if it is wrong, and a typo in base58 + often still decodes - just to different bytes - so the length check is the part that + catches real mistakes. + """ + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must be a non-empty base58 string.") + value = value.strip() + try: + decoded = base58.b58decode(value) + except Exception as exc: + raise ValueError(f"{name} is not valid base58.") from exc + if len(decoded) != PUBLIC_KEY_BYTES: + raise ValueError( + f"{name} decodes to {len(decoded)} bytes; a Solana address is " + f"{PUBLIC_KEY_BYTES} bytes." + ) + return value diff --git a/rest/api_agent_keys.py b/rest/api_agent_keys.py index 0b8fe96..0847cb4 100644 --- a/rest/api_agent_keys.py +++ b/rest/api_agent_keys.py @@ -1,139 +1,140 @@ -""" -This example shows how to bind an api agent key (also called agent wallet) -to an account and use the api agent key to sign on behalf of the account -to create a market order. -""" - -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -BIND_AGENT_WALLET_API_URL = f"{REST_URL}/agent/bind" -MARKET_ORDER_API_URL = f"{REST_URL}/orders/create_market" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Generate a new agent wallet - agent_wallet_private_key = Keypair() - agent_wallet_public_key = str(agent_wallet_private_key.pubkey()) - - # --------------------------------------------------------------- - # Bind agent wallet - # --------------------------------------------------------------- - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "bind_agent_wallet", - } - - # Construct the signature payload - signature_payload = { - "agent_wallet": agent_wallet_public_key, - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(BIND_AGENT_WALLET_API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Agent Wallet: {agent_wallet_public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - print("\n") - - # --------------------------------------------------------------- - # Create market order - # --------------------------------------------------------------- - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_market_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "reduce_only": False, - "amount": "0.1", - "side": "bid", - "slippage_percent": "0.5", - "client_order_id": str(uuid.uuid4()), - } - - # Use the helper function to sign the message, with the agent wallet's private key - message, signature = sign_message( - signature_header, signature_payload, agent_wallet_private_key - ) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "agent_wallet": agent_wallet_public_key, # use the agent wallet's public key - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(MARKET_ORDER_API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +""" +This example shows how to bind an api agent key (also called agent wallet) +to an account and use the api agent key to sign on behalf of the account +to create a market order. +""" + +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +BIND_AGENT_WALLET_API_URL = f"{REST_URL}/agent/bind" +MARKET_ORDER_API_URL = f"{REST_URL}/orders/create_market" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Generate a new agent wallet + agent_wallet_private_key = Keypair() + agent_wallet_public_key = str(agent_wallet_private_key.pubkey()) + + # --------------------------------------------------------------- + # Bind agent wallet + # --------------------------------------------------------------- + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "bind_agent_wallet", + } + + # Construct the signature payload + signature_payload = { + "agent_wallet": agent_wallet_public_key, + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(BIND_AGENT_WALLET_API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Agent Wallet: {agent_wallet_public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + print("\n") + + # --------------------------------------------------------------- + # Create market order + # --------------------------------------------------------------- + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_market_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "reduce_only": False, + "amount": "0.1", + "side": "bid", + "slippage_percent": "0.5", + "client_order_id": str(uuid.uuid4()), + } + + # Use the helper function to sign the message, with the agent wallet's private key + message, signature = sign_message( + signature_header, signature_payload, agent_wallet_private_key + ) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "agent_wallet": agent_wallet_public_key, # use the agent wallet's public key + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(MARKET_ORDER_API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/api_agent_keys_detailed.py b/rest/api_agent_keys_detailed.py index 8ed63bc..0d0d8d3 100644 --- a/rest/api_agent_keys_detailed.py +++ b/rest/api_agent_keys_detailed.py @@ -1,351 +1,351 @@ -import time -import uuid -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - -# Agent Wallet Management Endpoints -BIND_ENDPOINT = f"{REST_URL}/agent/bind" -LIST_ENDPOINT = f"{REST_URL}/agent/list" -REVOKE_ENDPOINT = f"{REST_URL}/agent/revoke" -REVOKE_ALL_ENDPOINT = f"{REST_URL}/agent/revoke_all" - -# IP Whitelist Management Endpoints -IP_LIST_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/list" -IP_ADD_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/add" -IP_REMOVE_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/remove" -IP_TOGGLE_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/toggle" - - -def bind_agent_wallet(keypair: Keypair, agent_wallet_address: str): - """Bind an agent wallet to your account.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "bind_agent_wallet", - } - - # Construct the signature payload. - signature_payload = { - "agent_wallet": agent_wallet_address, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - print(f"Message: {message}") - print(f"Signature: {signature}") - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(BIND_ENDPOINT, json=request, headers=headers) - - return response - - -def list_agent_wallets(keypair: Keypair): - """List all bound agent wallets.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "list_agent_wallets", - } - - # Construct the signature payload. - signature_payload = {} - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(LIST_ENDPOINT, json=request, headers=headers) - - return response - - -def revoke_agent_wallet(keypair: Keypair, agent_wallet_address: str): - """Revoke a specific agent wallet.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "revoke_agent_wallet", - } - - # Construct the signature payload. - signature_payload = { - "agent_wallet": agent_wallet_address, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(REVOKE_ENDPOINT, json=request, headers=headers) - - return response - - -def revoke_all_agent_wallets(keypair: Keypair): - """Revoke all agent wallets.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "revoke_all_agent_wallets", - } - - # Construct the signature payload. - signature_payload = {} - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(REVOKE_ALL_ENDPOINT, json=request, headers=headers) - - return response - - -def list_ip_whitelist(keypair: Keypair, agent_wallet_address: str): - """List IP addresses in the whitelist for an agent wallet.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "list_agent_ip_whitelist", - } - - # Construct the signature payload. - signature_payload = { - "api_agent_key": agent_wallet_address, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(IP_LIST_ENDPOINT, json=request, headers=headers) - - return response - - -def add_ip_to_whitelist(keypair: Keypair, agent_wallet_address: str, ip_address: str): - """Add an IP address to the whitelist.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "add_agent_whitelisted_ip", - } - - # Construct the signature payload. - signature_payload = { - "agent_wallet": agent_wallet_address, - "ip_address": ip_address, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(IP_ADD_ENDPOINT, json=request, headers=headers) - - return response - - -def remove_ip_from_whitelist( - keypair: Keypair, agent_wallet_address: str, ip_address: str -): - """Remove an IP address from the whitelist.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "remove_agent_whitelisted_ip", - } - - # Construct the signature payload. - signature_payload = { - "agent_wallet": agent_wallet_address, - "ip_address": ip_address, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(IP_REMOVE_ENDPOINT, json=request, headers=headers) - - return response - - -def toggle_ip_whitelist(keypair: Keypair, agent_wallet_address: str, enabled: bool): - """Enable or disable IP whitelist enforcement.""" - public_key = str(keypair.pubkey()) - - # Scaffold the signature header. - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "set_agent_ip_whitelist_enabled", - } - - # Construct the signature payload. - signature_payload = { - "agent_wallet": agent_wallet_address, - "enabled": enabled, - } - - # Use the helper function to sign the message. - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields. - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(IP_TOGGLE_ENDPOINT, json=request, headers=headers) - - return response +import time +import uuid +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message + +# Agent Wallet Management Endpoints +BIND_ENDPOINT = f"{REST_URL}/agent/bind" +LIST_ENDPOINT = f"{REST_URL}/agent/list" +REVOKE_ENDPOINT = f"{REST_URL}/agent/revoke" +REVOKE_ALL_ENDPOINT = f"{REST_URL}/agent/revoke_all" + +# IP Whitelist Management Endpoints +IP_LIST_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/list" +IP_ADD_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/add" +IP_REMOVE_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/remove" +IP_TOGGLE_ENDPOINT = f"{REST_URL}/agent/ip_whitelist/toggle" + + +def bind_agent_wallet(keypair: Keypair, agent_wallet_address: str): + """Bind an agent wallet to your account.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "bind_agent_wallet", + } + + # Construct the signature payload. + signature_payload = { + "agent_wallet": agent_wallet_address, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + print(f"Message: {message}") + print(f"Signature: {signature}") + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(BIND_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def list_agent_wallets(keypair: Keypair): + """List all bound agent wallets.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "list_agent_wallets", + } + + # Construct the signature payload. + signature_payload = {} + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(LIST_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def revoke_agent_wallet(keypair: Keypair, agent_wallet_address: str): + """Revoke a specific agent wallet.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "revoke_agent_wallet", + } + + # Construct the signature payload. + signature_payload = { + "agent_wallet": agent_wallet_address, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(REVOKE_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def revoke_all_agent_wallets(keypair: Keypair): + """Revoke all agent wallets.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "revoke_all_agent_wallets", + } + + # Construct the signature payload. + signature_payload = {} + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(REVOKE_ALL_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def list_ip_whitelist(keypair: Keypair, agent_wallet_address: str): + """List IP addresses in the whitelist for an agent wallet.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "list_agent_ip_whitelist", + } + + # Construct the signature payload. + signature_payload = { + "api_agent_key": agent_wallet_address, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(IP_LIST_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def add_ip_to_whitelist(keypair: Keypair, agent_wallet_address: str, ip_address: str): + """Add an IP address to the whitelist.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "add_agent_whitelisted_ip", + } + + # Construct the signature payload. + signature_payload = { + "agent_wallet": agent_wallet_address, + "ip_address": ip_address, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(IP_ADD_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def remove_ip_from_whitelist( + keypair: Keypair, agent_wallet_address: str, ip_address: str +): + """Remove an IP address from the whitelist.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "remove_agent_whitelisted_ip", + } + + # Construct the signature payload. + signature_payload = { + "agent_wallet": agent_wallet_address, + "ip_address": ip_address, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(IP_REMOVE_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def toggle_ip_whitelist(keypair: Keypair, agent_wallet_address: str, enabled: bool): + """Enable or disable IP whitelist enforcement.""" + public_key = str(keypair.pubkey()) + + # Scaffold the signature header. + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "set_agent_ip_whitelist_enabled", + } + + # Construct the signature payload. + signature_payload = { + "agent_wallet": agent_wallet_address, + "enabled": enabled, + } + + # Use the helper function to sign the message. + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields. + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(IP_TOGGLE_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response diff --git a/rest/api_config_keys.py b/rest/api_config_keys.py index 3ddbad8..1c20bf9 100644 --- a/rest/api_config_keys.py +++ b/rest/api_config_keys.py @@ -1,173 +1,174 @@ -""" -This example shows how to create, revoke, and list api config keys for an account. -Please refer to https://docs.pacifica.fi/api-documentation/api/rate-limits/api-config-keys#using-a-pacifica-api-config-key -for the use of API Config Keys. -""" - -import time -import json - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -CREATE_ENDPOINT = f"{REST_URL}/account/api_keys/create" -REVOKE_ENDPOINT = f"{REST_URL}/account/api_keys/revoke" -LIST_ENDPOINT = f"{REST_URL}/account/api_keys" - -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def create_api_config_key(keypair: Keypair): - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "create_api_key", - } - - # Construct the signature payload - signature_payload = {} - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - print(f"Message: {message}") - print(f"Signature: {signature}") - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(CREATE_ENDPOINT, json=request, headers=headers) - - return response - - -def revoke_api_config_key(keypair: Keypair, api_key: str): - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "revoke_api_key", - } - - # Construct the signature payload - signature_payload = { - "api_key": api_key, - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - print(f"Message: {message}") - print(f"Signature: {signature}") - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(REVOKE_ENDPOINT, json=request, headers=headers) - - return response - - -def list_api_config_keys(keypair: Keypair): - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5000, - "type": "list_api_keys", - } - - # Construct the signature payload - signature_payload = {} - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - print(f"Message: {message}") - print(f"Signature: {signature}") - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "agent_wallet": None, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - response = requests.post(LIST_ENDPOINT, json=request, headers=headers) - - return response - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - - print("Creating API Config Key") - response = create_api_config_key(keypair) - print(json.dumps(response.json(), indent=4)) - - api_key = response.json()["data"]["api_key"] - - print("Listing API Config Keys") - response = list_api_config_keys(keypair) - print(json.dumps(response.json(), indent=4)) - - print(f"Revoking API Config Key {api_key}") - response = revoke_api_config_key(keypair, api_key) - print(json.dumps(response.json(), indent=4)) - - print("Listing API Keys") - response = list_api_config_keys(keypair) - print(json.dumps(response.json(), indent=4)) - - -if __name__ == "__main__": - main() +""" +This example shows how to create, revoke, and list api config keys for an account. +Please refer to https://docs.pacifica.fi/api-documentation/api/rate-limits/api-config-keys#using-a-pacifica-api-config-key +for the use of API Config Keys. +""" + +import time +import json + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +CREATE_ENDPOINT = f"{REST_URL}/account/api_keys/create" +REVOKE_ENDPOINT = f"{REST_URL}/account/api_keys/revoke" +LIST_ENDPOINT = f"{REST_URL}/account/api_keys" + +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def create_api_config_key(keypair: Keypair): + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "create_api_key", + } + + # Construct the signature payload + signature_payload = {} + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + print(f"Message: {message}") + print(f"Signature: {signature}") + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(CREATE_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def revoke_api_config_key(keypair: Keypair, api_key: str): + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "revoke_api_key", + } + + # Construct the signature payload + signature_payload = { + "api_key": api_key, + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + print(f"Message: {message}") + print(f"Signature: {signature}") + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(REVOKE_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def list_api_config_keys(keypair: Keypair): + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5000, + "type": "list_api_keys", + } + + # Construct the signature payload + signature_payload = {} + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + print(f"Message: {message}") + print(f"Signature: {signature}") + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "agent_wallet": None, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + response = requests.post(LIST_ENDPOINT, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + return response + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + + print("Creating API Config Key") + response = create_api_config_key(keypair) + print(json.dumps(response.json(), indent=4)) + + api_key = response.json()["data"]["api_key"] + + print("Listing API Config Keys") + response = list_api_config_keys(keypair) + print(json.dumps(response.json(), indent=4)) + + print(f"Revoking API Config Key {api_key}") + response = revoke_api_config_key(keypair, api_key) + print(json.dumps(response.json(), indent=4)) + + print("Listing API Keys") + response = list_api_config_keys(keypair) + print(json.dumps(response.json(), indent=4)) + + +if __name__ == "__main__": + main() diff --git a/rest/batch_orders.py b/rest/batch_orders.py index d1b44e2..f054de6 100644 --- a/rest/batch_orders.py +++ b/rest/batch_orders.py @@ -1,111 +1,115 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/batch" -PRIVATE_KEY = "" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - timestamp = int(time.time() * 1_000) - request_list = [] - - # BATCH ORDER 1: CREATE ORDER - - # Scaffold the signature header - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "price": str(100_000), - "reduce_only": False, - "amount": "0.1", - "side": "bid", - "tif": "GTC", - "client_order_id": str(uuid.uuid4()), - } - - # Use the helper function to sign the message - _, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - request = { - **request_header, - **signature_payload, - } - request_list.append( - { - "type": "Create", - "data": request, - } - ) - - # BATCH ORDER 2: CANCEL ORDER - - # Scaffold the signature header - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "cancel_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "order_id": 42069, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - - # Use the helper function to sign the message - _, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - request = { - **request_header, - **signature_payload, - } - request_list.append( - { - "type": "Cancel", - "data": request, - } - ) - - # Send the request - headers = {"Content-Type": "application/json"} - request_payload = {"actions": request_list} - response = requests.post(API_URL, json=request_payload, headers=headers) - - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Requests: {requests}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/batch" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + timestamp = int(time.time() * 1_000) + request_list = [] + + # BATCH ORDER 1: CREATE ORDER + + # Scaffold the signature header + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "price": str(100_000), + "reduce_only": False, + "amount": "0.1", + "side": "bid", + "tif": "GTC", + "client_order_id": str(uuid.uuid4()), + } + + # Use the helper function to sign the message + _, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + request = { + **request_header, + **signature_payload, + } + request_list.append( + { + "type": "Create", + "data": request, + } + ) + + # BATCH ORDER 2: CANCEL ORDER + + # Scaffold the signature header + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "cancel_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "order_id": 42069, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + + # Use the helper function to sign the message + _, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + request = { + **request_header, + **signature_payload, + } + request_list.append( + { + "type": "Cancel", + "data": request, + } + ) + + # Send the request + headers = {"Content-Type": "application/json"} + request_payload = {"actions": request_list} + response = requests.post(API_URL, json=request_payload, headers=headers, timeout=REQUEST_TIMEOUT) + + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + # `requests` here is the HTTP library module, not the payload - the original line + # printed `` and its filesystem path instead of the + # batch that was sent, which is the one thing this debug line existed to show. + print(f"Requests: {request_payload}") + + +if __name__ == "__main__": + main() diff --git a/rest/cancel_all_orders.py b/rest/cancel_all_orders.py index 9717b3f..6898cf9 100644 --- a/rest/cancel_all_orders.py +++ b/rest/cancel_all_orders.py @@ -1,65 +1,66 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - -API_URL = f"{REST_URL}/orders/cancel_all" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "cancel_all_orders", - } - - # Construct the signature payload - signature_payload = { - "all_symbols": True, - "exclude_reduce_only": False, - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + +API_URL = f"{REST_URL}/orders/cancel_all" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "cancel_all_orders", + } + + # Construct the signature payload + signature_payload = { + "all_symbols": True, + "exclude_reduce_only": False, + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/cancel_order.py b/rest/cancel_order.py index 5716b2f..5407a5c 100644 --- a/rest/cancel_order.py +++ b/rest/cancel_order.py @@ -1,66 +1,67 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/cancel" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "cancel_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "order_id": 42069, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/cancel" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "cancel_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "order_id": 42069, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/cancel_twap_order.py b/rest/cancel_twap_order.py index d3c2c88..6e8196a 100644 --- a/rest/cancel_twap_order.py +++ b/rest/cancel_twap_order.py @@ -1,66 +1,67 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/twap/cancel" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "cancel_twap_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "order_id": 3, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/twap/cancel" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "cancel_twap_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "order_id": 3, # or "client_order_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_lake.py b/rest/create_lake.py index 9cb0581..d01efce 100644 --- a/rest/create_lake.py +++ b/rest/create_lake.py @@ -1,66 +1,67 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/lake/create" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_lake", - } - - # Construct the signature payload - signature_payload = { - "manager": public_key, # or other valid account, cannot be a sublake - "nickname": "Moraine Lake", # optional field - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/lake/create" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_lake", + } + + # Construct the signature payload + signature_payload = { + "manager": public_key, # or other valid account, cannot be a sublake + "nickname": "Moraine Lake", # optional field + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_limit_order.py b/rest/create_limit_order.py index 0d93f9c..53d2aa5 100644 --- a/rest/create_limit_order.py +++ b/rest/create_limit_order.py @@ -1,72 +1,73 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/create" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "price": str(100_000), - "reduce_only": False, - "amount": "0.1", - "side": "bid", - "tif": "GTC", - "client_order_id": str(uuid.uuid4()), - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/create" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "price": str(100_000), + "reduce_only": False, + "amount": "0.1", + "side": "bid", + "tif": "GTC", + "client_order_id": str(uuid.uuid4()), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_market_order.py b/rest/create_market_order.py index 820cfdb..c2bc822 100644 --- a/rest/create_market_order.py +++ b/rest/create_market_order.py @@ -1,71 +1,72 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/create_market" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_market_order", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "reduce_only": False, - "amount": "0.1", - "side": "bid", - "slippage_percent": "0.5", - "client_order_id": str(uuid.uuid4()), - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/create_market" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_market_order", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "reduce_only": False, + "amount": "0.1", + "side": "bid", + "slippage_percent": "0.5", + "client_order_id": str(uuid.uuid4()), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_position_tpsl.py b/rest/create_position_tpsl.py index c590445..a0de1e4 100644 --- a/rest/create_position_tpsl.py +++ b/rest/create_position_tpsl.py @@ -1,80 +1,81 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -# Assume a BTC long position has already been opened -API_URL = f"{REST_URL}/positions/tpsl" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "set_position_tpsl", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "side": "ask", - "take_profit": { - "stop_price": "120000", - "limit_price": "120300", - "amount": "0.1", - "client_order_id": str(uuid.uuid4()), - }, - "stop_loss": { - "stop_price": "99800", - # omitting limit_price to place a market order at trigger - # omitting amount to use the full position size - # client_order_id is optional - }, - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +# Assume a BTC long position has already been opened +API_URL = f"{REST_URL}/positions/tpsl" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "set_position_tpsl", + } + + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "side": "ask", + "take_profit": { + "stop_price": "120000", + "limit_price": "120300", + "amount": "0.1", + "client_order_id": str(uuid.uuid4()), + }, + "stop_loss": { + "stop_price": "99800", + # omitting limit_price to place a market order at trigger + # omitting amount to use the full position size + # client_order_id is optional + }, + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_subaccount.py b/rest/create_subaccount.py index 0e1961b..d8c0c12 100644 --- a/rest/create_subaccount.py +++ b/rest/create_subaccount.py @@ -1,138 +1,139 @@ -""" -## Authentication Flow - -The authentication flow uses a cross-signature scheme to ensure that both the main account and the subaccount consent to the relationship. This is necessary because: - -1. The main account must authorize the creation of a subaccount under its control -2. The subaccount must consent to being controlled by the main account -3. The API server must verify both signatures to prevent unauthorized subaccount creation - -``` -┌─────────────┐ ┌────────────┐ ┌────────────┐ -│ Main Account│ │ Subaccount │ │ API Server │ -└──────┬──────┘ └─────┬──────┘ └─────┬──────┘ - │ │ │ - │ │ │ - │ Step 1: Sign main_pubkey │ │ - │◄────────────────────────────┤ │ - │ │ │ - │ │ │ - │ Step 2: Sign sub_signature │ │ - ├────────────────────────────►│ │ - │ │ │ - │ │ │ - │ Step 3: Send both signature │ │ - └─────────────────────────────┼───────────────────────────►│ - │ │ - │ │ - │ Step 4: Verify - │ signatures - │ │ - │ │ - │ Step 5: Create - │ relationship - │ │ -``` - -## Authentication Steps - -1. **Subaccount Signs Main Account's Public Key**: - - - The subaccount signs the main account's public key using its private key - - This creates the `sub_signature` which proves the subaccount consents to the relationship - -2. **Main Account Signs the Subaccount's Signature**: - - - The main account signs the `sub_signature` using its private key - - This creates the `main_signature` which proves the main account consents to the relationship - -3. **API Server Verification**: - - The API server verifies that `sub_signature` was created by the subaccount's private key by signing the main account's public key - - The API server verifies that `main_signature` was created by the main account's private key by signing the `sub_signature` - - If both verifications succeed, the subaccount relationship is established -""" - -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - -API_URL = f"{REST_URL}/account/subaccount/create" -MAIN_PRIVATE_KEY = "" -SUB_PRIVATE_KEY = "" - - -def main(): - - # Generate main and sub accounts from private keys - main_keypair = Keypair.from_base58_string(MAIN_PRIVATE_KEY) - sub_keypair = Keypair.from_base58_string(SUB_PRIVATE_KEY) - - # Generate a timestamp and expiry window - # Both signatures must have the same timestamp and expiry window. - timestamp = int(time.time() * 1_000) - expiry_window = 5_000 - - # Get public keys - main_public_key = str(main_keypair.pubkey()) - sub_public_key = str(sub_keypair.pubkey()) - - # Step 1: Subaccount signs the main account's public key - subaccount_signature_header = { - "timestamp": timestamp, - "expiry_window": expiry_window, - "type": "subaccount_initiate", - } - - payload = {"account": main_public_key} - - subaccount_message, subaccount_signature = sign_message( - subaccount_signature_header, payload, sub_keypair - ) - - # Step 2: Main account signs the sub_signature - main_account_signature_header = { - "timestamp": timestamp, - "expiry_window": expiry_window, - "type": "subaccount_confirm", - } - - payload = {"signature": subaccount_signature} - - main_account_message, main_signature = sign_message( - main_account_signature_header, payload, main_keypair - ) - - # Step 3: Create and send the request - request = { - "main_account": main_public_key, - "subaccount": sub_public_key, - "main_signature": main_signature, - "sub_signature": subaccount_signature, - "timestamp": timestamp, - "expiry_window": expiry_window, - } - - # Send the request - headers = {"Content-Type": "application/json"} - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Main Account: {main_public_key}") - print(f"Main Message: {main_account_message}") - print(f"Main Signature: {main_signature}") - print(f"Sub Account: {sub_public_key}") - print(f"Sub Message: {subaccount_message}") - print(f"Sub Signature: {subaccount_signature}") - - -if __name__ == "__main__": - main() +""" +## Authentication Flow + +The authentication flow uses a cross-signature scheme to ensure that both the main account and the subaccount consent to the relationship. This is necessary because: + +1. The main account must authorize the creation of a subaccount under its control +2. The subaccount must consent to being controlled by the main account +3. The API server must verify both signatures to prevent unauthorized subaccount creation + +``` +┌─────────────┐ ┌────────────┐ ┌────────────┐ +│ Main Account│ │ Subaccount │ │ API Server │ +└──────┬──────┘ └─────┬──────┘ └─────┬──────┘ + │ │ │ + │ │ │ + │ Step 1: Sign main_pubkey │ │ + │◄────────────────────────────┤ │ + │ │ │ + │ │ │ + │ Step 2: Sign sub_signature │ │ + ├────────────────────────────►│ │ + │ │ │ + │ │ │ + │ Step 3: Send both signature │ │ + └─────────────────────────────┼───────────────────────────►│ + │ │ + │ │ + │ Step 4: Verify + │ signatures + │ │ + │ │ + │ Step 5: Create + │ relationship + │ │ +``` + +## Authentication Steps + +1. **Subaccount Signs Main Account's Public Key**: + + - The subaccount signs the main account's public key using its private key + - This creates the `sub_signature` which proves the subaccount consents to the relationship + +2. **Main Account Signs the Subaccount's Signature**: + + - The main account signs the `sub_signature` using its private key + - This creates the `main_signature` which proves the main account consents to the relationship + +3. **API Server Verification**: + - The API server verifies that `sub_signature` was created by the subaccount's private key by signing the main account's public key + - The API server verifies that `main_signature` was created by the main account's private key by signing the `sub_signature` + - If both verifications succeed, the subaccount relationship is established +""" + +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + +API_URL = f"{REST_URL}/account/subaccount/create" +MAIN_PRIVATE_KEY = load_private_key("PACIFICA_MAIN_PRIVATE_KEY") +SUB_PRIVATE_KEY = load_private_key("PACIFICA_SUB_PRIVATE_KEY") + + +def main(): + + # Generate main and sub accounts from private keys + main_keypair = Keypair.from_base58_string(MAIN_PRIVATE_KEY) + sub_keypair = Keypair.from_base58_string(SUB_PRIVATE_KEY) + + # Generate a timestamp and expiry window + # Both signatures must have the same timestamp and expiry window. + timestamp = int(time.time() * 1_000) + expiry_window = 5_000 + + # Get public keys + main_public_key = str(main_keypair.pubkey()) + sub_public_key = str(sub_keypair.pubkey()) + + # Step 1: Subaccount signs the main account's public key + subaccount_signature_header = { + "timestamp": timestamp, + "expiry_window": expiry_window, + "type": "subaccount_initiate", + } + + payload = {"account": main_public_key} + + subaccount_message, subaccount_signature = sign_message( + subaccount_signature_header, payload, sub_keypair + ) + + # Step 2: Main account signs the sub_signature + main_account_signature_header = { + "timestamp": timestamp, + "expiry_window": expiry_window, + "type": "subaccount_confirm", + } + + payload = {"signature": subaccount_signature} + + main_account_message, main_signature = sign_message( + main_account_signature_header, payload, main_keypair + ) + + # Step 3: Create and send the request + request = { + "main_account": main_public_key, + "subaccount": sub_public_key, + "main_signature": main_signature, + "sub_signature": subaccount_signature, + "timestamp": timestamp, + "expiry_window": expiry_window, + } + + # Send the request + headers = {"Content-Type": "application/json"} + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Main Account: {main_public_key}") + print(f"Main Message: {main_account_message}") + print(f"Main Signature: {main_signature}") + print(f"Sub Account: {sub_public_key}") + print(f"Sub Message: {subaccount_message}") + print(f"Sub Signature: {subaccount_signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_subaccount_hardware.py b/rest/create_subaccount_hardware.py index 2f7bbee..5cb63ab 100644 --- a/rest/create_subaccount_hardware.py +++ b/rest/create_subaccount_hardware.py @@ -1,88 +1,89 @@ -import time -import json - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message, sign_with_hardware_wallet - -API_URL = f"{REST_URL}/account/subaccount/create" -MAIN_HARDWARE_PUB_KEY = "" -MAIN_HARDWARE_PATH = "" # e.g. "usb://ledger?key=1" -SUB_PRIVATE_KEY = "" - - -def main(): - - # Generate subaccount from private key - sub_keypair = Keypair.from_base58_string(SUB_PRIVATE_KEY) - - # Generate a timestamp and expiry window - # Both signatures must have the same timestamp and expiry window. - timestamp = int(time.time() * 1_000) - expiry_window = 200_000 - - # Get public keys - sub_public_key = str(sub_keypair.pubkey()) - - # Step 1: Subaccount signs the main account's public key - subaccount_signature_header = { - "timestamp": timestamp, - "expiry_window": expiry_window, - "type": "subaccount_initiate", - } - - payload = {"account": MAIN_HARDWARE_PUB_KEY} - - subaccount_message, subaccount_signature = sign_message( - subaccount_signature_header, payload, sub_keypair - ) - - # Step 2: Main account signs the sub_signature - main_account_signature_header = { - "timestamp": timestamp, - "expiry_window": expiry_window, - "type": "subaccount_confirm", - } - - payload = {"signature": subaccount_signature} - - print("Signing with hardware wallet...") - main_account_message, main_signature = sign_with_hardware_wallet( - main_account_signature_header, payload, MAIN_HARDWARE_PATH - ) - - # Step 3: Create and send the request - request = { - "main_account": MAIN_HARDWARE_PUB_KEY, - "subaccount": sub_public_key, - "main_signature": { - "type": "hardware", - "value": main_signature, - }, - "sub_signature": subaccount_signature, - "timestamp": timestamp, - "expiry_window": expiry_window, - } - - # Send the request - headers = {"Content-Type": "application/json"} - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Main Account: {MAIN_HARDWARE_PUB_KEY}") - print(f"Main Message: {main_account_message}") - print(f"Main Signature: {main_signature}") - print(f"Sub Account: {sub_public_key}") - print(f"Sub Message: {subaccount_message}") - print(f"Sub Signature: {subaccount_signature}") - - -if __name__ == "__main__": - main() +import time +import json + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message, sign_with_hardware_wallet +from common.env import load_private_key, load_public_key, load_wallet_path + +API_URL = f"{REST_URL}/account/subaccount/create" +MAIN_HARDWARE_PUB_KEY = load_public_key("PACIFICA_MAIN_HARDWARE_PUB_KEY") +MAIN_HARDWARE_PATH = load_wallet_path("PACIFICA_MAIN_HARDWARE_PATH") +SUB_PRIVATE_KEY = load_private_key("PACIFICA_SUB_PRIVATE_KEY") + + +def main(): + + # Generate subaccount from private key + sub_keypair = Keypair.from_base58_string(SUB_PRIVATE_KEY) + + # Generate a timestamp and expiry window + # Both signatures must have the same timestamp and expiry window. + timestamp = int(time.time() * 1_000) + expiry_window = 200_000 + + # Get public keys + sub_public_key = str(sub_keypair.pubkey()) + + # Step 1: Subaccount signs the main account's public key + subaccount_signature_header = { + "timestamp": timestamp, + "expiry_window": expiry_window, + "type": "subaccount_initiate", + } + + payload = {"account": MAIN_HARDWARE_PUB_KEY} + + subaccount_message, subaccount_signature = sign_message( + subaccount_signature_header, payload, sub_keypair + ) + + # Step 2: Main account signs the sub_signature + main_account_signature_header = { + "timestamp": timestamp, + "expiry_window": expiry_window, + "type": "subaccount_confirm", + } + + payload = {"signature": subaccount_signature} + + print("Signing with hardware wallet...") + main_account_message, main_signature = sign_with_hardware_wallet( + main_account_signature_header, payload, MAIN_HARDWARE_PATH + ) + + # Step 3: Create and send the request + request = { + "main_account": MAIN_HARDWARE_PUB_KEY, + "subaccount": sub_public_key, + "main_signature": { + "type": "hardware", + "value": main_signature, + }, + "sub_signature": subaccount_signature, + "timestamp": timestamp, + "expiry_window": expiry_window, + } + + # Send the request + headers = {"Content-Type": "application/json"} + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Main Account: {MAIN_HARDWARE_PUB_KEY}") + print(f"Main Message: {main_account_message}") + print(f"Main Signature: {main_signature}") + print(f"Sub Account: {sub_public_key}") + print(f"Sub Message: {subaccount_message}") + print(f"Sub Signature: {subaccount_signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/create_twap_order.py b/rest/create_twap_order.py index 364b33a..1f34630 100644 --- a/rest/create_twap_order.py +++ b/rest/create_twap_order.py @@ -1,73 +1,74 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/twap/create" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "create_twap_order", - } - - planned_sub_order_count = 7 - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "reduce_only": False, - "amount": "1", - "side": "bid", - "slippage_percent": "0.5", - "duration_in_seconds": 30 * (planned_sub_order_count - 1), - "client_order_id": str(uuid.uuid4()), - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + + +API_URL = f"{REST_URL}/orders/twap/create" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "create_twap_order", + } + + planned_sub_order_count = 7 + # Construct the signature payload + signature_payload = { + "symbol": "BTC", + "reduce_only": False, + "amount": "1", + "side": "bid", + "slippage_percent": "0.5", + "duration_in_seconds": 30 * (planned_sub_order_count - 1), + "client_order_id": str(uuid.uuid4()), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/deposit.py b/rest/deposit.py index ac6e03b..3f15cd5 100644 --- a/rest/deposit.py +++ b/rest/deposit.py @@ -1,86 +1,98 @@ -from borsh_construct import CStruct, U64 -from solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta -from solders.pubkey import Pubkey -from solana.rpc.api import Client -from solana.transaction import Transaction -from spl.token.constants import TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID -import hashlib - -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" -DEPOSIT_AMOUNT = 4200.69 # minimum amount is 10 - -PROGRAM_ID = Pubkey.from_string("PCFA5iYgmqK6MqPhWNKg7Yv7auX7VZ4Cx7T1eJyrAMH") -CENTRAL_STATE = Pubkey.from_string("9Gdmhq4Gv1LnNMp7aiS1HSVd7pNnXNMsbuXALCQRmGjY") -PACIFICA_VAULT = Pubkey.from_string("72R843XwZxqWhsJceARQQTTbYtWy6Zw9et2YV4FpRHTa") -USDC_MINT = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") -SYS_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") - -RPC_URL = "https://api.mainnet-beta.solana.com" - -deposit_layout = CStruct("amount" / U64) - - -def get_discriminator(name: str) -> bytes: - return hashlib.sha256(f"global:{name}".encode()).digest()[:8] - - -def build_deposit_instruction_data(amount: float) -> bytes: - borsh_args = deposit_layout.build( - {"amount": int(round(amount * 1_000_000))} - ) # 6 decimals - return get_discriminator("deposit") + borsh_args - - -def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: - return Pubkey.find_program_address( - [ - bytes(owner), - bytes(TOKEN_PROGRAM_ID), - bytes(mint), - ], - ASSOCIATED_TOKEN_PROGRAM_ID, - )[0] - - -def main(): - # Load user keypair - keypair = Keypair.from_base58_string(PRIVATE_KEY) - client = Client(RPC_URL) - - # Get associated token address - user_usdc_ata = get_associated_token_address(keypair.pubkey(), USDC_MINT) - event_authority, _ = Pubkey.find_program_address([b"__event_authority"], PROGRAM_ID) - - # Prepare accounts - keys = [ - AccountMeta( - pubkey=keypair.pubkey(), is_signer=True, is_writable=True - ), # depositor - AccountMeta( - pubkey=user_usdc_ata, is_signer=False, is_writable=True - ), # depositorUsdcAccount - AccountMeta(pubkey=CENTRAL_STATE, is_signer=False, is_writable=True), - AccountMeta(pubkey=PACIFICA_VAULT, is_signer=False, is_writable=True), - AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), - AccountMeta( - pubkey=ASSOCIATED_TOKEN_PROGRAM_ID, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=USDC_MINT, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), - AccountMeta(pubkey=event_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=PROGRAM_ID, is_signer=False, is_writable=False), - ] - - # Build instruction - data = build_deposit_instruction_data(DEPOSIT_AMOUNT) - ix = Instruction(program_id=PROGRAM_ID, accounts=keys, data=data) - - # Build and send transaction - tx = Transaction().add(ix) - resp = client.send_transaction(tx, keypair) - print("Deposit transaction signature:", resp) - - -if __name__ == "__main__": - main() +from borsh_construct import CStruct, U64 +from solders.keypair import Keypair +from solders.instruction import Instruction, AccountMeta +from solders.pubkey import Pubkey +from solana.rpc.api import Client +from solana.transaction import Transaction +from spl.token.constants import TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID +import hashlib +from common.env import load_private_key + +from common.env import load_private_key +from common.validate import check_amount, to_base_units + +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") +# The amount is a *string*, validated into a `Decimal` below. It used to be the float +# literal `4200.69`, which the instruction encoder then scaled with +# `int(round(amount * 1_000_000))` - and `4200.69` is not representable in binary +# floating point, so the base-unit count depended on how that product happened to round. +# For an irreversible on-chain transfer, the number of base units must follow from the +# number the user wrote, not from the rounding of an intermediate double. +DEPOSIT_AMOUNT = "4200.69" # minimum amount is 10 + +PROGRAM_ID = Pubkey.from_string("PCFA5iYgmqK6MqPhWNKg7Yv7auX7VZ4Cx7T1eJyrAMH") +CENTRAL_STATE = Pubkey.from_string("9Gdmhq4Gv1LnNMp7aiS1HSVd7pNnXNMsbuXALCQRmGjY") +PACIFICA_VAULT = Pubkey.from_string("72R843XwZxqWhsJceARQQTTbYtWy6Zw9et2YV4FpRHTa") +USDC_MINT = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") +SYS_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") + +RPC_URL = "https://api.mainnet-beta.solana.com" + +deposit_layout = CStruct("amount" / U64) + + +def get_discriminator(name: str) -> bytes: + return hashlib.sha256(f"global:{name}".encode()).digest()[:8] + + +def build_deposit_instruction_data(amount: str) -> bytes: + # `check_amount` enforces the documented 10 USDC floor and rejects anything with + # more than six decimal places; `to_base_units` then does the scaling in exact + # decimal arithmetic and refuses any value that would not land on a whole base unit. + validated = check_amount(amount, name="DEPOSIT_AMOUNT", minimum="10") + borsh_args = deposit_layout.build({"amount": to_base_units(validated)}) # 6 decimals + return get_discriminator("deposit") + borsh_args + + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + return Pubkey.find_program_address( + [ + bytes(owner), + bytes(TOKEN_PROGRAM_ID), + bytes(mint), + ], + ASSOCIATED_TOKEN_PROGRAM_ID, + )[0] + + +def main(): + # Load user keypair + keypair = Keypair.from_base58_string(PRIVATE_KEY) + client = Client(RPC_URL) + + # Get associated token address + user_usdc_ata = get_associated_token_address(keypair.pubkey(), USDC_MINT) + event_authority, _ = Pubkey.find_program_address([b"__event_authority"], PROGRAM_ID) + + # Prepare accounts + keys = [ + AccountMeta( + pubkey=keypair.pubkey(), is_signer=True, is_writable=True + ), # depositor + AccountMeta( + pubkey=user_usdc_ata, is_signer=False, is_writable=True + ), # depositorUsdcAccount + AccountMeta(pubkey=CENTRAL_STATE, is_signer=False, is_writable=True), + AccountMeta(pubkey=PACIFICA_VAULT, is_signer=False, is_writable=True), + AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta( + pubkey=ASSOCIATED_TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), + AccountMeta(pubkey=USDC_MINT, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=event_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # Build instruction + data = build_deposit_instruction_data(DEPOSIT_AMOUNT) + ix = Instruction(program_id=PROGRAM_ID, accounts=keys, data=data) + + # Build and send transaction + tx = Transaction().add(ix) + resp = client.send_transaction(tx, keypair) + print("Deposit transaction signature:", resp) + + +if __name__ == "__main__": + main() diff --git a/rest/get_open_twap_order.py b/rest/get_open_twap_order.py index fbc340a..c8cbb93 100644 --- a/rest/get_open_twap_order.py +++ b/rest/get_open_twap_order.py @@ -1,28 +1,32 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/twap" -PUBLIC_KEY = "" # e.g. "dev1S2tC8CSZXzTQzVacYvkqWwD37dTqiCKaeJCWhwM" - - -def main(): - - request = API_URL+"?account="+PUBLIC_KEY - response = requests.get(request) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Account: {PUBLIC_KEY}") - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_public_key + + +API_URL = f"{REST_URL}/orders/twap" +PUBLIC_KEY = load_public_key("PACIFICA_PUBLIC_KEY") + + +def main(): + # `params=` rather than string concatenation: `requests` then percent-encodes the + # value, so an address containing a `&`, `#` or space cannot graft extra query + # parameters onto the request. It also keeps the URL and its query separable in + # logs. The timeout is explicit because `requests` has no default one. + params = {"account": PUBLIC_KEY} + response = requests.get(API_URL, params=params, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {response.url}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Account: {PUBLIC_KEY}") + +if __name__ == "__main__": + main() diff --git a/rest/get_twap_order_history.py b/rest/get_twap_order_history.py index e3f9a2b..d0590a2 100644 --- a/rest/get_twap_order_history.py +++ b/rest/get_twap_order_history.py @@ -1,28 +1,31 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/twap/history" -PUBLIC_KEY = "" # e.g. "dev1S2tC8CSZXzTQzVacYvkqWwD37dTqiCKaeJCWhwM" - -def main(): - - request = API_URL+"?account="+PUBLIC_KEY - response = requests.get(request) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Account: {PUBLIC_KEY}") - - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_public_key + + +API_URL = f"{REST_URL}/orders/twap/history" +PUBLIC_KEY = load_public_key("PACIFICA_PUBLIC_KEY") + +def main(): + # See the note in get_open_twap_order.py: `params=` percent-encodes the account so + # it cannot inject additional query parameters, and the timeout is explicit because + # `requests` does not apply one by default. + params = {"account": PUBLIC_KEY} + response = requests.get(API_URL, params=params, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {response.url}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Account: {PUBLIC_KEY}") + + +if __name__ == "__main__": + main() diff --git a/rest/get_twap_order_history_by_id.py b/rest/get_twap_order_history_by_id.py index bff5d83..17010e9 100644 --- a/rest/get_twap_order_history_by_id.py +++ b/rest/get_twap_order_history_by_id.py @@ -1,28 +1,33 @@ -import time -import uuid - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/orders/twap/history_by_id" -ORDER_ID = "" # e.g. 6 - - -def main(): - - request = API_URL+"?order_id="+ORDER_ID - response = requests.get(request); - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Order id: {ORDER_ID}") - -if __name__ == "__main__": - main() +import time +import uuid + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import require_env + + +API_URL = f"{REST_URL}/orders/twap/history_by_id" +# Not a secret, but read from the environment for the same reason as the keys: nothing +# about running an example should require editing a tracked file. +ORDER_ID = require_env("PACIFICA_TWAP_ORDER_ID", hint='e.g. `export PACIFICA_TWAP_ORDER_ID="6"`.') + + +def main(): + # See the note in get_open_twap_order.py. `order_id` is passed through `params=` + # rather than concatenated, so a value that is not a bare number cannot append + # further query parameters of its own. + params = {"order_id": ORDER_ID} + response = requests.get(API_URL, params=params, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {response.url}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Order id: {ORDER_ID}") + +if __name__ == "__main__": + main() diff --git a/rest/lake_deposit.py b/rest/lake_deposit.py index 49037a5..bfb245c 100644 --- a/rest/lake_deposit.py +++ b/rest/lake_deposit.py @@ -1,68 +1,69 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/lake/deposit" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" -LAKE_ADDRESS = "" # same base58 address as a regular account -AMOUNT = 100_000 - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "deposit_to_lake", - } - - # Construct the signature payload - signature_payload = { - "lake": LAKE_ADDRESS, - "amount": str(AMOUNT), - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key, load_public_key + + +API_URL = f"{REST_URL}/lake/deposit" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") +LAKE_ADDRESS = load_public_key("PACIFICA_LAKE_ADDRESS") +AMOUNT = 100_000 + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "deposit_to_lake", + } + + # Construct the signature payload + signature_payload = { + "lake": LAKE_ADDRESS, + "amount": str(AMOUNT), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/lake_withdraw.py b/rest/lake_withdraw.py index ad1636d..1a203d6 100644 --- a/rest/lake_withdraw.py +++ b/rest/lake_withdraw.py @@ -1,68 +1,69 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/lake/withdraw" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" -LAKE_ADDRESS = "" # same base58 address as a regular account -SHARES = 100 - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "withdraw_from_lake", - } - - # Construct the signature payload - signature_payload = { - "lake": LAKE_ADDRESS, - "shares": str(SHARES), - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key, load_public_key + + +API_URL = f"{REST_URL}/lake/withdraw" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") +LAKE_ADDRESS = load_public_key("PACIFICA_LAKE_ADDRESS") +SHARES = 100 + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "withdraw_from_lake", + } + + # Construct the signature payload + signature_payload = { + "lake": LAKE_ADDRESS, + "shares": str(SHARES), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/list_subaccounts.py b/rest/list_subaccounts.py index 9543573..7990b03 100644 --- a/rest/list_subaccounts.py +++ b/rest/list_subaccounts.py @@ -1,76 +1,77 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - -API_URL = f"{REST_URL}/account/subaccount/list" -PRIVATE_KEY = "" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Generate a timestamp and expiry window - timestamp = int(time.time() * 1000) - - # Create the signed message for listing subaccounts - signature_header = { - "expiry_window": 5_000, - "timestamp": timestamp, - "type": "list_subaccounts", - } - - signature_payload = {} # No additional data needed for listing - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - headers = {"Content-Type": "application/json"} - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - if response.status_code == 200: - data = response.json() - if data.get("success") and "data" in data: - subaccounts = data["data"]["subaccounts"] - print(f"\nFound {len(subaccounts)} subaccounts:") - for i, subaccount in enumerate(subaccounts, 1): - print(f" {i}. Address: {subaccount['address']}") - print(f" Balance: {subaccount['balance']}") - print(f" Fee Level: {subaccount['fee_level']}") - print(f" Fee Mode: {subaccount['fee_mode']}") - print(f" Created: {subaccount['created_at']}") - print() - else: - print("No subaccounts found or error in response") - else: - print("Error occurred while fetching subaccounts") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key + +API_URL = f"{REST_URL}/account/subaccount/list" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Generate a timestamp and expiry window + timestamp = int(time.time() * 1000) + + # Create the signed message for listing subaccounts + signature_header = { + "expiry_window": 5_000, + "timestamp": timestamp, + "type": "list_subaccounts", + } + + signature_payload = {} # No additional data needed for listing + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + headers = {"Content-Type": "application/json"} + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + if response.status_code == 200: + data = response.json() + if data.get("success") and "data" in data: + subaccounts = data["data"]["subaccounts"] + print(f"\nFound {len(subaccounts)} subaccounts:") + for i, subaccount in enumerate(subaccounts, 1): + print(f" {i}. Address: {subaccount['address']}") + print(f" Balance: {subaccount['balance']}") + print(f" Fee Level: {subaccount['fee_level']}") + print(f" Fee Mode: {subaccount['fee_mode']}") + print(f" Created: {subaccount['created_at']}") + print() + else: + print("No subaccounts found or error in response") + else: + print("Error occurred while fetching subaccounts") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/transfer_subaccount_fund.py b/rest/transfer_subaccount_fund.py index d30a068..ef1706e 100644 --- a/rest/transfer_subaccount_fund.py +++ b/rest/transfer_subaccount_fund.py @@ -1,68 +1,75 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/account/subaccount/transfer" -FROM_PRIVATE_KEY = "" # must be a main account or a subaccount -TO_PUBLIC_KEY = "" # must be the above's child subaccount or parent main account - - -def main(): - # Generate account based on private key - from_keypair = Keypair.from_base58_string(FROM_PRIVATE_KEY) - from_public_key = str(from_keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "transfer_funds", - } - - # Construct the signature payload - signature_payload = { - "to_account": TO_PUBLIC_KEY, - "amount": "420.69", - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, from_keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": from_public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"From Account: {from_public_key}") - print(f"To Account: {TO_PUBLIC_KEY}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key, load_public_key +from common.validate import check_address, check_amount + + +API_URL = f"{REST_URL}/account/subaccount/transfer" +FROM_PRIVATE_KEY = load_private_key("PACIFICA_FROM_PRIVATE_KEY") +TO_PUBLIC_KEY = load_public_key("PACIFICA_TO_PUBLIC_KEY") + + +def main(): + # Generate account based on private key + from_keypair = Keypair.from_base58_string(FROM_PRIVATE_KEY) + from_public_key = str(from_keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "transfer_funds", + } + + # Construct the signature payload + # + # The amount is kept as a string and validated as a `Decimal`: the signature is + # computed over these exact characters, and a transfer is not reversible, so the + # value that is signed must be the value that was written. `check_address` catches a + # destination typo that still base58-decodes but to the wrong length. + signature_payload = { + "to_account": check_address(TO_PUBLIC_KEY, name="to_account"), + "amount": str(check_amount("420.69", name="amount")), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, from_keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": from_public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"From Account: {from_public_key}") + print(f"To Account: {TO_PUBLIC_KEY}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/transfer_subaccount_fund_hardware.py b/rest/transfer_subaccount_fund_hardware.py index de159f9..5f6a91a 100644 --- a/rest/transfer_subaccount_fund_hardware.py +++ b/rest/transfer_subaccount_fund_hardware.py @@ -1,71 +1,72 @@ -import time -import json - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_with_hardware_wallet - - -API_URL = f"{REST_URL}/account/subaccount/transfer" -HARDWARE_PATH = "" # e.g. "usb://ledger?key=1" -FROM_HARDWARE_PUB_KEY = "" # must be a main account in hardware wallet -TO_PUBLIC_KEY = "" # must be the above's child subaccount - - -def main(): - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 200_000, - "type": "transfer_funds", - } - - # Construct the signature payload - signature_payload = { - "to_account": TO_PUBLIC_KEY, - "amount": "420.69", - } - - print("Signing with hardware wallet...") - message, signature = sign_with_hardware_wallet( - signature_header, signature_payload, HARDWARE_PATH - ) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": FROM_HARDWARE_PUB_KEY, - "signature": { - "type": "hardware", - "value": signature, - }, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"From Account: {FROM_HARDWARE_PUB_KEY}") - print(f"To Account: {TO_PUBLIC_KEY}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time +import json + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_with_hardware_wallet +from common.env import load_public_key, load_wallet_path + + +API_URL = f"{REST_URL}/account/subaccount/transfer" +HARDWARE_PATH = load_wallet_path("PACIFICA_HARDWARE_PATH") +FROM_HARDWARE_PUB_KEY = load_public_key("PACIFICA_FROM_HARDWARE_PUB_KEY") +TO_PUBLIC_KEY = load_public_key("PACIFICA_TO_PUBLIC_KEY") + + +def main(): + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 200_000, + "type": "transfer_funds", + } + + # Construct the signature payload + signature_payload = { + "to_account": TO_PUBLIC_KEY, + "amount": "420.69", + } + + print("Signing with hardware wallet...") + message, signature = sign_with_hardware_wallet( + signature_header, signature_payload, HARDWARE_PATH + ) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": FROM_HARDWARE_PUB_KEY, + "signature": { + "type": "hardware", + "value": signature, + }, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"From Account: {FROM_HARDWARE_PUB_KEY}") + print(f"To Account: {TO_PUBLIC_KEY}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/rest/update_leverage.py b/rest/update_leverage.py index d091757..cfed560 100644 --- a/rest/update_leverage.py +++ b/rest/update_leverage.py @@ -1,66 +1,73 @@ -import time - -import requests -from solders.keypair import Keypair - -from common.constants import REST_URL -from common.utils import sign_message - - -API_URL = f"{REST_URL}/account/leverage" -PRIVATE_KEY = "" # e.g. "2Z2Wn4kN5ZNhZzuFTQSyTiN4ixX8U6ew5wPDJbHngZaC3zF3uWNj4dQ63cnGfXpw1cESZPCqvoZE7VURyuj9kf8b" - - -def main(): - # Generate account based on private key - keypair = Keypair.from_base58_string(PRIVATE_KEY) - public_key = str(keypair.pubkey()) - - # Scaffold the signature header - timestamp = int(time.time() * 1_000) - - signature_header = { - "timestamp": timestamp, - "expiry_window": 5_000, - "type": "update_leverage", - } - - # Construct the signature payload - signature_payload = { - "symbol": "BTC", - "leverage": 42, - } - - # Use the helper function to sign the message - message, signature = sign_message(signature_header, signature_payload, keypair) - - # Construct the request reusing the payload and constructing common request fields - request_header = { - "account": public_key, - "signature": signature, - "timestamp": signature_header["timestamp"], - "expiry_window": signature_header["expiry_window"], - } - - # Send the request - headers = {"Content-Type": "application/json"} - - request = { - **request_header, - **signature_payload, - } - - response = requests.post(API_URL, json=request, headers=headers) - print(f"Status Code: {response.status_code}") - print(f"Response: {response.text}") - print(f"Request: {request}") - - # Print details for debugging - print("\nDebug Info:") - print(f"Address: {public_key}") - print(f"Message: {message}") - print(f"Signature: {signature}") - - -if __name__ == "__main__": - main() +import time + +import requests +from solders.keypair import Keypair + +from common.constants import REQUEST_TIMEOUT, REST_URL +from common.utils import sign_message +from common.env import load_private_key +from common.validate import check_leverage + + +API_URL = f"{REST_URL}/account/leverage" +PRIVATE_KEY = load_private_key("PACIFICA_PRIVATE_KEY") + + +def main(): + # Generate account based on private key + keypair = Keypair.from_base58_string(PRIVATE_KEY) + public_key = str(keypair.pubkey()) + + # Scaffold the signature header + timestamp = int(time.time() * 1_000) + + signature_header = { + "timestamp": timestamp, + "expiry_window": 5_000, + "type": "update_leverage", + } + + # Construct the signature payload + # + # `check_leverage` rejects a value outside Pacifica's 1-50x range before it is + # signed. This script has no confirmation step, so an accidental `420` here would + # otherwise be submitted as-is and rejected only by the API - after the account's + # leverage had already been changed on any earlier successful attempt. + signature_payload = { + "symbol": "BTC", + "leverage": check_leverage(42), + } + + # Use the helper function to sign the message + message, signature = sign_message(signature_header, signature_payload, keypair) + + # Construct the request reusing the payload and constructing common request fields + request_header = { + "account": public_key, + "signature": signature, + "timestamp": signature_header["timestamp"], + "expiry_window": signature_header["expiry_window"], + } + + # Send the request + headers = {"Content-Type": "application/json"} + + request = { + **request_header, + **signature_payload, + } + + response = requests.post(API_URL, json=request, headers=headers, timeout=REQUEST_TIMEOUT) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + print(f"Request: {request}") + + # Print details for debugging + print("\nDebug Info:") + print(f"Address: {public_key}") + print(f"Message: {message}") + print(f"Signature: {signature}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 0000000..012dd19 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,158 @@ +"""Tests for common/validate.py. + +Run from the repository root with: + + python -m unittest discover -s tests -v + +`base58` is not installed in every environment these examples are read in, and the +audit that produced these tests was not authorised to install packages, so a minimal +base58 decoder is injected into `sys.modules` before importing the module under test. +It is deliberately only a *decoder* - that is the whole surface `check_address` uses - +and it is implemented straightforwardly from the alphabet so the length assertions +below are testing real base58 semantics rather than a stub that agrees with them. +""" + +import sys +import types +import unittest +from decimal import Decimal +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +def _b58decode(value): + if isinstance(value, bytes): + value = value.decode("ascii") + number = 0 + for char in value: + index = _ALPHABET.find(char) + if index < 0: + raise ValueError(f"invalid base58 character: {char!r}") + number = number * 58 + index + body = number.to_bytes((number.bit_length() + 7) // 8, "big") if number else b"" + # Each leading '1' encodes one leading zero byte. + leading_zeros = len(value) - len(value.lstrip("1")) + return b"\x00" * leading_zeros + body + + +if "base58" not in sys.modules: + stub = types.ModuleType("base58") + stub.b58decode = _b58decode + sys.modules["base58"] = stub + +from common.validate import ( # noqa: E402 (import must follow the stub above) + MAX_LEVERAGE, + check_address, + check_amount, + check_leverage, + to_base_units, +) + +# A real 32-byte Solana address (the SPL token program), used because it is public, +# well-known, and not anyone's account. +VALID_ADDRESS = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + + +class TestCheckAmount(unittest.TestCase): + def test_accepts_a_decimal_string(self): + self.assertEqual(check_amount("420.69"), Decimal("420.69")) + + def test_rejects_a_float(self): + # The regression this guards: `4200.69` as a float has already lost precision + # by the time it arrives, so it is refused rather than quietly accepted. + with self.assertRaises(TypeError): + check_amount(4200.69) + + def test_rejects_zero_and_negative(self): + for value in ["0", "-1", "-0.000001"]: + with self.subTest(value=value): + with self.assertRaises(ValueError): + check_amount(value) + + def test_rejects_non_numeric(self): + for value in ["", "abc", "1.2.3", "1e"]: + with self.subTest(value=value): + with self.assertRaises(ValueError): + check_amount(value) + + def test_rejects_nan_and_infinity(self): + for value in ["NaN", "Infinity", "-Infinity"]: + with self.subTest(value=value): + with self.assertRaises(ValueError): + check_amount(value) + + def test_rejects_more_precision_than_the_token_has(self): + check_amount("1.123456") + with self.assertRaises(ValueError): + check_amount("1.1234567") + + def test_enforces_a_minimum(self): + with self.assertRaises(ValueError): + check_amount("9.99", minimum="10") + self.assertEqual(check_amount("10.01", minimum="10"), Decimal("10.01")) + + def test_enforces_a_maximum_when_given(self): + with self.assertRaises(ValueError): + check_amount("1000", maximum="500") + self.assertEqual(check_amount("500", maximum="500"), Decimal("500")) + + +class TestToBaseUnits(unittest.TestCase): + def test_scales_exactly(self): + self.assertEqual(to_base_units(Decimal("4200.69")), 4_200_690_000) + self.assertEqual(to_base_units(Decimal("0.000001")), 1) + + def test_matches_the_written_decimal_not_the_float(self): + # `int(round(4200.69 * 1_000_000))` - the previous implementation - is compared + # against the exact decimal result. They agree for this literal, which is why + # the old code appeared to work; the point is that the new path does not depend + # on that coincidence. `0.1 + 0.2` is asserted separately as the case where + # binary floating point visibly diverges. + self.assertEqual(to_base_units(Decimal("4200.69")), int(round(4200.69 * 1_000_000))) + self.assertEqual(to_base_units(Decimal("0.1") + Decimal("0.2")), 300_000) + self.assertNotEqual(0.1 + 0.2, 0.3) + + +class TestCheckLeverage(unittest.TestCase): + def test_accepts_the_supported_range(self): + self.assertEqual(check_leverage(1), 1) + self.assertEqual(check_leverage(MAX_LEVERAGE), MAX_LEVERAGE) + + def test_rejects_out_of_range(self): + for value in [0, -1, MAX_LEVERAGE + 1, 420]: + with self.subTest(value=value): + with self.assertRaises(ValueError): + check_leverage(value) + + def test_rejects_non_integers(self): + for value in ["10", 10.5, True, None]: + with self.subTest(value=value): + with self.assertRaises(TypeError): + check_leverage(value) + + +class TestCheckAddress(unittest.TestCase): + def test_accepts_a_32_byte_address(self): + self.assertEqual(check_address(VALID_ADDRESS), VALID_ADDRESS) + + def test_trims_surrounding_whitespace(self): + self.assertEqual(check_address(f" {VALID_ADDRESS}\n"), VALID_ADDRESS) + + def test_rejects_the_wrong_length(self): + # A truncated address is still valid base58, which is exactly why the length + # check is the part that catches a real paste error. + with self.assertRaises(ValueError): + check_address(VALID_ADDRESS[:-4]) + + def test_rejects_non_base58_and_empty(self): + for value in ["not base58 !", "", " ", None, 42]: + with self.subTest(value=value): + with self.assertRaises(ValueError): + check_address(value) + + +if __name__ == "__main__": + unittest.main()