Description
exchange.multi_sig() always returns {"status": "err", "response": "Invalid multi-sig outer signer"} when used with User-Signed Actions like sendAsset. The inner signatures are generated using sign_multi_sig_user_signed_action_payload(), and the outer signature is generated by sign_multi_sig_action() inside exchange.multi_sig().
Environment
- SDK version: 0.22.0 (also tested with 0.20.0)
- Python: 3.11 and 3.13
- Network: Mainnet (
https://api.hyperliquid.xyz)
Steps to Reproduce
- Create a 2/2 multi-sig account with two authorized users (A and B)
- Sign a
sendAsset inner action with both A and B using sign_multi_sig_user_signed_action_payload()
- Submit via
exchange.multi_sig() where the Exchange is initialized with wallet A (an authorized user)
- Response:
{"status": "err", "response": "Invalid multi-sig outer signer"}
Code
```python
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils.signing import SEND_ASSET_SIGN_TYPES, sign_multi_sig_user_signed_action_payload
MULTISIG_USER = "0x5c99..." # multi-sig account
KEY_A = "0x..." # authorized user A
KEY_B = "0x..." # authorized user B
outer_wallet = Account.from_key(KEY_A)
outer_signer = outer_wallet.address.lower()
nonce = int(time.time() * 1000)
action = {
"type": "sendAsset", "nonce": nonce - 1, "token": "USDC", "amount": "5.0",
"sourceDex": "", "destination": "0x...", "destinationDex": "", "fromSubAccount": "",
}
sig1 = sign_multi_sig_user_signed_action_payload(
wallet=Account.from_key(KEY_A), action=action, is_mainnet=True,
sign_types=SEND_ASSET_SIGN_TYPES, tx_type="HyperliquidTransaction:SendAsset",
payload_multi_sig_user=MULTISIG_USER, outer_signer=outer_signer,
)
sig2 = sign_multi_sig_user_signed_action_payload(
wallet=Account.from_key(KEY_B), action=action, is_mainnet=True,
sign_types=SEND_ASSET_SIGN_TYPES, tx_type="HyperliquidTransaction:SendAsset",
payload_multi_sig_user=MULTISIG_USER, outer_signer=outer_signer,
)
action["hyperliquidChain"] = "Mainnet"
action["signatureChainId"] = "0x66eee"
exchange = Exchange(outer_wallet, "https://api.hyperliquid.xyz")
result = exchange.multi_sig(MULTISIG_USER, action, [sig1, sig2], nonce)
print(result) # {"status": "err", "response": "Invalid multi-sig outer signer"}
```
Key observations
- The same wallet (A) that is verified as an authorized user on-chain (confirmed via
userToMultiSigSigners API) produces Invalid multi-sig outer signer
- Normal (non-multi-sig)
sendAsset with the same wallet works fine
- A freshly created multi-sig account with brand new wallets also fails the same way
- The TypeScript SDK (viem) with identical parameters succeeds, suggesting the issue is specific to the Python SDK's
sign_multi_sig_action() or action_hash() implementation
Possibly related
Could there be a msgpack serialization mismatch between the Python SDK and the Hyperliquid backend when computing the action hash for the outer SendMultiSig signature?
Description
exchange.multi_sig()always returns{"status": "err", "response": "Invalid multi-sig outer signer"}when used with User-Signed Actions likesendAsset. The inner signatures are generated usingsign_multi_sig_user_signed_action_payload(), and the outer signature is generated bysign_multi_sig_action()insideexchange.multi_sig().Environment
https://api.hyperliquid.xyz)Steps to Reproduce
sendAssetinner action with both A and B usingsign_multi_sig_user_signed_action_payload()exchange.multi_sig()where the Exchange is initialized with wallet A (an authorized user){"status": "err", "response": "Invalid multi-sig outer signer"}Code
```python
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils.signing import SEND_ASSET_SIGN_TYPES, sign_multi_sig_user_signed_action_payload
MULTISIG_USER = "0x5c99..." # multi-sig account
KEY_A = "0x..." # authorized user A
KEY_B = "0x..." # authorized user B
outer_wallet = Account.from_key(KEY_A)
outer_signer = outer_wallet.address.lower()
nonce = int(time.time() * 1000)
action = {
"type": "sendAsset", "nonce": nonce - 1, "token": "USDC", "amount": "5.0",
"sourceDex": "", "destination": "0x...", "destinationDex": "", "fromSubAccount": "",
}
sig1 = sign_multi_sig_user_signed_action_payload(
wallet=Account.from_key(KEY_A), action=action, is_mainnet=True,
sign_types=SEND_ASSET_SIGN_TYPES, tx_type="HyperliquidTransaction:SendAsset",
payload_multi_sig_user=MULTISIG_USER, outer_signer=outer_signer,
)
sig2 = sign_multi_sig_user_signed_action_payload(
wallet=Account.from_key(KEY_B), action=action, is_mainnet=True,
sign_types=SEND_ASSET_SIGN_TYPES, tx_type="HyperliquidTransaction:SendAsset",
payload_multi_sig_user=MULTISIG_USER, outer_signer=outer_signer,
)
action["hyperliquidChain"] = "Mainnet"
action["signatureChainId"] = "0x66eee"
exchange = Exchange(outer_wallet, "https://api.hyperliquid.xyz")
result = exchange.multi_sig(MULTISIG_USER, action, [sig1, sig2], nonce)
print(result) # {"status": "err", "response": "Invalid multi-sig outer signer"}
```
Key observations
userToMultiSigSignersAPI) producesInvalid multi-sig outer signersendAssetwith the same wallet works finesign_multi_sig_action()oraction_hash()implementationPossibly related
Could there be a msgpack serialization mismatch between the Python SDK and the Hyperliquid backend when computing the action hash for the outer
SendMultiSigsignature?