diff --git a/.gitignore b/.gitignore index 5c38e82e..3e469b21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env /target .idea/ -Cargo.lock \ No newline at end of file +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index e8e7c53b..bb5cf479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/hyperliquid-dex/hyperliquid-rust-sdk" [dependencies] chrono = "0.4.26" env_logger = "0.10.0" -ethers = {version = "2.0.14", features = ["eip712", "abigen"]} +ethers = { version = "2.0.14", features = ["eip712", "abigen"] } futures-util = "0.3.28" hex = "0.4.3" http = "0.2.9" @@ -22,10 +22,16 @@ lazy_static = "1.3" log = "0.4.19" rand = "0.8.5" reqwest = "0.11.18" -serde = {version = "1.0.175", features = ["derive"]} +serde = { version = "1.0.175", features = ["derive"] } serde_json = "1.0.103" rmp-serde = "1.0.0" thiserror = "1.0.44" -tokio = {version = "1.29.1", features = ["full"]} -tokio-tungstenite = {version = "0.20.0", features = ["native-tls"]} -uuid = {version = "1.6.1", features = ["v4"]} +tokio = { version = "1.29.1", features = ["full"] } +tokio-tungstenite = { version = "0.20.0", features = ["native-tls"] } +uuid = { version = "1.6.1", features = ["v4"] } +privy = "0.8.0" +# privy = { path = "../listen/privy" } +async-trait = "0.1.88" + +[dev-dependencies] +dotenv = "0.15.0" diff --git a/src/bin/agent.rs b/src/bin/agent.rs index e24e239f..a4214018 100644 --- a/src/bin/agent.rs +++ b/src/bin/agent.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use log::info; use ethers::signers::{LocalWallet, Signer}; @@ -11,9 +13,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); /* Create a new wallet with the agent. @@ -27,9 +30,10 @@ async fn main() { info!("Agent address: {:?}", wallet.address()); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let order = ClientOrderRequest { asset: "ETH".to_string(), diff --git a/src/bin/approve_builder_fee.rs b/src/bin/approve_builder_fee.rs index a5078c5e..538e5bf9 100644 --- a/src/bin/approve_builder_fee.rs +++ b/src/bin/approve_builder_fee.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,6 +12,8 @@ async fn main() { .parse() .unwrap(); + let wallet = Arc::new(wallet); + let exchange_client = ExchangeClient::new(None, wallet.clone(), Some(BaseUrl::Testnet), None, None) .await @@ -19,7 +23,11 @@ async fn main() { let builder = "0x1ab189B7801140900C711E458212F9c76F8dAC79".to_lowercase(); let resp = exchange_client - .approve_builder_fee(builder.to_string(), max_fee_rate.to_string(), Some(&wallet)) + .approve_builder_fee( + builder.to_string(), + max_fee_rate.to_string(), + Some(wallet.as_ref()), + ) .await; info!("resp: {resp:#?}"); } diff --git a/src/bin/bridge_withdraw.rs b/src/bin/bridge_withdraw.rs index b6577b4c..56289f7c 100644 --- a/src/bin/bridge_withdraw.rs +++ b/src/bin/bridge_withdraw.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,9 +12,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let usd = "5"; // 5 USD let destination = "0x0D1d9635D0640821d15e323ac8AdADfA9c111414"; diff --git a/src/bin/class_transfer.rs b/src/bin/class_transfer.rs index 06d13d65..3f6ea37b 100644 --- a/src/bin/class_transfer.rs +++ b/src/bin/class_transfer.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,9 +12,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let usdc = 1.0; // 1 USD let to_perp = false; diff --git a/src/bin/leverage.rs b/src/bin/leverage.rs index 5cd4cd1b..3406c1f4 100644 --- a/src/bin/leverage.rs +++ b/src/bin/leverage.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::{LocalWallet, Signer}; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient, InfoClient}; use log::info; @@ -12,9 +14,10 @@ async fn main() { .unwrap(); let address = wallet.address(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let info_client = InfoClient::new(None, Some(BaseUrl::Testnet)).await.unwrap(); let response = exchange_client diff --git a/src/bin/market_maker.rs b/src/bin/market_maker.rs index 9fff3a49..28a1646a 100644 --- a/src/bin/market_maker.rs +++ b/src/bin/market_maker.rs @@ -3,6 +3,8 @@ This is an example of a basic market making strategy. We subscribe to the current mid price and build a market around this price. Whenever our market becomes outdated, we place and cancel orders to renew it. */ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{MarketMaker, MarketMakerInput}; @@ -21,7 +23,7 @@ async fn main() { half_spread: 1, max_absolute_position_size: 0.5, decimals: 1, - wallet, + wallet: Arc::new(wallet), }; MarketMaker::new(market_maker_input).await.start().await } diff --git a/src/bin/market_order_and_cancel.rs b/src/bin/market_order_and_cancel.rs index 5bfe1d59..3f9bb669 100644 --- a/src/bin/market_order_and_cancel.rs +++ b/src/bin/market_order_and_cancel.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use log::info; @@ -15,9 +17,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); // Market open order let market_open_params = MarketOrderParams { diff --git a/src/bin/market_order_with_builder_and_cancel.rs b/src/bin/market_order_with_builder_and_cancel.rs index f8fa561a..2d5444c5 100644 --- a/src/bin/market_order_with_builder_and_cancel.rs +++ b/src/bin/market_order_with_builder_and_cancel.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use log::info; @@ -15,9 +17,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); // Market open order let market_open_params = MarketOrderParams { diff --git a/src/bin/order_and_cancel.rs b/src/bin/order_and_cancel.rs index 9c108e20..4fe97db2 100644 --- a/src/bin/order_and_cancel.rs +++ b/src/bin/order_and_cancel.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use log::info; @@ -15,9 +17,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let order = ClientOrderRequest { asset: "ETH".to_string(), diff --git a/src/bin/order_and_cancel_cloid.rs b/src/bin/order_and_cancel_cloid.rs index 2dfdbfba..f2d8c563 100644 --- a/src/bin/order_and_cancel_cloid.rs +++ b/src/bin/order_and_cancel_cloid.rs @@ -1,5 +1,6 @@ use ethers::signers::LocalWallet; use log::info; +use std::sync::Arc; use hyperliquid_rust_sdk::{ BaseUrl, ClientCancelRequestCloid, ClientLimit, ClientOrder, ClientOrderRequest, ExchangeClient, @@ -15,9 +16,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); // Order and Cancel with cloid let cloid = Uuid::new_v4(); diff --git a/src/bin/order_with_builder_and_cancel.rs b/src/bin/order_with_builder_and_cancel.rs index 33622ca4..b6c5e194 100644 --- a/src/bin/order_with_builder_and_cancel.rs +++ b/src/bin/order_with_builder_and_cancel.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use log::info; @@ -15,9 +17,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let order = ClientOrderRequest { asset: "ETH".to_string(), diff --git a/src/bin/set_referrer.rs b/src/bin/set_referrer.rs index 7d9f190e..191e91f5 100644 --- a/src/bin/set_referrer.rs +++ b/src/bin/set_referrer.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; @@ -11,9 +13,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let code = "TESTNET".to_string(); diff --git a/src/bin/spot_order.rs b/src/bin/spot_order.rs index 2c96b661..89d12c52 100644 --- a/src/bin/spot_order.rs +++ b/src/bin/spot_order.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use log::info; @@ -15,9 +17,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let order = ClientOrderRequest { asset: "XYZTWO/USDC".to_string(), diff --git a/src/bin/spot_transfer.rs b/src/bin/spot_transfer.rs index eea4cfd0..9057971d 100644 --- a/src/bin/spot_transfer.rs +++ b/src/bin/spot_transfer.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,9 +12,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let amount = "1"; let destination = "0x0D1d9635D0640821d15e323ac8AdADfA9c111414"; diff --git a/src/bin/usdc_transfer.rs b/src/bin/usdc_transfer.rs index f76426e3..f03379f7 100644 --- a/src/bin/usdc_transfer.rs +++ b/src/bin/usdc_transfer.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,9 +12,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let amount = "1"; // 1 USD let destination = "0x0D1d9635D0640821d15e323ac8AdADfA9c111414"; diff --git a/src/bin/vault_transfer.rs b/src/bin/vault_transfer.rs index 770addd9..0769d2ca 100644 --- a/src/bin/vault_transfer.rs +++ b/src/bin/vault_transfer.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ethers::signers::LocalWallet; use hyperliquid_rust_sdk::{BaseUrl, ExchangeClient}; use log::info; @@ -10,9 +12,10 @@ async fn main() { .parse() .unwrap(); - let exchange_client = ExchangeClient::new(None, wallet, Some(BaseUrl::Testnet), None, None) - .await - .unwrap(); + let exchange_client = + ExchangeClient::new(None, Arc::new(wallet), Some(BaseUrl::Testnet), None, None) + .await + .unwrap(); let usd = 5_000_000; // at least 5 USD let is_deposit = true; diff --git a/src/exchange/exchange_client.rs b/src/exchange/exchange_client.rs index bc1a8dbf..77ac0cac 100644 --- a/src/exchange/exchange_client.rs +++ b/src/exchange/exchange_client.rs @@ -15,27 +15,29 @@ use crate::{ prelude::*, req::HttpClient, signature::sign_l1_action, + signer::Signer, BaseUrl, BulkCancelCloid, Error, ExchangeResponseStatus, }; use crate::{ClassTransfer, SpotSend, SpotUser, VaultTransfer, Withdraw3}; use ethers::{ abi::AbiEncode, - signers::{LocalWallet, Signer}, types::{Signature, H160, H256}, }; use log::debug; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::sync::Arc; use super::cancel::ClientCancelRequestCloid; use super::order::{MarketCloseParams, MarketOrderParams}; use super::{BuilderInfo, ClientLimit, ClientOrder}; +use ethers::signers::LocalWallet; #[derive(Debug)] pub struct ExchangeClient { pub http_client: HttpClient, - pub wallet: LocalWallet, + pub wallet: Arc, pub meta: Meta, pub vault_address: Option, pub coin_to_asset: HashMap, @@ -88,7 +90,7 @@ impl Actions { impl ExchangeClient { pub async fn new( client: Option, - wallet: LocalWallet, + wallet: Arc, base_url: Option, meta: Option, vault_address: Option, @@ -153,9 +155,9 @@ impl ExchangeClient { &self, amount: &str, destination: &str, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let hyperliquid_chain = if self.http_client.is_mainnet() { "Mainnet".to_string() } else { @@ -170,7 +172,7 @@ impl ExchangeClient { amount: amount.to_string(), time: timestamp, }; - let signature = sign_typed_data(&usd_send, wallet)?; + let signature = sign_typed_data(&usd_send, wallet).await?; let action = serde_json::to_value(Actions::UsdSend(usd_send)) .map_err(|e| Error::JsonParse(e.to_string()))?; @@ -181,7 +183,7 @@ impl ExchangeClient { &self, usdc: f64, to_perp: bool, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { // payload expects usdc without decimals let usdc = (usdc * 1e6).round() as u64; @@ -195,7 +197,7 @@ impl ExchangeClient { let connection_id = action.hash(timestamp, self.vault_address)?; let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -205,7 +207,7 @@ impl ExchangeClient { is_deposit: bool, usd: u64, vault_address: Option, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let vault_address = self .vault_address @@ -223,7 +225,7 @@ impl ExchangeClient { let connection_id = action.hash(timestamp, self.vault_address)?; let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -383,7 +385,7 @@ impl ExchangeClient { pub async fn order( &self, order: ClientOrderRequest, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { self.bulk_order(vec![order], wallet).await } @@ -391,7 +393,7 @@ impl ExchangeClient { pub async fn order_with_builder( &self, order: ClientOrderRequest, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, builder: BuilderInfo, ) -> Result { self.bulk_order_with_builder(vec![order], wallet, builder) @@ -401,9 +403,9 @@ impl ExchangeClient { pub async fn bulk_order( &self, orders: Vec, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let timestamp = next_nonce(); let mut transformed_orders = Vec::new(); @@ -421,14 +423,14 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } pub async fn bulk_order_with_builder( &self, orders: Vec, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, mut builder: BuilderInfo, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); @@ -451,14 +453,14 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } pub async fn cancel( &self, cancel: ClientCancelRequest, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { self.bulk_cancel(vec![cancel], wallet).await } @@ -466,7 +468,7 @@ impl ExchangeClient { pub async fn bulk_cancel( &self, cancels: Vec, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); let timestamp = next_nonce(); @@ -490,7 +492,7 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -498,7 +500,7 @@ impl ExchangeClient { pub async fn modify( &self, modify: ClientModifyRequest, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { self.bulk_modify(vec![modify], wallet).await } @@ -506,7 +508,7 @@ impl ExchangeClient { pub async fn bulk_modify( &self, modifies: Vec, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); let timestamp = next_nonce(); @@ -526,7 +528,7 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -534,7 +536,7 @@ impl ExchangeClient { pub async fn cancel_by_cloid( &self, cancel: ClientCancelRequestCloid, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { self.bulk_cancel_by_cloid(vec![cancel], wallet).await } @@ -542,7 +544,7 @@ impl ExchangeClient { pub async fn bulk_cancel_by_cloid( &self, cancels: Vec, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); let timestamp = next_nonce(); @@ -566,7 +568,7 @@ impl ExchangeClient { let connection_id = action.hash(timestamp, self.vault_address)?; let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -576,7 +578,7 @@ impl ExchangeClient { leverage: u32, coin: &str, is_cross: bool, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); @@ -591,7 +593,7 @@ impl ExchangeClient { let connection_id = action.hash(timestamp, self.vault_address)?; let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -600,7 +602,7 @@ impl ExchangeClient { &self, amount: f64, coin: &str, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { let wallet = wallet.unwrap_or(&self.wallet); @@ -616,16 +618,16 @@ impl ExchangeClient { let connection_id = action.hash(timestamp, self.vault_address)?; let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } pub async fn approve_agent( &self, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result<(String, ExchangeResponseStatus)> { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let key = H256::from(generate_random_key()?).encode_hex()[2..].to_string(); let address = key @@ -647,7 +649,7 @@ impl ExchangeClient { agent_name: None, nonce, }; - let signature = sign_typed_data(&approve_agent, wallet)?; + let signature = sign_typed_data(&approve_agent, wallet).await?; let action = serde_json::to_value(Actions::ApproveAgent(approve_agent)) .map_err(|e| Error::JsonParse(e.to_string()))?; Ok((key, self.post(action, signature, nonce).await?)) @@ -657,9 +659,9 @@ impl ExchangeClient { &self, amount: &str, destination: &str, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let hyperliquid_chain = if self.http_client.is_mainnet() { "Mainnet".to_string() } else { @@ -674,7 +676,7 @@ impl ExchangeClient { amount: amount.to_string(), time: timestamp, }; - let signature = sign_typed_data(&withdraw, wallet)?; + let signature = sign_typed_data(&withdraw, wallet).await?; let action = serde_json::to_value(Actions::Withdraw3(withdraw)) .map_err(|e| Error::JsonParse(e.to_string()))?; @@ -686,9 +688,9 @@ impl ExchangeClient { amount: &str, destination: &str, token: &str, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let hyperliquid_chain = if self.http_client.is_mainnet() { "Mainnet".to_string() } else { @@ -704,7 +706,7 @@ impl ExchangeClient { time: timestamp, token: token.to_string(), }; - let signature = sign_typed_data(&spot_send, wallet)?; + let signature = sign_typed_data(&spot_send, wallet).await?; let action = serde_json::to_value(Actions::SpotSend(spot_send)) .map_err(|e| Error::JsonParse(e.to_string()))?; @@ -714,9 +716,9 @@ impl ExchangeClient { pub async fn set_referrer( &self, code: String, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let timestamp = next_nonce(); let action = Actions::SetReferrer(SetReferrer { code }); @@ -725,7 +727,7 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } @@ -733,9 +735,9 @@ impl ExchangeClient { &self, builder: String, max_fee_rate: String, - wallet: Option<&LocalWallet>, + wallet: Option<&dyn Signer>, ) -> Result { - let wallet = wallet.unwrap_or(&self.wallet); + let wallet = wallet.unwrap_or(self.wallet.as_ref()); let timestamp = next_nonce(); let hyperliquid_chain = if self.http_client.is_mainnet() { @@ -756,7 +758,7 @@ impl ExchangeClient { let action = serde_json::to_value(&action).map_err(|e| Error::JsonParse(e.to_string()))?; let is_mainnet = self.http_client.is_mainnet(); - let signature = sign_l1_action(wallet, connection_id, is_mainnet)?; + let signature = sign_l1_action(wallet, connection_id, is_mainnet).await?; self.post(action, signature, timestamp).await } } @@ -783,16 +785,19 @@ mod tests { exchange::order::{Limit, OrderRequest, Trigger}, Order, }; + use ethers::signers::LocalWallet; - fn get_wallet() -> Result { + fn get_wallet() -> Result> { let priv_key = "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"; - priv_key - .parse::() - .map_err(|e| Error::Wallet(e.to_string())) + Ok(Arc::new( + priv_key + .parse::() + .map_err(|e| Error::Wallet(e.to_string()))?, + )) } - #[test] - fn test_limit_order_action_hashing() -> Result<()> { + #[tokio::test] + async fn test_limit_order_action_hashing() -> Result<()> { let wallet = get_wallet()?; let action = Actions::Order(BulkOrder { orders: vec![OrderRequest { @@ -811,17 +816,17 @@ mod tests { }); let connection_id = action.hash(1583838, None)?; - let signature = sign_l1_action(&wallet, connection_id, true)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, true).await?; assert_eq!(signature.to_string(), "77957e58e70f43b6b68581f2dc42011fc384538a2e5b7bf42d5b936f19fbb67360721a8598727230f67080efee48c812a6a4442013fd3b0eed509171bef9f23f1c"); - let signature = sign_l1_action(&wallet, connection_id, false)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, false).await?; assert_eq!(signature.to_string(), "cd0925372ff1ed499e54883e9a6205ecfadec748f80ec463fe2f84f1209648776377961965cb7b12414186b1ea291e95fd512722427efcbcfb3b0b2bcd4d79d01c"); Ok(()) } - #[test] - fn test_limit_order_action_hashing_with_cloid() -> Result<()> { + #[tokio::test] + async fn test_limit_order_action_hashing_with_cloid() -> Result<()> { let cloid = uuid::Uuid::from_str("1e60610f-0b3d-4205-97c8-8c1fed2ad5ee") .map_err(|_e| uuid::Uuid::new_v4()); let wallet = get_wallet()?; @@ -842,17 +847,17 @@ mod tests { }); let connection_id = action.hash(1583838, None)?; - let signature = sign_l1_action(&wallet, connection_id, true)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, true).await?; assert_eq!(signature.to_string(), "d3e894092eb27098077145714630a77bbe3836120ee29df7d935d8510b03a08f456de5ec1be82aa65fc6ecda9ef928b0445e212517a98858cfaa251c4cd7552b1c"); - let signature = sign_l1_action(&wallet, connection_id, false)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, false).await?; assert_eq!(signature.to_string(), "3768349dbb22a7fd770fc9fc50c7b5124a7da342ea579b309f58002ceae49b4357badc7909770919c45d850aabb08474ff2b7b3204ae5b66d9f7375582981f111c"); Ok(()) } - #[test] - fn test_tpsl_order_action_hashing() -> Result<()> { + #[tokio::test] + async fn test_tpsl_order_action_hashing() -> Result<()> { for (tpsl, mainnet_signature, testnet_signature) in [ ( "tp", @@ -887,17 +892,17 @@ mod tests { }); let connection_id = action.hash(1583838, None)?; - let signature = sign_l1_action(&wallet, connection_id, true)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, true).await?; assert_eq!(signature.to_string(), mainnet_signature); - let signature = sign_l1_action(&wallet, connection_id, false)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, false).await?; assert_eq!(signature.to_string(), testnet_signature); } Ok(()) } - #[test] - fn test_cancel_action_hashing() -> Result<()> { + #[tokio::test] + async fn test_cancel_action_hashing() -> Result<()> { let wallet = get_wallet()?; let action = Actions::Cancel(BulkCancel { cancels: vec![CancelRequest { @@ -907,10 +912,10 @@ mod tests { }); let connection_id = action.hash(1583838, None)?; - let signature = sign_l1_action(&wallet, connection_id, true)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, true).await?; assert_eq!(signature.to_string(), "02f76cc5b16e0810152fa0e14e7b219f49c361e3325f771544c6f54e157bf9fa17ed0afc11a98596be85d5cd9f86600aad515337318f7ab346e5ccc1b03425d51b"); - let signature = sign_l1_action(&wallet, connection_id, false)?; + let signature = sign_l1_action(wallet.as_ref(), connection_id, false).await?; assert_eq!(signature.to_string(), "6ffebadfd48067663390962539fbde76cfa36f53be65abe2ab72c9db6d0db44457720db9d7c4860f142a484f070c84eb4b9694c3a617c83f0d698a27e55fd5e01c"); Ok(()) diff --git a/src/exchange/exchange_responses.rs b/src/exchange/exchange_responses.rs index 63d3de5c..7bbdf8cd 100644 --- a/src/exchange/exchange_responses.rs +++ b/src/exchange/exchange_responses.rs @@ -1,11 +1,11 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RestingOrder { pub oid: u64, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct FilledOrder { pub total_sz: String, @@ -13,7 +13,7 @@ pub struct FilledOrder { pub oid: u64, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub enum ExchangeDataStatus { Success, @@ -24,19 +24,19 @@ pub enum ExchangeDataStatus { Filled(FilledOrder), } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ExchangeDataStatuses { pub statuses: Vec, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ExchangeResponse { #[serde(rename = "type")] pub response_type: String, pub data: Option, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[serde(tag = "status", content = "response")] pub enum ExchangeResponseStatus { diff --git a/src/exchange/order.rs b/src/exchange/order.rs index f1c30b85..82b870f6 100644 --- a/src/exchange/order.rs +++ b/src/exchange/order.rs @@ -2,8 +2,8 @@ use crate::{ errors::Error, helpers::{float_to_string_for_hashing, uuid_to_hex_string}, prelude::*, + signer::Signer, }; -use ethers::signers::LocalWallet; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; @@ -67,7 +67,7 @@ pub struct MarketOrderParams<'a> { pub px: Option, pub slippage: Option, pub cloid: Option, - pub wallet: Option<&'a LocalWallet>, + pub wallet: Option<&'a dyn Signer>, } #[derive(Debug)] @@ -77,7 +77,7 @@ pub struct MarketCloseParams<'a> { pub px: Option, pub slippage: Option, pub cloid: Option, - pub wallet: Option<&'a LocalWallet>, + pub wallet: Option<&'a dyn Signer>, } #[derive(Debug)] diff --git a/src/info/response_structs.rs b/src/info/response_structs.rs index 4f7487ad..9ebe0419 100644 --- a/src/info/response_structs.rs +++ b/src/info/response_structs.rs @@ -2,9 +2,9 @@ use crate::{ info::{AssetPosition, Level, MarginSummary}, DailyUserVlm, Delta, FeeSchedule, OrderInfo, Referrer, ReferrerState, UserTokenBalance, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UserStateResponse { pub asset_positions: Vec, @@ -13,12 +13,12 @@ pub struct UserStateResponse { pub withdrawable: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct UserTokenBalanceResponse { pub balances: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UserFeesResponse { pub active_referral_discount: String, @@ -28,7 +28,7 @@ pub struct UserFeesResponse { pub user_cross_rate: String, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct OpenOrdersResponse { pub coin: String, @@ -39,7 +39,7 @@ pub struct OpenOrdersResponse { pub timestamp: u64, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UserFillsResponse { pub closed_pnl: String, @@ -56,7 +56,7 @@ pub struct UserFillsResponse { pub fee: String, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct FundingHistoryResponse { pub coin: String, @@ -65,14 +65,14 @@ pub struct FundingHistoryResponse { pub time: u64, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct UserFundingResponse { pub time: u64, pub hash: String, pub delta: Delta, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct L2SnapshotResponse { pub coin: String, @@ -80,7 +80,7 @@ pub struct L2SnapshotResponse { pub time: u64, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct RecentTradesResponse { pub coin: String, @@ -91,7 +91,7 @@ pub struct RecentTradesResponse { pub hash: String, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct CandlesSnapshotResponse { #[serde(rename = "t")] pub time_open: u64, @@ -115,7 +115,7 @@ pub struct CandlesSnapshotResponse { pub num_trades: u64, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct OrderStatusResponse { pub status: String, /// `None` if the order is not found @@ -123,7 +123,7 @@ pub struct OrderStatusResponse { pub order: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ReferralResponse { pub referred_by: Option, diff --git a/src/info/sub_structs.rs b/src/info/sub_structs.rs index 4f86eddf..ceedb538 100644 --- a/src/info/sub_structs.rs +++ b/src/info/sub_structs.rs @@ -1,7 +1,7 @@ use ethers::types::H160; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Leverage { #[serde(rename = "type")] @@ -10,7 +10,7 @@ pub struct Leverage { pub raw_usd: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CumulativeFunding { pub all_time: String, @@ -18,7 +18,7 @@ pub struct CumulativeFunding { pub since_change: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct PositionData { pub coin: String, @@ -34,14 +34,14 @@ pub struct PositionData { pub cum_funding: CumulativeFunding, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct AssetPosition { pub position: PositionData, #[serde(rename = "type")] pub type_string: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MarginSummary { pub account_value: String, @@ -50,7 +50,7 @@ pub struct MarginSummary { pub total_raw_usd: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Level { pub n: u64, @@ -58,7 +58,7 @@ pub struct Level { pub sz: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Delta { #[serde(rename = "type")] @@ -69,7 +69,7 @@ pub struct Delta { pub funding_rate: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct DailyUserVlm { pub date: String, @@ -78,7 +78,7 @@ pub struct DailyUserVlm { pub user_cross: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct FeeSchedule { pub add: String, @@ -87,20 +87,20 @@ pub struct FeeSchedule { pub tiers: Tiers, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Tiers { pub mm: Vec, pub vip: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Mm { pub add: String, pub maker_fraction_cutoff: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Vip { pub add: String, @@ -108,7 +108,7 @@ pub struct Vip { pub ntl_cutoff: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UserTokenBalance { pub coin: String, @@ -117,7 +117,7 @@ pub struct UserTokenBalance { pub entry_ntl: String, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct OrderInfo { pub order: BasicOrderInfo, @@ -125,7 +125,7 @@ pub struct OrderInfo { pub status_timestamp: u64, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct BasicOrderInfo { pub coin: String, @@ -145,21 +145,21 @@ pub struct BasicOrderInfo { pub cloid: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Referrer { pub referrer: H160, pub code: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ReferrerState { pub stage: String, pub data: ReferrerData, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ReferrerData { pub required: String, diff --git a/src/lib.rs b/src/lib.rs index 37634203..638800e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ mod prelude; mod proxy_digest; mod req; mod signature; +pub mod signer; mod ws; pub use consts::{EPSILON, LOCAL_API_URL, MAINNET_API_URL, TESTNET_API_URL}; pub use errors::Error; diff --git a/src/market_maker.rs b/src/market_maker.rs index 4582ab26..f73ad74f 100644 --- a/src/market_maker.rs +++ b/src/market_maker.rs @@ -1,7 +1,7 @@ -use ethers::{ - signers::{LocalWallet, Signer}, - types::H160, -}; +use std::sync::Arc; + +use crate::signer::Signer; +use ethers::types::H160; use log::{error, info}; use tokio::sync::mpsc::unbounded_channel; @@ -26,7 +26,7 @@ pub struct MarketMakerInput { pub max_bps_diff: u16, // Max deviation before we cancel and put new orders on the book (in BPS) pub max_absolute_position_size: f64, // Absolute value of the max position we can take on pub decimals: u32, // Decimals to round to for pricing - pub wallet: LocalWallet, // Wallet containing private key + pub wallet: Arc, // Wallet containing private key } #[derive(Debug)] diff --git a/src/signature/create_signature.rs b/src/signature/create_signature.rs index 65289a6e..7e424643 100644 --- a/src/signature/create_signature.rs +++ b/src/signature/create_signature.rs @@ -1,13 +1,8 @@ -use ethers::{ - core::k256::{elliptic_curve::FieldBytes, Secp256k1}, - signers::LocalWallet, - types::{transaction::eip712::Eip712, Signature, H256, U256}, -}; +use crate::{prelude::*, signature::agent::l1, signer::Signer, Error}; +use ethers::types::{transaction::eip712::Eip712, Signature, H256}; -use crate::{prelude::*, proxy_digest::Sha256Proxy, signature::agent::l1, Error}; - -pub(crate) fn sign_l1_action( - wallet: &LocalWallet, +pub(crate) async fn sign_l1_action( + wallet: &dyn Signer, connection_id: H256, is_mainnet: bool, ) -> Result { @@ -19,47 +14,39 @@ pub(crate) fn sign_l1_action( }, wallet, ) + .await } -pub(crate) fn sign_typed_data(payload: &T, wallet: &LocalWallet) -> Result { +pub(crate) async fn sign_typed_data( + payload: &T, + wallet: &dyn Signer, +) -> Result { let encoded = payload .encode_eip712() .map_err(|e| Error::Eip712(e.to_string()))?; - sign_hash(H256::from(encoded), wallet) -} - -fn sign_hash(hash: H256, wallet: &LocalWallet) -> Result { - let (sig, rec_id) = wallet - .signer() - .sign_digest_recoverable(Sha256Proxy::from(hash)) - .map_err(|e| Error::SignatureFailure(e.to_string()))?; - - let v = u8::from(rec_id) as u64 + 27; - - let r_bytes: FieldBytes = sig.r().into(); - let s_bytes: FieldBytes = sig.s().into(); - let r = U256::from_big_endian(r_bytes.as_slice()); - let s = U256::from_big_endian(s_bytes.as_slice()); - - Ok(Signature { r, s, v }) + wallet.secp256k1_sign(H256::from(encoded)).await } #[cfg(test)] mod tests { use super::*; use crate::{UsdSend, Withdraw3}; + use ethers::signers::LocalWallet; use std::str::FromStr; + use std::sync::Arc; - fn get_wallet() -> Result { + fn get_wallet() -> Result> { let priv_key = "e908f86dbb4d55ac876378565aafeabc187f6690f046459397b17d9b9a19688e"; - priv_key - .parse::() - .map_err(|e| Error::Wallet(e.to_string())) + Ok(Arc::new( + priv_key + .parse::() + .map_err(|e| Error::Wallet(e.to_string()))?, + )) } - #[test] - fn test_sign_l1_action() -> Result<()> { + #[tokio::test] + async fn test_sign_l1_action() -> Result<()> { let wallet = get_wallet()?; let connection_id = H256::from_str("0xde6c4037798a4434ca03cd05f00e3b803126221375cd1e7eaaaf041768be06eb") @@ -67,19 +54,23 @@ mod tests { let expected_mainnet_sig = "fa8a41f6a3fa728206df80801a83bcbfbab08649cd34d9c0bfba7c7b2f99340f53a00226604567b98a1492803190d65a201d6805e5831b7044f17fd530aec7841c"; assert_eq!( - sign_l1_action(&wallet, connection_id, true)?.to_string(), + sign_l1_action(wallet.as_ref(), connection_id, true) + .await? + .to_string(), expected_mainnet_sig ); let expected_testnet_sig = "1713c0fc661b792a50e8ffdd59b637b1ed172d9a3aa4d801d9d88646710fb74b33959f4d075a7ccbec9f2374a6da21ffa4448d58d0413a0d335775f680a881431c"; assert_eq!( - sign_l1_action(&wallet, connection_id, false)?.to_string(), + sign_l1_action(wallet.as_ref(), connection_id, false) + .await? + .to_string(), expected_testnet_sig ); Ok(()) } - #[test] - fn test_sign_usd_transfer_action() -> Result<()> { + #[tokio::test] + async fn test_sign_usd_transfer_action() -> Result<()> { let wallet = get_wallet()?; let usd_send = UsdSend { @@ -92,14 +83,16 @@ mod tests { let expected_sig = "214d507bbdaebba52fa60928f904a8b2df73673e3baba6133d66fe846c7ef70451e82453a6d8db124e7ed6e60fa00d4b7c46e4d96cb2bd61fd81b6e8953cc9d21b"; assert_eq!( - sign_typed_data(&usd_send, &wallet)?.to_string(), + sign_typed_data(&usd_send, wallet.as_ref()) + .await? + .to_string(), expected_sig ); Ok(()) } - #[test] - fn test_sign_withdraw_from_bridge_action() -> Result<()> { + #[tokio::test] + async fn test_sign_withdraw_from_bridge_action() -> Result<()> { let wallet = get_wallet()?; let usd_send = Withdraw3 { @@ -112,7 +105,9 @@ mod tests { let expected_sig = "b3172e33d2262dac2b4cb135ce3c167fda55dafa6c62213564ab728b9f9ba76b769a938e9f6d603dae7154c83bf5a4c3ebab81779dc2db25463a3ed663c82ae41c"; assert_eq!( - sign_typed_data(&usd_send, &wallet)?.to_string(), + sign_typed_data(&usd_send, wallet.as_ref()) + .await? + .to_string(), expected_sig ); Ok(()) diff --git a/src/signer/mod.rs b/src/signer/mod.rs new file mode 100644 index 00000000..8a0f1f37 --- /dev/null +++ b/src/signer/mod.rs @@ -0,0 +1,149 @@ +use crate::prelude::*; +use crate::proxy_digest::Sha256Proxy; +use crate::Error; +use async_trait::async_trait; +use ethers::core::k256::{elliptic_curve::FieldBytes, Secp256k1}; +use ethers::types::{Address, Signature, H256, U256}; +use ethers::utils::hex::ToHexExt; +use privy::Privy; +use std::sync::Arc; + +#[async_trait] +pub trait Signer: Send + Sync + std::fmt::Debug { + async fn secp256k1_sign(&self, message: H256) -> Result; + fn address(&self) -> Address; +} + +#[async_trait] +impl Signer for Arc { + async fn secp256k1_sign(&self, message: H256) -> Result { + (**self).secp256k1_sign(message).await + } + fn address(&self) -> Address { + (**self).address() + } +} + +#[derive(Debug)] +pub struct PrivySigner { + pub privy: Privy, + pub wallet_id: String, + pub address: String, +} + +impl PrivySigner { + pub fn new(privy: Privy, wallet_id: String, address: String) -> Self { + Self { + privy, + wallet_id, + address, + } + } +} + +pub fn signature_string_to_ethers_signature(signature: String) -> Result { + // Privy returns a hex string like "0x..." that is 65 bytes (130 hex chars + 2 for "0x") + let signature = signature.strip_prefix("0x").unwrap_or(&signature); + + // The signature is in format: r (32 bytes) + s (32 bytes) + v (1 byte) + let r = U256::from_str_radix(&signature[0..64], 16) + .map_err(|e| Error::SignatureFailure(format!("Failed to parse r: {}", e)))?; + let s = U256::from_str_radix(&signature[64..128], 16) + .map_err(|e| Error::SignatureFailure(format!("Failed to parse s: {}", e)))?; + let v = u64::from_str_radix(&signature[128..130], 16) + .map_err(|e| Error::SignatureFailure(format!("Failed to parse v: {}", e)))?; + + Ok(Signature { r, s, v }) +} + +#[async_trait] +impl Signer for PrivySigner { + fn address(&self) -> Address { + self.address.parse().unwrap() + } + + async fn secp256k1_sign(&self, message: H256) -> Result { + log::debug!("Signing message: {}", message.to_string()); + let signature = self + .privy + .secp256k1_sign( + self.wallet_id.clone(), + format!("0x{}", message.as_bytes().encode_hex()), + ) + .await + .map_err(|e| Error::SignatureFailure(e.to_string()))?; + + signature_string_to_ethers_signature(signature) + } +} + +#[async_trait] +impl Signer for ethers::signers::LocalWallet { + async fn secp256k1_sign(&self, message: H256) -> Result { + let (sig, rec_id) = self + .signer() + .sign_digest_recoverable(Sha256Proxy::from(message)) + .map_err(|e| Error::SignatureFailure(e.to_string()))?; + + let v = u8::from(rec_id) as u64 + 27; + + let r_bytes: FieldBytes = sig.r().into(); + let s_bytes: FieldBytes = sig.s().into(); + let r = U256::from_big_endian(r_bytes.as_slice()); + let s = U256::from_big_endian(s_bytes.as_slice()); + + Ok(Signature { r, s, v }) + } + + fn address(&self) -> Address { + ethers::signers::Signer::address(self) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use ethers::utils::hex::ToHexExt; + use privy::config::PrivyConfig; + + use super::*; + + const TEST_WALLET_ID: &str = "k0pq0k5an1fvo35m5gm3wn8d"; + const TEST_ADDRESS: &str = "0xCCC48877a33a2C14e40c82da843Cf4c607ABF770"; + + #[tokio::test] + #[ignore = "this requires a private key thats also a privy wallet and PRIVY_* env vars"] + async fn test_secp256k1_sign_convergence() { + dotenv::dotenv().ok(); + let privy_signer = Arc::new(PrivySigner::new( + Privy::new(PrivyConfig::from_env().unwrap()), + TEST_WALLET_ID.to_string(), + TEST_ADDRESS.to_string(), + )); + let private_key = std::env::var("PRIVATE_KEY").unwrap(); + let local_wallet: Arc = + Arc::new(private_key.parse().unwrap()); + + let message = + H256::from_str("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .unwrap(); + + let privy_signature = privy_signer.secp256k1_sign(message).await.unwrap(); + let local_signature = local_wallet.secp256k1_sign(message).await.unwrap(); + + assert_eq!(privy_signature, local_signature); + } + + #[test] + fn test_hash_manipulation() { + let message_str = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let message = H256::from_str(message_str).unwrap(); + H256::from_str("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + .unwrap(); + + let message_hex = format!("0x{}", message.as_bytes().encode_hex()); + + assert_eq!(message_hex, message_str); + } +}