소스 검색

下单接口准备了一半了

skyfffire 3 주 전
부모
커밋
735675e86c
5개의 변경된 파일911개의 추가작업 그리고 789개의 파일을 삭제
  1. 4 0
      Cargo.toml
  2. 14 5
      src/exchange/extended_account.rs
  3. 121 13
      src/exchange/extended_rest_client.rs
  4. 423 423
      src/utils/lib.rs
  5. 349 348
      src/utils/starknet_messages.rs

+ 4 - 0
Cargo.toml

@@ -82,6 +82,10 @@ sha2 = "0.10.8"
 starknet = { git = "https://github.com/xJonathanLEI/starknet-rs", tag = "starknet/v0.17.0" }
 starknet-crypto = "0.8.1"  # `starknet` crate doesn't re-export `PoseidonHasher`
 
+# 随机数发生工具
+rand = "0.8"
+uuid = { version = "1.0", features = ["v4", "fast-rng", "macro-diagnostics"] }
+
 # =======================================================
 # 以下是一些在开发过程中可能会用到的devDependencies,只用于开发和测试,不包含在最终发布版本中
 [dev-dependencies]

+ 14 - 5
src/exchange/extended_account.rs

@@ -1,15 +1,16 @@
 #[derive(Clone, Debug)]
 pub struct ExtendedAccount {
-    // pub access_key: String,
-    // pub secret_key: String,
-    // pub pass_key: String,
     pub api_key: String,
+    pub stark_public_key: String,
+    pub stark_private_key: String,
 }
 
 impl ExtendedAccount {
-    pub fn new(api_key: String) -> Self {
+    pub fn new(api_key: &str, stark_public_key: &str, stark_private_key: &str) -> Self {
         ExtendedAccount {
-            api_key,
+            api_key: api_key.to_string(),
+            stark_public_key: stark_public_key.to_string(),
+            stark_private_key: stark_private_key.to_string(),
         }
     }
     
@@ -17,6 +18,14 @@ impl ExtendedAccount {
         if self.api_key.is_empty() {
             return false;
         }
+
+        if self.stark_public_key.is_empty() {
+            return false;
+        }
+
+        if self.stark_private_key.is_empty() {
+            return false;
+        }
         
         true
     }

+ 121 - 13
src/exchange/extended_rest_client.rs

@@ -1,18 +1,23 @@
+use chrono::Utc;
+use anyhow::{anyhow, Result};
 use reqwest::Client;
 use reqwest::header::HeaderMap;
 use serde_json::{json, Value};
 use tracing::{error};
+use uuid::{Uuid};
 use crate::exchange::extended_account::ExtendedAccount;
 use crate::utils::response::Response;
 use crate::utils::rest_utils::RestUtils;
 
 pub struct ExtendedRestClient {
     pub tag: String,
+    pub market: String,
 
     // 一些私有变量
     base_url: String,
     client: Client,
     account: Option<ExtendedAccount>,
+    market_info: Value,
 
     // 延迟统计工具
     delays: Vec<i64>,
@@ -21,29 +26,59 @@ pub struct ExtendedRestClient {
 }
 
 impl ExtendedRestClient {
-    pub fn new(tag: String, account: Option<ExtendedAccount>) -> Self {
-        ExtendedRestClient {
-            tag,
+    pub async fn new(tag: &str, account: Option<ExtendedAccount>, market: &str) -> Result<Self> {
+        let mut client = ExtendedRestClient {
             // base_url: "https://api.starknet.extended.exchange".to_string(),
             base_url: "https://api.starknet.sepolia.extended.exchange".to_string(),
+
+            tag: tag.to_string(),
+            market: market.to_string(),
+
             client: Client::new(),
             account,
+            market_info: Value::Null,
 
             delays: vec![],
             max_delay: 0,
             avg_delay: 0,
-        }
+        };
+
+        // 获取该client要操作的market的info
+        let response = client.get_market_info(None).await;
+        client.market_info = response.data;
+        
+        Err(anyhow!("固定报错不要慌张"))
+
+        // Ok(client)
     }
 
     // =================================== 公共方法区 ====================================
+    pub async fn get_market_info(&mut self, market_option: Option<&str>) -> Response {
+        let query_market = if let Some(market) = market_option {
+            market
+        } else {
+            self.market.as_str()
+        };
+
+        let params = json!({
+            "market": query_market,
+        });
+
+        self.request("GET",
+                     "/api/v1",
+                     "/info/markets",
+                     true,
+                     params,
+        ).await
+    }
 
     // =================================== 私有方法区 ====================================
     pub async fn get_open_orders(&mut self) -> Response {
         let params = json!({});
 
         self.request("GET",
-                     "/api/v1/user",
-                     "/orders",
+                     "/api/v1",
+                     "/user/orders",
                      true,
                      params,
         ).await
@@ -53,8 +88,8 @@ impl ExtendedRestClient {
         let params = json!({});
 
         self.request("DELETE",
-                     "/api/v1/user",
-                     format!("/order/{}", id).as_str(),
+                     "/api/v1",
+                     format!("/user/order/{}", id).as_str(),
                      true,
                      params,
         ).await
@@ -66,8 +101,43 @@ impl ExtendedRestClient {
         });
 
         self.request("DELETE",
-                     "/api/v1/user",
-                     "/order",
+                     "/api/v1",
+                     "/user/order",
+                     true,
+                     params,
+        ).await
+    }
+
+    pub async fn post_order(&mut self, order_type: &str, side: &str, qty: &str, price: &str) -> Response {
+        // 需要传给extended的参数整理
+        let id = Uuid::new_v4().to_string().as_str();
+        let market = self.market.as_str();
+        // type
+        // side
+        // qty
+        // price
+        let time_in_force = "GTT";
+        let expiry_epoch_millis = Utc::now().timestamp_millis() + (24 * 60 * 60 * 1000);
+        let fee = "";
+        let nonce_u32: u32 = rand::random();
+        let nonce = nonce_u32.to_string().as_str();
+        let self_trade_protection_level = "ACCOUNT";
+
+
+        // 准备OrderHash
+
+        // 生成settlement
+        let settlement = json!({});
+
+        // 组装最后参数
+        let params = json!({
+
+        });
+
+        // 发送订单
+        self.request("POST",
+                     "/api/v1",
+                     "/user/order",
                      true,
                      params,
         ).await
@@ -245,12 +315,50 @@ mod tests {
     use crate::exchange::extended_rest_client::ExtendedRestClient;
     use crate::utils::log_setup::setup_logging;
 
+    async fn get_client() -> ExtendedRestClient {
+        let account = ExtendedAccount::new(
+            "a7b197d06d35de11387b8b71f34c87e4",
+            "0x41efadf5ceebc77b0798b0af797fb97e610c87c669494bea54338c5ef8c0f19",
+            "0x484b399394c4d76cdc62a1dc490f96cf5197f0e307832e59fdeec2e16c50078",
+        );
+        let tag = "Extended";
+        let market = "BTC-USD";
+
+        let client_result = ExtendedRestClient::new(tag, Some(account), market).await;
+
+        match client_result {
+            Ok(client) => {
+                client
+            }
+            Err(err) => {
+                panic!("实例化 ExtendedRestClient 时出现错误: {}", err.to_string())
+            }
+        }
+    }
+
+    #[tokio::test]
+    async fn test_get_market_info() {
+        let _guard = setup_logging().unwrap();
+        let mut client = get_client().await;
+        let response = client.get_market_info(Some("ETH-USD")).await;
+
+        info!("{}", serde_json::to_string_pretty(&response.data).unwrap());
+    }
+
+    #[tokio::test]
+    async fn test_get_orders() {
+        let _guard = setup_logging().unwrap();
+        let mut client = get_client().await;
+        let response = client.get_open_orders().await;
+
+        info!("{}", serde_json::to_string_pretty(&response.data).unwrap());
+    }
+
     #[tokio::test]
     async fn test_cancel_order() {
         let _guard = setup_logging().unwrap();
-        let account = ExtendedAccount::new("a7b197d06d35de11387b8b71f34c87e4".to_string());
-        let mut client = ExtendedRestClient::new("Extended".to_string(), Some(account));
-        let response = client.cancel_order("123456".to_string()).await;
+        let mut client = get_client().await;
+        let response = client.cancel_order("1978656082822787072".to_string()).await;
 
         info!("{}", serde_json::to_string_pretty(&response.data).unwrap());
     }

+ 423 - 423
src/utils/lib.rs

@@ -1,423 +1,423 @@
-// use hex;
-// use num_bigint::BigUint;
-// use sha2::{Digest, Sha256};
-// use starknet::core::crypto::ecdsa_sign;
-// use starknet::core::types::Felt;
-// use std::str::FromStr;
-// 
-// use crate::utils::starknet_messages::{
-//     AssetId, OffChainMessage, Order, PositionId, StarknetDomain, Timestamp, TransferArgs,
-// };
-// use utils::starknet_messages;
-// use crate::utils;
-// 
-// pub struct StarkSignature {
-//     pub r: Felt,
-//     pub s: Felt,
-//     pub v: Felt,
-// }
-// 
-// fn grind_key(key_seed: BigUint) -> BigUint {
-//     let two_256 = BigUint::from_str(
-//         "115792089237316195423570985008687907853269984665640564039457584007913129639936",
-//     )
-//     .unwrap();
-//     let key_value_limit = BigUint::from_str(
-//         "3618502788666131213697322783095070105526743751716087489154079457884512865583",
-//     )
-//     .unwrap();
-// 
-//     let max_allowed_value = two_256.clone() - (two_256.clone() % (&key_value_limit));
-//     let mut index = BigUint::ZERO;
-//     loop {
-//         let hash_input = {
-//             let mut input = Vec::new();
-//             input.extend_from_slice(&key_seed.to_bytes_be());
-//             input.extend_from_slice(&index.to_bytes_be());
-//             input
-//         };
-//         let hash_result = Sha256::digest(&hash_input);
-//         let hash = hash_result.as_slice();
-//         let key = BigUint::from_bytes_be(&hash);
-// 
-//         if key < max_allowed_value {
-//             return key % (&key_value_limit);
-//         }
-// 
-//         index += BigUint::from_str("1").unwrap();
-//     }
-// }
-// 
-// pub fn get_private_key_from_eth_signature(signature: &str) -> Result<Felt, String> {
-//     let eth_sig_truncated = signature.trim_start_matches("0x");
-//     if eth_sig_truncated.len() < 64 {
-//         return Err("Invalid signature length".to_string());
-//     }
-//     let r = &eth_sig_truncated[..64];
-//     let r_bytes = hex::decode(r).map_err(|e| format!("Failed to decode r as hex: {:?}", e))?;
-//     let r_int = BigUint::from_bytes_be(&r_bytes);
-// 
-//     let ground_key = grind_key(r_int);
-//     return Ok(Felt::from_hex(&ground_key.to_str_radix(16)).unwrap());
-// }
-// 
-// pub fn sign_message(message: &Felt, private_key: &Felt) -> Result<StarkSignature, String> {
-//     return ecdsa_sign(private_key, &message)
-//         .map(|extended_signature| StarkSignature {
-//             r: extended_signature.r,
-//             s: extended_signature.s,
-//             v: extended_signature.v,
-//         })
-//         .map_err(|e| format!("Failed to sign message: {:?}", e));
-// }
-// 
-// // these functions are designed to be called from other languages, such as Python or JavaScript,
-// // so they take string arguments.
-// pub fn get_order_hash(
-//     position_id: String,
-//     base_asset_id_hex: String,
-//     base_amount: String,
-//     quote_asset_id_hex: String,
-//     quote_amount: String,
-//     fee_asset_id_hex: String,
-//     fee_amount: String,
-//     expiration: String,
-//     salt: String,
-//     user_public_key_hex: String,
-//     domain_name: String,
-//     domain_version: String,
-//     domain_chain_id: String,
-//     domain_revision: String,
-// ) -> Result<Felt, String> {
-//     let base_asset_id = Felt::from_hex(&base_asset_id_hex)
-//         .map_err(|e| format!("Invalid base_asset_id_hex: {:?}", e))?;
-//     let quote_asset_id = Felt::from_hex(&quote_asset_id_hex)
-//         .map_err(|e| format!("Invalid quote_asset_id_hex: {:?}", e))?;
-//     let fee_asset_id = Felt::from_hex(&fee_asset_id_hex)
-//         .map_err(|e| format!("Invalid fee_asset_id_hex: {:?}", e))?;
-//     let user_key = Felt::from_hex(&user_public_key_hex)
-//         .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
-// 
-//     let position_id = u32::from_str_radix(&position_id, 10)
-//         .map_err(|e| format!("Invalid position_id: {:?}", e))?;
-//     let base_amount = i64::from_str_radix(&base_amount, 10)
-//         .map_err(|e| format!("Invalid base_amount: {:?}", e))?;
-//     let quote_amount = i64::from_str_radix(&quote_amount, 10)
-//         .map_err(|e| format!("Invalid quote_amount: {:?}", e))?;
-//     let fee_amount =
-//         u64::from_str_radix(&fee_amount, 10).map_err(|e| format!("Invalid fee_amount: {:?}", e))?;
-//     let expiration =
-//         u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
-//     let salt = u64::from_str_radix(&salt, 10).map_err(|e| format!("Invalid salt: {:?}", e))?;
-//     let revision = u32::from_str_radix(&domain_revision, 10)
-//         .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
-// 
-//     let order = Order {
-//         position_id: PositionId { value: position_id },
-//         base_asset_id: AssetId {
-//             value: base_asset_id,
-//         },
-//         base_amount,
-//         quote_asset_id: AssetId {
-//             value: quote_asset_id,
-//         },
-//         quote_amount,
-//         fee_asset_id: AssetId {
-//             value: fee_asset_id,
-//         },
-//         fee_amount,
-//         expiration: Timestamp {
-//             seconds: expiration,
-//         },
-//         salt: salt
-//             .try_into()
-//             .map_err(|e| format!("Invalid salt vault: {:?}", e))?,
-//     };
-//     let domain = StarknetDomain {
-//         name: domain_name,
-//         version: domain_version,
-//         chain_id: domain_chain_id,
-//         revision,
-//     };
-//     order
-//         .message_hash(&domain, user_key)
-//         .map_err(|e| format!("Failed to compute message hash: {:?}", e))
-// }
-// 
-// pub fn get_transfer_hash(
-//     recipient_position_id: String,
-//     sender_position_id: String,
-//     collateral_id_hex: String,
-//     amount: String,
-//     expiration: String,
-//     salt: String,
-//     user_public_key_hex: String,
-//     domain_name: String,
-//     domain_version: String,
-//     domain_chain_id: String,
-//     domain_revision: String,
-// ) -> Result<Felt, String> {
-//     let collateral_id = Felt::from_hex(&collateral_id_hex)
-//         .map_err(|e| format!("Invalid collateral_id_hex: {:?}", e))?;
-//     let user_key = Felt::from_hex(&user_public_key_hex)
-//         .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
-// 
-//     let recipient = u32::from_str_radix(&recipient_position_id, 10)
-//         .map_err(|e| format!("Invalid recipient_position_id: {:?}", e))?;
-//     let position_id = u32::from_str_radix(&sender_position_id, 10)
-//         .map_err(|e| format!("Invalid sender_position_id: {:?}", e))?;
-//     let amount =
-//         u64::from_str_radix(&amount, 10).map_err(|e| format!("Invalid amount: {:?}", e))?;
-//     let expiration =
-//         u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
-//     let salt = Felt::from_dec_str(&salt).map_err(|e| format!("Invalid salt: {:?}", e))?;
-//     let revision = u32::from_str_radix(&domain_revision, 10)
-//         .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
-// 
-//     let transfer_args = TransferArgs {
-//         recipient: PositionId { value: recipient },
-//         position_id: PositionId { value: position_id },
-//         collateral_id: AssetId {
-//             value: collateral_id,
-//         },
-//         amount,
-//         expiration: Timestamp {
-//             seconds: expiration,
-//         },
-//         salt,
-//     };
-//     let domain = StarknetDomain {
-//         name: domain_name,
-//         version: domain_version,
-//         chain_id: domain_chain_id,
-//         revision,
-//     };
-//     transfer_args
-//         .message_hash(&domain, user_key)
-//         .map_err(|e| format!("Failed to compute message hash: {:?}", e))
-// }
-// 
-// pub fn get_withdrawal_hash(
-//     recipient_hex: String,
-//     position_id: String,
-//     collateral_id_hex: String,
-//     amount: String,
-//     expiration: String,
-//     salt: String,
-//     user_public_key_hex: String,
-//     domain_name: String,
-//     domain_version: String,
-//     domain_chain_id: String,
-//     domain_revision: String,
-// ) -> Result<Felt, String> {
-//     let collateral_id = Felt::from_hex(&collateral_id_hex)
-//         .map_err(|e| format!("Invalid collateral_id_hex: {:?}", e))?;
-//     let user_key = Felt::from_hex(&user_public_key_hex)
-//         .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
-// 
-//     let recipient =
-//         Felt::from_hex(&recipient_hex).map_err(|e| format!("Invalid recipient_hex: {:?}", e))?;
-//     let position_id = u32::from_str_radix(&position_id, 10)
-//         .map_err(|e| format!("Invalid position_id: {:?}", e))?;
-//     let amount =
-//         u64::from_str_radix(&amount, 10).map_err(|e| format!("Invalid amount: {:?}", e))?;
-//     let expiration =
-//         u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
-//     let salt = Felt::from_dec_str(&salt).map_err(|e| format!("Invalid salt: {:?}", e))?;
-//     let revision = u32::from_str_radix(&domain_revision, 10)
-//         .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
-// 
-//     let withdrawal_args = starknet_messages::WithdrawalArgs {
-//         recipient,
-//         position_id: PositionId { value: position_id },
-//         collateral_id: AssetId {
-//             value: collateral_id,
-//         },
-//         amount,
-//         expiration: Timestamp {
-//             seconds: expiration,
-//         },
-//         salt,
-//     };
-//     let domain = StarknetDomain {
-//         name: domain_name,
-//         version: domain_version,
-//         chain_id: domain_chain_id,
-//         revision,
-//     };
-//     withdrawal_args
-//         .message_hash(&domain, user_key)
-//         .map_err(|e| {
-//             format!(
-//                 "Failed to compute message hash for withdrawal args: {:?}",
-//                 e
-//             )
-//         })
-// }
-// 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-// 
-//     #[test]
-//     fn test_get_private_key_from_eth_signature() {
-//         let signature = "0x9ef64d5936681edf44b4a7ad713f3bc24065d4039562af03fccf6a08d6996eab367df11439169b417b6a6d8ce81d409edb022597ce193916757c7d5d9cbf97301c";
-//         let result = get_private_key_from_eth_signature(signature);
-// 
-//         match result {
-//             Ok(private_key) => {
-//                 assert_eq!(private_key, Felt::from_dec_str("3554363360756768076148116215296798451844584215587910826843139626172125285444").unwrap());
-//             }
-//             Err(err) => {
-//                 panic!("Expected Ok, got Err: {}", err);
-//             }
-//         }
-//     }
-// 
-//     #[test]
-//     fn test_get_transfer_msg() {
-//         let recipient_position_id = "1".to_string();
-//         let sender_position_id = "2".to_string();
-//         let collateral_id_hex = "0x3".to_string();
-//         let amount = "4".to_string();
-//         let expiration = "5".to_string();
-//         let salt = "6".to_string();
-//         let user_public_key_hex =
-//             "0x5d05989e9302dcebc74e241001e3e3ac3f4402ccf2f8e6f74b034b07ad6a904".to_string();
-//         let domain_name = "Perpetuals".to_string();
-//         let domain_version = "v0".to_string();
-//         let domain_chain_id = "SN_SEPOLIA".to_string();
-//         let domain_revision = "1".to_string();
-// 
-//         let result = get_transfer_hash(
-//             recipient_position_id,
-//             sender_position_id,
-//             collateral_id_hex,
-//             amount,
-//             expiration,
-//             salt,
-//             user_public_key_hex,
-//             domain_name,
-//             domain_version,
-//             domain_chain_id,
-//             domain_revision,
-//         );
-// 
-//         match result {
-//             Ok(hash) => {
-//                 assert_eq!(
-//                     hash,
-//                     Felt::from_hex(
-//                         "0x56c7b21d13b79a33d7700dda20e22246c25e89818249504148174f527fc3f8f"
-//                     )
-//                     .unwrap()
-//                 );
-//             }
-//             Err(err) => {
-//                 panic!("Expected Ok, got Err: {}", err);
-//             }
-//         }
-//     }
-// 
-//     #[test]
-//     fn test_get_order_hash() {
-//         let position_id = "100".to_string();
-//         let base_asset_id_hex = "0x2".to_string();
-//         let base_amount = "100".to_string();
-//         let quote_asset_id_hex = "0x1".to_string();
-//         let quote_amount = "-156".to_string();
-//         let fee_asset_id_hex = "0x1".to_string();
-//         let fee_amount = "74".to_string();
-//         let expiration = "100".to_string();
-//         let salt = "123".to_string();
-//         let user_public_key_hex =
-//             "0x5d05989e9302dcebc74e241001e3e3ac3f4402ccf2f8e6f74b034b07ad6a904".to_string();
-//         let domain_name = "Perpetuals".to_string();
-//         let domain_version = "v0".to_string();
-//         let domain_chain_id = "SN_SEPOLIA".to_string();
-//         let domain_revision = "1".to_string();
-// 
-//         let result = get_order_hash(
-//             position_id,
-//             base_asset_id_hex,
-//             base_amount,
-//             quote_asset_id_hex,
-//             quote_amount,
-//             fee_asset_id_hex,
-//             fee_amount,
-//             expiration,
-//             salt,
-//             user_public_key_hex,
-//             domain_name,
-//             domain_version,
-//             domain_chain_id,
-//             domain_revision,
-//         );
-// 
-//         match result {
-//             Ok(hash) => {
-//                 assert_eq!(
-//                     hash,
-//                     Felt::from_hex(
-//                         "0x4de4c009e0d0c5a70a7da0e2039fb2b99f376d53496f89d9f437e736add6b48"
-//                     )
-//                     .unwrap()
-//                 );
-//             }
-//             Err(err) => {
-//                 panic!("Expected Ok, got Err: {}", err);
-//             }
-//         }
-//     }
-// 
-//     #[test]
-//     fn test_get_withdrawal_hash() {
-//         let recipient_hex = Felt::from_dec_str(
-//             "206642948138484946401984817000601902748248360221625950604253680558965863254",
-//         )
-//         .unwrap()
-//         .to_hex_string();
-//         let position_id = "2".to_string();
-//         let collateral_id_hex = Felt::from_dec_str(
-//             "1386727789535574059419576650469753513512158569780862144831829362722992755422",
-//         )
-//         .unwrap()
-//         .to_hex_string();
-//         let amount = "1000".to_string();
-//         let expiration = "0".to_string();
-//         let salt = "0".to_string();
-//         let user_public_key_hex =
-//             "0x5D05989E9302DCEBC74E241001E3E3AC3F4402CCF2F8E6F74B034B07AD6A904".to_string();
-//         let domain_name = "Perpetuals".to_string();
-//         let domain_version = "v0".to_string();
-//         let domain_chain_id = "SN_SEPOLIA".to_string();
-//         let domain_revision = "1".to_string();
-//         let result = get_withdrawal_hash(
-//             recipient_hex,
-//             position_id,
-//             collateral_id_hex,
-//             amount,
-//             expiration,
-//             salt,
-//             user_public_key_hex,
-//             domain_name,
-//             domain_version,
-//             domain_chain_id,
-//             domain_revision,
-//         );
-//         match result {
-//             Ok(hash) => {
-//                 assert_eq!(
-//                     hash,
-//                     Felt::from_dec_str(
-//                         "2182119571682827544073774098906745929330860211691330979324731407862023927178"
-//                     )
-//                     .unwrap()
-//                 );
-//             }
-//             Err(err) => {
-//                 panic!("Expected Ok, got Err: {}", err);
-//             }
-//         }
-//     }
-// }
+use hex;
+use num_bigint::BigUint;
+use sha2::{Digest, Sha256};
+use starknet::core::crypto::ecdsa_sign;
+use starknet::core::types::Felt;
+use std::str::FromStr;
+
+use crate::utils::starknet_messages::{
+    AssetId, OffChainMessage, Order, PositionId, StarknetDomain, Timestamp, TransferArgs,
+};
+use utils::starknet_messages;
+use crate::utils;
+
+pub struct StarkSignature {
+    pub r: Felt,
+    pub s: Felt,
+    pub v: Felt,
+}
+
+fn grind_key(key_seed: BigUint) -> BigUint {
+    let two_256 = BigUint::from_str(
+        "115792089237316195423570985008687907853269984665640564039457584007913129639936",
+    )
+    .unwrap();
+    let key_value_limit = BigUint::from_str(
+        "3618502788666131213697322783095070105526743751716087489154079457884512865583",
+    )
+    .unwrap();
+
+    let max_allowed_value = two_256.clone() - (two_256.clone() % (&key_value_limit));
+    let mut index = BigUint::ZERO;
+    loop {
+        let hash_input = {
+            let mut input = Vec::new();
+            input.extend_from_slice(&key_seed.to_bytes_be());
+            input.extend_from_slice(&index.to_bytes_be());
+            input
+        };
+        let hash_result = Sha256::digest(&hash_input);
+        let hash = hash_result.as_slice();
+        let key = BigUint::from_bytes_be(&hash);
+
+        if key < max_allowed_value {
+            return key % (&key_value_limit);
+        }
+
+        index += BigUint::from_str("1").unwrap();
+    }
+}
+
+pub fn get_private_key_from_eth_signature(signature: &str) -> Result<Felt, String> {
+    let eth_sig_truncated = signature.trim_start_matches("0x");
+    if eth_sig_truncated.len() < 64 {
+        return Err("Invalid signature length".to_string());
+    }
+    let r = &eth_sig_truncated[..64];
+    let r_bytes = hex::decode(r).map_err(|e| format!("Failed to decode r as hex: {:?}", e))?;
+    let r_int = BigUint::from_bytes_be(&r_bytes);
+
+    let ground_key = grind_key(r_int);
+    return Ok(Felt::from_hex(&ground_key.to_str_radix(16)).unwrap());
+}
+
+pub fn sign_message(message: &Felt, private_key: &Felt) -> Result<StarkSignature, String> {
+    return ecdsa_sign(private_key, &message)
+        .map(|extended_signature| StarkSignature {
+            r: extended_signature.r,
+            s: extended_signature.s,
+            v: extended_signature.v,
+        })
+        .map_err(|e| format!("Failed to sign message: {:?}", e));
+}
+
+// these functions are designed to be called from other languages, such as Python or JavaScript,
+// so they take string arguments.
+pub fn get_order_hash(
+    position_id: String,
+    base_asset_id_hex: String,
+    base_amount: String,
+    quote_asset_id_hex: String,
+    quote_amount: String,
+    fee_asset_id_hex: String,
+    fee_amount: String,
+    expiration: String,
+    salt: String,
+    user_public_key_hex: String,
+    domain_name: String,
+    domain_version: String,
+    domain_chain_id: String,
+    domain_revision: String,
+) -> Result<Felt, String> {
+    let base_asset_id = Felt::from_hex(&base_asset_id_hex)
+        .map_err(|e| format!("Invalid base_asset_id_hex: {:?}", e))?;
+    let quote_asset_id = Felt::from_hex(&quote_asset_id_hex)
+        .map_err(|e| format!("Invalid quote_asset_id_hex: {:?}", e))?;
+    let fee_asset_id = Felt::from_hex(&fee_asset_id_hex)
+        .map_err(|e| format!("Invalid fee_asset_id_hex: {:?}", e))?;
+    let user_key = Felt::from_hex(&user_public_key_hex)
+        .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
+
+    let position_id = u32::from_str_radix(&position_id, 10)
+        .map_err(|e| format!("Invalid position_id: {:?}", e))?;
+    let base_amount = i64::from_str_radix(&base_amount, 10)
+        .map_err(|e| format!("Invalid base_amount: {:?}", e))?;
+    let quote_amount = i64::from_str_radix(&quote_amount, 10)
+        .map_err(|e| format!("Invalid quote_amount: {:?}", e))?;
+    let fee_amount =
+        u64::from_str_radix(&fee_amount, 10).map_err(|e| format!("Invalid fee_amount: {:?}", e))?;
+    let expiration =
+        u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
+    let salt = u64::from_str_radix(&salt, 10).map_err(|e| format!("Invalid salt: {:?}", e))?;
+    let revision = u32::from_str_radix(&domain_revision, 10)
+        .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
+
+    let order = Order {
+        position_id: PositionId { value: position_id },
+        base_asset_id: AssetId {
+            value: base_asset_id,
+        },
+        base_amount,
+        quote_asset_id: AssetId {
+            value: quote_asset_id,
+        },
+        quote_amount,
+        fee_asset_id: AssetId {
+            value: fee_asset_id,
+        },
+        fee_amount,
+        expiration: Timestamp {
+            seconds: expiration,
+        },
+        salt: salt
+            .try_into()
+            .map_err(|e| format!("Invalid salt vault: {:?}", e))?,
+    };
+    let domain = StarknetDomain {
+        name: domain_name,
+        version: domain_version,
+        chain_id: domain_chain_id,
+        revision,
+    };
+    order
+        .message_hash(&domain, user_key)
+        .map_err(|e| format!("Failed to compute message hash: {:?}", e))
+}
+
+pub fn get_transfer_hash(
+    recipient_position_id: String,
+    sender_position_id: String,
+    collateral_id_hex: String,
+    amount: String,
+    expiration: String,
+    salt: String,
+    user_public_key_hex: String,
+    domain_name: String,
+    domain_version: String,
+    domain_chain_id: String,
+    domain_revision: String,
+) -> Result<Felt, String> {
+    let collateral_id = Felt::from_hex(&collateral_id_hex)
+        .map_err(|e| format!("Invalid collateral_id_hex: {:?}", e))?;
+    let user_key = Felt::from_hex(&user_public_key_hex)
+        .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
+
+    let recipient = u32::from_str_radix(&recipient_position_id, 10)
+        .map_err(|e| format!("Invalid recipient_position_id: {:?}", e))?;
+    let position_id = u32::from_str_radix(&sender_position_id, 10)
+        .map_err(|e| format!("Invalid sender_position_id: {:?}", e))?;
+    let amount =
+        u64::from_str_radix(&amount, 10).map_err(|e| format!("Invalid amount: {:?}", e))?;
+    let expiration =
+        u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
+    let salt = Felt::from_dec_str(&salt).map_err(|e| format!("Invalid salt: {:?}", e))?;
+    let revision = u32::from_str_radix(&domain_revision, 10)
+        .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
+
+    let transfer_args = TransferArgs {
+        recipient: PositionId { value: recipient },
+        position_id: PositionId { value: position_id },
+        collateral_id: AssetId {
+            value: collateral_id,
+        },
+        amount,
+        expiration: Timestamp {
+            seconds: expiration,
+        },
+        salt,
+    };
+    let domain = StarknetDomain {
+        name: domain_name,
+        version: domain_version,
+        chain_id: domain_chain_id,
+        revision,
+    };
+    transfer_args
+        .message_hash(&domain, user_key)
+        .map_err(|e| format!("Failed to compute message hash: {:?}", e))
+}
+
+pub fn get_withdrawal_hash(
+    recipient_hex: String,
+    position_id: String,
+    collateral_id_hex: String,
+    amount: String,
+    expiration: String,
+    salt: String,
+    user_public_key_hex: String,
+    domain_name: String,
+    domain_version: String,
+    domain_chain_id: String,
+    domain_revision: String,
+) -> Result<Felt, String> {
+    let collateral_id = Felt::from_hex(&collateral_id_hex)
+        .map_err(|e| format!("Invalid collateral_id_hex: {:?}", e))?;
+    let user_key = Felt::from_hex(&user_public_key_hex)
+        .map_err(|e| format!("Invalid user_public_key_hex: {:?}", e))?;
+
+    let recipient =
+        Felt::from_hex(&recipient_hex).map_err(|e| format!("Invalid recipient_hex: {:?}", e))?;
+    let position_id = u32::from_str_radix(&position_id, 10)
+        .map_err(|e| format!("Invalid position_id: {:?}", e))?;
+    let amount =
+        u64::from_str_radix(&amount, 10).map_err(|e| format!("Invalid amount: {:?}", e))?;
+    let expiration =
+        u64::from_str_radix(&expiration, 10).map_err(|e| format!("Invalid expiration: {:?}", e))?;
+    let salt = Felt::from_dec_str(&salt).map_err(|e| format!("Invalid salt: {:?}", e))?;
+    let revision = u32::from_str_radix(&domain_revision, 10)
+        .map_err(|e| format!("Invalid domain_revision: {:?}", e))?;
+
+    let withdrawal_args = starknet_messages::WithdrawalArgs {
+        recipient,
+        position_id: PositionId { value: position_id },
+        collateral_id: AssetId {
+            value: collateral_id,
+        },
+        amount,
+        expiration: Timestamp {
+            seconds: expiration,
+        },
+        salt,
+    };
+    let domain = StarknetDomain {
+        name: domain_name,
+        version: domain_version,
+        chain_id: domain_chain_id,
+        revision,
+    };
+    withdrawal_args
+        .message_hash(&domain, user_key)
+        .map_err(|e| {
+            format!(
+                "Failed to compute message hash for withdrawal args: {:?}",
+                e
+            )
+        })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_get_private_key_from_eth_signature() {
+        let signature = "0x9ef64d5936681edf44b4a7ad713f3bc24065d4039562af03fccf6a08d6996eab367df11439169b417b6a6d8ce81d409edb022597ce193916757c7d5d9cbf97301c";
+        let result = get_private_key_from_eth_signature(signature);
+
+        match result {
+            Ok(private_key) => {
+                assert_eq!(private_key, Felt::from_dec_str("3554363360756768076148116215296798451844584215587910826843139626172125285444").unwrap());
+            }
+            Err(err) => {
+                panic!("Expected Ok, got Err: {}", err);
+            }
+        }
+    }
+
+    #[test]
+    fn test_get_transfer_msg() {
+        let recipient_position_id = "1".to_string();
+        let sender_position_id = "2".to_string();
+        let collateral_id_hex = "0x3".to_string();
+        let amount = "4".to_string();
+        let expiration = "5".to_string();
+        let salt = "6".to_string();
+        let user_public_key_hex =
+            "0x5d05989e9302dcebc74e241001e3e3ac3f4402ccf2f8e6f74b034b07ad6a904".to_string();
+        let domain_name = "Perpetuals".to_string();
+        let domain_version = "v0".to_string();
+        let domain_chain_id = "SN_SEPOLIA".to_string();
+        let domain_revision = "1".to_string();
+
+        let result = get_transfer_hash(
+            recipient_position_id,
+            sender_position_id,
+            collateral_id_hex,
+            amount,
+            expiration,
+            salt,
+            user_public_key_hex,
+            domain_name,
+            domain_version,
+            domain_chain_id,
+            domain_revision,
+        );
+
+        match result {
+            Ok(hash) => {
+                assert_eq!(
+                    hash,
+                    Felt::from_hex(
+                        "0x56c7b21d13b79a33d7700dda20e22246c25e89818249504148174f527fc3f8f"
+                    )
+                    .unwrap()
+                );
+            }
+            Err(err) => {
+                panic!("Expected Ok, got Err: {}", err);
+            }
+        }
+    }
+
+    #[test]
+    fn test_get_order_hash() {
+        let position_id = "100".to_string();
+        let base_asset_id_hex = "0x2".to_string();
+        let base_amount = "100".to_string();
+        let quote_asset_id_hex = "0x1".to_string();
+        let quote_amount = "-156".to_string();
+        let fee_asset_id_hex = "0x1".to_string();
+        let fee_amount = "74".to_string();
+        let expiration = "100".to_string();
+        let salt = "123".to_string();
+        let user_public_key_hex =
+            "0x5d05989e9302dcebc74e241001e3e3ac3f4402ccf2f8e6f74b034b07ad6a904".to_string();
+        let domain_name = "Perpetuals".to_string();
+        let domain_version = "v0".to_string();
+        let domain_chain_id = "SN_SEPOLIA".to_string();
+        let domain_revision = "1".to_string();
+
+        let result = get_order_hash(
+            position_id,
+            base_asset_id_hex,
+            base_amount,
+            quote_asset_id_hex,
+            quote_amount,
+            fee_asset_id_hex,
+            fee_amount,
+            expiration,
+            salt,
+            user_public_key_hex,
+            domain_name,
+            domain_version,
+            domain_chain_id,
+            domain_revision,
+        );
+
+        match result {
+            Ok(hash) => {
+                assert_eq!(
+                    hash,
+                    Felt::from_hex(
+                        "0x4de4c009e0d0c5a70a7da0e2039fb2b99f376d53496f89d9f437e736add6b48"
+                    )
+                    .unwrap()
+                );
+            }
+            Err(err) => {
+                panic!("Expected Ok, got Err: {}", err);
+            }
+        }
+    }
+
+    #[test]
+    fn test_get_withdrawal_hash() {
+        let recipient_hex = Felt::from_dec_str(
+            "206642948138484946401984817000601902748248360221625950604253680558965863254",
+        )
+        .unwrap()
+        .to_hex_string();
+        let position_id = "2".to_string();
+        let collateral_id_hex = Felt::from_dec_str(
+            "1386727789535574059419576650469753513512158569780862144831829362722992755422",
+        )
+        .unwrap()
+        .to_hex_string();
+        let amount = "1000".to_string();
+        let expiration = "0".to_string();
+        let salt = "0".to_string();
+        let user_public_key_hex =
+            "0x5D05989E9302DCEBC74E241001E3E3AC3F4402CCF2F8E6F74B034B07AD6A904".to_string();
+        let domain_name = "Perpetuals".to_string();
+        let domain_version = "v0".to_string();
+        let domain_chain_id = "SN_SEPOLIA".to_string();
+        let domain_revision = "1".to_string();
+        let result = get_withdrawal_hash(
+            recipient_hex,
+            position_id,
+            collateral_id_hex,
+            amount,
+            expiration,
+            salt,
+            user_public_key_hex,
+            domain_name,
+            domain_version,
+            domain_chain_id,
+            domain_revision,
+        );
+        match result {
+            Ok(hash) => {
+                assert_eq!(
+                    hash,
+                    Felt::from_dec_str(
+                        "2182119571682827544073774098906745929330860211691330979324731407862023927178"
+                    )
+                    .unwrap()
+                );
+            }
+            Err(err) => {
+                panic!("Expected Ok, got Err: {}", err);
+            }
+        }
+    }
+}

+ 349 - 348
src/utils/starknet_messages.rs

@@ -1,348 +1,349 @@
-// use starknet::core::utils::cairo_short_string_to_felt;
-// 
-// use starknet::core::types::Felt;
-// use starknet::macros::selector;
-// use starknet_crypto::PoseidonHasher;
-// 
-// use std::sync::LazyLock;
-// 
-// static MESSAGE_FELT: LazyLock<Felt> =
-//     LazyLock::new(|| cairo_short_string_to_felt("StarkNet Message").unwrap());
-// 
-// pub trait Hashable {
-//     const SELECTOR: Felt;
-//     fn hash(&self) -> Felt;
-// }
-// 
-// pub trait OffChainMessage: Hashable {
-//     fn message_hash(
-//         &self,
-//         stark_domain: &StarknetDomain,
-//         public_key: Felt,
-//     ) -> Result<Felt, String> {
-//         let mut hasher = PoseidonHasher::new();
-//         hasher.update(*MESSAGE_FELT);
-//         hasher.update(stark_domain.hash());
-//         hasher.update(public_key);
-//         hasher.update(self.hash());
-//         Ok(hasher.finalize())
-//     }
-// }
-// 
-// pub struct StarknetDomain {
-//     pub name: String,
-//     pub version: String,
-//     pub chain_id: String,
-//     pub revision: u32,
-// }
-// 
-// impl Hashable for StarknetDomain {
-//     const SELECTOR: Felt = selector!("\"StarknetDomain\"(\"name\":\"shortstring\",\"version\":\"shortstring\",\"chainId\":\"shortstring\",\"revision\":\"shortstring\")");
-//     fn hash(&self) -> Felt {
-//         let mut hasher = PoseidonHasher::new();
-//         hasher.update(Self::SELECTOR);
-//         hasher.update(cairo_short_string_to_felt(&self.name).unwrap());
-//         hasher.update(cairo_short_string_to_felt(&self.version).unwrap());
-//         hasher.update(cairo_short_string_to_felt(&self.chain_id).unwrap());
-//         hasher.update(self.revision.into());
-//         let hash = hasher.finalize();
-//         return hash;
-//     }
-// }
-// 
-// pub struct AssetId {
-//     pub value: Felt,
-// }
-// pub struct PositionId {
-//     pub value: u32,
-// }
-// 
-// pub struct AssetAmount {
-//     pub asset_id: AssetId,
-//     pub amount: i64,
-// }
-// 
-// pub struct Timestamp {
-//     pub seconds: u64,
-// }
-// 
-// pub struct Order {
-//     pub position_id: PositionId,
-//     pub base_asset_id: AssetId,
-//     pub base_amount: i64,
-//     pub quote_asset_id: AssetId,
-//     pub quote_amount: i64,
-//     pub fee_asset_id: AssetId,
-//     pub fee_amount: u64,
-//     pub expiration: Timestamp,
-//     pub salt: Felt,
-// }
-// 
-// impl Hashable for Order {
-//     const SELECTOR: Felt = selector!("\"Order\"(\"position_id\":\"felt\",\"base_asset_id\":\"AssetId\",\"base_amount\":\"i64\",\"quote_asset_id\":\"AssetId\",\"quote_amount\":\"i64\",\"fee_asset_id\":\"AssetId\",\"fee_amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
-//     fn hash(&self) -> Felt {
-//         let mut hasher = PoseidonHasher::new();
-//         hasher.update(Self::SELECTOR);
-//         hasher.update(self.position_id.value.into());
-//         hasher.update(self.base_asset_id.value.into());
-//         hasher.update(self.base_amount.into());
-//         hasher.update(self.quote_asset_id.value.into());
-//         hasher.update(self.quote_amount.into());
-//         hasher.update(self.fee_asset_id.value.into());
-//         hasher.update(self.fee_amount.into());
-//         hasher.update(self.expiration.seconds.into());
-//         hasher.update(self.salt);
-//         hasher.finalize()
-//     }
-// }
-// impl OffChainMessage for Order {}
-// 
-// pub struct TransferArgs {
-//     pub recipient: PositionId,
-//     pub position_id: PositionId,
-//     pub collateral_id: AssetId,
-//     pub amount: u64,
-//     pub expiration: Timestamp,
-//     pub salt: Felt,
-// }
-// 
-// impl Hashable for TransferArgs {
-//     const SELECTOR: Felt = selector!("\"TransferArgs\"(\"recipient\":\"PositionId\",\"position_id\":\"PositionId\",\"collateral_id\":\"AssetId\",\"amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
-//     fn hash(&self) -> Felt {
-//         let mut hasher = PoseidonHasher::new();
-//         hasher.update(Self::SELECTOR);
-//         hasher.update(self.recipient.value.into());
-//         hasher.update(self.position_id.value.into());
-//         hasher.update(self.collateral_id.value.into());
-//         hasher.update(self.amount.into());
-//         hasher.update(self.expiration.seconds.into());
-//         hasher.update(self.salt);
-//         hasher.finalize()
-//     }
-// }
-// 
-// impl OffChainMessage for TransferArgs {}
-// 
-// pub static SEPOLIA_DOMAIN: LazyLock<StarknetDomain> = LazyLock::new(|| StarknetDomain {
-//     name: "Perpetuals".to_string(),
-//     version: "v0".to_string(),
-//     chain_id: "SN_SEPOLIA".to_string(),
-//     revision: 1,
-// });
-// 
-// pub struct WithdrawalArgs {
-//     pub recipient: Felt,
-//     pub position_id: PositionId,
-//     pub collateral_id: AssetId,
-//     pub amount: u64,
-//     pub expiration: Timestamp,
-//     pub salt: Felt,
-// }
-// 
-// impl Hashable for WithdrawalArgs {
-//     const SELECTOR: Felt = selector!( "\"WithdrawArgs\"(\"recipient\":\"ContractAddress\",\"position_id\":\"PositionId\",\"collateral_id\":\"AssetId\",\"amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
-//     fn hash(&self) -> Felt {
-//         let mut hasher = PoseidonHasher::new();
-//         hasher.update(Self::SELECTOR);
-//         hasher.update(self.recipient);
-//         hasher.update(self.position_id.value.into());
-//         hasher.update(self.collateral_id.value.into());
-//         hasher.update(self.amount.into());
-//         hasher.update(self.expiration.seconds.into());
-//         hasher.update(self.salt);
-//         hasher.finalize()
-//     }
-// }
-// 
-// impl OffChainMessage for WithdrawalArgs {}
-// 
-// #[cfg(test)]
-// mod tests {
-//     use starknet::macros::{felt, felt_hex};
-// 
-//     use super::*;
-// 
-//     #[test]
-//     fn test_starknet_domain_selector() {
-//         let expected = Felt::from_hex_unchecked(
-//             "0x1ff2f602e42168014d405a94f75e8a93d640751d71d16311266e140d8b0a210",
-//         );
-//         let actual = StarknetDomain::SELECTOR;
-//         assert_eq!(expected, actual);
-//     }
-// 
-//     #[test]
-//     fn test_starknet_domain_hashing() {
-//         let domain = StarknetDomain {
-//             name: "Perpetuals".to_string(),
-//             version: "v0".to_string(),
-//             chain_id: "SN_SEPOLIA".to_string(),
-//             revision: 1,
-//         };
-// 
-//         let actual = domain.hash();
-//         let expected =
-//             felt!("2788850828067604540663615870177667078542240404906059806659101905868929188327");
-//         assert_eq!(actual, expected, "Hashes do not match for StarknetDomain");
-//     }
-// 
-//     #[test]
-//     fn test_order_selector() {
-//         let expected = Felt::from_hex_unchecked(
-//             "0x36da8d51815527cabfaa9c982f564c80fa7429616739306036f1f9b608dd112",
-//         );
-//         let actual = Order::SELECTOR;
-//         assert_eq!(expected, actual);
-//     }
-// 
-//     #[test]
-//     fn test_transfer_args_selector() {
-//         let expected = Felt::from_hex_unchecked(
-//             "0x1db88e2709fdf2c59e651d141c3296a42b209ce770871b40413ea109846a3b4",
-//         );
-//         let actual = TransferArgs::SELECTOR;
-//         assert_eq!(expected, actual);
-//     }
-// 
-//     #[test]
-//     fn test_transfer_args_hashing() {
-//         let transfer_args = TransferArgs {
-//             recipient: PositionId { value: 1 },
-//             position_id: PositionId { value: 2 },
-//             collateral_id: AssetId {
-//                 value: Felt::from_dec_str("3").unwrap(),
-//             },
-//             amount: 4,
-//             expiration: Timestamp { seconds: 5 },
-//             salt: Felt::from_dec_str("6").unwrap(),
-//         };
-// 
-//         let actual = transfer_args.hash();
-//         let expected = Felt::from_dec_str(
-//             "2223969487713427665389808888239017784545324676732964616876966103908214316949",
-//         )
-//         .unwrap();
-//         assert_eq!(actual, expected, "Hashes do not match for TransferArgs");
-//     }
-// 
-//     #[test]
-//     fn test_message_hash_transfer() {
-//         let transfer_args = TransferArgs {
-//             recipient: PositionId { value: 1 },
-//             position_id: PositionId { value: 2 },
-//             collateral_id: AssetId {
-//                 value: Felt::from_dec_str("3").unwrap(),
-//             },
-//             amount: 4,
-//             expiration: Timestamp { seconds: 5 },
-//             salt: Felt::from_dec_str("6").unwrap(),
-//         };
-// 
-//         let user_key = Felt::from_dec_str(
-//             "2629686405885377265612250192330550814166101744721025672593857097107510831364",
-//         )
-//         .unwrap();
-// 
-//         let actual = transfer_args
-//             .message_hash(&SEPOLIA_DOMAIN, user_key)
-//             .unwrap();
-// 
-//         let expected =
-//             felt_hex!("0x56c7b21d13b79a33d7700dda20e22246c25e89818249504148174f527fc3f8f");
-//         assert_eq!(actual, expected, "Hashes do not match for TransferArgs");
-//     }
-// 
-//     #[test]
-//     fn test_order_hashing() {
-//         let order = Order {
-//             position_id: PositionId { value: 1 },
-//             base_asset_id: AssetId {
-//                 value: Felt::from_dec_str("2").unwrap(),
-//             },
-//             base_amount: 3,
-//             quote_asset_id: AssetId {
-//                 value: Felt::from_dec_str("4").unwrap(),
-//             },
-//             quote_amount: 5,
-//             fee_asset_id: AssetId {
-//                 value: Felt::from_dec_str("6").unwrap(),
-//             },
-//             fee_amount: 7,
-//             expiration: Timestamp { seconds: 8 },
-//             salt: Felt::from_dec_str("9").unwrap(),
-//         };
-// 
-//         let actual = order.hash();
-//         let expected = Felt::from_dec_str(
-//             "1329353150252109345267997901008558234696410103652961347079636617692652241760",
-//         )
-//         .unwrap();
-//         assert_eq!(actual, expected, "Hashes do not match for Order");
-//     }
-// 
-//     #[test]
-//     fn test_message_hash_order() {
-//         let order = Order {
-//             position_id: PositionId { value: 1 },
-//             base_asset_id: AssetId {
-//                 value: Felt::from_dec_str("2").unwrap(),
-//             },
-//             base_amount: 3,
-//             quote_asset_id: AssetId {
-//                 value: Felt::from_dec_str("4").unwrap(),
-//             },
-//             quote_amount: 5,
-//             fee_asset_id: AssetId {
-//                 value: Felt::from_dec_str("6").unwrap(),
-//             },
-//             fee_amount: 7,
-//             expiration: Timestamp { seconds: 8 },
-//             salt: Felt::from_dec_str("9").unwrap(),
-//         };
-// 
-//         let user_key = Felt::from_dec_str(
-//             "1528491859474308181214583355362479091084733880193869257167008343298409336538",
-//         )
-//         .unwrap();
-// 
-//         let hash = order.message_hash(&SEPOLIA_DOMAIN, user_key).unwrap();
-//         let expected_hash = Felt::from_dec_str(
-//             "2788960362996410178586013462192086205585543858281504820767681025777602529597",
-//         )
-//         .unwrap();
-//         println!("{}", expected_hash.to_hex_string());
-//         assert_eq!(hash, expected_hash);
-//     }
-// 
-//     #[test]
-//     fn test_withdrawal_args_selector() {
-//         let expected = Felt::from_hex_unchecked(
-//             "0x250a5fa378e8b771654bd43dcb34844534f9d1e29e16b14760d7936ea7f4b1d",
-//         );
-//         let actual = WithdrawalArgs::SELECTOR;
-//         assert_eq!(expected, actual);
-//     }
-// 
-//     #[test]
-//     fn test_withdrawal_args_hashing() {
-//         let withdrawal_args = WithdrawalArgs {
-//             recipient: Felt::from_hex(
-//                 "0x019ec96d4aea6fdc6f0b5f393fec3f186aefa8f0b8356f43d07b921ff48aa5da",
-//             )
-//             .unwrap(),
-//             position_id: PositionId { value: 1 },
-//             collateral_id: AssetId {
-//                 value: Felt::from_dec_str("4").unwrap(),
-//             },
-//             amount: 1000,
-//             expiration: Timestamp { seconds: 5 },
-//             salt: Felt::from_dec_str("123").unwrap(),
-//         };
-// 
-//         let actual = withdrawal_args.hash();
-//         let expected =
-//             Felt::from_hex("0x04c22f625c59651e1219c60d03055f11f5dc23959929de35861548d86c0bc4ec")
-//                 .unwrap();
-//         assert_eq!(actual, expected, "Hashes do not match for WithdrawalArgs");
-//     }
-// }
+use starknet::core::utils::cairo_short_string_to_felt;
+
+use starknet::core::types::Felt;
+use starknet::macros::selector;
+use starknet_crypto::PoseidonHasher;
+
+use std::sync::LazyLock;
+
+static MESSAGE_FELT: LazyLock<Felt> =
+    LazyLock::new(|| cairo_short_string_to_felt("StarkNet Message").unwrap());
+
+pub trait Hashable {
+    const SELECTOR: Felt;
+    fn hash(&self) -> Felt;
+}
+
+pub trait OffChainMessage: Hashable {
+    fn message_hash(
+        &self,
+        stark_domain: &StarknetDomain,
+        public_key: Felt,
+    ) -> Result<Felt, String> {
+        let mut hasher = PoseidonHasher::new();
+        hasher.update(*MESSAGE_FELT);
+        hasher.update(stark_domain.hash());
+        hasher.update(public_key);
+        hasher.update(self.hash());
+        Ok(hasher.finalize())
+    }
+}
+
+pub struct StarknetDomain {
+    pub name: String,
+    pub version: String,
+    pub chain_id: String,
+    pub revision: u32,
+}
+
+impl Hashable for StarknetDomain {
+    const SELECTOR: Felt = selector!("\"StarknetDomain\"(\"name\":\"shortstring\",\"version\":\"shortstring\",\"chainId\":\"shortstring\",\"revision\":\"shortstring\")");
+    fn hash(&self) -> Felt {
+        let mut hasher = PoseidonHasher::new();
+        hasher.update(Self::SELECTOR);
+        hasher.update(cairo_short_string_to_felt(&self.name).unwrap());
+        hasher.update(cairo_short_string_to_felt(&self.version).unwrap());
+        hasher.update(cairo_short_string_to_felt(&self.chain_id).unwrap());
+        hasher.update(self.revision.into());
+        let hash = hasher.finalize();
+        return hash;
+    }
+}
+
+pub struct AssetId {
+    pub value: Felt,
+}
+pub struct PositionId {
+    pub value: u32,
+}
+
+pub struct AssetAmount {
+    pub asset_id: AssetId,
+    pub amount: i64,
+}
+
+pub struct Timestamp {
+    pub seconds: u64,
+}
+
+pub struct Order {
+    pub position_id: PositionId,
+    pub base_asset_id: AssetId,
+    pub base_amount: i64,
+    pub quote_asset_id: AssetId,
+    pub quote_amount: i64,
+    pub fee_asset_id: AssetId,
+    pub fee_amount: u64,
+    pub expiration: Timestamp,
+    pub salt: Felt,
+}
+
+impl Hashable for Order {
+    const SELECTOR: Felt = selector!("\"Order\"(\"position_id\":\"felt\",\"base_asset_id\":\"AssetId\",\"base_amount\":\"i64\",\"quote_asset_id\":\"AssetId\",\"quote_amount\":\"i64\",\"fee_asset_id\":\"AssetId\",\"fee_amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
+    fn hash(&self) -> Felt {
+        let mut hasher = PoseidonHasher::new();
+        hasher.update(Self::SELECTOR);
+        hasher.update(self.position_id.value.into());
+        hasher.update(self.base_asset_id.value.into());
+        hasher.update(self.base_amount.into());
+        hasher.update(self.quote_asset_id.value.into());
+        hasher.update(self.quote_amount.into());
+        hasher.update(self.fee_asset_id.value.into());
+        hasher.update(self.fee_amount.into());
+        hasher.update(self.expiration.seconds.into());
+        hasher.update(self.salt);
+        hasher.finalize()
+    }
+}
+impl OffChainMessage for Order {}
+
+pub struct TransferArgs {
+    pub recipient: PositionId,
+    pub position_id: PositionId,
+    pub collateral_id: AssetId,
+    pub amount: u64,
+    pub expiration: Timestamp,
+    pub salt: Felt,
+}
+
+impl Hashable for TransferArgs {
+    const SELECTOR: Felt = selector!("\"TransferArgs\"(\"recipient\":\"PositionId\",\"position_id\":\"PositionId\",\"collateral_id\":\"AssetId\",\"amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
+    fn hash(&self) -> Felt {
+        let mut hasher = PoseidonHasher::new();
+        hasher.update(Self::SELECTOR);
+        hasher.update(self.recipient.value.into());
+        hasher.update(self.position_id.value.into());
+        hasher.update(self.collateral_id.value.into());
+        hasher.update(self.amount.into());
+        hasher.update(self.expiration.seconds.into());
+        hasher.update(self.salt);
+        hasher.finalize()
+    }
+}
+
+impl OffChainMessage for TransferArgs {}
+
+pub static SEPOLIA_DOMAIN: LazyLock<StarknetDomain> = LazyLock::new(|| StarknetDomain {
+    name: "Perpetuals".to_string(),
+    version: "v0".to_string(),
+    chain_id: "SN_SEPOLIA".to_string(),
+    revision: 1,
+});
+
+pub struct WithdrawalArgs {
+    pub recipient: Felt,
+    pub position_id: PositionId,
+    pub collateral_id: AssetId,
+    pub amount: u64,
+    pub expiration: Timestamp,
+    pub salt: Felt,
+}
+
+impl Hashable for WithdrawalArgs {
+    const SELECTOR: Felt = selector!( "\"WithdrawArgs\"(\"recipient\":\"ContractAddress\",\"position_id\":\"PositionId\",\"collateral_id\":\"AssetId\",\"amount\":\"u64\",\"expiration\":\"Timestamp\",\"salt\":\"felt\")\"PositionId\"(\"value\":\"u32\")\"AssetId\"(\"value\":\"felt\")\"Timestamp\"(\"seconds\":\"u64\")");
+    fn hash(&self) -> Felt {
+        let mut hasher = PoseidonHasher::new();
+        hasher.update(Self::SELECTOR);
+        hasher.update(self.recipient);
+        hasher.update(self.position_id.value.into());
+        hasher.update(self.collateral_id.value.into());
+        hasher.update(self.amount.into());
+        hasher.update(self.expiration.seconds.into());
+        hasher.update(self.salt);
+        hasher.finalize()
+    }
+}
+
+impl OffChainMessage for WithdrawalArgs {}
+
+#[cfg(test)]
+mod tests {
+    use starknet::macros::{felt, felt_hex};
+
+    use super::*;
+
+    #[test]
+    fn test_starknet_domain_selector() {
+        let expected = Felt::from_hex_unchecked(
+            "0x1ff2f602e42168014d405a94f75e8a93d640751d71d16311266e140d8b0a210",
+        );
+        let actual = StarknetDomain::SELECTOR;
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_starknet_domain_hashing() {
+        let domain = StarknetDomain {
+            name: "Perpetuals".to_string(),
+            version: "v0".to_string(),
+            chain_id: "SN_SEPOLIA".to_string(),
+            revision: 1,
+        };
+
+        let actual = domain.hash();
+        let expected =
+            felt!("2788850828067604540663615870177667078542240404906059806659101905868929188327");
+        assert_eq!(actual, expected, "Hashes do not match for StarknetDomain");
+    }
+
+    #[test]
+    fn test_order_selector() {
+        let expected = Felt::from_hex_unchecked(
+            "0x36da8d51815527cabfaa9c982f564c80fa7429616739306036f1f9b608dd112",
+        );
+        let actual = Order::SELECTOR;
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_transfer_args_selector() {
+        let expected = Felt::from_hex_unchecked(
+            "0x1db88e2709fdf2c59e651d141c3296a42b209ce770871b40413ea109846a3b4",
+        );
+        let actual = TransferArgs::SELECTOR;
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_transfer_args_hashing() {
+        let transfer_args = TransferArgs {
+            recipient: PositionId { value: 1 },
+            position_id: PositionId { value: 2 },
+            collateral_id: AssetId {
+                value: Felt::from_dec_str("3").unwrap(),
+            },
+            amount: 4,
+            expiration: Timestamp { seconds: 5 },
+            salt: Felt::from_dec_str("6").unwrap(),
+        };
+
+        let actual = transfer_args.hash();
+        let expected = Felt::from_dec_str(
+            "2223969487713427665389808888239017784545324676732964616876966103908214316949",
+        )
+        .unwrap();
+        assert_eq!(actual, expected, "Hashes do not match for TransferArgs");
+    }
+
+    #[test]
+    fn test_message_hash_transfer() {
+        let transfer_args = TransferArgs {
+            recipient: PositionId { value: 1 },
+            position_id: PositionId { value: 2 },
+            collateral_id: AssetId {
+                value: Felt::from_dec_str("3").unwrap(),
+            },
+            amount: 4,
+            expiration: Timestamp { seconds: 5 },
+            salt: Felt::from_dec_str("6").unwrap(),
+        };
+
+        let user_key = Felt::from_dec_str(
+            "2629686405885377265612250192330550814166101744721025672593857097107510831364",
+        )
+        .unwrap();
+
+        let actual = transfer_args
+            .message_hash(&SEPOLIA_DOMAIN, user_key)
+            .unwrap();
+
+        let expected =
+            felt_hex!("0x56c7b21d13b79a33d7700dda20e22246c25e89818249504148174f527fc3f8f");
+        assert_eq!(actual, expected, "Hashes do not match for TransferArgs");
+    }
+
+    #[test]
+    fn test_order_hashing() {
+        let order = Order {
+            position_id: PositionId { value: 1 },
+            base_asset_id: AssetId {
+                value: Felt::from_dec_str("2").unwrap(),
+            },
+            base_amount: 3,
+            quote_asset_id: AssetId {
+                value: Felt::from_dec_str("4").unwrap(),
+            },
+            quote_amount: 5,
+            fee_asset_id: AssetId {
+                value: Felt::from_dec_str("6").unwrap(),
+            },
+            fee_amount: 7,
+            expiration: Timestamp { seconds: 8 },
+            // salt (nonce)
+            salt: Felt::from_dec_str("9").unwrap(),
+        };
+
+        let actual = order.hash();
+        let expected = Felt::from_dec_str(
+            "1329353150252109345267997901008558234696410103652961347079636617692652241760",
+        )
+        .unwrap();
+        assert_eq!(actual, expected, "Hashes do not match for Order");
+    }
+
+    #[test]
+    fn test_message_hash_order() {
+        let order = Order {
+            position_id: PositionId { value: 1 },
+            base_asset_id: AssetId {
+                value: Felt::from_dec_str("2").unwrap(),
+            },
+            base_amount: 3,
+            quote_asset_id: AssetId {
+                value: Felt::from_dec_str("4").unwrap(),
+            },
+            quote_amount: 5,
+            fee_asset_id: AssetId {
+                value: Felt::from_dec_str("6").unwrap(),
+            },
+            fee_amount: 7,
+            expiration: Timestamp { seconds: 8 },
+            salt: Felt::from_dec_str("9").unwrap(),
+        };
+
+        let user_key = Felt::from_dec_str(
+            "1528491859474308181214583355362479091084733880193869257167008343298409336538",
+        )
+        .unwrap();
+
+        let hash = order.message_hash(&SEPOLIA_DOMAIN, user_key).unwrap();
+        let expected_hash = Felt::from_dec_str(
+            "2788960362996410178586013462192086205585543858281504820767681025777602529597",
+        )
+        .unwrap();
+        println!("{}", expected_hash.to_hex_string());
+        assert_eq!(hash, expected_hash);
+    }
+
+    #[test]
+    fn test_withdrawal_args_selector() {
+        let expected = Felt::from_hex_unchecked(
+            "0x250a5fa378e8b771654bd43dcb34844534f9d1e29e16b14760d7936ea7f4b1d",
+        );
+        let actual = WithdrawalArgs::SELECTOR;
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_withdrawal_args_hashing() {
+        let withdrawal_args = WithdrawalArgs {
+            recipient: Felt::from_hex(
+                "0x019ec96d4aea6fdc6f0b5f393fec3f186aefa8f0b8356f43d07b921ff48aa5da",
+            )
+            .unwrap(),
+            position_id: PositionId { value: 1 },
+            collateral_id: AssetId {
+                value: Felt::from_dec_str("4").unwrap(),
+            },
+            amount: 1000,
+            expiration: Timestamp { seconds: 5 },
+            salt: Felt::from_dec_str("123").unwrap(),
+        };
+
+        let actual = withdrawal_args.hash();
+        let expected =
+            Felt::from_hex("0x04c22f625c59651e1219c60d03055f11f5dc23959929de35861548d86c0bc4ec")
+                .unwrap();
+        assert_eq!(actual, expected, "Hashes do not match for WithdrawalArgs");
+    }
+}