From 654cb57af0535576db92fa173632545c8f0c4227 Mon Sep 17 00:00:00 2001 From: yruej301 Date: Tue, 25 Feb 2025 17:25:03 -0500 Subject: [PATCH 1/5] support place_isolated_order --- tests/conftest.py | 12 + .../test_place_isolated_order.py | 362 ++++++++++++++++++ tests/engine_client/test_place_order.py | 3 +- .../test_place_trigger_order.py | 3 +- vertex_protocol/client/apis/market/execute.py | 16 + vertex_protocol/contracts/eip712/types.py | 10 + vertex_protocol/contracts/types.py | 1 + vertex_protocol/engine_client/execute.py | 21 + .../engine_client/types/execute.py | 57 +++ vertex_protocol/utils/execute.py | 28 +- 10 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 tests/engine_client/test_place_isolated_order.py diff --git a/tests/conftest.py b/tests/conftest.py index a076f4f..68771c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -135,6 +135,18 @@ def order_params(senders: list[str]) -> dict: } +@pytest.fixture +def isolated_order_params(senders: list[str]) -> dict: + return { + "sender": hex_to_bytes32(senders[0]), + "priceX18": 28898000000000000000000, + "amount": -10000000000000000, + "expiration": 4611687701117784255, + "nonce": 1764428860167815857, + "margin": 10000000000000000, + } + + @pytest.fixture def cancellation_params(senders: str) -> dict: return { diff --git a/tests/engine_client/test_place_isolated_order.py b/tests/engine_client/test_place_isolated_order.py new file mode 100644 index 0000000..4d7b25f --- /dev/null +++ b/tests/engine_client/test_place_isolated_order.py @@ -0,0 +1,362 @@ +import json +from unittest.mock import MagicMock + +from eth_account import Account +from eth_account.signers.local import LocalAccount +from vertex_protocol.contracts.eip712.sign import ( + build_eip712_typed_data, + sign_eip712_typed_data, +) +from vertex_protocol.engine_client import EngineClient +from vertex_protocol.contracts.types import VertexExecuteType + +from vertex_protocol.engine_client.types.execute import ( + IsolatedOrderParams, + PlaceIsolatedOrderParams, + PlaceIsolatedOrderRequest, + to_execute_request, +) +from vertex_protocol.utils.bytes32 import ( + bytes32_to_hex, + hex_to_bytes32, + subaccount_to_bytes32, +) +import pytest +from vertex_protocol.utils.exceptions import ExecuteFailedException + +from vertex_protocol.utils.nonce import gen_order_nonce +from vertex_protocol.utils.subaccount import SubaccountParams + + +def test_place_isolated_order_params( + senders: list[str], owners: list[str], isolated_order_params: dict +): + product_id = 1 + sender = hex_to_bytes32(senders[0]) + params_from_dict = PlaceIsolatedOrderParams( + **{ + "product_id": product_id, + "isolated_order": { + "sender": senders[0], + "priceX18": isolated_order_params["priceX18"], + "amount": isolated_order_params["amount"], + "expiration": isolated_order_params["expiration"], + "margin": isolated_order_params["margin"], + }, + } + ) + params_from_obj = PlaceIsolatedOrderParams( + product_id=product_id, + isolated_order=IsolatedOrderParams( + sender=senders[0], + priceX18=isolated_order_params["priceX18"], + amount=isolated_order_params["amount"], + expiration=isolated_order_params["expiration"], + margin=isolated_order_params["margin"], + ), + ) + bytes32_sender = PlaceIsolatedOrderParams( + product_id=product_id, + isolated_order=IsolatedOrderParams( + sender=hex_to_bytes32(senders[0]), + priceX18=isolated_order_params["priceX18"], + amount=isolated_order_params["amount"], + expiration=isolated_order_params["expiration"], + margin=isolated_order_params["margin"], + ), + ) + subaccount_params_sender = PlaceIsolatedOrderParams( + product_id=product_id, + isolated_order=IsolatedOrderParams( + sender=SubaccountParams( + subaccount_owner=owners[0], subaccount_name="default" + ), + priceX18=isolated_order_params["priceX18"], + amount=isolated_order_params["amount"], + expiration=isolated_order_params["expiration"], + margin=isolated_order_params["margin"], + ), + ) + + assert ( + params_from_dict + == params_from_obj + == bytes32_sender + == subaccount_params_sender + ) + + assert params_from_dict.product_id == product_id + assert params_from_dict.isolated_order.sender == sender + assert params_from_dict.isolated_order.amount == isolated_order_params["amount"] + assert params_from_dict.isolated_order.priceX18 == isolated_order_params["priceX18"] + assert ( + params_from_dict.isolated_order.expiration + == isolated_order_params["expiration"] + ) + assert params_from_dict.signature is None + + params_from_dict.signature = ( + "0x51ba8762bc5f77957a4e896dba34e17b553b872c618ffb83dba54878796f2821" + ) + params_from_dict.isolated_order.nonce = gen_order_nonce() + place_isolated_order_req = PlaceIsolatedOrderRequest( + place_isolated_order=params_from_dict + ) + assert place_isolated_order_req == to_execute_request(params_from_dict) + assert place_isolated_order_req.dict() == { + "place_isolated_order": { + "product_id": product_id, + "isolated_order": { + "sender": senders[0].lower(), + "priceX18": str(isolated_order_params["priceX18"]), + "amount": str(isolated_order_params["amount"]), + "expiration": str(isolated_order_params["expiration"]), + "nonce": str(params_from_dict.isolated_order.nonce), + "margin": str(isolated_order_params["margin"]), + }, + "signature": params_from_dict.signature, + } + } + + params_from_dict.id = 100 + place_isolated_order_req = PlaceIsolatedOrderRequest( + place_isolated_order=params_from_dict + ) + assert place_isolated_order_req == to_execute_request(params_from_dict) + assert place_isolated_order_req.dict() == { + "place_isolated_order": { + "id": 100, + "product_id": product_id, + "isolated_order": { + "sender": senders[0].lower(), + "priceX18": str(isolated_order_params["priceX18"]), + "amount": str(isolated_order_params["amount"]), + "expiration": str(isolated_order_params["expiration"]), + "nonce": str(params_from_dict.isolated_order.nonce), + "margin": str(isolated_order_params["margin"]), + }, + "signature": params_from_dict.signature, + } + } + + +def test_place_isolated_order_execute_fails_incomplete_client( + mock_post: MagicMock, + url: str, + chain_id: int, + book_addrs: list[str], + private_keys: list[str], + senders: list[str], + isolated_order_params: dict, +): + engine_client = EngineClient({"url": url}) + place_isolated_order_params = { + "product_id": 1, + "isolated_order": isolated_order_params, + } + + with pytest.raises(AttributeError, match="Book addresses are not set"): + engine_client.place_isolated_order(place_isolated_order_params) + + engine_client.book_addrs = book_addrs + + with pytest.raises(AttributeError, match="Chain ID is not set."): + engine_client.place_isolated_order(place_isolated_order_params) + + engine_client.chain_id = chain_id + + with pytest.raises(AttributeError, match="Signer is not set."): + engine_client.place_isolated_order(place_isolated_order_params) + + engine_client.signer = Account.from_key(private_keys[0]) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "status": "success", + "signature": "xxx", + } + mock_post.return_value = mock_response + + res = engine_client.place_isolated_order(place_isolated_order_params) + place_isolated_order_req = PlaceIsolatedOrderRequest(**res.req) + + assert ( + place_isolated_order_req.place_isolated_order.isolated_order.sender.lower() + == senders[0].lower() + ) + + +def test_place_isolated_order_execute_success( + engine_client: EngineClient, mock_post: MagicMock, senders: list[str] +): + product_id = 1 + place_isolated_order_params = PlaceIsolatedOrderParams( + product_id=product_id, + isolated_order=IsolatedOrderParams( + sender=SubaccountParams(subaccount_name="default"), + priceX18=1000, + amount=1000, + expiration=1000, + nonce=1000, + margin=1000, + ), + ) + + isolated_order = place_isolated_order_params.isolated_order.copy(deep=True) + order_digest = "0x123" + isolated_order.sender = hex_to_bytes32(senders[0]) + + with pytest.raises( + ValueError, + match="Missing `product_id` to sign place_order or place_isolated_order execute", + ): + engine_client._sign( + VertexExecuteType.PLACE_ISOLATED_ORDER, isolated_order.dict() + ) + + expected_signature = engine_client._sign( + VertexExecuteType.PLACE_ISOLATED_ORDER, + isolated_order.dict(), + product_id=place_isolated_order_params.product_id, + ) + computed_signature = sign_eip712_typed_data( + typed_data=build_eip712_typed_data( + VertexExecuteType.PLACE_ISOLATED_ORDER, + isolated_order.dict(), + engine_client._opts.book_addrs[1], + engine_client.chain_id, + ), + signer=engine_client._opts.linked_signer, + ) + + assert expected_signature == computed_signature + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "status": "success", + "signature": expected_signature, + "data": {"digest": order_digest}, + } + mock_post.return_value = mock_response + + res = engine_client.place_isolated_order(place_isolated_order_params) + place_isolated_order_req = PlaceIsolatedOrderRequest(**res.req) + + assert ( + place_isolated_order_req.place_isolated_order.signature + == res.signature + == expected_signature + ) + assert ( + place_isolated_order_req.place_isolated_order.isolated_order.sender.lower() + == senders[0].lower() + ) + assert res.status == "success" + assert res.error is None + assert res.data.digest == order_digest + + mock_response.status_code = 200 + json_response = { + "status": "failure", + "error_code": 1000, + "error": "Too Many Requests!", + } + mock_response.json.return_value = json_response + mock_post.return_value.text = json.dumps(json_response) + + with pytest.raises(ExecuteFailedException, match=json.dumps(json_response)): + engine_client.place_isolated_order(place_isolated_order_params) + + # deactivate linked signer + engine_client.linked_signer = None + + expected_signature = sign_eip712_typed_data( + typed_data=build_eip712_typed_data( + VertexExecuteType.PLACE_ISOLATED_ORDER, + isolated_order.dict(), + engine_client._opts.book_addrs[1], + engine_client.chain_id, + ), + signer=engine_client._opts.signer, + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "status": "success", + "signature": expected_signature, + } + mock_post.return_value = mock_response + + res = engine_client.place_isolated_order(place_isolated_order_params) + place_isolated_order_req = PlaceIsolatedOrderRequest(**res.req) + + assert place_isolated_order_req.place_isolated_order.signature == expected_signature + + +def test_place_order_execute_provide_full_params( + mock_post: MagicMock, + url: str, + chain_id: int, + private_keys: list[str], + book_addrs: str, +): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "status": "success", + "signature": "xxx", + } + mock_post.return_value = mock_response + + engine_client = EngineClient({"url": url}) + signer: LocalAccount = Account.from_key(private_keys[0]) + sender = subaccount_to_bytes32(signer.address, "default") + product_id = 1 + isolated_order_params = { + "priceX18": 10000, + "amount": 10000, + "sender": sender, + "nonce": gen_order_nonce(), + "expiration": 10000, + "margin": 10000, + } + signature = engine_client.sign( + VertexExecuteType.PLACE_ISOLATED_ORDER, + isolated_order_params, + book_addrs[1], + chain_id, + signer, + ) + isolated_order_params["sender"] = bytes32_to_hex(isolated_order_params["sender"]) + res = engine_client.place_isolated_order( + { + "product_id": product_id, + "isolated_order": isolated_order_params, + "signature": signature, + } + ) + req = PlaceIsolatedOrderRequest(**res.req) + + assert req.place_isolated_order.signature == signature + assert req.place_isolated_order.isolated_order.amount == str( + isolated_order_params["amount"] + ) + assert req.place_isolated_order.isolated_order.priceX18 == str( + isolated_order_params["priceX18"] + ) + assert ( + req.place_isolated_order.isolated_order.sender + == isolated_order_params["sender"] + ) + assert req.place_isolated_order.isolated_order.nonce == str( + isolated_order_params["nonce"] + ) + assert req.place_isolated_order.isolated_order.expiration == str( + isolated_order_params["expiration"] + ) + assert req.place_isolated_order.isolated_order.margin == str( + isolated_order_params["margin"] + ) diff --git a/tests/engine_client/test_place_order.py b/tests/engine_client/test_place_order.py index 269bd78..8fe30d7 100644 --- a/tests/engine_client/test_place_order.py +++ b/tests/engine_client/test_place_order.py @@ -189,7 +189,8 @@ def test_place_order_execute_success( order.sender = hex_to_bytes32(senders[0]) with pytest.raises( - ValueError, match="Missing `product_id` to sign place_order execute" + ValueError, + match="Missing `product_id` to sign place_order or place_isolated_order execute", ): engine_client._sign(VertexExecuteType.PLACE_ORDER, order.dict()) diff --git a/tests/trigger_client/test_place_trigger_order.py b/tests/trigger_client/test_place_trigger_order.py index 17ab559..560cc8e 100644 --- a/tests/trigger_client/test_place_trigger_order.py +++ b/tests/trigger_client/test_place_trigger_order.py @@ -202,7 +202,8 @@ def test_place_order_execute_success( order.sender = hex_to_bytes32(senders[0]) with pytest.raises( - ValueError, match="Missing `product_id` to sign place_order execute" + ValueError, + match="Missing `product_id` to sign place_order or place_isolated_order execute", ): trigger_client._sign(VertexExecuteType.PLACE_ORDER, order.dict()) diff --git a/vertex_protocol/client/apis/market/execute.py b/vertex_protocol/client/apis/market/execute.py index cd6a01e..9846581 100644 --- a/vertex_protocol/client/apis/market/execute.py +++ b/vertex_protocol/client/apis/market/execute.py @@ -7,6 +7,7 @@ MintLpParams, PlaceMarketOrderParams, PlaceOrderParams, + PlaceIsolatedOrderParams, ) from vertex_protocol.client.apis.base import VertexBaseAPI from vertex_protocol.trigger_client.types.execute import ( @@ -76,6 +77,21 @@ def place_order(self, params: PlaceOrderParams) -> ExecuteResponse: """ return self.context.engine_client.place_order(params) + def place_isolated_order(self, params: PlaceIsolatedOrderParams) -> ExecuteResponse: + """ + Places an isolated order through the engine. + + Args: + params (PlaceIsolatedOrderParams): Parameters required to place an isolated order. + + Returns: + ExecuteResponse: The response from the engine execution. + + Raises: + Exception: If there is an error during the execution or the response status is not "success". + """ + return self.context.engine_client.place_isolated_order(params) + def place_market_order(self, params: PlaceMarketOrderParams) -> ExecuteResponse: """ Places a market order through the engine. diff --git a/vertex_protocol/contracts/eip712/types.py b/vertex_protocol/contracts/eip712/types.py index 5c9cc77..0c43ea4 100644 --- a/vertex_protocol/contracts/eip712/types.py +++ b/vertex_protocol/contracts/eip712/types.py @@ -71,6 +71,16 @@ def get_vertex_eip712_type(tx: VertexTxType) -> dict: {"name": "nonce", "type": "uint64"}, ] }, + VertexTxType.PLACE_ISOLATED_ORDER: { + "IsolatedOrder": [ + {"name": "sender", "type": "bytes32"}, + {"name": "priceX18", "type": "int128"}, + {"name": "amount", "type": "int128"}, + {"name": "expiration", "type": "uint64"}, + {"name": "nonce", "type": "uint64"}, + {"name": "margin", "type": "int128"}, + ] + }, VertexTxType.CANCEL_ORDERS: { "Cancellation": [ {"name": "sender", "type": "bytes32"}, diff --git a/vertex_protocol/contracts/types.py b/vertex_protocol/contracts/types.py index 8b1e4c5..9360fe4 100644 --- a/vertex_protocol/contracts/types.py +++ b/vertex_protocol/contracts/types.py @@ -140,6 +140,7 @@ class VertexExecuteType(StrEnum): """ PLACE_ORDER = "place_order" + PLACE_ISOLATED_ORDER = "place_isolated_order" CANCEL_ORDERS = "cancel_orders" CANCEL_PRODUCT_ORDERS = "cancel_product_orders" CANCEL_AND_PLACE = "cancel_and_place" diff --git a/vertex_protocol/engine_client/execute.py b/vertex_protocol/engine_client/execute.py index d5711b6..216e0ef 100644 --- a/vertex_protocol/engine_client/execute.py +++ b/vertex_protocol/engine_client/execute.py @@ -19,6 +19,7 @@ LiquidateSubaccountParams, MintLpParams, OrderParams, + PlaceIsolatedOrderParams, PlaceMarketOrderParams, PlaceOrderParams, WithdrawCollateralParams, @@ -150,6 +151,26 @@ def place_order(self, params: PlaceOrderParams) -> ExecuteResponse: ) return self.execute(params) + def place_isolated_order(self, params: PlaceIsolatedOrderParams) -> ExecuteResponse: + """ + Execute a place isolated order operation. + + Args: + params (PlaceIsolatedOrderParams): Parameters required for placing an isolated order. + The parameters include the isolated order details. + + Returns: + ExecuteResponse: Response of the execution, including status and potential error message. + """ + params = PlaceIsolatedOrderParams.parse_obj(params) + params.isolated_order = self.prepare_execute_params(params.isolated_order, True) + params.signature = params.signature or self._sign( + VertexExecuteType.PLACE_ISOLATED_ORDER, + params.isolated_order.dict(), + params.product_id, + ) + return self.execute(params) + def place_market_order(self, params: PlaceMarketOrderParams) -> ExecuteResponse: """ Places an FOK order using top of the book price with provided slippage. diff --git a/vertex_protocol/engine_client/types/execute.py b/vertex_protocol/engine_client/types/execute.py index 3024c03..57ec450 100644 --- a/vertex_protocol/engine_client/types/execute.py +++ b/vertex_protocol/engine_client/types/execute.py @@ -4,6 +4,7 @@ from vertex_protocol.engine_client.types.models import ResponseStatus from vertex_protocol.utils.execute import ( BaseParamsSigned, + IsolatedOrderParams, MarketOrderParams, OrderParams, SignatureParams, @@ -44,6 +45,29 @@ class PlaceOrderParams(SignatureParams): spot_leverage: Optional[bool] +class PlaceIsolatedOrderParams(SignatureParams): + """ + Class for defining the parameters needed to place an isolated order. + + Attributes: + id (Optional[int]): An optional custom order id that is echoed back in subscription events e.g: fill orders, etc. + + product_id (int): The id of the product for which the order is being placed. + + isolated_order (IsolatedOrderParams): The parameters of the isolated order. + + digest (Optional[str]): An optional hash of the order data. + + borrow_margin (Optional[bool]): An optional flag indicating whether the order should borrow margin. By default, margin is borrowed. + """ + + id: Optional[int] + product_id: int + isolated_order: IsolatedOrderParams + digest: Optional[str] + borrow_margin: Optional[bool] + + class PlaceMarketOrderParams(SignatureParams): """ Class for defining the parameters needed to place a market order. @@ -225,6 +249,7 @@ def serialize_signer(cls, v: Subaccount) -> bytes: ExecuteParams = Union[ PlaceOrderParams, + PlaceIsolatedOrderParams, CancelOrdersParams, CancelProductOrdersParams, WithdrawCollateralParams, @@ -261,6 +286,33 @@ def serialize(cls, v: PlaceOrderParams) -> PlaceOrderParams: return v +class PlaceIsolatedOrderRequest(VertexBaseModel): + """ + Parameters for a request to place an isolated order. + + Attributes: + place_isolated_order (PlaceIsolatedOrderParams): The parameters for the isolated order to be placed. + + Methods: + serialize: Validates and serializes the order parameters. + """ + + place_isolated_order: PlaceIsolatedOrderParams + + @validator("place_isolated_order") + def serialize(cls, v: PlaceIsolatedOrderParams) -> PlaceIsolatedOrderParams: + if v.isolated_order.nonce is None: + raise ValueError("Missing order `nonce`") + if v.signature is None: + raise ValueError("Missing `signature") + if isinstance(v.isolated_order.sender, bytes): + v.isolated_order.serialize_dict(["sender"], bytes32_to_hex) + v.isolated_order.serialize_dict( + ["nonce", "priceX18", "amount", "expiration", "margin"], str + ) + return v + + class TxRequest(VertexBaseModel): """ Parameters for a transaction request. @@ -520,6 +572,7 @@ def serialize(cls, v: LinkSignerParams) -> LinkSignerParams: ExecuteRequest = Union[ PlaceOrderRequest, + PlaceIsolatedOrderRequest, CancelOrdersRequest, CancelProductOrdersRequest, CancelAndPlaceRequest, @@ -594,6 +647,10 @@ def to_execute_request(params: ExecuteParams) -> ExecuteRequest: """ execute_request_mapping = { PlaceOrderParams: (PlaceOrderRequest, VertexExecuteType.PLACE_ORDER.value), + PlaceIsolatedOrderParams: ( + PlaceIsolatedOrderRequest, + VertexExecuteType.PLACE_ISOLATED_ORDER.value, + ), CancelOrdersParams: ( CancelOrdersRequest, VertexExecuteType.CANCEL_ORDERS.value, diff --git a/vertex_protocol/utils/execute.py b/vertex_protocol/utils/execute.py index 701f7d8..9523bad 100644 --- a/vertex_protocol/utils/execute.py +++ b/vertex_protocol/utils/execute.py @@ -105,6 +105,25 @@ class OrderParams(MarketOrderParams): expiration: int +class IsolatedOrderParams(OrderParams): + """ + Class for defining the parameters of an isolated order. + + Attributes: + priceX18 (int): The price of the order with a precision of 18 decimal places. + + expiration (int): The unix timestamp at which the order will expire. + + amount (int): The amount of the asset to be bought or sold in the order. Positive for a `long` position and negative for a `short`. + + nonce (Optional[int]): A unique number used to prevent replay attacks. + + margin (int): The margin amount for the isolated order. + """ + + margin: int + + class VertexBaseExecute: def __init__(self, opts: VertexClientOpts): self._opts = opts @@ -287,9 +306,14 @@ def _sign( - For 'PLACE_ORDER', it's derived from the book address associated with the product_id. - For other operations, it's the endpoint address. """ - is_place_order = execute == VertexExecuteType.PLACE_ORDER + is_place_order = ( + execute == VertexExecuteType.PLACE_ORDER + or execute == VertexExecuteType.PLACE_ISOLATED_ORDER + ) if is_place_order and product_id is None: - raise ValueError("Missing `product_id` to sign place_order execute") + raise ValueError( + "Missing `product_id` to sign place_order or place_isolated_order execute" + ) verifying_contract = ( self.book_addr(product_id) if is_place_order and product_id From ea3cde964aa4ee4f649164a7a260c01b1bd7fcb6 Mon Sep 17 00:00:00 2001 From: yruej301 Date: Tue, 25 Feb 2025 17:32:25 -0500 Subject: [PATCH 2/5] add isolated sanity --- pyproject.toml | 1 + sanity/isolated.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 sanity/isolated.py diff --git a/pyproject.toml b/pyproject.toml index 32bc164..d489de0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ contracts-sanity = "sanity.contracts:run" client-sanity = "sanity.vertex_client:run" rewards-sanity = "sanity.rewards:run" signing-sanity = "sanity.signing:run" +isolated-sanity = "sanity.isolated:run" [[tool.poetry.source]] name = "private" diff --git a/sanity/isolated.py b/sanity/isolated.py new file mode 100644 index 0000000..3688fca --- /dev/null +++ b/sanity/isolated.py @@ -0,0 +1,74 @@ +import time +from sanity import CLIENT_MODE, SIGNER_PRIVATE_KEY + +from vertex_protocol.client import VertexClient, create_vertex_client +from vertex_protocol.contracts.types import DepositCollateralParams +from vertex_protocol.engine_client.types.models import SpotProductBalance +from vertex_protocol.utils.bytes32 import subaccount_to_hex +from vertex_protocol.utils.execute import IsolatedOrderParams +from vertex_protocol.utils.expiration import OrderType, get_expiration_timestamp +from vertex_protocol.utils.math import to_pow_10, to_x18 +from vertex_protocol.utils.nonce import gen_order_nonce +from vertex_protocol.utils.subaccount import SubaccountParams + + +def run(): + print("setting up vertex client...") + client: VertexClient = create_vertex_client(CLIENT_MODE, SIGNER_PRIVATE_KEY) + + print("chain_id:", client.context.engine_client.get_contracts().chain_id) + + print("minting test tokens...") + mint_tx_hash = client.spot._mint_mock_erc20(0, to_pow_10(100000, 6)) + print("mint tx hash:", mint_tx_hash) + + print("approving allowance...") + approve_allowance_tx_hash = client.spot.approve_allowance(0, to_pow_10(100000, 6)) + print("approve allowance tx hash:", approve_allowance_tx_hash) + + print("querying my allowance...") + token_allowance = client.spot.get_token_allowance(0, client.context.signer.address) + print("token allowance:", token_allowance) + + print("depositing collateral...") + deposit_tx_hash = client.spot.deposit( + DepositCollateralParams( + subaccount_name="default", product_id=0, amount=to_pow_10(100000, 6) + ) + ) + print("deposit collateral tx hash:", deposit_tx_hash) + + subaccount = subaccount_to_hex(client.context.signer.address, "default") + + usdc_balance: SpotProductBalance = client.subaccount.get_engine_subaccount_summary( + subaccount + ).parse_subaccount_balance(0) + while int(usdc_balance.balance.amount) == 0: + print("waiting for deposit...") + usdc_balance: SpotProductBalance = ( + client.subaccount.get_engine_subaccount_summary( + subaccount + ).parse_subaccount_balance(0) + ) + time.sleep(1) + + order_price = 95_000 + + owner = client.context.engine_client.signer.address + print("placing isolated order...") + product_id = 2 + isolated_order = IsolatedOrderParams( + sender=SubaccountParams( + subaccount_owner=owner, + subaccount_name="default", + ), + priceX18=to_x18(order_price), + amount=to_pow_10(1, 17), + expiration=get_expiration_timestamp(OrderType.DEFAULT, int(time.time()) + 40), + nonce=gen_order_nonce(), + margin=to_pow_10(1000, 18), + ) + res = client.market.place_isolated_order( + {"product_id": product_id, "isolated_order": isolated_order} + ) + print("order result:", res.json(indent=2)) From b2aeca2dbc5f0724d02252dfa9f9f03c66f5e15b Mon Sep 17 00:00:00 2001 From: yruej301 Date: Tue, 25 Feb 2025 17:32:55 -0500 Subject: [PATCH 3/5] bump/v3.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d489de0..6ffb9d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "vertex-protocol" -version = "3.1.7" +version = "3.2.0" description = "Vertex Protocol SDK" authors = ["Jeury Mejia "] homepage = "https://vertexprotocol.com/" From 3a3eea412afe2582006fc1a7817ae96dcbff5c9c Mon Sep 17 00:00:00 2001 From: yruej301 Date: Tue, 25 Feb 2025 18:04:00 -0500 Subject: [PATCH 4/5] go --- sanity/isolated.py | 24 ++++++++++++++++++- vertex_protocol/client/apis/market/query.py | 13 ++++++++++ vertex_protocol/engine_client/query.py | 17 +++++++++++++ vertex_protocol/engine_client/types/models.py | 11 +++++++++ vertex_protocol/engine_client/types/query.py | 21 ++++++++++++++++ .../indexer_client/types/models.py | 5 ++++ vertex_protocol/indexer_client/types/query.py | 3 +++ 7 files changed, 93 insertions(+), 1 deletion(-) diff --git a/sanity/isolated.py b/sanity/isolated.py index 3688fca..cdae67a 100644 --- a/sanity/isolated.py +++ b/sanity/isolated.py @@ -64,7 +64,7 @@ def run(): ), priceX18=to_x18(order_price), amount=to_pow_10(1, 17), - expiration=get_expiration_timestamp(OrderType.DEFAULT, int(time.time()) + 40), + expiration=get_expiration_timestamp(OrderType.IOC, int(time.time()) + 40), nonce=gen_order_nonce(), margin=to_pow_10(1000, 18), ) @@ -72,3 +72,25 @@ def run(): {"product_id": product_id, "isolated_order": isolated_order} ) print("order result:", res.json(indent=2)) + + print("querying isolated positions...") + isolated_positions = client.market.get_isolated_positions(subaccount) + print("isolated positions:", isolated_positions.json(indent=2)) + + print("querying historical isolated orders...") + historical_isolated_orders = client.market.get_subaccount_historical_orders( + {"subaccount": subaccount, "isolated": True} + ) + print("historical isolated orders:", historical_isolated_orders.json(indent=2)) + + print("querying isolated matches...") + isolated_matches = client.context.indexer_client.get_matches( + {"subaccount": subaccount, "isolated": True} + ) + print("isolated matches:", isolated_matches.json(indent=2)) + + print("querying isolated events...") + isolated_events = client.context.indexer_client.get_events( + {"subaccount": subaccount, "limit": {"raw": 5}, "isolated": True} + ) + print("isolated events:", isolated_events.json(indent=2)) diff --git a/vertex_protocol/client/apis/market/query.py b/vertex_protocol/client/apis/market/query.py index 66a98cb..18a6815 100644 --- a/vertex_protocol/client/apis/market/query.py +++ b/vertex_protocol/client/apis/market/query.py @@ -10,6 +10,7 @@ SubaccountOpenOrdersData, SubaccountMultiProductsOpenOrdersData, QueryMaxOrderSizeParams, + IsolatedPositionsData, ) from vertex_protocol.indexer_client.types.query import ( IndexerCandlesticksData, @@ -295,3 +296,15 @@ def get_trigger_orders( if self.context.trigger_client is None: raise MissingTriggerClient() return self.context.trigger_client.list_trigger_orders(params) + + def get_isolated_positions(self, subaccount: str) -> IsolatedPositionsData: + """ + Retrieve isolated positions for a specific subaccount. + + Args: + subaccount (str): Unique identifier for the subaccount. + + Returns: + IsolatedPositionsData: A data class object containing information about the isolated positions for the specified subaccount. + """ + return self.context.engine_client.get_isolated_positions(subaccount) diff --git a/vertex_protocol/engine_client/query.py b/vertex_protocol/engine_client/query.py index b52a2b8..cc8bc1d 100644 --- a/vertex_protocol/engine_client/query.py +++ b/vertex_protocol/engine_client/query.py @@ -38,6 +38,7 @@ QuerySubaccountOpenOrdersParams, QuerySubaccountMultiProductOpenOrdersParams, QueryOrderParams, + QueryIsolatedPositionsParams, QueryRequest, QueryResponse, QueryStatusParams, @@ -50,6 +51,7 @@ AssetsData, MarketPairsData, SpotsAprData, + IsolatedPositionsData, ) from vertex_protocol.utils.exceptions import ( BadStatusCodeException, @@ -422,6 +424,21 @@ def get_linked_signer(self, subaccount: str) -> LinkedSignerData: LinkedSignerData, ) + def get_isolated_positions(self, subaccount: str) -> IsolatedPositionsData: + """ + Retrieves the isolated positions for a specific subaccount. + + Args: + subaccount (str): Identifier of the subaccount (owner's address + subaccount name) sent as a hex string. + + Returns: + IsolatedPositionsData: A data object containing the isolated positions for the specified subaccount. + """ + return ensure_data_type( + self.query(QueryIsolatedPositionsParams(subaccount=subaccount)).data, + IsolatedPositionsData, + ) + def _get_subaccount_product_position( self, subaccount: str, product_id: int ) -> SubaccountPosition: diff --git a/vertex_protocol/engine_client/types/models.py b/vertex_protocol/engine_client/types/models.py index 2e64e17..884e784 100644 --- a/vertex_protocol/engine_client/types/models.py +++ b/vertex_protocol/engine_client/types/models.py @@ -234,3 +234,14 @@ class Orderbook(VertexBaseModel): class MarketType(StrEnum): SPOT = "spot" PERP = "perp" + + +class IsolatedPosition(VertexBaseModel): + subaccount: str + quote_balance: SpotProductBalance + base_balance: PerpProductBalance + quote_product: SpotProduct + base_product: PerpProduct + healths: list[SubaccountHealth] + quote_healths: list + base_healths: list diff --git a/vertex_protocol/engine_client/types/query.py b/vertex_protocol/engine_client/types/query.py index e0e27b4..7dbd0ae 100644 --- a/vertex_protocol/engine_client/types/query.py +++ b/vertex_protocol/engine_client/types/query.py @@ -7,6 +7,7 @@ Asset, BurnLpTx, EngineStatus, + IsolatedPosition, MarketPair, MaxOrderSizeDirection, MintLpTx, @@ -45,6 +46,7 @@ class EngineQueryType(StrEnum): SUBACCOUNT_INFO = "subaccount_info" SUBACCOUNT_ORDERS = "subaccount_orders" ORDERS = "orders" + ISOLATED_POSITIONS = "isolated_positions" class QueryStatusParams(VertexBaseModel): @@ -82,6 +84,15 @@ class QueryOrderParams(VertexBaseModel): digest: str +class QueryIsolatedPositionsParams(VertexBaseModel): + """ + Parameters for querying the isolated positions of a specific subaccount. + """ + + type = EngineQueryType.ISOLATED_POSITIONS.value + subaccount: str + + QuerySubaccountInfoTx = Union[MintLpTx, BurnLpTx, ApplyDeltaTx] @@ -240,6 +251,7 @@ class QueryLinkedSignerParams(VertexBaseModel): QueryFeeRatesParams, QueryHealthGroupsParams, QueryLinkedSignerParams, + QueryIsolatedPositionsParams, ] StatusData = EngineStatus @@ -322,6 +334,14 @@ def parse_subaccount_balance( raise ValueError(f"Invalid product id provided: {product_id}") +class IsolatedPositionsData(VertexBaseModel): + """ + Data model for isolated positions of a subaccount. + """ + + isolated_positions: list[IsolatedPosition] + + class SubaccountOpenOrdersData(VertexBaseModel): """ Data model encapsulating open orders of a subaccount for a @@ -462,6 +482,7 @@ class SymbolsData(VertexBaseModel): HealthGroupsData, LinkedSignerData, ProductSymbolsData, + IsolatedPositionsData, ] diff --git a/vertex_protocol/indexer_client/types/models.py b/vertex_protocol/indexer_client/types/models.py index 431dd4d..a6462cb 100644 --- a/vertex_protocol/indexer_client/types/models.py +++ b/vertex_protocol/indexer_client/types/models.py @@ -25,6 +25,7 @@ class IndexerEventType(StrEnum): MANUAL_ASSERT = "manual_assert" LINK_SIGNER = "link_signer" TRANSFER_QUOTE = "transfer_quote" + CREATE_ISOLATED_SUBACCOUNT = "create_isolated_subaccount" class IndexerCandlesticksGranularity(IntEnum): @@ -66,6 +67,7 @@ class IndexerHistoricalOrder(IndexerOrderFill): price_x18: str expiration: str nonce: str + isolated: bool class IndexerSignedOrder(VertexBaseModel): @@ -78,6 +80,7 @@ class IndexerMatch(IndexerOrderFill): cumulative_fee: str cumulative_base_filled: str cumulative_quote_filled: str + isolated: bool class IndexerMatchOrdersTxData(VertexBaseModel): @@ -194,6 +197,8 @@ class IndexerEvent(IndexerBaseModel, IndexerEventTrackedData): product: IndexerProductData pre_balance: IndexerProductBalanceData post_balance: IndexerProductBalanceData + isolated: bool + isolated_product_id: Optional[int] class IndexerProduct(IndexerBaseModel): diff --git a/vertex_protocol/indexer_client/types/query.py b/vertex_protocol/indexer_client/types/query.py index d4c170d..7a32993 100644 --- a/vertex_protocol/indexer_client/types/query.py +++ b/vertex_protocol/indexer_client/types/query.py @@ -74,6 +74,7 @@ class IndexerSubaccountHistoricalOrdersParams(IndexerBaseParams): subaccount: str product_ids: Optional[list[int]] + isolated: Optional[bool] class IndexerHistoricalOrdersByDigestParams(VertexBaseModel): @@ -91,6 +92,7 @@ class IndexerMatchesParams(IndexerBaseParams): subaccount: Optional[str] product_ids: Optional[list[int]] + isolated: Optional[bool] class IndexerEventsRawLimit(VertexBaseModel): @@ -120,6 +122,7 @@ class IndexerEventsParams(IndexerBaseParams): subaccount: Optional[str] product_ids: Optional[list[int]] event_types: Optional[list[IndexerEventType]] + isolated: Optional[bool] limit: Optional[IndexerEventsLimit] # type: ignore From e609713fdf0e2c487f52f4d9c005e9b181d12527 Mon Sep 17 00:00:00 2001 From: yruej301 Date: Tue, 25 Feb 2025 18:07:40 -0500 Subject: [PATCH 5/5] clarify borrow_margin --- vertex_protocol/engine_client/types/execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertex_protocol/engine_client/types/execute.py b/vertex_protocol/engine_client/types/execute.py index 57ec450..b7738e5 100644 --- a/vertex_protocol/engine_client/types/execute.py +++ b/vertex_protocol/engine_client/types/execute.py @@ -58,7 +58,7 @@ class PlaceIsolatedOrderParams(SignatureParams): digest (Optional[str]): An optional hash of the order data. - borrow_margin (Optional[bool]): An optional flag indicating whether the order should borrow margin. By default, margin is borrowed. + borrow_margin (Optional[bool]): Whether the cross subaccount can borrow quote for the margin transfer into the isolated subaccount. If not provided, it defaults to true. """ id: Optional[int]