diff --git a/hyperliquid/exchange.py b/hyperliquid/exchange.py index 41a2f66b..6ee6c81e 100644 --- a/hyperliquid/exchange.py +++ b/hyperliquid/exchange.py @@ -665,7 +665,7 @@ def approve_builder_fee(self, builder: str, max_fee_rate: str) -> Any: def convert_to_multi_sig_user(self, authorized_users: List[str], threshold: int) -> Any: timestamp = get_timestamp_ms() - authorized_users = sorted(authorized_users) + authorized_users = sorted(authorized_user.lower() for authorized_user in authorized_users) signers = { "authorizedUsers": authorized_users, "threshold": threshold, diff --git a/tests/signing_test.py b/tests/signing_test.py index b6e89d9f..5053f060 100644 --- a/tests/signing_test.py +++ b/tests/signing_test.py @@ -1,8 +1,10 @@ +import json + import eth_account import pytest from eth_utils import to_hex -from hyperliquid.exchange import _multi_sig_payload_action +from hyperliquid.exchange import Exchange, _multi_sig_payload_action from hyperliquid.utils.signing import ( OrderRequest, ScheduleCancelAction, @@ -228,6 +230,39 @@ def test_multi_sig_user_set_abstraction_payload_uses_wire_enum(): assert action["abstraction"] == "disabled" +def test_convert_to_multi_sig_user_signers_do_not_depend_on_input_casing(): + # The sort exists to give the signers list a canonical order, but sorting mixed-case + # addresses orders on ASCII, so the result depends on how the caller cased its input. + wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") + checksummed = [ + "0xB1C4b0f9E7fD0B9d2F0c0F6C0B0D0E0F0A0b0C0D", + "0xa2c4B0F9e7Fd0b9D2f0C0f6c0b0d0e0f0A0B0c0D", + ] + expected = sorted(authorized_user.lower() for authorized_user in checksummed) + + posted = {} + + class CapturingExchange(Exchange): + def post(self, url_path, payload=None): + posted.update(payload) + return {} + + def signers_for(authorized_users): + posted.clear() + exchange = CapturingExchange( + wallet, + "https://api.hyperliquid-testnet.xyz", + meta={"universe": []}, + spot_meta={"universe": [], "tokens": []}, + ) + exchange.convert_to_multi_sig_user(authorized_users, 2) + return json.loads(posted["action"]["signers"])["authorizedUsers"] + + assert signers_for(checksummed) == expected + assert signers_for(expected) == expected + assert signers_for(list(reversed(checksummed))) == expected + + def test_create_sub_account_action(): wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") action = {