use alloy::{
dyn_abi::TypedData,
primitives::{Address, B256},
signers::{Signer, SignerSync},
};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::hypercore::{
Chain,
api::{Action, MultiSigAction, MultiSigPayload},
types::{CORE_MAINNET_EIP712_DOMAIN, Signature, solidity},
utils::{get_typed_data, rmp_hash},
};
#[inline(always)]
pub fn agent_signing_hash(chain: Chain, connection_id: B256) -> B256 {
use alloy::sol_types::SolStruct;
let agent = solidity::Agent {
source: if chain.is_mainnet() { "a" } else { "b" }.to_string(),
connectionId: connection_id,
};
agent.eip712_signing_hash(&CORE_MAINNET_EIP712_DOMAIN)
}
#[inline(always)]
pub async fn sign_l1_action<S: Signer + Send + Sync>(
signer: &S,
chain: Chain,
connection_id: B256,
) -> anyhow::Result<Signature> {
let agent = solidity::Agent {
source: if chain.is_mainnet() { "a" } else { "b" }.to_string(),
connectionId: connection_id,
};
let typed_data = TypedData::from_struct(&agent, Some(CORE_MAINNET_EIP712_DOMAIN.clone()));
let sig = signer.sign_dynamic_typed_data(&typed_data).await?;
Ok(sig.into())
}
pub fn multisig_lead_msg_sync<S: SignerSync>(
signer: &S,
action: MultiSigAction,
nonce: u64,
maybe_vault_address: Option<Address>,
maybe_expires_after: Option<DateTime<Utc>>,
chain: Chain,
) -> Result<crate::hypercore::api::ActionRequest> {
let expires_after = maybe_expires_after.map(|after| after.timestamp_millis() as u64);
let multsig_hash = rmp_hash(&action, nonce, maybe_vault_address, expires_after)?;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Envelope {
hyperliquid_chain: String,
multi_sig_action_hash: String,
nonce: u64,
}
let envelope = Envelope {
hyperliquid_chain: chain.to_string(),
multi_sig_action_hash: multsig_hash.to_string(),
nonce,
};
let typed_data = get_typed_data::<solidity::SendMultiSig>(&envelope, chain, None);
let sig = signer.sign_dynamic_typed_data_sync(&typed_data)?.into();
Ok(crate::hypercore::api::ActionRequest {
signature: sig,
action: Action::MultiSig(action),
nonce,
vault_address: maybe_vault_address,
expires_after,
})
}
pub async fn multisig_lead_msg<S: Signer + Send + Sync>(
signer: &S,
action: MultiSigAction,
nonce: u64,
maybe_vault_address: Option<Address>,
maybe_expires_after: Option<DateTime<Utc>>,
chain: Chain,
) -> Result<crate::hypercore::api::ActionRequest> {
let expires_after = maybe_expires_after.map(|after| after.timestamp_millis() as u64);
let multsig_hash = rmp_hash(&action, nonce, maybe_vault_address, expires_after)?;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Envelope {
hyperliquid_chain: String,
multi_sig_action_hash: String,
nonce: u64,
}
let envelope = Envelope {
hyperliquid_chain: chain.to_string(),
multi_sig_action_hash: multsig_hash.to_string(),
nonce,
};
let typed_data = get_typed_data::<solidity::SendMultiSig>(&envelope, chain, None);
let sig = signer.sign_dynamic_typed_data(&typed_data).await?.into();
Ok(crate::hypercore::api::ActionRequest {
signature: sig,
action: Action::MultiSig(action),
nonce,
vault_address: maybe_vault_address,
expires_after,
})
}
pub async fn multisig_collect_signatures<'a, S: Signer + Send + Sync + 'a>(
lead: Address,
multi_sig_user: Address,
signers: impl Iterator<Item = &'a S>,
signed: impl Iterator<Item = Signature>,
inner_action: Action,
nonce: u64,
chain: Chain,
) -> Result<MultiSigAction> {
multisig_collect_signatures_with_context(
lead,
multi_sig_user,
signers,
signed,
inner_action,
nonce,
None,
None,
chain,
)
.await
}
pub async fn multisig_collect_signatures_with_context<'a, S: Signer + Send + Sync + 'a>(
lead: Address,
multi_sig_user: Address,
signers: impl Iterator<Item = &'a S>,
signed: impl Iterator<Item = Signature>,
inner_action: Action,
nonce: u64,
vault_address: Option<Address>,
expires_after: Option<DateTime<Utc>>,
chain: Chain,
) -> Result<MultiSigAction> {
let payload = MultiSigPayload {
multi_sig_user: const_hex::encode_prefixed(multi_sig_user.as_slice()),
outer_signer: const_hex::encode_prefixed(lead.as_slice()),
action: Box::new(inner_action),
};
let mut signatures = Vec::new();
for signer in signers {
signatures.push(
payload
.sign_with_context(signer, nonce, vault_address, expires_after, chain)
.await?,
);
}
signatures.extend(signed);
Ok(MultiSigAction {
signature_chain_id: chain.arbitrum_id().to_owned(),
signatures,
payload,
})
}
#[cfg(test)]
mod tests {
use alloy::signers::local::PrivateKeySigner;
use super::*;
use crate::hypercore::{ARBITRUM_MAINNET_CHAIN_ID, types};
fn get_signer() -> PrivateKeySigner {
let priv_key = "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e";
priv_key.parse::<PrivateKeySigner>().unwrap()
}
#[test]
fn test_sign_usd_transfer_action() {
let signer = get_signer();
let usd_send = types::api::UsdSendAction {
signature_chain_id: ARBITRUM_MAINNET_CHAIN_ID.to_owned(),
hyperliquid_chain: Chain::Mainnet,
destination: "0x0D1d9635D0640821d15e323ac8AdADfA9c111414"
.parse()
.unwrap(),
amount: rust_decimal::Decimal::ONE,
time: 1690393044548,
};
let action = Action::UsdSend(usd_send);
let nonce = 1690393044548u64;
let signed = action
.sign_sync(&signer, nonce, None, None, Chain::Mainnet)
.unwrap();
let expected_sig = "0xeca6267bcaadc4c0ae1aed73f5a2c45fcdbb7271f2e9356992404e5d4bad75a3572e08fe93f17755abadb7f84be7d1e9c4ce48bb5633e339bc430c672d5a20ed1b";
assert_eq!(signed.signature.to_string(), expected_sig);
}
#[test]
fn test_recover_usd_send() {
let signer = get_signer();
let expected_address = signer.address();
let usd_send = types::api::UsdSendAction {
signature_chain_id: ARBITRUM_MAINNET_CHAIN_ID.to_owned(),
hyperliquid_chain: Chain::Mainnet,
destination: "0x0D1d9635D0640821d15e323ac8AdADfA9c111414"
.parse()
.unwrap(),
amount: rust_decimal::Decimal::ONE,
time: 1690393044548,
};
let action = Action::UsdSend(usd_send.clone());
let nonce = 1690393044548u64;
let action_request = action
.sign_sync(&signer, nonce, None, None, Chain::Mainnet)
.unwrap();
let recovered_address = Action::UsdSend(usd_send)
.recover(&action_request.signature, nonce, None, None, Chain::Mainnet)
.unwrap();
assert_eq!(
recovered_address, expected_address,
"Recovered address should match the signer's address"
);
}
#[test]
fn test_recover_batch_order() {
use rust_decimal::dec;
use types::{BatchOrder, OrderGrouping, OrderRequest, OrderTypePlacement, TimeInForce};
let signer = get_signer();
let expected_address = signer.address();
let order = BatchOrder {
orders: vec![OrderRequest {
asset: 0,
is_buy: true,
limit_px: dec!(50000),
sz: dec!(0.1),
reduce_only: false,
order_type: OrderTypePlacement::Limit {
tif: TimeInForce::Gtc,
},
cloid: Default::default(),
}],
grouping: OrderGrouping::Na,
builder: None,
};
let action = Action::Order(order.clone());
let nonce = chrono::Utc::now().timestamp_millis() as u64;
let action_request = action
.sign_sync(&signer, nonce, None, None, Chain::Mainnet)
.unwrap();
let recovered_address = Action::Order(order)
.recover(&action_request.signature, nonce, None, None, Chain::Mainnet)
.unwrap();
assert_eq!(
recovered_address, expected_address,
"Recovered address should match the signer's address for RMP-based action"
);
}
#[test]
fn test_recover_batch_order_with_priority_rate() {
use rust_decimal::dec;
use types::{BatchOrder, OrderGrouping, OrderRequest, OrderTypePlacement, TimeInForce};
let signer = get_signer();
let expected_address = signer.address();
let batch = BatchOrder {
orders: vec![OrderRequest {
asset: 0,
is_buy: true,
limit_px: dec!(50000),
sz: dec!(0.1),
reduce_only: false,
order_type: OrderTypePlacement::Limit {
tif: TimeInForce::Ioc,
},
cloid: Default::default(),
}],
grouping: OrderGrouping::PriorityRate(80_000),
builder: None,
};
let action = Action::Order(batch.clone());
let nonce = chrono::Utc::now().timestamp_millis() as u64;
let action_request = action
.sign_sync(&signer, nonce, None, None, Chain::Mainnet)
.unwrap();
let recovered = Action::Order(batch)
.recover(&action_request.signature, nonce, None, None, Chain::Mainnet)
.unwrap();
assert_eq!(recovered, expected_address);
}
}