From 299cf2bf1cd8a276d07492daa1885a8f91597898 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 23 Oct 2024 19:42:52 -0700 Subject: [PATCH] Version 0.8.0 --- assets/images/coverage.svg | 4 +- examples/basic_convert_to_multi_sig_signer.py | 19 ++++++ examples/basic_convert_to_multi_sig_user.py | 21 +++++++ examples/basic_multi_sig.py | 32 ++++++++++ examples/example_utils.py | 15 +++++ examples/multi_sig_wallets.json.example | 12 ++++ hyperliquid/exchange.py | 58 +++++++++++++++++++ hyperliquid/info.py | 6 ++ hyperliquid/utils/signing.py | 29 ++++++++++ pyproject.toml | 2 +- 10 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 examples/basic_convert_to_multi_sig_signer.py create mode 100644 examples/basic_convert_to_multi_sig_user.py create mode 100644 examples/basic_multi_sig.py create mode 100644 examples/multi_sig_wallets.json.example diff --git a/assets/images/coverage.svg b/assets/images/coverage.svg index f86374a..5cc1bb5 100644 --- a/assets/images/coverage.svg +++ b/assets/images/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 43% - 43% + 42% + 42% diff --git a/examples/basic_convert_to_multi_sig_signer.py b/examples/basic_convert_to_multi_sig_signer.py new file mode 100644 index 0000000..0ad133e --- /dev/null +++ b/examples/basic_convert_to_multi_sig_signer.py @@ -0,0 +1,19 @@ +from hyperliquid.utils import constants +import example_utils + + +def main(): + address, info, exchange = example_utils.setup(constants.TESTNET_API_URL, skip_ws=True) + + if exchange.account_address != exchange.wallet.address: + raise Exception("Agents do not have permission to convert to multi-sig signer") + + # the user owning this signer (either the user itself or agent's user) should already be registered as authorized user for the multi-sig user + signer = "0x0000000000000000000000000000000000000000" + multi_sig_user = "0x0000000000000000000000000000000000000001" + convert_result = exchange.convert_to_multi_sig_signer(signer, multi_sig_user) + print(convert_result) + + +if __name__ == "__main__": + main() diff --git a/examples/basic_convert_to_multi_sig_user.py b/examples/basic_convert_to_multi_sig_user.py new file mode 100644 index 0000000..9405a0e --- /dev/null +++ b/examples/basic_convert_to_multi_sig_user.py @@ -0,0 +1,21 @@ +from hyperliquid.utils import constants +import example_utils + + +def main(): + address, info, exchange = example_utils.setup(constants.TESTNET_API_URL, skip_ws=True) + + if exchange.account_address != exchange.wallet.address: + raise Exception("Agents do not have permission to convert to multi-sig user") + + # authorized users are the users for which one can themselves or agents use as signers + # for multi-sig actions + authorized_user_1 = "0x0000000000000000000000000000000000000000" + authorized_user_2 = "0x0000000000000000000000000000000000000001" + threshold = 1 + convert_result = exchange.convert_to_multi_sig_user([authorized_user_1, authorized_user_2], threshold) + print(convert_result) + + +if __name__ == "__main__": + main() diff --git a/examples/basic_multi_sig.py b/examples/basic_multi_sig.py new file mode 100644 index 0000000..1ec381a --- /dev/null +++ b/examples/basic_multi_sig.py @@ -0,0 +1,32 @@ +from hyperliquid.utils import constants +from hyperliquid.utils.signing import sign_usd_transfer_action, get_timestamp_ms +from hyperliquid.utils.types import Any, List +import example_utils + + +def main(): + address, info, exchange = example_utils.setup(constants.TESTNET_API_URL, skip_ws=True) + multi_sig_wallets = example_utils.setup_multi_sig_wallets() + + multi_sig_user = "0x0000000000000000000000000000000000000005" + + timestamp = get_timestamp_ms() + action = { + "type": "usdSend", + "signatureChainId": "0x66eee", + "hyperliquidChain": "Testnet", + "destination": "0x0000000000000000000000000000000000000000", + "amount": "100.0", + "time": timestamp, + } + signatures: List[Any] = [] + for wallet in multi_sig_wallets: + signature = sign_usd_transfer_action(wallet, action, False) + signatures.append(signature) + + multi_sig_result = exchange.multi_sig(multi_sig_user, action, signatures, timestamp) + print(multi_sig_result) + + +if __name__ == "__main__": + main() diff --git a/examples/example_utils.py b/examples/example_utils.py index 40b77d9..21af0a4 100644 --- a/examples/example_utils.py +++ b/examples/example_utils.py @@ -30,3 +30,18 @@ def setup(base_url=None, skip_ws=False): raise Exception(error_string) exchange = Exchange(account, base_url, account_address=address) return address, info, exchange + + +def setup_multi_sig_wallets(): + config_path = os.path.join(os.path.dirname(__file__), "multi_sig_wallets.json") + with open(config_path) as f: + config = json.load(f) + wallets = [] + for wallet_config in config: + account: LocalAccount = eth_account.Account.from_key(wallet_config["secret_key"]) + address = wallet_config["account_address"] + if account.address != address: + raise Exception(f"provided signer address {address} does not match private key") + print("loaded multi-sig signer", address) + wallets.append(account) + return wallets diff --git a/examples/multi_sig_wallets.json.example b/examples/multi_sig_wallets.json.example new file mode 100644 index 0000000..d062dc9 --- /dev/null +++ b/examples/multi_sig_wallets.json.example @@ -0,0 +1,12 @@ +[ + { + "comment": "signer 1", + "secret_key": "", + "account_address": "" + }, + { + "comment": "signer 2", + "secret_key": "", + "account_address": "" + } +] diff --git a/hyperliquid/exchange.py b/hyperliquid/exchange.py index 85c8b4a..843e1d6 100644 --- a/hyperliquid/exchange.py +++ b/hyperliquid/exchange.py @@ -1,3 +1,4 @@ +import json import logging import secrets @@ -27,6 +28,8 @@ sign_usd_class_transfer_action, sign_usd_transfer_action, sign_withdraw_from_bridge_action, + sign_convert_to_multi_sig_user_action, + sign_convert_to_multi_sig_signer_action, ) from hyperliquid.utils.types import Any, BuilderInfo, Cloid, List, Meta, Optional, SpotMeta, Tuple @@ -549,3 +552,58 @@ def approve_builder_fee(self, builder: str, max_fee_rate: str) -> Any: action = {"maxFeeRate": max_fee_rate, "builder": builder, "nonce": timestamp, "type": "approveBuilderFee"} signature = sign_approve_builder_fee(self.wallet, action, self.base_url == MAINNET_API_URL) return self._post_action(action, signature, timestamp) + + def convert_to_multi_sig_user(self, authorized_users: List[str], threshold: int) -> Any: + timestamp = get_timestamp_ms() + authorized_users = sorted(authorized_users) + signers = { + "authorizedUsers": authorized_users, + "threshold": threshold, + } + action = { + "type": "convertToMultiSigUser", + "signers": json.dumps(signers), + "nonce": timestamp, + } + signature = sign_convert_to_multi_sig_user_action(self.wallet, action, self.base_url == MAINNET_API_URL) + return self._post_action( + action, + signature, + timestamp, + ) + + def convert_to_multi_sig_signer(self, signer: str, multi_sig_user: str) -> Any: + timestamp = get_timestamp_ms() + action = { + "type": "convertToMultiSigUser", + "signer": signer, + "multi_sig_user": multi_sig_user, + "nonce": timestamp, + } + signature = sign_convert_to_multi_sig_signer_action(self.wallet, action, self.base_url == MAINNET_API_URL) + return self._post_action( + action, + signature, + timestamp, + ) + + def multi_sig(self, multi_sig_user, inner_action, signatures, nonce, vault_address=None): + multi_sig_action = { + "type": "multiSig", + "user": multi_sig_user.lower(), + "signatures": signatures, + "inner": inner_action, + } + signature = sign_l1_action( + self.wallet, + multi_sig_action, + vault_address, + nonce, + self.base_url == MAINNET_API_URL, + ) + + return self._post_action( + multi_sig_action, + signature, + nonce, + ) diff --git a/hyperliquid/info.py b/hyperliquid/info.py index b63f275..438a30d 100644 --- a/hyperliquid/info.py +++ b/hyperliquid/info.py @@ -472,6 +472,12 @@ def query_referral_state(self, user: str) -> Any: def query_sub_accounts(self, user: str) -> Any: return self.post("/info", {"type": "subAccounts", "user": user}) + def query_user_to_multi_sig_signers(self, multi_sig_user: str) -> Any: + return self.post("/info", {"type": "userToMultiSigSigners", "user": multi_sig_user}) + + def query_signer_to_multi_sig_user(self, signer: str) -> Any: + return self.post("/info", {"type": "signerToMultiSigUser", "signer": signer}) + def subscribe(self, subscription: Subscription, callback: Callable[[Any], None]) -> int: if subscription["type"] == "l2Book" or subscription["type"] == "trades" or subscription["type"] == "candle": subscription["coin"] = self.name_to_coin[subscription["coin"]] diff --git a/hyperliquid/utils/signing.py b/hyperliquid/utils/signing.py index 2e06334..0bc267e 100644 --- a/hyperliquid/utils/signing.py +++ b/hyperliquid/utils/signing.py @@ -222,6 +222,35 @@ def sign_usd_class_transfer_action(wallet, action, is_mainnet): ) +def sign_convert_to_multi_sig_user_action(wallet, action, is_mainnet): + return sign_user_signed_action( + wallet, + action, + [ + {"name": "hyperliquidChain", "type": "string"}, + {"name": "signers", "type": "string"}, + {"name": "nonce", "type": "uint64"}, + ], + "HyperliquidTransaction:ConvertToMultiSigUser", + is_mainnet, + ) + + +def sign_convert_to_multi_sig_signer_action(wallet, action, is_mainnet): + return sign_user_signed_action( + wallet, + action, + [ + {"name": "hyperliquidChain", "type": "string"}, + {"name": "signer", "type": "address"}, + {"name": "multiSigUser", "type": "address"}, + {"name": "nonce", "type": "uint64"}, + ], + "HyperliquidTransaction:ConvertToMultiSigSigner", + is_mainnet, + ) + + def sign_agent(wallet, action, is_mainnet): return sign_user_signed_action( wallet, diff --git a/pyproject.toml b/pyproject.toml index 8eb303d..e3e1a89 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.7.1" +version = "0.8.0" description = "SDK for Hyperliquid API trading with Python." readme = "README.md" authors = ["Hyperliquid "]