Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f0910a1
Add multi-sig example binaries
Sep 18, 2025
c5d76bd
Add complete multi-signature functionality with EIP-712 signing, acco…
Oct 21, 2025
61a10da
Change requests applied
Nov 8, 2025
638353b
Implement proper multi-sig signature collection pattern
Nov 9, 2025
7b7b4ec
remove SendAsset payload_multi_sig_user & outer_signer
luca992 Nov 10, 2025
189e36f
Merge pull request #1 from luca992/multi_sig
nicolad Nov 10, 2025
b5e353e
Revert "remove SendAsset payload_multi_sig_user & outer_signer"
luca992 Nov 10, 2025
fb71f22
add back outer_signer and payload_multi_sig_user to SendAsset but do …
luca992 Nov 10, 2025
4acc577
lowercase destinations
luca992 Nov 10, 2025
6abff7b
lowercase payload addresses
luca992 Nov 10, 2025
d1fe9e2
try signature_chain_id: "0x1"
luca992 Nov 11, 2025
9145271
set fixed nonce for debugging
luca992 Nov 11, 2025
4451ce2
log action_without_type
luca992 Nov 11, 2025
6960c72
serde_json enable preserve_order
luca992 Nov 11, 2025
8c46664
preserve map order when removing from action
luca992 Nov 11, 2025
1180276
insert type at the start of the action
luca992 Nov 11, 2025
11a489a
Revert "set fixed nonce for debugging"
luca992 Nov 11, 2025
3b03853
Revert "try signature_chain_id: "0x1""
luca992 Nov 11, 2025
5d81c38
Revert "log action_without_type"
luca992 Nov 11, 2025
d5ad466
Reapply "try signature_chain_id: "0x1""
luca992 Nov 11, 2025
b79d86a
debug with hardcoded nonce
luca992 Nov 11, 2025
c45b036
Reapply "log action_without_type"
luca992 Nov 11, 2025
97d4d1f
log hex
luca992 Nov 11, 2025
9351159
Prevent serializing actions to value before needed
luca992 Nov 11, 2025
c660e12
Revert "debug with hardcoded nonce"
luca992 Nov 11, 2025
3c72658
serialize Signature r & s to strings
luca992 Nov 11, 2025
700679d
Reapply "debug with hardcoded nonce"
luca992 Nov 11, 2025
97303d3
MultiSigEnvelope use the signature_chain_id from the MultiSigAction
luca992 Nov 11, 2025
48b33bd
Revert "Reapply "debug with hardcoded nonce""
luca992 Nov 11, 2025
2bc8732
revert debug print
luca992 Nov 11, 2025
5ef467f
revert adding preserve_order serde_json feature
luca992 Nov 11, 2025
81b6f51
Revert "revert adding preserve_order serde_json feature"
luca992 Nov 11, 2025
33ab804
Merge pull request #2 from luca992/multi_sig
nicolad Nov 11, 2025
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
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ repository = "https://github.com/hyperliquid-dex/hyperliquid-rust-sdk"

[dependencies]
alloy = { version = "1.0", default-features = false, features = [
"dyn-abi",
"sol-types",
"signer-local",
"dyn-abi",
"sol-types",
"signer-local",
] }
chrono = "0.4.26"
env_logger = "0.11.8"
Expand All @@ -24,9 +24,9 @@ lazy_static = "1.0"
log = "0.4.19"
reqwest = "0.12.19"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = { version = "1.0", features = ["preserve_order"] }
rmp-serde = "1.0"
thiserror = "2.0"
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = { version = "0.20.0", features = ["native-tls"] }
tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] }
uuid = { version = "1.0", features = ["v4"] }
101 changes: 101 additions & 0 deletions src/bin/convert_to_multi_sig_user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use alloy::{primitives::Address, signers::local::PrivateKeySigner};
use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient};
use log::info;

async fn setup_exchange_client() -> (Address, ExchangeClient) {
// Key was randomly generated for testing and shouldn't be used with any real funds
let wallet: PrivateKeySigner =
"e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"
.parse()
.unwrap();

let address = wallet.address();
let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None)
.await
.unwrap();

(address, exchange_client)
}

