Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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");
}
151 changes: 151 additions & 0 deletions src/bin/multi_sig_order_signature_collection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/// Example: Multi-sig order placement with signature collection workflow
///
/// This demonstrates how to collect signatures for L1 actions (orders)
/// where each signer creates their signature independently.
///
/// Usage:
/// cargo run --bin multi_sig_order_signature_collection
use alloy::signers::{local::PrivateKeySigner, Signature};
use hyperliquid_rust_sdk::sign_multi_sig_l1_action_single;
use log::info;
use std::str::FromStr;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

fn main() -> Result<()> {
env_logger::init();

info!("=== Multi-Sig Order Signature Collection Demo ===\n");

demonstrate_order_signature_collection()?;

Ok(())
}

fn demonstrate_order_signature_collection() -> Result<()> {
// Setup: Define the multi-sig parameters
let multi_sig_user =
alloy::primitives::Address::from_str("0x0000000000000000000000000000000000000005")?;
let outer_signer =
alloy::primitives::Address::from_str("0x0d1d9635d0640821d15e323ac8adadfa9c111414")?;
let nonce = 1234567890u64;

info!("Multi-sig parameters:");
info!(" Multi-sig user: {}", multi_sig_user);
info!(" Outer signer: {}", outer_signer);
info!(" Nonce: {}\n", nonce);

// Create the order action
// All signers must create the exact same action
let action = serde_json::json!({
"type": "order",
"orders": [{
"a": 0, // asset index (0 = BTC)
"b": true, // is_buy
"p": "30000", // limit price
"s": "0.1", // size
"r": false, // reduce_only
"t": {"limit": {"tif": "Gtc"}}
}],
"grouping": "na"
});

info!("Order action:");
info!("{}\n", serde_json::to_string_pretty(&action)?);

// Step 1: Each signer creates their signature independently
info!("Step 1: Each signer creates their signature\n");

let signer1_wallet = "0xe908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"
.parse::<PrivateKeySigner>()?;
let signer2_wallet = "0x0000000000000000000000000000000000000000000000000000000000000001"
.parse::<PrivateKeySigner>()?;

info!("Signer 1 address: {}", signer1_wallet.address());
info!("Signer 2 address: {}", signer2_wallet.address());

// Signer 1 signs the L1 action
let sig1 = sign_multi_sig_l1_action_single(
&signer1_wallet,
&action,
multi_sig_user,
outer_signer,
None, // vault_address
nonce,
None, // expires_after
false, // is_mainnet = false (testnet)
)?;
info!("\nSigner 1 signature: {}", sig1);

// Signer 2 signs the L1 action
let sig2 = sign_multi_sig_l1_action_single(
&signer2_wallet,
&action,
multi_sig_user,
outer_signer,
None,
nonce,
None,
false,
)?;
info!("Signer 2 signature: {}", sig2);

// Step 2: Signatures are serialized and transmitted
info!("\nStep 2: Signatures are exported for transmission\n");

let sig1_string = sig1.to_string();
let sig2_string = sig2.to_string();

info!("Sig 1 exported: {}", sig1_string);
info!("Sig 2 exported: {}", sig2_string);

// Step 3: Submitter collects and imports signatures
info!("\nStep 3: Submitter collects signatures\n");

let collected_signatures = [sig1_string, sig2_string];
info!("Collected {} signatures", collected_signatures.len());

// Import signatures
let signatures: Vec<Signature> = collected_signatures
.iter()
.map(|s| s.parse().expect("Failed to import signature"))
.collect();

info!("Successfully imported {} signatures", signatures.len());

// Step 4: Show how to submit (commented out to avoid actual submission)
info!("\nStep 4: Submit order (example - not executed)\n");

info!("To submit, the outer signer would run:");
info!("```rust");
info!("let submitter_wallet = \"YOUR_KEY\".parse::<PrivateKeySigner>()?;");
info!("let sdk = ExchangeClient::new(submitter_wallet, Some(BaseUrl::Testnet), None).await?;");
info!("");
info!("let order = ClientOrderRequest {{");
info!(" asset: \"BTC\".to_string(),");
info!(" is_buy: true,");
info!(" reduce_only: false,");
info!(" limit_px: 30000.0,");
info!(" sz: 0.1,");
info!(" order_type: ClientOrderType::Limit(ClientLimit {{");
info!(" tif: \"Gtc\".to_string(),");
info!(" }}),");
info!(" cloid: None,");
info!("}};");
info!("");
info!("sdk.multi_sig_order_with_signatures(");
info!(" multi_sig_user,");
info!(" order,");
info!(" signatures,");
info!(").await?;");
info!("```");

info!("\n=== Demo Complete ===");
info!("\nKey differences for L1 actions (orders):");
info!("1. Use sign_multi_sig_l1_action_single instead of sign_multi_sig_user_signed_action_single");
info!("2. Sign the JSON action directly (type + orders/cancels/etc)");
info!("3. Must specify vault_address and expires_after parameters");
info!("4. Network parameter (is_mainnet) affects the signature");

Ok(())
}
Loading