From cf6d7083351dd66c3ae50d3e3a7474ee88588f53 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:06:58 -0800 Subject: [PATCH 01/16] Add tests for exchange initializer --- assets/images/coverage.svg | 4 ++-- tests/test_exchange.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/test_exchange.py diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 5cc1bb5..4f8c185 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 42% - 42% + 50% + 50% diff --git a/tests/test_exchange.py b/tests/test_exchange.py new file mode 100644 index 0000000..3bae519 --- /dev/null +++ b/tests/test_exchange.py @@ -0,0 +1,49 @@ +import pytest +from eth_account import Account +from eth_account.signers.local import LocalAccount + +from hyperliquid.exchange import Exchange +from hyperliquid.utils.constants import MAINNET_API_URL +from hyperliquid.utils.types import Meta, SpotMeta + +TEST_META: Meta = {"universe": []} +TEST_SPOT_META: SpotMeta = {"universe": [], "tokens": []} + +@pytest.fixture +def wallet() -> LocalAccount: + """Create a test wallet""" + return Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") + +@pytest.fixture +def exchange(wallet): + """Fixture that provides an Exchange instance""" + return Exchange(wallet) + +def test_initializer(exchange, wallet): + """Test that the Exchange class initializes with correct default values""" + assert exchange.base_url == MAINNET_API_URL + assert exchange.wallet == wallet + assert exchange.vault_address is None + assert exchange.account_address is None + assert exchange.info is not None + +def test_initializer_with_custom_values(wallet): + """Test that the Exchange class can be initialized with custom values""" + custom_url = "https://custom.api.url" + vault_address = "0x1234567890123456789012345678901234567890" + account_address = "0x0987654321098765432109876543210987654321" + + exchange = Exchange( + wallet=wallet, + base_url=custom_url, + meta=TEST_META, + vault_address=vault_address, + account_address=account_address, + spot_meta=TEST_SPOT_META + ) + + assert exchange.base_url == custom_url + assert exchange.wallet == wallet + assert exchange.vault_address == vault_address + assert exchange.account_address == account_address + assert exchange.info is not None From 7b0e47540b8053546f8b91683fc3dd8857c7b193 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:12:36 -0800 Subject: [PATCH 02/16] Add tests for post and slippage price --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 4f8c185..d4eaeaa 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 50% - 50% + 52% + 52% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 3bae519..322b4fc 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -1,6 +1,7 @@ import pytest from eth_account import Account from eth_account.signers.local import LocalAccount +from unittest.mock import Mock, patch from hyperliquid.exchange import Exchange from hyperliquid.utils.constants import MAINNET_API_URL @@ -47,3 +48,110 @@ def test_initializer_with_custom_values(wallet): assert exchange.vault_address == vault_address assert exchange.account_address == account_address assert exchange.info is not None + +@patch('hyperliquid.api.API.post') +def test_post_action(mock_post, exchange): + """Test _post_action method""" + # Setup + mock_post.return_value = {"status": "ok"} + action = {"type": "someAction", "data": "test"} + signature = "test_signature" + nonce = 123456789 + + # Test with regular action + response = exchange._post_action(action, signature, nonce) + mock_post.assert_called_once_with( + "/exchange", + { + "action": action, + "nonce": nonce, + "signature": signature, + "vaultAddress": None + } + ) + assert response == {"status": "ok"} + + # Test with vault address + mock_post.reset_mock() + exchange.vault_address = "0x1234" + response = exchange._post_action(action, signature, nonce) + mock_post.assert_called_once_with( + "/exchange", + { + "action": action, + "nonce": nonce, + "signature": signature, + "vaultAddress": "0x1234" + } + ) + + # Test with usdClassTransfer action + mock_post.reset_mock() + action["type"] = "usdClassTransfer" + response = exchange._post_action(action, signature, nonce) + mock_post.assert_called_once_with( + "/exchange", + { + "action": action, + "nonce": nonce, + "signature": signature, + "vaultAddress": None + } + ) + +@patch('hyperliquid.info.Info.all_mids') +def test_slippage_price_perp(mock_all_mids, exchange): + """Test _slippage_price method for perpetual contracts""" + # Setup + mock_all_mids.return_value = {"ETH": "2000.0"} + exchange.info.name_to_coin = {"ETH": "ETH"} + exchange.info.coin_to_asset = {"ETH": 1} # Asset ID less than 10000 for perp + + # Test buy with default price + price = exchange._slippage_price("ETH", True, 0.05) # 5% slippage + assert price == 2100.0 # 2000 * (1 + 0.05) + + # Test sell with default price + price = exchange._slippage_price("ETH", False, 0.05) + assert price == 1900.0 # 2000 * (1 - 0.05) + + # Test with custom price + price = exchange._slippage_price("ETH", True, 0.05, 1000.0) + assert price == 1050.0 # 1000 * (1 + 0.05) + +@patch('hyperliquid.info.Info.all_mids') +def test_slippage_price_spot(mock_all_mids, exchange): + """Test _slippage_price method for spot trading""" + # Setup + mock_all_mids.return_value = {"BTC/USDC": "40000.0"} + exchange.info.name_to_coin = {"BTC/USDC": "BTC/USDC"} + exchange.info.coin_to_asset = {"BTC/USDC": 10001} # Asset ID >= 10000 for spot + + # Test buy with default price + price = exchange._slippage_price("BTC/USDC", True, 0.05) + assert price == 42000.0 # 40000 * (1 + 0.05) + + # Test sell with default price + price = exchange._slippage_price("BTC/USDC", False, 0.05) + assert price == 38000.0 # 40000 * (1 - 0.05) + +@patch('hyperliquid.info.Info.all_mids') +def test_slippage_price_rounding(mock_all_mids, exchange): + """Test price rounding in _slippage_price method""" + # Setup for perp + mock_all_mids.return_value = {"ETH": "1999.123456789"} + exchange.info.name_to_coin = {"ETH": "ETH"} + exchange.info.coin_to_asset = {"ETH": 1} + + # Test perp rounding (6 decimals) + price = exchange._slippage_price("ETH", True, 0.05) + assert str(price).count('.') == 0 or len(str(price).split('.')[1]) <= 6 + + # Setup for spot + mock_all_mids.return_value = {"BTC/USDC": "40000.123456789"} + exchange.info.name_to_coin = {"BTC/USDC": "BTC/USDC"} + exchange.info.coin_to_asset = {"BTC/USDC": 10001} + + # Test spot rounding (8 decimals) + price = exchange._slippage_price("BTC/USDC", True, 0.05) + assert str(price).count('.') == 0 or len(str(price).split('.')[1]) <= 8 From fc030adf06223eef5b5bf1673344587061c74928 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:22:49 -0800 Subject: [PATCH 03/16] Add tests for orders --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 154 +++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index d4eaeaa..d1f8965 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 52% - 52% + 53% + 53% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 322b4fc..e952540 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -155,3 +155,157 @@ def test_slippage_price_rounding(mock_all_mids, exchange): # Test spot rounding (8 decimals) price = exchange._slippage_price("BTC/USDC", True, 0.05) assert str(price).count('.') == 0 or len(str(price).split('.')[1]) <= 8 + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_bulk_orders(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test bulk_orders method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 # Mock name_to_asset to return 1 for any input + + # Test single order + order_requests = [ + { + "coin": "ETH", + "is_buy": True, + "sz": 1.0, + "limit_px": 2000.0, + "order_type": {"limit": {"tif": "Gtc"}}, + "reduce_only": False, + } + ] + + response = exchange.bulk_orders(order_requests) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + assert call_args[0] == exchange.wallet # wallet + assert call_args[1]["type"] == "order" # action + assert call_args[2] == exchange.vault_address # vault_address + assert call_args[3] == 1234567890 # timestamp + assert call_args[4] == (exchange.base_url == MAINNET_API_URL) # is_mainnet + + # Verify _post_action was called correctly + mock_post_action.assert_called_once_with( + mock_sign.call_args[0][1], # action + "test_signature", # signature + 1234567890 # timestamp + ) + + # Test with builder + mock_sign.reset_mock() + mock_post_action.reset_mock() + + builder = {"b": "TEST_BUILDER", "r": 0.001} # Using uppercase to test lowercasing + response = exchange.bulk_orders(order_requests, builder) + + assert response == {"status": "ok"} + + # Verify builder was included in the action + call_args = mock_sign.call_args[0] + action = call_args[1] # Get the action argument + assert action["type"] == "order" + # The builder object should be passed through as is (after lowercase conversion) + assert action["builder"]["b"] == "test_builder" + assert action["builder"]["r"] == 0.001 + +@patch('hyperliquid.exchange.Exchange.bulk_orders') +def test_order(mock_bulk_orders, exchange): + """Test order method with various scenarios""" + # Setup + mock_bulk_orders.return_value = { + "status": "ok", + "response": { + "data": { + "statuses": [{"resting": {"oid": 123}}] + } + } + } + exchange.info.name_to_asset = lambda x: 1 + + # Test 1: Basic limit order + response = exchange.order( + name="ETH", + is_buy=True, + sz=1.0, + limit_px=2000.0, + order_type={"limit": {"tif": "Gtc"}}, + ) + + assert response["status"] == "ok" + assert response["response"]["data"]["statuses"][0]["resting"]["oid"] == 123 + + mock_bulk_orders.assert_called_once_with( + [ + { + "coin": "ETH", + "is_buy": True, + "sz": 1.0, + "limit_px": 2000.0, + "order_type": {"limit": {"tif": "Gtc"}}, + "reduce_only": False, + } + ], + None + ) + + # Test 2: Order with builder fee + mock_bulk_orders.reset_mock() + builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} + + response = exchange.order( + name="ETH", + is_buy=True, + sz=0.05, + limit_px=2000.0, + order_type={"limit": {"tif": "Ioc"}}, + builder=builder + ) + + assert response["status"] == "ok" + mock_bulk_orders.assert_called_once() + call_args = mock_bulk_orders.call_args[0] + assert call_args[1]["b"].lower() == builder["b"].lower() + assert call_args[1]["r"] == builder["r"] + + # Test 3: TPSL order + mock_bulk_orders.reset_mock() + response = exchange.order( + name="ETH", + is_buy=True, + sz=100, + limit_px=100, + order_type={"trigger": {"triggerPx": 103, "isMarket": True, "tpsl": "sl"}}, + reduce_only=True + ) + + assert response["status"] == "ok" + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["order_type"]["trigger"]["tpsl"] == "sl" + assert order_request["reduce_only"] is True + + # Test 4: Order with cloid + mock_bulk_orders.reset_mock() + cloid = "0x00000000000000000000000000000001" + + response = exchange.order( + name="ETH", + is_buy=True, + sz=1.0, + limit_px=2000.0, + order_type={"limit": {"tif": "Gtc"}}, + cloid=cloid + ) + + assert response["status"] == "ok" + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["cloid"] == cloid From 95dddb2de7e03d6936733533d7c2db183f7faa9c Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:28:19 -0800 Subject: [PATCH 04/16] Add tests for modify order --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 124 +++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index d1f8965..f9eb6b4 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 53% - 53% + 55% + 55% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index e952540..6222b30 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -309,3 +309,127 @@ def test_order(mock_bulk_orders, exchange): mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] assert order_request["cloid"] == cloid + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test modify_order method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + # Test 1: Basic modify order with oid + oid = 12345 + response = exchange.modify_order( + oid=oid, + name="ETH", + is_buy=True, + sz=0.1, + limit_px=1105, + order_type={"limit": {"tif": "Gtc"}}, + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "batchModify" + assert len(action["modifies"]) == 1 + assert action["modifies"][0]["oid"] == oid + + # Test 2: Modify order with cloid + mock_sign.reset_mock() + mock_post_action.reset_mock() + + from hyperliquid.utils.types import Cloid + cloid = Cloid.from_str("0x00000000000000000000000000000001") + new_cloid = Cloid.from_str("0x00000000000000000000000000000002") + + response = exchange.modify_order( + oid=cloid, + name="ETH", + is_buy=True, + sz=0.1, + limit_px=1105, + order_type={"limit": {"tif": "Gtc"}}, + reduce_only=True, + cloid=new_cloid + ) + + assert response == {"status": "ok"} + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["modifies"][0]["oid"] == cloid.to_raw() + assert action["modifies"][0]["order"]["r"] is True # reduce_only + assert "c" in action["modifies"][0]["order"] # cloid in wire format + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test bulk_modify_orders_new method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + from hyperliquid.utils.types import Cloid + cloid1 = Cloid.from_str("0x00000000000000000000000000000001") + cloid2 = Cloid.from_str("0x00000000000000000000000000000002") + + # Test multiple order modifications + modify_requests = [ + { + "oid": 12345, + "order": { + "coin": "ETH", + "is_buy": True, + "sz": 0.1, + "limit_px": 1105, + "order_type": {"limit": {"tif": "Gtc"}}, + "reduce_only": False, + "cloid": None, + }, + }, + { + "oid": cloid1, + "order": { + "coin": "BTC", + "is_buy": False, + "sz": 1.0, + "limit_px": 50000, + "order_type": {"limit": {"tif": "Ioc"}}, + "reduce_only": True, + "cloid": cloid2, + }, + }, + ] + + response = exchange.bulk_modify_orders_new(modify_requests) + assert response == {"status": "ok"} + + # Verify the action structure + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + + assert action["type"] == "batchModify" + assert len(action["modifies"]) == 2 + + # Verify first modification + assert action["modifies"][0]["oid"] == 12345 + assert action["modifies"][0]["order"]["s"] == "0.1" # size as string + assert action["modifies"][0]["order"]["p"] == "1105" # price as string + + # Verify second modification + assert action["modifies"][1]["oid"] == cloid1.to_raw() + assert action["modifies"][1]["order"]["s"] == "1" # size as string + assert action["modifies"][1]["order"]["p"] == "50000" # price as string + assert action["modifies"][1]["order"]["r"] is True + assert "c" in action["modifies"][1]["order"] # cloid in wire format From 566a30d0e1a21bd425b41fec6c2370b2b5e26896 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:31:09 -0800 Subject: [PATCH 05/16] Add tests for market open and market close --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 116 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index f9eb6b4..4713f1f 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 55% - 55% + 57% + 57% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 6222b30..c399061 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -433,3 +433,119 @@ def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exc assert action["modifies"][1]["order"]["p"] == "50000" # price as string assert action["modifies"][1]["order"]["r"] is True assert "c" in action["modifies"][1]["order"] # cloid in wire format + +@patch('hyperliquid.exchange.Exchange.bulk_orders') +def test_market_open(mock_bulk_orders, exchange): + """Test market_open method""" + # Setup + mock_bulk_orders.return_value = { + "status": "ok", + "response": { + "data": { + "statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}] + } + } + } + exchange.info.name_to_asset = lambda x: 1 + + # Test 1: Basic market open + response = exchange.market_open( + name="ETH", + is_buy=True, + sz=0.05, + ) + + assert response["status"] == "ok" + assert response["response"]["data"]["statuses"][0]["filled"]["oid"] == 123 + + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["coin"] == "ETH" + assert order_request["is_buy"] is True + assert order_request["sz"] == 0.05 + assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} + assert order_request["reduce_only"] is False + + # Test 2: Market open with slippage and builder + mock_bulk_orders.reset_mock() + builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} + + response = exchange.market_open( + name="ETH", + is_buy=True, + sz=0.05, + builder=builder, + slippage=0.01 # 1% slippage + ) + + assert response["status"] == "ok" + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["coin"] == "ETH" + assert order_request["sz"] == 0.05 + assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} + # Verify builder was passed correctly + assert mock_bulk_orders.call_args[0][1]["b"].lower() == builder["b"].lower() + assert mock_bulk_orders.call_args[0][1]["r"] == builder["r"] + +@patch('hyperliquid.exchange.Exchange.bulk_orders') +def test_market_close(mock_bulk_orders, exchange): + """Test market_close method""" + # Setup + mock_bulk_orders.return_value = { + "status": "ok", + "response": { + "data": { + "statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}] + } + } + } + exchange.info.name_to_asset = lambda x: 1 + + # Mock user_state to return a position + exchange.info.user_state = lambda x: { + "assetPositions": [ + { + "position": { + "coin": "ETH", + "szi": "0.05", + "entryPx": "2000", + "positionValue": "100" + } + } + ] + } + + # Test 1: Basic market close + response = exchange.market_close("ETH") + + assert response["status"] == "ok" + assert response["response"]["data"]["statuses"][0]["filled"]["oid"] == 123 + + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["coin"] == "ETH" + assert order_request["sz"] == 0.05 + assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} + assert order_request["reduce_only"] is True + + # Test 2: Market close with slippage and builder + mock_bulk_orders.reset_mock() + builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} + + response = exchange.market_close( + coin="ETH", + builder=builder, + slippage=0.01 # 1% slippage + ) + + assert response["status"] == "ok" + mock_bulk_orders.assert_called_once() + order_request = mock_bulk_orders.call_args[0][0][0] + assert order_request["coin"] == "ETH" + assert order_request["sz"] == 0.05 + assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} + assert order_request["reduce_only"] is True + # Verify builder was passed correctly + assert mock_bulk_orders.call_args[0][1]["b"].lower() == builder["b"].lower() + assert mock_bulk_orders.call_args[0][1]["r"] == builder["r"] From c726c1a7aa040422ed9ec5d58992ab95ada6d875 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:33:41 -0800 Subject: [PATCH 06/16] Add tests for cancel, cancel_by_cloid, bulk_cancel, and bulk_cancel_by_cloid --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 119 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 4713f1f..eb83f3f 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 57% - 57% + 59% + 59% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index c399061..9a5ab51 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -549,3 +549,122 @@ def test_market_close(mock_bulk_orders, exchange): # Verify builder was passed correctly assert mock_bulk_orders.call_args[0][1]["b"].lower() == builder["b"].lower() assert mock_bulk_orders.call_args[0][1]["r"] == builder["r"] + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test cancel method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + # Test basic cancel + response = exchange.cancel("ETH", 12345) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "cancel" + assert len(action["cancels"]) == 1 + assert action["cancels"][0]["a"] == 1 # asset + assert action["cancels"][0]["o"] == 12345 # oid + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test cancel_by_cloid method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + from hyperliquid.utils.types import Cloid + cloid = Cloid.from_str("0x00000000000000000000000000000001") + + # Test cancel by cloid + response = exchange.cancel_by_cloid("ETH", cloid) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "cancelByCloid" + assert len(action["cancels"]) == 1 + assert action["cancels"][0]["asset"] == 1 + assert action["cancels"][0]["cloid"] == cloid.to_raw() + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_bulk_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test bulk_cancel method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + # Test multiple cancels + cancel_requests = [ + {"coin": "ETH", "oid": 12345}, + {"coin": "BTC", "oid": 67890} + ] + + response = exchange.bulk_cancel(cancel_requests) + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "cancel" + assert len(action["cancels"]) == 2 + assert action["cancels"][0]["a"] == 1 + assert action["cancels"][0]["o"] == 12345 + assert action["cancels"][1]["a"] == 1 + assert action["cancels"][1]["o"] == 67890 + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_bulk_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test bulk_cancel_by_cloid method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + from hyperliquid.utils.types import Cloid + cloid1 = Cloid.from_str("0x00000000000000000000000000000001") + cloid2 = Cloid.from_str("0x00000000000000000000000000000002") + + # Test multiple cancels by cloid + cancel_requests = [ + {"coin": "ETH", "cloid": cloid1}, + {"coin": "BTC", "cloid": cloid2} + ] + + response = exchange.bulk_cancel_by_cloid(cancel_requests) + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "cancelByCloid" + assert len(action["cancels"]) == 2 + assert action["cancels"][0]["asset"] == 1 + assert action["cancels"][0]["cloid"] == cloid1.to_raw() + assert action["cancels"][1]["asset"] == 1 + assert action["cancels"][1]["cloid"] == cloid2.to_raw() From 415a5c117ca0354a277b087acb2c308d6f9b9791 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:37:31 -0800 Subject: [PATCH 07/16] Add tests for schedule_cancel --- assets/images/coverage.svg | 6 ++--- hyperliquid/exchange.py | 2 +- tests/test_exchange.py | 48 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index eb83f3f..9f708fe 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -9,13 +9,13 @@ - + coverage coverage - 59% - 59% + 60% + 60% diff --git a/hyperliquid/exchange.py b/hyperliquid/exchange.py index f0c5d47..662c11d 100644 --- a/hyperliquid/exchange.py +++ b/hyperliquid/exchange.py @@ -297,7 +297,7 @@ def bulk_cancel_by_cloid(self, cancel_requests: List[CancelByCloidRequest]) -> A timestamp, ) - def schedule_cancel(self, time: Optional[int]) -> Any: + def schedule_cancel(self, time: Optional[int] = None) -> Any: """Schedules a time (in UTC millis) to cancel all open orders. The time must be at least 5 seconds after the current time. Once the time comes, all open orders will be canceled and a trigger count will be incremented. The max number of triggers per day is 10. This trigger count is reset at 00:00 UTC. diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 9a5ab51..53c8770 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -668,3 +668,51 @@ def test_bulk_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, excha assert action["cancels"][0]["cloid"] == cloid1.to_raw() assert action["cancels"][1]["asset"] == 1 assert action["cancels"][1]["cloid"] == cloid2.to_raw() + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_schedule_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test schedule_cancel method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test 1: Basic schedule cancel without time (uses current timestamp) + response = exchange.schedule_cancel() + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "scheduleCancel" + assert "time" not in action # No specific time provided + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature + + # Test 2: Schedule cancel with specific time + mock_sign.reset_mock() + mock_post_action.reset_mock() + + cancel_time = 1234567890 + 10000 # 10 seconds from now + response = exchange.schedule_cancel(cancel_time) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "scheduleCancel" + assert action["time"] == cancel_time + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature From 3bf0c1c0170e45e3526c9599a53e8c38fbe13e2f Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:39:25 -0800 Subject: [PATCH 08/16] Add tests for update_leverage --- tests/test_exchange.py | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 53c8770..80f81e1 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -716,3 +716,59 @@ def test_schedule_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test update_leverage method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + # Test 1: Update leverage with default cross margin + response = exchange.update_leverage(leverage=10, name="ETH") + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "updateLeverage" + assert action["asset"] == 1 + assert action["leverage"] == 10 + assert action["isCross"] is True + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature + + # Test 2: Update leverage with isolated margin + mock_sign.reset_mock() + mock_post_action.reset_mock() + + response = exchange.update_leverage( + leverage=5, + name="BTC", + is_cross=False + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "updateLeverage" + assert action["asset"] == 1 + assert action["leverage"] == 5 + assert action["isCross"] is False + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature From cdc45bde7096e1cd8d2399d926bbc92fe60ffb1b Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:43:23 -0800 Subject: [PATCH 09/16] Add tests for update_isolated_margin --- assets/images/coverage.svg | 4 ++-- tests/test_exchange.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 9f708fe..dd6df12 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 60% - 60% + 61% + 61% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 80f81e1..7842896 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -772,3 +772,36 @@ def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_update_isolated_margin(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test update_isolated_margin method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + exchange.info.name_to_asset = lambda x: 1 + + # Test: Update isolated margin + response = exchange.update_isolated_margin( + amount=1000.0, + name="ETH" + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "updateIsolatedMargin" + assert action["asset"] == 1 + assert action["isBuy"] is True # isBuy is always True in the implementation + assert "ntli" in action # ntli (notional) should be present + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature From 9adf93dfc542a70850de21f11bb13e4925cf6a2d Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:45:28 -0800 Subject: [PATCH 10/16] Add tests for set_referrer, create_sub_account --- assets/images/coverage.svg | 4 +-- tests/test_exchange.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index dd6df12..2fad913 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 61% - 61% + 62% + 62% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 7842896..3c04420 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -805,3 +805,57 @@ def test_update_isolated_margin(mock_post_action, mock_timestamp, mock_sign, exc mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_set_referrer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test set_referrer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test setting referrer code + response = exchange.set_referrer("ASDFASDF") + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "setReferrer" + assert action["code"] == "ASDFASDF" + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_create_sub_account(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test create_sub_account method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test creating sub account + response = exchange.create_sub_account("example") + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "createSubAccount" + assert action["name"] == "example" + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature From 97b82d5c38bbf846ec9f092111f9cf60e554f77f Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:48:43 -0800 Subject: [PATCH 11/16] Add tests for usd_class_transfer and sub_account_transfer --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 92 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 2fad913..6d68f47 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 62% - 62% + 64% + 64% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 3c04420..02f94d1 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -859,3 +859,95 @@ def test_create_sub_account(mock_post_action, mock_timestamp, mock_sign, exchang mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_usd_class_transfer_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_usd_class_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test usd_class_transfer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test 1: Basic transfer without vault address + response = exchange.usd_class_transfer(amount=1000.0, to_perp=True) + + assert response == {"status": "ok"} + + # Verify sign_usd_class_transfer_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "usdClassTransfer" + assert action["amount"] == "1000.0" + assert action["toPerp"] is True + assert action["nonce"] == 1234567890 + + # Test 2: Transfer with vault address + mock_sign.reset_mock() + mock_post_action.reset_mock() + exchange.vault_address = "0x1234" + + response = exchange.usd_class_transfer(amount=500.5, to_perp=False) + + assert response == {"status": "ok"} + + # Verify sign_usd_class_transfer_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "usdClassTransfer" + assert action["amount"] == "500.5 subaccount:0x1234" + assert action["toPerp"] is False + assert action["nonce"] == 1234567890 + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test sub_account_transfer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test deposit to sub account + sub_account = "0x1d9470d4b963f552e6f671a81619d395877bf409" + response = exchange.sub_account_transfer( + sub_account_user=sub_account, + is_deposit=True, + usd=1000 + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "subAccountTransfer" + assert action["subAccountUser"] == sub_account + assert action["isDeposit"] is True + assert action["usd"] == 1000 + + # Test withdrawal from sub account + mock_sign.reset_mock() + mock_post_action.reset_mock() + + response = exchange.sub_account_transfer( + sub_account_user=sub_account, + is_deposit=False, + usd=500 + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "subAccountTransfer" + assert action["subAccountUser"] == sub_account + assert action["isDeposit"] is False + assert action["usd"] == 500 From 3cf83574ef3809aba36e3dd276ae3c5d542030e3 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:02:17 -0800 Subject: [PATCH 12/16] Add tests for vault_usd_transfer and usd_transfer --- assets/images/coverage.svg | 4 +-- tests/test_exchange.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index 6d68f47..c97e504 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 64% - 64% + 66% + 66% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 02f94d1..e8da953 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -951,3 +951,73 @@ def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, excha assert action["subAccountUser"] == sub_account assert action["isDeposit"] is False assert action["usd"] == 500 + +@patch('hyperliquid.exchange.sign_l1_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_vault_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test vault_usd_transfer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test vault transfer + vault_address = "0xa15099a30bbf2e68942d6f4c43d70d04faeab0a0" + response = exchange.vault_usd_transfer( + vault_address=vault_address, + is_deposit=True, + usd=5_000_000 + ) + + assert response == {"status": "ok"} + + # Verify sign_l1_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "vaultTransfer" + assert action["vaultAddress"] == vault_address + assert action["isDeposit"] is True + assert action["usd"] == 5_000_000 + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" # signature + +@patch('hyperliquid.exchange.sign_usd_transfer_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test usd_transfer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test USD transfer + destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" + response = exchange.usd_transfer( + destination=destination, + amount=1000.0 + ) + + assert response == {"status": "ok"} + + # Verify sign_usd_transfer_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + message = call_args[1] + assert message["destination"] == destination + assert message["amount"] == "1000.0" + assert message["time"] == 1234567890 + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + action = call_args[0] + assert action["type"] == "usdSend" # Corrected action type + assert action["destination"] == destination + assert action["amount"] == "1000.0" + assert action["time"] == 1234567890 From 66640335a1f7499bb7d59dc4aeaa4758e11b7768 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:08:37 -0800 Subject: [PATCH 13/16] Add tests for spot_transfer and withdraw_from_bridge --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index c97e504..dfb99a1 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 66% - 66% + 67% + 67% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index e8da953..d51df29 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -1021,3 +1021,80 @@ def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["destination"] == destination assert action["amount"] == "1000.0" assert action["time"] == 1234567890 + +@patch('hyperliquid.exchange.sign_spot_transfer_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_spot_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test spot_transfer method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test spot transfer + destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" + response = exchange.spot_transfer( + amount=1000.0, + destination=destination, + token="ETH" + ) + + assert response == {"status": "ok"} + + # Verify sign_spot_transfer_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + message = call_args[1] + assert message["destination"] == destination + assert message["amount"] == "1000.0" + assert message["token"] == "ETH" + assert message["time"] == 1234567890 + assert message["type"] == "spotSend" + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + action = call_args[0] + assert action["type"] == "spotSend" + assert action["destination"] == destination + assert action["amount"] == "1000.0" + assert action["token"] == "ETH" + assert action["time"] == 1234567890 + +@patch('hyperliquid.exchange.sign_withdraw_from_bridge_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test withdraw_from_bridge method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test bridge withdrawal + destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" + response = exchange.withdraw_from_bridge( + amount=1000.0, + destination=destination + ) + + assert response == {"status": "ok"} + + # Verify sign_withdraw_from_bridge_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "withdraw3" + assert action["destination"] == destination + assert action["amount"] == "1000.0" + assert action["time"] == 1234567890 + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + action = call_args[0] + assert action["type"] == "withdraw3" + assert action["destination"] == destination + assert action["amount"] == "1000.0" + assert action["time"] == 1234567890 From b806e8dbd836a9bcf76133bf10d9fc4abd91a069 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:13:37 -0800 Subject: [PATCH 14/16] Add tests for test_approve_agent and test_approve_builder_fee --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index dfb99a1..a4262d3 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 67% - 67% + 69% + 69% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index d51df29..7708efc 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -1098,3 +1098,85 @@ def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, excha assert action["destination"] == destination assert action["amount"] == "1000.0" assert action["time"] == 1234567890 + +@patch('hyperliquid.exchange.sign_agent') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +@patch('hyperliquid.exchange.secrets.token_hex') +def test_approve_agent(mock_token_hex, mock_post_action, mock_timestamp, mock_sign, exchange): + """Test approve_agent method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + # Generate a 32-byte hex string (64 characters) + mock_token_hex.return_value = "a" * 64 + + # Test 1: approve agent without name + response, agent_key = exchange.approve_agent() + + assert response == {"status": "ok"} + assert agent_key == "0x" + ("a" * 64) + + # Verify sign_agent was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "approveAgent" + assert "agentAddress" in action + assert action["nonce"] == 1234567890 + assert "agentName" not in action + + # Test 2: approve agent with name + mock_sign.reset_mock() + mock_post_action.reset_mock() + + response, agent_key = exchange.approve_agent(name="test_agent") + + assert response == {"status": "ok"} + assert agent_key == "0x" + ("a" * 64) + + # Verify sign_agent was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "approveAgent" + assert "agentAddress" in action + assert action["nonce"] == 1234567890 + assert action["agentName"] == "test_agent" + +@patch('hyperliquid.exchange.sign_approve_builder_fee') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test approve_builder_fee method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test approving builder fee + response = exchange.approve_builder_fee( + builder="0x1234567890123456789012345678901234567890", + max_fee_rate="0.001" + ) + + assert response == {"status": "ok"} + + # Verify sign_approve_builder_fee was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "approveBuilderFee" + assert action["builder"] == "0x1234567890123456789012345678901234567890" + assert action["maxFeeRate"] == "0.001" + assert action["nonce"] == 1234567890 + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + action = call_args[0] + assert action["type"] == "approveBuilderFee" + assert action["builder"] == "0x1234567890123456789012345678901234567890" + assert action["maxFeeRate"] == "0.001" + assert action["nonce"] == 1234567890 From 969de810c6d491c62583ab897f672b3017b0a539 Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:15:51 -0800 Subject: [PATCH 15/16] Add tests for convert_to_multi_sig_user and multi_sig --- assets/images/coverage.svg | 4 +- tests/test_exchange.py | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index a4262d3..ffd257b 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 69% - 69% + 71% + 71% diff --git a/tests/test_exchange.py b/tests/test_exchange.py index 7708efc..e72001a 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -2,6 +2,7 @@ from eth_account import Account from eth_account.signers.local import LocalAccount from unittest.mock import Mock, patch +import json from hyperliquid.exchange import Exchange from hyperliquid.utils.constants import MAINNET_API_URL @@ -1180,3 +1181,84 @@ def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchan assert action["builder"] == "0x1234567890123456789012345678901234567890" assert action["maxFeeRate"] == "0.001" assert action["nonce"] == 1234567890 + +@patch('hyperliquid.exchange.sign_convert_to_multi_sig_user_action') +@patch('hyperliquid.exchange.get_timestamp_ms') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_convert_to_multi_sig_user(mock_post_action, mock_timestamp, mock_sign, exchange): + """Test convert_to_multi_sig_user method""" + # Setup + mock_timestamp.return_value = 1234567890 + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test converting to multi-sig user + authorized_users = [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000" # Note: Will be sorted + ] + threshold = 1 + response = exchange.convert_to_multi_sig_user(authorized_users, threshold) + + assert response == {"status": "ok"} + + # Verify sign_convert_to_multi_sig_user_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "convertToMultiSigUser" + assert action["nonce"] == 1234567890 + + # Verify signers JSON structure + signers = json.loads(action["signers"]) + assert signers["threshold"] == 1 + assert signers["authorizedUsers"] == sorted(authorized_users) + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[1] == "test_signature" + +@patch('hyperliquid.exchange.sign_multi_sig_action') +@patch('hyperliquid.exchange.Exchange._post_action') +def test_multi_sig(mock_post_action, mock_sign, exchange): + """Test multi_sig method""" + # Setup + mock_sign.return_value = "test_signature" + mock_post_action.return_value = {"status": "ok"} + + # Test parameters + multi_sig_user = "0xABCD" + inner_action = {"type": "order", "data": "test"} + signatures = ["sig1", "sig2"] + nonce = 1234567890 + vault_address = "0x1234" + + # Test multi-sig action + response = exchange.multi_sig( + multi_sig_user=multi_sig_user, + inner_action=inner_action, + signatures=signatures, + nonce=nonce, + vault_address=vault_address + ) + + assert response == {"status": "ok"} + + # Verify sign_multi_sig_action was called correctly + mock_sign.assert_called_once() + call_args = mock_sign.call_args[0] + action = call_args[1] + assert action["type"] == "multiSig" + assert action["signatureChainId"] == "0x66eee" + assert action["signatures"] == signatures + assert action["payload"]["multiSigUser"] == multi_sig_user.lower() + assert action["payload"]["outerSigner"] == exchange.wallet.address.lower() + assert action["payload"]["action"] == inner_action + + # Verify _post_action was called correctly + mock_post_action.assert_called_once() + call_args = mock_post_action.call_args[0] + assert call_args[0] == action + assert call_args[1] == "test_signature" + assert call_args[2] == nonce From 4ca46bbaf13fb67148fd27fa5cf467f9aa8b127c Mon Sep 17 00:00:00 2001 From: perplover <184728147+perplover@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:16:16 -0800 Subject: [PATCH 16/16] Run make codestyle --- tests/test_exchange.py | 574 ++++++++++++++++++----------------------- 1 file changed, 255 insertions(+), 319 deletions(-) diff --git a/tests/test_exchange.py b/tests/test_exchange.py index e72001a..fb6978d 100644 --- a/tests/test_exchange.py +++ b/tests/test_exchange.py @@ -1,8 +1,9 @@ +import json +from unittest.mock import Mock, patch + import pytest from eth_account import Account from eth_account.signers.local import LocalAccount -from unittest.mock import Mock, patch -import json from hyperliquid.exchange import Exchange from hyperliquid.utils.constants import MAINNET_API_URL @@ -11,16 +12,19 @@ TEST_META: Meta = {"universe": []} TEST_SPOT_META: SpotMeta = {"universe": [], "tokens": []} + @pytest.fixture def wallet() -> LocalAccount: """Create a test wallet""" return Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") + @pytest.fixture def exchange(wallet): """Fixture that provides an Exchange instance""" return Exchange(wallet) + def test_initializer(exchange, wallet): """Test that the Exchange class initializes with correct default values""" assert exchange.base_url == MAINNET_API_URL @@ -29,28 +33,30 @@ def test_initializer(exchange, wallet): assert exchange.account_address is None assert exchange.info is not None + def test_initializer_with_custom_values(wallet): """Test that the Exchange class can be initialized with custom values""" custom_url = "https://custom.api.url" vault_address = "0x1234567890123456789012345678901234567890" account_address = "0x0987654321098765432109876543210987654321" - + exchange = Exchange( wallet=wallet, base_url=custom_url, meta=TEST_META, vault_address=vault_address, account_address=account_address, - spot_meta=TEST_SPOT_META + spot_meta=TEST_SPOT_META, ) - + assert exchange.base_url == custom_url assert exchange.wallet == wallet assert exchange.vault_address == vault_address assert exchange.account_address == account_address assert exchange.info is not None -@patch('hyperliquid.api.API.post') + +@patch("hyperliquid.api.API.post") def test_post_action(mock_post, exchange): """Test _post_action method""" # Setup @@ -62,13 +68,7 @@ def test_post_action(mock_post, exchange): # Test with regular action response = exchange._post_action(action, signature, nonce) mock_post.assert_called_once_with( - "/exchange", - { - "action": action, - "nonce": nonce, - "signature": signature, - "vaultAddress": None - } + "/exchange", {"action": action, "nonce": nonce, "signature": signature, "vaultAddress": None} ) assert response == {"status": "ok"} @@ -77,13 +77,7 @@ def test_post_action(mock_post, exchange): exchange.vault_address = "0x1234" response = exchange._post_action(action, signature, nonce) mock_post.assert_called_once_with( - "/exchange", - { - "action": action, - "nonce": nonce, - "signature": signature, - "vaultAddress": "0x1234" - } + "/exchange", {"action": action, "nonce": nonce, "signature": signature, "vaultAddress": "0x1234"} ) # Test with usdClassTransfer action @@ -91,16 +85,11 @@ def test_post_action(mock_post, exchange): action["type"] = "usdClassTransfer" response = exchange._post_action(action, signature, nonce) mock_post.assert_called_once_with( - "/exchange", - { - "action": action, - "nonce": nonce, - "signature": signature, - "vaultAddress": None - } + "/exchange", {"action": action, "nonce": nonce, "signature": signature, "vaultAddress": None} ) -@patch('hyperliquid.info.Info.all_mids') + +@patch("hyperliquid.info.Info.all_mids") def test_slippage_price_perp(mock_all_mids, exchange): """Test _slippage_price method for perpetual contracts""" # Setup @@ -120,7 +109,8 @@ def test_slippage_price_perp(mock_all_mids, exchange): price = exchange._slippage_price("ETH", True, 0.05, 1000.0) assert price == 1050.0 # 1000 * (1 + 0.05) -@patch('hyperliquid.info.Info.all_mids') + +@patch("hyperliquid.info.Info.all_mids") def test_slippage_price_spot(mock_all_mids, exchange): """Test _slippage_price method for spot trading""" # Setup @@ -136,7 +126,8 @@ def test_slippage_price_spot(mock_all_mids, exchange): price = exchange._slippage_price("BTC/USDC", False, 0.05) assert price == 38000.0 # 40000 * (1 - 0.05) -@patch('hyperliquid.info.Info.all_mids') + +@patch("hyperliquid.info.Info.all_mids") def test_slippage_price_rounding(mock_all_mids, exchange): """Test price rounding in _slippage_price method""" # Setup for perp @@ -146,7 +137,7 @@ def test_slippage_price_rounding(mock_all_mids, exchange): # Test perp rounding (6 decimals) price = exchange._slippage_price("ETH", True, 0.05) - assert str(price).count('.') == 0 or len(str(price).split('.')[1]) <= 6 + assert str(price).count(".") == 0 or len(str(price).split(".")[1]) <= 6 # Setup for spot mock_all_mids.return_value = {"BTC/USDC": "40000.123456789"} @@ -155,11 +146,12 @@ def test_slippage_price_rounding(mock_all_mids, exchange): # Test spot rounding (8 decimals) price = exchange._slippage_price("BTC/USDC", True, 0.05) - assert str(price).count('.') == 0 or len(str(price).split('.')[1]) <= 8 + assert str(price).count(".") == 0 or len(str(price).split(".")[1]) <= 8 + -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_bulk_orders(mock_post_action, mock_timestamp, mock_sign, exchange): """Test bulk_orders method""" # Setup @@ -181,9 +173,9 @@ def test_bulk_orders(mock_post_action, mock_timestamp, mock_sign, exchange): ] response = exchange.bulk_orders(order_requests) - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -195,20 +187,18 @@ def test_bulk_orders(mock_post_action, mock_timestamp, mock_sign, exchange): # Verify _post_action was called correctly mock_post_action.assert_called_once_with( - mock_sign.call_args[0][1], # action - "test_signature", # signature - 1234567890 # timestamp + mock_sign.call_args[0][1], "test_signature", 1234567890 # action # signature # timestamp ) # Test with builder mock_sign.reset_mock() mock_post_action.reset_mock() - + builder = {"b": "TEST_BUILDER", "r": 0.001} # Using uppercase to test lowercasing response = exchange.bulk_orders(order_requests, builder) - + assert response == {"status": "ok"} - + # Verify builder was included in the action call_args = mock_sign.call_args[0] action = call_args[1] # Get the action argument @@ -217,18 +207,12 @@ def test_bulk_orders(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["builder"]["b"] == "test_builder" assert action["builder"]["r"] == 0.001 -@patch('hyperliquid.exchange.Exchange.bulk_orders') + +@patch("hyperliquid.exchange.Exchange.bulk_orders") def test_order(mock_bulk_orders, exchange): """Test order method with various scenarios""" # Setup - mock_bulk_orders.return_value = { - "status": "ok", - "response": { - "data": { - "statuses": [{"resting": {"oid": 123}}] - } - } - } + mock_bulk_orders.return_value = {"status": "ok", "response": {"data": {"statuses": [{"resting": {"oid": 123}}]}}} exchange.info.name_to_asset = lambda x: 1 # Test 1: Basic limit order @@ -239,10 +223,10 @@ def test_order(mock_bulk_orders, exchange): limit_px=2000.0, order_type={"limit": {"tif": "Gtc"}}, ) - + assert response["status"] == "ok" assert response["response"]["data"]["statuses"][0]["resting"]["oid"] == 123 - + mock_bulk_orders.assert_called_once_with( [ { @@ -254,22 +238,17 @@ def test_order(mock_bulk_orders, exchange): "reduce_only": False, } ], - None + None, ) # Test 2: Order with builder fee mock_bulk_orders.reset_mock() builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} - + response = exchange.order( - name="ETH", - is_buy=True, - sz=0.05, - limit_px=2000.0, - order_type={"limit": {"tif": "Ioc"}}, - builder=builder + name="ETH", is_buy=True, sz=0.05, limit_px=2000.0, order_type={"limit": {"tif": "Ioc"}}, builder=builder ) - + assert response["status"] == "ok" mock_bulk_orders.assert_called_once() call_args = mock_bulk_orders.call_args[0] @@ -284,9 +263,9 @@ def test_order(mock_bulk_orders, exchange): sz=100, limit_px=100, order_type={"trigger": {"triggerPx": 103, "isMarket": True, "tpsl": "sl"}}, - reduce_only=True + reduce_only=True, ) - + assert response["status"] == "ok" mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] @@ -296,24 +275,20 @@ def test_order(mock_bulk_orders, exchange): # Test 4: Order with cloid mock_bulk_orders.reset_mock() cloid = "0x00000000000000000000000000000001" - + response = exchange.order( - name="ETH", - is_buy=True, - sz=1.0, - limit_px=2000.0, - order_type={"limit": {"tif": "Gtc"}}, - cloid=cloid + name="ETH", is_buy=True, sz=1.0, limit_px=2000.0, order_type={"limit": {"tif": "Gtc"}}, cloid=cloid ) - + assert response["status"] == "ok" mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] assert order_request["cloid"] == cloid -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): """Test modify_order method""" # Setup @@ -332,9 +307,9 @@ def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): limit_px=1105, order_type={"limit": {"tif": "Gtc"}}, ) - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -342,15 +317,16 @@ def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["type"] == "batchModify" assert len(action["modifies"]) == 1 assert action["modifies"][0]["oid"] == oid - + # Test 2: Modify order with cloid mock_sign.reset_mock() mock_post_action.reset_mock() - + from hyperliquid.utils.types import Cloid + cloid = Cloid.from_str("0x00000000000000000000000000000001") new_cloid = Cloid.from_str("0x00000000000000000000000000000002") - + response = exchange.modify_order( oid=cloid, name="ETH", @@ -359,9 +335,9 @@ def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): limit_px=1105, order_type={"limit": {"tif": "Gtc"}}, reduce_only=True, - cloid=new_cloid + cloid=new_cloid, ) - + assert response == {"status": "ok"} call_args = mock_sign.call_args[0] action = call_args[1] @@ -369,9 +345,10 @@ def test_modify_order(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["modifies"][0]["order"]["r"] is True # reduce_only assert "c" in action["modifies"][0]["order"] # cloid in wire format -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exchange): """Test bulk_modify_orders_new method""" # Setup @@ -381,6 +358,7 @@ def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exc exchange.info.name_to_asset = lambda x: 1 from hyperliquid.utils.types import Cloid + cloid1 = Cloid.from_str("0x00000000000000000000000000000001") cloid2 = Cloid.from_str("0x00000000000000000000000000000002") @@ -411,23 +389,23 @@ def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exc }, }, ] - + response = exchange.bulk_modify_orders_new(modify_requests) assert response == {"status": "ok"} - + # Verify the action structure mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] - + assert action["type"] == "batchModify" assert len(action["modifies"]) == 2 - + # Verify first modification assert action["modifies"][0]["oid"] == 12345 assert action["modifies"][0]["order"]["s"] == "0.1" # size as string assert action["modifies"][0]["order"]["p"] == "1105" # price as string - + # Verify second modification assert action["modifies"][1]["oid"] == cloid1.to_raw() assert action["modifies"][1]["order"]["s"] == "1" # size as string @@ -435,17 +413,14 @@ def test_bulk_modify_orders_new(mock_post_action, mock_timestamp, mock_sign, exc assert action["modifies"][1]["order"]["r"] is True assert "c" in action["modifies"][1]["order"] # cloid in wire format -@patch('hyperliquid.exchange.Exchange.bulk_orders') + +@patch("hyperliquid.exchange.Exchange.bulk_orders") def test_market_open(mock_bulk_orders, exchange): """Test market_open method""" # Setup mock_bulk_orders.return_value = { "status": "ok", - "response": { - "data": { - "statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}] - } - } + "response": {"data": {"statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}]}}, } exchange.info.name_to_asset = lambda x: 1 @@ -455,10 +430,10 @@ def test_market_open(mock_bulk_orders, exchange): is_buy=True, sz=0.05, ) - + assert response["status"] == "ok" assert response["response"]["data"]["statuses"][0]["filled"]["oid"] == 123 - + mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] assert order_request["coin"] == "ETH" @@ -466,19 +441,13 @@ def test_market_open(mock_bulk_orders, exchange): assert order_request["sz"] == 0.05 assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} assert order_request["reduce_only"] is False - + # Test 2: Market open with slippage and builder mock_bulk_orders.reset_mock() builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} - - response = exchange.market_open( - name="ETH", - is_buy=True, - sz=0.05, - builder=builder, - slippage=0.01 # 1% slippage - ) - + + response = exchange.market_open(name="ETH", is_buy=True, sz=0.05, builder=builder, slippage=0.01) # 1% slippage + assert response["status"] == "ok" mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] @@ -489,57 +458,41 @@ def test_market_open(mock_bulk_orders, exchange): assert mock_bulk_orders.call_args[0][1]["b"].lower() == builder["b"].lower() assert mock_bulk_orders.call_args[0][1]["r"] == builder["r"] -@patch('hyperliquid.exchange.Exchange.bulk_orders') + +@patch("hyperliquid.exchange.Exchange.bulk_orders") def test_market_close(mock_bulk_orders, exchange): """Test market_close method""" # Setup mock_bulk_orders.return_value = { "status": "ok", - "response": { - "data": { - "statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}] - } - } + "response": {"data": {"statuses": [{"filled": {"oid": 123, "totalSz": "0.05", "avgPx": "1950.5"}}]}}, } exchange.info.name_to_asset = lambda x: 1 - + # Mock user_state to return a position exchange.info.user_state = lambda x: { - "assetPositions": [ - { - "position": { - "coin": "ETH", - "szi": "0.05", - "entryPx": "2000", - "positionValue": "100" - } - } - ] + "assetPositions": [{"position": {"coin": "ETH", "szi": "0.05", "entryPx": "2000", "positionValue": "100"}}] } # Test 1: Basic market close response = exchange.market_close("ETH") - + assert response["status"] == "ok" assert response["response"]["data"]["statuses"][0]["filled"]["oid"] == 123 - + mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] assert order_request["coin"] == "ETH" assert order_request["sz"] == 0.05 assert order_request["order_type"] == {"limit": {"tif": "Ioc"}} assert order_request["reduce_only"] is True - + # Test 2: Market close with slippage and builder mock_bulk_orders.reset_mock() builder = {"b": "0x8c967E73E7B15087c42A10D344cFf4c96D877f1D", "r": 0.001} - - response = exchange.market_close( - coin="ETH", - builder=builder, - slippage=0.01 # 1% slippage - ) - + + response = exchange.market_close(coin="ETH", builder=builder, slippage=0.01) # 1% slippage + assert response["status"] == "ok" mock_bulk_orders.assert_called_once() order_request = mock_bulk_orders.call_args[0][0][0] @@ -551,9 +504,10 @@ def test_market_close(mock_bulk_orders, exchange): assert mock_bulk_orders.call_args[0][1]["b"].lower() == builder["b"].lower() assert mock_bulk_orders.call_args[0][1]["r"] == builder["r"] -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): """Test cancel method""" # Setup @@ -564,9 +518,9 @@ def test_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): # Test basic cancel response = exchange.cancel("ETH", 12345) - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -576,9 +530,10 @@ def test_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["cancels"][0]["a"] == 1 # asset assert action["cancels"][0]["o"] == 12345 # oid -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): """Test cancel_by_cloid method""" # Setup @@ -588,13 +543,14 @@ def test_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): exchange.info.name_to_asset = lambda x: 1 from hyperliquid.utils.types import Cloid + cloid = Cloid.from_str("0x00000000000000000000000000000001") # Test cancel by cloid response = exchange.cancel_by_cloid("ETH", cloid) - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -604,9 +560,10 @@ def test_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["cancels"][0]["asset"] == 1 assert action["cancels"][0]["cloid"] == cloid.to_raw() -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_bulk_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): """Test bulk_cancel method""" # Setup @@ -616,14 +573,11 @@ def test_bulk_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): exchange.info.name_to_asset = lambda x: 1 # Test multiple cancels - cancel_requests = [ - {"coin": "ETH", "oid": 12345}, - {"coin": "BTC", "oid": 67890} - ] - + cancel_requests = [{"coin": "ETH", "oid": 12345}, {"coin": "BTC", "oid": 67890}] + response = exchange.bulk_cancel(cancel_requests) assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -635,9 +589,10 @@ def test_bulk_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["cancels"][1]["a"] == 1 assert action["cancels"][1]["o"] == 67890 -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_bulk_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, exchange): """Test bulk_cancel_by_cloid method""" # Setup @@ -647,18 +602,16 @@ def test_bulk_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, excha exchange.info.name_to_asset = lambda x: 1 from hyperliquid.utils.types import Cloid + cloid1 = Cloid.from_str("0x00000000000000000000000000000001") cloid2 = Cloid.from_str("0x00000000000000000000000000000002") # Test multiple cancels by cloid - cancel_requests = [ - {"coin": "ETH", "cloid": cloid1}, - {"coin": "BTC", "cloid": cloid2} - ] - + cancel_requests = [{"coin": "ETH", "cloid": cloid1}, {"coin": "BTC", "cloid": cloid2}] + response = exchange.bulk_cancel_by_cloid(cancel_requests) assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -670,9 +623,10 @@ def test_bulk_cancel_by_cloid(mock_post_action, mock_timestamp, mock_sign, excha assert action["cancels"][1]["asset"] == 1 assert action["cancels"][1]["cloid"] == cloid2.to_raw() -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_schedule_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): """Test schedule_cancel method""" # Setup @@ -682,45 +636,46 @@ def test_schedule_cancel(mock_post_action, mock_timestamp, mock_sign, exchange): # Test 1: Basic schedule cancel without time (uses current timestamp) response = exchange.schedule_cancel() - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] assert action["type"] == "scheduleCancel" assert "time" not in action # No specific time provided - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature - + # Test 2: Schedule cancel with specific time mock_sign.reset_mock() mock_post_action.reset_mock() - + cancel_time = 1234567890 + 10000 # 10 seconds from now response = exchange.schedule_cancel(cancel_time) - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] assert action["type"] == "scheduleCancel" assert action["time"] == cancel_time - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): """Test update_leverage method""" # Setup @@ -731,9 +686,9 @@ def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): # Test 1: Update leverage with default cross margin response = exchange.update_leverage(leverage=10, name="ETH") - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -742,24 +697,20 @@ def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["asset"] == 1 assert action["leverage"] == 10 assert action["isCross"] is True - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature - + # Test 2: Update leverage with isolated margin mock_sign.reset_mock() mock_post_action.reset_mock() - - response = exchange.update_leverage( - leverage=5, - name="BTC", - is_cross=False - ) - + + response = exchange.update_leverage(leverage=5, name="BTC", is_cross=False) + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -768,15 +719,16 @@ def test_update_leverage(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["asset"] == 1 assert action["leverage"] == 5 assert action["isCross"] is False - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_update_isolated_margin(mock_post_action, mock_timestamp, mock_sign, exchange): """Test update_isolated_margin method""" # Setup @@ -786,13 +738,10 @@ def test_update_isolated_margin(mock_post_action, mock_timestamp, mock_sign, exc exchange.info.name_to_asset = lambda x: 1 # Test: Update isolated margin - response = exchange.update_isolated_margin( - amount=1000.0, - name="ETH" - ) - + response = exchange.update_isolated_margin(amount=1000.0, name="ETH") + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -801,15 +750,16 @@ def test_update_isolated_margin(mock_post_action, mock_timestamp, mock_sign, exc assert action["asset"] == 1 assert action["isBuy"] is True # isBuy is always True in the implementation assert "ntli" in action # ntli (notional) should be present - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_set_referrer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test set_referrer method""" # Setup @@ -819,24 +769,25 @@ def test_set_referrer(mock_post_action, mock_timestamp, mock_sign, exchange): # Test setting referrer code response = exchange.set_referrer("ASDFASDF") - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] assert action["type"] == "setReferrer" assert action["code"] == "ASDFASDF" - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_create_sub_account(mock_post_action, mock_timestamp, mock_sign, exchange): """Test create_sub_account method""" # Setup @@ -846,24 +797,25 @@ def test_create_sub_account(mock_post_action, mock_timestamp, mock_sign, exchang # Test creating sub account response = exchange.create_sub_account("example") - + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] assert action["type"] == "createSubAccount" assert action["name"] == "example" - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_usd_class_transfer_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_usd_class_transfer_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_usd_class_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test usd_class_transfer method""" # Setup @@ -873,9 +825,9 @@ def test_usd_class_transfer(mock_post_action, mock_timestamp, mock_sign, exchang # Test 1: Basic transfer without vault address response = exchange.usd_class_transfer(amount=1000.0, to_perp=True) - + assert response == {"status": "ok"} - + # Verify sign_usd_class_transfer_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -884,16 +836,16 @@ def test_usd_class_transfer(mock_post_action, mock_timestamp, mock_sign, exchang assert action["amount"] == "1000.0" assert action["toPerp"] is True assert action["nonce"] == 1234567890 - + # Test 2: Transfer with vault address mock_sign.reset_mock() mock_post_action.reset_mock() exchange.vault_address = "0x1234" - + response = exchange.usd_class_transfer(amount=500.5, to_perp=False) - + assert response == {"status": "ok"} - + # Verify sign_usd_class_transfer_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -903,9 +855,10 @@ def test_usd_class_transfer(mock_post_action, mock_timestamp, mock_sign, exchang assert action["toPerp"] is False assert action["nonce"] == 1234567890 -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test sub_account_transfer method""" # Setup @@ -915,14 +868,10 @@ def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, excha # Test deposit to sub account sub_account = "0x1d9470d4b963f552e6f671a81619d395877bf409" - response = exchange.sub_account_transfer( - sub_account_user=sub_account, - is_deposit=True, - usd=1000 - ) - + response = exchange.sub_account_transfer(sub_account_user=sub_account, is_deposit=True, usd=1000) + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -931,19 +880,15 @@ def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, excha assert action["subAccountUser"] == sub_account assert action["isDeposit"] is True assert action["usd"] == 1000 - + # Test withdrawal from sub account mock_sign.reset_mock() mock_post_action.reset_mock() - - response = exchange.sub_account_transfer( - sub_account_user=sub_account, - is_deposit=False, - usd=500 - ) - + + response = exchange.sub_account_transfer(sub_account_user=sub_account, is_deposit=False, usd=500) + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -953,9 +898,10 @@ def test_sub_account_transfer(mock_post_action, mock_timestamp, mock_sign, excha assert action["isDeposit"] is False assert action["usd"] == 500 -@patch('hyperliquid.exchange.sign_l1_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_l1_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_vault_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test vault_usd_transfer method""" # Setup @@ -965,14 +911,10 @@ def test_vault_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchang # Test vault transfer vault_address = "0xa15099a30bbf2e68942d6f4c43d70d04faeab0a0" - response = exchange.vault_usd_transfer( - vault_address=vault_address, - is_deposit=True, - usd=5_000_000 - ) - + response = exchange.vault_usd_transfer(vault_address=vault_address, is_deposit=True, usd=5_000_000) + assert response == {"status": "ok"} - + # Verify sign_l1_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -981,15 +923,16 @@ def test_vault_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchang assert action["vaultAddress"] == vault_address assert action["isDeposit"] is True assert action["usd"] == 5_000_000 - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" # signature -@patch('hyperliquid.exchange.sign_usd_transfer_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_usd_transfer_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test usd_transfer method""" # Setup @@ -999,13 +942,10 @@ def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): # Test USD transfer destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" - response = exchange.usd_transfer( - destination=destination, - amount=1000.0 - ) - + response = exchange.usd_transfer(destination=destination, amount=1000.0) + assert response == {"status": "ok"} - + # Verify sign_usd_transfer_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1013,7 +953,7 @@ def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): assert message["destination"] == destination assert message["amount"] == "1000.0" assert message["time"] == 1234567890 - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] @@ -1023,9 +963,10 @@ def test_usd_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["amount"] == "1000.0" assert action["time"] == 1234567890 -@patch('hyperliquid.exchange.sign_spot_transfer_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_spot_transfer_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_spot_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): """Test spot_transfer method""" # Setup @@ -1035,14 +976,10 @@ def test_spot_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): # Test spot transfer destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" - response = exchange.spot_transfer( - amount=1000.0, - destination=destination, - token="ETH" - ) - + response = exchange.spot_transfer(amount=1000.0, destination=destination, token="ETH") + assert response == {"status": "ok"} - + # Verify sign_spot_transfer_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1052,7 +989,7 @@ def test_spot_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): assert message["token"] == "ETH" assert message["time"] == 1234567890 assert message["type"] == "spotSend" - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] @@ -1063,9 +1000,10 @@ def test_spot_transfer(mock_post_action, mock_timestamp, mock_sign, exchange): assert action["token"] == "ETH" assert action["time"] == 1234567890 -@patch('hyperliquid.exchange.sign_withdraw_from_bridge_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_withdraw_from_bridge_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, exchange): """Test withdraw_from_bridge method""" # Setup @@ -1075,13 +1013,10 @@ def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, excha # Test bridge withdrawal destination = "0x5e9ee1089755c3435139848e47e6635505d5a13a" - response = exchange.withdraw_from_bridge( - amount=1000.0, - destination=destination - ) - + response = exchange.withdraw_from_bridge(amount=1000.0, destination=destination) + assert response == {"status": "ok"} - + # Verify sign_withdraw_from_bridge_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1090,7 +1025,7 @@ def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, excha assert action["destination"] == destination assert action["amount"] == "1000.0" assert action["time"] == 1234567890 - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] @@ -1100,10 +1035,11 @@ def test_withdraw_from_bridge(mock_post_action, mock_timestamp, mock_sign, excha assert action["amount"] == "1000.0" assert action["time"] == 1234567890 -@patch('hyperliquid.exchange.sign_agent') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') -@patch('hyperliquid.exchange.secrets.token_hex') + +@patch("hyperliquid.exchange.sign_agent") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") +@patch("hyperliquid.exchange.secrets.token_hex") def test_approve_agent(mock_token_hex, mock_post_action, mock_timestamp, mock_sign, exchange): """Test approve_agent method""" # Setup @@ -1115,10 +1051,10 @@ def test_approve_agent(mock_token_hex, mock_post_action, mock_timestamp, mock_si # Test 1: approve agent without name response, agent_key = exchange.approve_agent() - + assert response == {"status": "ok"} assert agent_key == "0x" + ("a" * 64) - + # Verify sign_agent was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1127,16 +1063,16 @@ def test_approve_agent(mock_token_hex, mock_post_action, mock_timestamp, mock_si assert "agentAddress" in action assert action["nonce"] == 1234567890 assert "agentName" not in action - + # Test 2: approve agent with name mock_sign.reset_mock() mock_post_action.reset_mock() - + response, agent_key = exchange.approve_agent(name="test_agent") - + assert response == {"status": "ok"} assert agent_key == "0x" + ("a" * 64) - + # Verify sign_agent was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1146,9 +1082,10 @@ def test_approve_agent(mock_token_hex, mock_post_action, mock_timestamp, mock_si assert action["nonce"] == 1234567890 assert action["agentName"] == "test_agent" -@patch('hyperliquid.exchange.sign_approve_builder_fee') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_approve_builder_fee") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchange): """Test approve_builder_fee method""" # Setup @@ -1157,13 +1094,10 @@ def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchan mock_post_action.return_value = {"status": "ok"} # Test approving builder fee - response = exchange.approve_builder_fee( - builder="0x1234567890123456789012345678901234567890", - max_fee_rate="0.001" - ) - + response = exchange.approve_builder_fee(builder="0x1234567890123456789012345678901234567890", max_fee_rate="0.001") + assert response == {"status": "ok"} - + # Verify sign_approve_builder_fee was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1172,7 +1106,7 @@ def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchan assert action["builder"] == "0x1234567890123456789012345678901234567890" assert action["maxFeeRate"] == "0.001" assert action["nonce"] == 1234567890 - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] @@ -1182,9 +1116,10 @@ def test_approve_builder_fee(mock_post_action, mock_timestamp, mock_sign, exchan assert action["maxFeeRate"] == "0.001" assert action["nonce"] == 1234567890 -@patch('hyperliquid.exchange.sign_convert_to_multi_sig_user_action') -@patch('hyperliquid.exchange.get_timestamp_ms') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_convert_to_multi_sig_user_action") +@patch("hyperliquid.exchange.get_timestamp_ms") +@patch("hyperliquid.exchange.Exchange._post_action") def test_convert_to_multi_sig_user(mock_post_action, mock_timestamp, mock_sign, exchange): """Test convert_to_multi_sig_user method""" # Setup @@ -1195,56 +1130,57 @@ def test_convert_to_multi_sig_user(mock_post_action, mock_timestamp, mock_sign, # Test converting to multi-sig user authorized_users = [ "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000000" # Note: Will be sorted + "0x0000000000000000000000000000000000000000", # Note: Will be sorted ] threshold = 1 response = exchange.convert_to_multi_sig_user(authorized_users, threshold) - + assert response == {"status": "ok"} - + # Verify sign_convert_to_multi_sig_user_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] action = call_args[1] assert action["type"] == "convertToMultiSigUser" assert action["nonce"] == 1234567890 - + # Verify signers JSON structure signers = json.loads(action["signers"]) assert signers["threshold"] == 1 assert signers["authorizedUsers"] == sorted(authorized_users) - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0] assert call_args[1] == "test_signature" -@patch('hyperliquid.exchange.sign_multi_sig_action') -@patch('hyperliquid.exchange.Exchange._post_action') + +@patch("hyperliquid.exchange.sign_multi_sig_action") +@patch("hyperliquid.exchange.Exchange._post_action") def test_multi_sig(mock_post_action, mock_sign, exchange): """Test multi_sig method""" # Setup mock_sign.return_value = "test_signature" mock_post_action.return_value = {"status": "ok"} - + # Test parameters multi_sig_user = "0xABCD" inner_action = {"type": "order", "data": "test"} signatures = ["sig1", "sig2"] nonce = 1234567890 vault_address = "0x1234" - + # Test multi-sig action response = exchange.multi_sig( multi_sig_user=multi_sig_user, inner_action=inner_action, signatures=signatures, nonce=nonce, - vault_address=vault_address + vault_address=vault_address, ) - + assert response == {"status": "ok"} - + # Verify sign_multi_sig_action was called correctly mock_sign.assert_called_once() call_args = mock_sign.call_args[0] @@ -1255,7 +1191,7 @@ def test_multi_sig(mock_post_action, mock_sign, exchange): assert action["payload"]["multiSigUser"] == multi_sig_user.lower() assert action["payload"]["outerSigner"] == exchange.wallet.address.lower() assert action["payload"]["action"] == inner_action - + # Verify _post_action was called correctly mock_post_action.assert_called_once() call_args = mock_post_action.call_args[0]