#[tokio::main]
async fn main() {
env_logger::init();

let (address, exchange_client) = setup_exchange_client().await;

// Ensure we're using the actual user's wallet, not an agent
if address != exchange_client.wallet.address() {
panic!("Agents do not have permission to convert to multi-sig user");
}

// Addresses that will be authorized to sign for the multi-sig account
let authorized_user_1: Address = "0x0000000000000000000000000000000000000000"
.parse()
.unwrap();
let authorized_user_2: Address = "0x0000000000000000000000000000000000000001"
.parse()
.unwrap();

// Threshold: minimum number of signatures required to execute any transaction
// This matches the Python example where threshold is 1
let threshold = 1;

info!("=== Convert to Multi-Sig User Example ===");
info!("Current user address: {}", address);
info!("Connected to: {:?}", exchange_client.http_client.base_url);
info!("");
info!("Configuration:");
info!(" Authorized user 1: {}", authorized_user_1);
info!(" Authorized user 2: {}", authorized_user_2);
info!(" Threshold: {}", threshold);
info!("");

// Step 1: Convert the user to a multi-sig account
info!("Step 1: Converting to multi-sig account...");
match exchange_client.convert_to_multi_sig(threshold, None).await {
Ok(response) => {
info!("Convert to multi-sig response: {:?}", response);
info!("Successfully converted to multi-sig!");
}
Err(e) => {
info!("Convert to multi-sig failed (this is expected if already converted or on testnet): {}", e);
}
}

// Step 2: Add authorized addresses
info!("Step 2: Adding authorized addresses...");
match exchange_client
.update_multi_sig_addresses(
vec![authorized_user_1, authorized_user_2],
vec![], // No addresses to remove
None,
)
.await
{
Ok(response) => {
info!("Update multi-sig addresses response: {:?}", response);
info!("Successfully added authorized addresses!");
}
Err(e) => {
info!("Update multi-sig addresses failed: {}", e);
}
}

info!("");
info!("Multi-sig setup complete!");
info!("Now you can use the multi-sig methods with the authorized wallets:");
info!("- multi_sig_order()");
info!("- multi_sig_usdc_transfer()");
info!("- multi_sig_spot_transfer()");
info!("");
info!("IMPORTANT: After converting to multi-sig:");
info!("1. The account can only be controlled by the authorized addresses");
info!(
"2. You need {} signatures to execute any transaction",
threshold
);
info!("3. Make sure you have access to the authorized private keys!");
info!("4. This is a one-way conversion - test on testnet first!");

info!("Example completed - multi-sig conversion functionality demonstrated");
}
122 changes: 122 additions & 0 deletions src/bin/multi_sig_order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use alloy::{primitives::Address, signers::local::PrivateKeySigner};
use hyperliquid_rust_sdk::{BaseUrl, ClientLimit, ClientOrder, ClientOrderRequest, ExchangeClient};
use log::info;

fn setup_multi_sig_wallets() -> Vec<PrivateKeySigner> {
// These are example private keys - in production, these would be the authorized
// user wallets that have permission to sign for the multi-sig account
let wallets = vec![
"0x1234567890123456789012345678901234567890123456789012345678901234",
"0x2345678901234567890123456789012345678901234567890123456789012345",
"0x3456789012345678901234567890123456789012345678901234567890123456",
];

wallets
.into_iter()
.map(|key| key.parse().unwrap())
.collect()
}

async fn setup_exchange_client() -> (Address, ExchangeClient) {
// Key was randomly generated for testing and shouldn't be used with any real funds
let wallet: PrivateKeySigner =
"e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"
.parse()
.unwrap();

let address = wallet.address();
let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None)
.await
.unwrap();

(address, exchange_client)
}

#[tokio::main]
async fn main() {
env_logger::init();

let (address, exchange_client) = setup_exchange_client().await;

// Set up the multi-sig wallets that are authorized to sign for the multi-sig user
// Each wallet must belong to a user that has been added as an authorized signer
let multi_sig_wallets = setup_multi_sig_wallets();

// The outer signer is required to be an authorized user or an agent of the
// 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
// required for that multi-sig user
let multi_sig_user: Address = "0x0000000000000000000000000000000000000005"
.parse()
.unwrap();

info!("=== Multi-Sig Order Example ===");
info!("Multi-sig user address: {}", multi_sig_user);
info!("Outer signer (current wallet): {}", address);
info!(
"Exchange client connected to: {:?}",
exchange_client.http_client.base_url
);
info!(
"Authorized wallets ({} total): {:?}",
multi_sig_wallets.len(),
multi_sig_wallets
.iter()
.map(|w| w.address())
.collect::<Vec<_>>()
);

// Define the multi-sig inner action - in this case, placing an order
// This matches the Python example: asset index 4, buy, price 1100, size 0.2
let order = ClientOrderRequest {
asset: "ETH".to_string(), // Asset index 4 in Python corresponds to ETH
is_buy: true,
reduce_only: false,
limit_px: 1100.0,
sz: 0.2,
cloid: None,
order_type: ClientOrder::Limit(ClientLimit {
tif: "Gtc".to_string(),
}),
};

info!("");
info!("Order details: {:?}", order);
info!("Executing multi-sig order...");
info!(
"Collecting signatures from {} authorized wallets...",
multi_sig_wallets.len()
);

// Execute the multi-sig order
// This will collect signatures from all provided wallets and submit them together
// The action will only succeed if enough valid signatures are provided (>= threshold)
match exchange_client
.multi_sig_order(multi_sig_user, order, &multi_sig_wallets)
.await
{
Ok(response) => {
info!("✓ Multi-sig order placed successfully!");
info!("Response: {:?}", response);
}
Err(e) => {
info!("✗ Multi-sig order failed: {}", e);
info!("");
info!("This is expected if:");
info!(" • The multi-sig user is not properly configured");
info!(" • The provided wallets are not authorized signers");
info!(" • Not enough signatures provided to meet threshold");
info!("");
info!("To use in production:");
info!(" 1. Convert a user to multi-sig: convert_to_multi_sig()");
info!(" 2. Add authorized addresses: update_multi_sig_addresses()");
info!(" 3. Use those authorized wallets to sign transactions");
info!(" 4. Ensure you provide >= threshold number of valid signatures");
}
}

info!("");
info!("Example completed");
}
Loading