Multi-sig
Hyperliquid has native multi-sig accounts: a set of authorized users and a
threshold of signatures every action needs. Once an account is converted,
all of its actions go through
ExchangeClient.MultiSig;
its own key can no longer act alone.
Convert an account
The account to convert signs
ConvertToMultiSigUser
with its own key. Authorized users (at most 10) must be existing Hyperliquid
users; Threshold is the number of their signatures each action needs.
err := accountEx.ConvertToMultiSigUser(ctx, hyperliquid.ConvertToMultiSigUserAction{
Signers: &hyperliquid.MultiSigSigners{
AuthorizedUsers: []hyperliquid.Address{aliceAddr, bobAddr, carolAddr},
Threshold: 2,
},
})
if err != nil {
log.Fatal(err)
}Read the current signer set with
UserToMultiSigSigners;
it returns nil for a normal account:
signers, err := info.UserToMultiSigSigners(ctx, hyperliquid.UserToMultiSigSignersRequest{User: multiSigUser})
if err != nil {
log.Fatal(err)
}
if signers == nil {
fmt.Println("not a multi-sig account")
} else {
fmt.Println(len(signers.AuthorizedUsers), "signers, threshold", signers.Threshold)
}Send an action
One authorized user, the leader, submits the action. Every signature must cover the same action, multi-sig account, leader address and nonce:
- The leader picks a nonce with
NextNonceand shares it, with the action, with the other signers. Only the leader’s nonce set is checked, so the nonce must come from the leader’s side. - Each signer (the leader too, if its signature counts toward the
threshold) calls
SignMultiSigwith its own client. - The leader collects at least
Thresholdsignatures and callsMultiSig.
// Leader (alice): choose the action and nonce.
action := hyperliquid.USDSendAction{Destination: destination, Amount: "100"}
nonce := hyperliquid.NextNonce()
leaderAddr := alice.Signer().Address()
// Each signer, typically in its own process, with the same inputs.
aliceSig, err := alice.SignMultiSig(ctx, action, multiSigUser, leaderAddr, nonce)
if err != nil {
log.Fatal(err)
}
bobSig, err := bob.SignMultiSig(ctx, action, multiSigUser, leaderAddr, nonce)
if err != nil {
log.Fatal(err)
}
// Leader: submit.
_, err = alice.MultiSig(ctx, multiSigUser, action, nonce, []hyperliquid.Signature{aliceSig, bobSig})
if err != nil {
log.Fatal(err)
}Signature marshals to and
from JSON, so signatures can travel between processes in whatever channel
you use to coordinate.
Any action works, user-signed (transfers, withdrawals) or L1 (orders,
cancels). MultiSig returns the inner action’s raw response data; for
batch actions such as orders, rejected items also come back as joined
*StatusError values, like
ExchangeClient.Order:
data, err := alice.MultiSig(ctx, multiSigUser, action, nonce, sigs)
if rejected, ok := errors.AsType[*hyperliquid.StatusError](err); ok {
log.Printf("order %d rejected: %s", rejected.Index, rejected.Message)
} else if err != nil {
log.Fatal(err)
}
var result struct {
Statuses []hyperliquid.OrderResult `json:"statuses"`
}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatal(err)
}WithVault
and WithExpiresAfter
settings, and the same network: they are part of what is signed. The leader
may be an API wallet of an authorized user; pass that wallet’s address as
the leader address when signing.Change signers or revert
Changing the signer set or threshold, and converting back to a normal
account, are themselves multi-sig actions: wrap a ConvertToMultiSigUser
in MultiSig. Nil Signers reverts to a normal account.
revert := hyperliquid.ConvertToMultiSigUserAction{} // nil Signers
nonce := hyperliquid.NextNonce()
leaderAddr := alice.Signer().Address()
aliceSig, err := alice.SignMultiSig(ctx, revert, multiSigUser, leaderAddr, nonce)
if err != nil {
log.Fatal(err)
}
bobSig, err := bob.SignMultiSig(ctx, revert, multiSigUser, leaderAddr, nonce)
if err != nil {
log.Fatal(err)
}
if _, err := alice.MultiSig(ctx, multiSigUser, revert, nonce, []hyperliquid.Signature{aliceSig, bobSig}); err != nil {
log.Fatal(err)
}See Hyperliquid’s multi-sig docs for the protocol rules.