Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions examples/multi_sig_user_set_abstraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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()

# 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.
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,
exchange.wallet.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()
25 changes: 24 additions & 1 deletion hyperliquid/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ def _get_dex(coin: str) -> str:
return coin.split(":")[0] if ":" in coin else ""


USER_SET_ABSTRACTION_WIRE_VALUES = {
"default": "e",
"disabled": "i",
"dexAbstraction": "d",
"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
Expand Down Expand Up @@ -1078,14 +1100,15 @@ 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",
"signatures": signatures,
"payload": {
"multiSigUser": multi_sig_user,
"outerSigner": self.wallet.address.lower(),
"action": inner_action,
"action": payload_action,
},
}
is_mainnet = self.base_url == MAINNET_API_URL
Expand Down
2 changes: 1 addition & 1 deletion hyperliquid/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@

# b is the public address of the builder, f is the amount of the fee in tenths of basis points. e.g. 10 means 1 basis point
BuilderInfo = TypedDict("BuilderInfo", {"b": str, "f": int})
Abstraction = Literal["unifiedAccount", "portfolioMargin", "disabled"]
Abstraction = Literal["default", "dexAbstraction", "unifiedAccount", "portfolioMargin", "disabled"]
AgentAbstraction = Literal["u", "p", "i"]

PerpDexSchemaInput = TypedDict(
Expand Down
17 changes: 17 additions & 0 deletions tests/signing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down