1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// US-002: Credentials struct
use alloy_primitives::Address;
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::Eip712Domain;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::rest::error::AsterDexError;
/// SDK credentials — holds wallet addresses and EIP-712 signing key.
/// No Debug derive — private key must NEVER be logged.
pub struct Credentials {
pub user: Address,
pub signer: Address,
signer_wallet: PrivateKeySigner,
/// Last issued nonce (microseconds). Lock-free; guarantees strict monotonicity
/// even under clock skew / NTP rollback.
last_nonce: AtomicU64,
/// Pre-computed "0x…" string for `user` — avoids one heap alloc per signed request.
user_str: String,
/// Pre-computed "0x…" string for `signer` — avoids one heap alloc per signed request.
signer_str: String,
/// Cached EIP-712 domain — built once at construction, reused on every signature.
domain: Eip712Domain,
}
impl Credentials {
/// Create from explicit string values
pub fn new(
user: &str,
signer: &str,
private_key_hex: &str,
chain_id: u64,
) -> Result<Self, AsterDexError> {
// Strip 0x prefix if present
let key_hex = private_key_hex.trim_start_matches("0x");
let wallet = key_hex.parse::<PrivateKeySigner>().map_err(|e| {
AsterDexError::ConfigError {
message: format!("invalid signer private key: {e}"),
}
})?;
let user_addr = user.parse::<Address>().map_err(|e| {
AsterDexError::ConfigError {
message: format!("invalid user address: {e}"),
}
})?;
let signer_addr = signer.parse::<Address>().map_err(|e| {
AsterDexError::ConfigError {
message: format!("invalid signer address: {e}"),
}
})?;
// Pre-compute 0x-prefixed lowercase hex strings once.
let user_str = format!("{:?}", user_addr);
let signer_str = format!("{:?}", signer_addr);
// Build EIP-712 domain once; reused on every sign_query call.
let domain = crate::auth::eip712::get_domain(chain_id);
Ok(Self {
user: user_addr,
signer: signer_addr,
signer_wallet: wallet,
last_nonce: AtomicU64::new(0),
user_str,
signer_str,
domain,
})
}
/// Create from environment variables
pub fn from_env() -> Result<Self, AsterDexError> {
let user = std::env::var("ASTERDEX_USER").map_err(|_| AsterDexError::ConfigError {
message: "missing env var: ASTERDEX_USER".to_string(),
})?;
let signer =
std::env::var("ASTERDEX_SIGNER_ADDRESS").map_err(|_| AsterDexError::ConfigError {
message: "missing env var: ASTERDEX_SIGNER_ADDRESS".to_string(),
})?;
let key = std::env::var("ASTERDEX_SIGNER_PRIVATE_KEY").map_err(|_| {
AsterDexError::ConfigError {
message: "missing env var: ASTERDEX_SIGNER_PRIVATE_KEY".to_string(),
}
})?;
let chain_id = match std::env::var("ASTERDEX_CHAIN_ID") {
Ok(s) => s.parse::<u64>().map_err(|_| AsterDexError::ConfigError {
message: format!("ASTERDEX_CHAIN_ID is not a valid integer: {s}"),
})?,
Err(_) => 1666, // mainnet default
};
Self::new(&user, &signer, &key, chain_id)
}
/// Generate a strictly monotonically increasing nonce.
///
/// Format follows BR-001 / FORMULA-001: `unix_seconds * 1_000_000 + sub_second_index`.
///
/// Implementation notes:
/// - Lock-free via `AtomicU64::compare_exchange` → never blocks other async tasks.
/// - If the wall clock jumps backwards (NTP rollback, VM pause/restore), we still
/// return `prev + 1` to preserve strict monotonicity.
/// - If more than 1_000_000 nonces are requested within one wall-clock second,
/// nonces simply continue counting into the next "virtual second" — still
/// strictly monotonic, still uniquely identifying the request.
/// - Never panics (vs. previous `.expect("time before epoch")`).
pub fn new_nonce(&self) -> u64 {
let base = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros() as u64)
.unwrap_or(0);
loop {
let prev = self.last_nonce.load(Ordering::Relaxed);
let next = base.max(prev.saturating_add(1));
match self.last_nonce.compare_exchange(
prev,
next,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return next,
Err(_) => continue,
}
}
}
/// URL-encode params, add auth fields, sign with EIP-712.
/// Returns the complete signed query string with `&signature=<hex>` appended.
///
/// Optimisations vs. the previous implementation:
/// - Address strings pre-computed at construction — no per-call format allocation.
/// - Param string built in a single `String` with `push_str` — no intermediate `Vec<String>`.
/// - EIP-712 domain cached at construction — no per-call struct rebuild.
/// - Nonce / user / signer values are all ASCII-unreserved — URL-encoding skipped.
pub fn sign_query(
&self,
params: &[(&str, &str)],
) -> Result<String, AsterDexError> {
let nonce = self.new_nonce();
let nonce_str = nonce.to_string();
// Pre-allocate a single buffer for the entire query string.
// Rough estimate: per-param ~64 bytes, plus 3 fixed auth fields + 130-char signature.
let capacity = params.len() * 64 + 256;
let mut param_string = String::with_capacity(capacity);
// Caller-supplied params (URL-encoded, values may contain special chars).
for (k, v) in params {
if !param_string.is_empty() {
param_string.push('&');
}
param_string.push_str(k);
param_string.push('=');
param_string.push_str(&urlencoding::encode(v));
}
// Auth fields — values contain only ASCII-unreserved characters (digits + hex),
// so URL-encoding is a no-op and we skip it.
if !param_string.is_empty() {
param_string.push('&');
}
param_string.push_str("nonce=");
param_string.push_str(&nonce_str);
param_string.push_str("&user=");
param_string.push_str(&self.user_str);
param_string.push_str("&signer=");
param_string.push_str(&self.signer_str);
// EIP-712 sign using the cached domain.
let hash = crate::auth::eip712::signing_hash(¶m_string, &self.domain);
let sig = self.signer_wallet.sign_hash_sync(&hash).map_err(|e| {
AsterDexError::ConfigError {
message: format!("signing failed: {e}"),
}
})?;
// Append signature as hex (130 chars, NO 0x prefix per FORMULA-001).
let sig_str = format!("{}", sig);
let sig_hex = sig_str.strip_prefix("0x").unwrap_or(&sig_str);
param_string.push_str("&signature=");
param_string.push_str(sig_hex);
Ok(param_string)
}
}