Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ chrono = { version = "0.4", features = ["serde"] }

# Utilities
dirs = "6.0"
uuid = { version = "1.7", features = ["v4"] }

# Output formatting
tabled = "0.18"
2 changes: 1 addition & 1 deletion crates/standx-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ chrono.workspace = true
dirs.workspace = true
rpassword = "7.3"
regex = "1.10"
uuid = { version = "1.7", features = ["v4"] }
uuid.workspace = true

[dev-dependencies]
tokio-test = "0.4"
Expand Down
1 change: 1 addition & 0 deletions crates/standx-cli/src/commands/maker/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ pub(super) async fn maker_cycle(
match client
.create_order(CreateOrderParams {
symbol: symbol.to_string(),
cl_ord_id: None,
side: q.side,
order_type: OrderType::Limit,
quantity: format_decimals(q.qty, cfg.qty_decimals),
Expand Down
1 change: 1 addition & 0 deletions crates/standx-cli/src/commands/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub async fn handle_order(command: OrderCommands) -> Result<()> {

let params = CreateOrderParams {
symbol,
cl_ord_id: None,
side,
order_type,
quantity: qty,
Expand Down
1 change: 1 addition & 0 deletions crates/standx-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ chrono.workspace = true

# Utilities
dirs.workspace = true
uuid.workspace = true
hex = "0.4"
base64 = "0.22"

Expand Down
54 changes: 37 additions & 17 deletions crates/standx-sdk/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub use credentials::Credentials;
pub struct StandXSigner {
signing_key: SigningKey,
verifying_key: VerifyingKey,
request_id: String,
}

/// Request signature headers
Expand Down Expand Up @@ -41,41 +40,48 @@ impl StandXSigner {
})?);

let verifying_key = signing_key.verifying_key();
let request_id = hex::encode(verifying_key.as_bytes());

Ok(Self {
signing_key,
verifying_key,
request_id,
})
}

/// Get the request ID (hex-encoded public key)
pub fn request_id(&self) -> &str {
&self.request_id
}

/// Get the public key in hex format
pub fn pubkey_hex(&self) -> String {
hex::encode(self.verifying_key.as_bytes())
}

/// Sign a request
pub fn sign_request(&self, timestamp: u64, payload: &str) -> RequestSignature {
/// Sign a request with a caller-provided request ID.
///
/// This is useful for deterministic tests and for protocols that need to
/// correlate an externally-created UUID with an asynchronous response.
pub fn sign_request_with_id(
&self,
request_id: &str,
timestamp: u64,
payload: &str,
) -> RequestSignature {
let version = "v1";
let message = format!("{},{},{},{}", version, self.request_id, timestamp, payload);
let message = format!("{version},{request_id},{timestamp},{payload}");

let signature = self.signing_key.sign(message.as_bytes());

RequestSignature {
version: version.to_string(),
request_id: self.request_id.clone(),
request_id: request_id.to_string(),
timestamp,
signature: STANDARD.encode(signature.to_bytes()),
pubkey: self.pubkey_hex(),
}
}

/// Sign a request with a fresh UUID request ID.
pub fn sign_request(&self, timestamp: u64, payload: &str) -> RequestSignature {
let request_id = uuid::Uuid::new_v4().to_string();
self.sign_request_with_id(&request_id, timestamp, payload)
}

/// Sign a request with the current timestamp
pub fn sign_request_now(&self, payload: &str) -> RequestSignature {
let timestamp = chrono::Utc::now().timestamp_millis() as u64;
Expand All @@ -96,7 +102,6 @@ mod tests {

let signer = StandXSigner::from_base58(&private_key_base58).unwrap();

assert!(!signer.request_id().is_empty());
assert!(!signer.pubkey_hex().is_empty());
}

Expand All @@ -115,7 +120,7 @@ mod tests {
assert_eq!(sig.timestamp, 1700000000000);
assert!(!sig.signature.is_empty());
assert!(!sig.pubkey.is_empty());
assert_eq!(sig.request_id, signer.request_id());
assert!(uuid::Uuid::parse_str(&sig.request_id).is_ok());
}

#[test]
Expand Down Expand Up @@ -181,9 +186,10 @@ mod tests {
let payload = r#"{"symbol":"BTC-USD","side":"buy"}"#;
let timestamp = 1700000000000u64;

// Sign same payload twice
let sig1 = signer.sign_request(timestamp, payload);
let sig2 = signer.sign_request(timestamp, payload);
// Sign the same request twice with a fixed correlation ID.
let request_id = "6f071beb-1b81-4c68-a8bf-66f1589c2146";
let sig1 = signer.sign_request_with_id(request_id, timestamp, payload);
let sig2 = signer.sign_request_with_id(request_id, timestamp, payload);

// Same signer should produce same request_id and pubkey
assert_eq!(sig1.request_id, sig2.request_id);
Expand All @@ -192,4 +198,18 @@ mod tests {
// Ed25519 is deterministic, so signatures should be identical
assert_eq!(sig1.signature, sig2.signature);
}

#[test]
fn test_sign_request_uses_unique_uuid() {
let signing_key = SigningKey::generate(&mut rand::thread_rng());
let private_key_base58 = bs58::encode(signing_key.to_bytes()).into_string();
let signer = StandXSigner::from_base58(&private_key_base58).unwrap();

let sig1 = signer.sign_request(1700000000000, "{}");
let sig2 = signer.sign_request(1700000000000, "{}");

assert_ne!(sig1.request_id, sig2.request_id);
assert!(uuid::Uuid::parse_str(&sig1.request_id).is_ok());
assert!(uuid::Uuid::parse_str(&sig2.request_id).is_ok());
}
}
Loading
Loading