diff --git a/examples/multi_sig_user_set_abstraction.py b/examples/multi_sig_user_set_abstraction.py new file mode 100644 index 00000000..5e7e8715 --- /dev/null +++ b/examples/multi_sig_user_set_abstraction.py @@ -0,0 +1,59 @@ +import example_utils + +from hyperliquid.utils import constants +from hyperliquid.utils.signing import ( + USER_SET_ABSTRACTION_SIGN_TYPES, + get_timestamp_ms, + sign_multi_sig_user_signed_action_payload, +) + + +def main(): + address, info, exchange = example_utils.setup(constants.TESTNET_API_URL, skip_ws=True) + multi_sig_wallets = example_utils.setup_multi_sig_wallets() + + # The outer signer is required to be an authorized user or an agent of an authorized user of the multi-sig user. + + # Address of the multi-sig user that the action will be executed for. + # Executing the action requires at least the specified threshold of signatures for that multi-sig user. + multi_sig_user = "0x0000000000000000000000000000000000000005" + + # userSetAbstraction may target the multi-sig user itself or a sub-account user controlled by the multi-sig user. + target_user = multi_sig_user + abstraction = "disabled" + timestamp = get_timestamp_ms() + + # Must use the human abstraction string here. Exchange.multi_sig canonicalizes this action to the wire enum value + # when it builds and signs the outer multi-sig payload. It is expected that the different signatures need different + # versions of this value. + action = { + "type": "userSetAbstraction", + "signatureChainId": "0x66eee", + "hyperliquidChain": "Testnet", + "user": target_user.lower(), + "abstraction": abstraction, + "nonce": timestamp, + } + signatures = [] + + # Collect signatures from each wallet in multi_sig_wallets. Each wallet must belong to a user. + for wallet in multi_sig_wallets: + signature = sign_multi_sig_user_signed_action_payload( + wallet, + action, + exchange.base_url == constants.MAINNET_API_URL, + USER_SET_ABSTRACTION_SIGN_TYPES, + "HyperliquidTransaction:UserSetAbstraction", + multi_sig_user, + address, + ) + signatures.append(signature) + + print("current user abstraction state:", info.query_user_abstraction_state(target_user)) + multi_sig_result = exchange.multi_sig(multi_sig_user, action, signatures, timestamp) + print("multi-sig userSetAbstraction result:", multi_sig_result) + print("updated user abstraction state:", info.query_user_abstraction_state(target_user)) + + +if __name__ == "__main__": + main() diff --git a/hyperliquid/exchange.py b/hyperliquid/exchange.py index 8e560f3b..41a2f66b 100644 --- a/hyperliquid/exchange.py +++ b/hyperliquid/exchange.py @@ -56,6 +56,26 @@ def _get_dex(coin: str) -> str: return coin.split(":")[0] if ":" in coin else "" +USER_SET_ABSTRACTION_WIRE_VALUES = { + "disabled": "i", + "unifiedAccount": "u", + "portfolioMargin": "p", +} + + +def _multi_sig_payload_action(inner_action): + if inner_action.get("type") != "userSetAbstraction": + return inner_action + + abstraction = inner_action.get("abstraction") + if abstraction not in USER_SET_ABSTRACTION_WIRE_VALUES: + return inner_action + + payload_action = inner_action.copy() + payload_action["abstraction"] = USER_SET_ABSTRACTION_WIRE_VALUES[abstraction] + return payload_action + + class Exchange(API): # Default Max Slippage for Market Orders 5% DEFAULT_SLIPPAGE = 0.05 @@ -1078,6 +1098,7 @@ def c_validator_unregister(self) -> Any: def multi_sig(self, multi_sig_user, inner_action, signatures, nonce, vault_address=None): multi_sig_user = multi_sig_user.lower() + payload_action = _multi_sig_payload_action(inner_action) multi_sig_action = { "type": "multiSig", "signatureChainId": "0x66eee", @@ -1085,7 +1106,7 @@ def multi_sig(self, multi_sig_user, inner_action, signatures, nonce, vault_addre "payload": { "multiSigUser": multi_sig_user, "outerSigner": self.wallet.address.lower(), - "action": inner_action, + "action": payload_action, }, } is_mainnet = self.base_url == MAINNET_API_URL diff --git a/pyproject.toml b/pyproject.toml index ba2e07d5..30656a81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "hyperliquid-python-sdk" -version = "0.23.0" +version = "0.24.0" description = "SDK for Hyperliquid API trading with Python." readme = "README.md" authors = ["Hyperliquid "] diff --git a/tests/signing_test.py b/tests/signing_test.py index 060c0800..b6e89d9f 100644 --- a/tests/signing_test.py +++ b/tests/signing_test.py @@ -2,6 +2,7 @@ import pytest from eth_utils import to_hex +from hyperliquid.exchange import _multi_sig_payload_action from hyperliquid.utils.signing import ( OrderRequest, ScheduleCancelAction, @@ -211,6 +212,22 @@ def test_sign_withdraw_from_bridge_action(): assert signature["v"] == 28 +def test_multi_sig_user_set_abstraction_payload_uses_wire_enum(): + action = { + "type": "userSetAbstraction", + "signatureChainId": "0x66eee", + "hyperliquidChain": "Testnet", + "user": "0x3b4d2cc2e144a0044002506c8b44508e9ace82e9", + "abstraction": "disabled", + "nonce": 1780130409592, + } + + payload_action = _multi_sig_payload_action(action) + + assert payload_action["abstraction"] == "i" + assert action["abstraction"] == "disabled" + + def test_create_sub_account_action(): wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") action = {