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 "]