Merge pull request #70 from floor-licker/fix/fail-fast-api-credentials

fix: fail fast on invalid api credentials
This commit is contained in:
floor-licker
2026-06-22 21:21:40 -04:00
committed by GitHub
4 changed files with 58 additions and 20 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ fn benchmark_order_submit_payload_auth(c: &mut Criterion) {
secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(), secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
passphrase: "benchmark-passphrase".to_string(), passphrase: "benchmark-passphrase".to_string(),
}; };
let prepared_api_creds = PreparedApiCredentials::new(api_creds.clone()); let prepared_api_creds = PreparedApiCredentials::try_new(api_creds.clone()).unwrap();
c.bench_function("order_submit_body_and_l2_headers", |b| { c.bench_function("order_submit_body_and_l2_headers", |b| {
b.iter(|| { b.iter(|| {
+1 -1
View File
@@ -128,7 +128,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with API credentials only (no private key needed for custodial trading) // Create client with API credentials only (no private key needed for custodial trading)
let mut client = ClobClient::new("https://clob.polymarket.com"); let mut client = ClobClient::new("https://clob.polymarket.com");
client.set_api_creds(api_creds); client.set_api_creds(api_creds)?;
println!("✅ Client configured for custodial API trading"); println!("✅ Client configured for custodial API trading");
+6 -9
View File
@@ -38,20 +38,20 @@ pub trait HmacApiCredentials {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PreparedApiCredentials { pub struct PreparedApiCredentials {
credentials: ApiCredentials, credentials: ApiCredentials,
decoded_secret: std::result::Result<Arc<[u8]>, String>, decoded_secret: Arc<[u8]>,
} }
impl PreparedApiCredentials { impl PreparedApiCredentials {
pub fn new(credentials: ApiCredentials) -> Self { pub fn try_new(credentials: ApiCredentials) -> Result<Self> {
let decoded_secret = base64::engine::general_purpose::URL_SAFE let decoded_secret = base64::engine::general_purpose::URL_SAFE
.decode(&credentials.secret) .decode(&credentials.secret)
.map(Into::into) .map(Into::into)
.map_err(|e| format!("Failed to decode base64 secret: {}", e)); .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {e}")))?;
Self { Ok(Self {
credentials, credentials,
decoded_secret, decoded_secret,
} })
} }
pub fn credentials(&self) -> &ApiCredentials { pub fn credentials(&self) -> &ApiCredentials {
@@ -91,10 +91,7 @@ impl HmacApiCredentials for PreparedApiCredentials {
} }
fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> { fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> {
match &self.decoded_secret { Ok(Cow::Borrowed(self.decoded_secret.as_ref()))
Ok(decoded_secret) => Ok(Cow::Borrowed(decoded_secret.as_ref())),
Err(err) => Err(PolyfillError::crypto(err.clone())),
}
} }
} }
+50 -9
View File
@@ -124,7 +124,7 @@ pub struct ClobClient {
#[derive(Default)] #[derive(Default)]
struct ClientAuthConfig { struct ClientAuthConfig {
signer: Option<PrivateKeySigner>, signer: Option<PrivateKeySigner>,
api_creds: Option<ApiCreds>, api_creds: Option<PreparedApiCredentials>,
builder_code: Option<String>, builder_code: Option<String>,
sig_type: Option<crate::orders::SigType>, sig_type: Option<crate::orders::SigType>,
funder: Option<Address>, funder: Option<Address>,
@@ -162,14 +162,12 @@ impl ClobClient {
.clone() .clone()
.map(|signer| crate::orders::OrderBuilder::new(signer, auth.sig_type, auth.funder)); .map(|signer| crate::orders::OrderBuilder::new(signer, auth.sig_type, auth.funder));
let api_creds = auth.api_creds.map(PreparedApiCredentials::new);
Self { Self {
http_client, http_client,
base_url: host.to_string(), base_url: host.to_string(),
chain_id, chain_id,
signer: auth.signer, signer: auth.signer,
api_creds, api_creds: auth.api_creds,
builder_code: auth.builder_code, builder_code: auth.builder_code,
order_builder, order_builder,
connection_manager, connection_manager,
@@ -223,7 +221,10 @@ impl ClobClient {
http_client, http_client,
ClientAuthConfig { ClientAuthConfig {
signer, signer,
api_creds: config.api_credentials, api_creds: config
.api_credentials
.map(PreparedApiCredentials::try_new)
.transpose()?,
builder_code: config.builder_code, builder_code: config.builder_code,
sig_type, sig_type,
funder, funder,
@@ -284,8 +285,9 @@ impl ClobClient {
} }
/// Set API credentials /// Set API credentials
pub fn set_api_creds(&mut self, api_creds: ApiCreds) { pub fn set_api_creds(&mut self, api_creds: ApiCreds) -> Result<()> {
self.api_creds = Some(PreparedApiCredentials::new(api_creds)); self.api_creds = Some(PreparedApiCredentials::try_new(api_creds)?);
Ok(())
} }
/// Start background keep-alive to maintain warm connection /// Start background keep-alive to maintain warm connection
@@ -2637,15 +2639,54 @@ mod tests {
let api_creds = ApiCredentials { let api_creds = ApiCredentials {
api_key: "test_key".to_string(), api_key: "test_key".to_string(),
secret: "test_secret".to_string(), secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
passphrase: "test_passphrase".to_string(), passphrase: "test_passphrase".to_string(),
}; };
client.set_api_creds(api_creds.clone()); client.set_api_creds(api_creds.clone()).unwrap();
assert!(client.api_creds.is_some()); assert!(client.api_creds.is_some());
assert_eq!(client.api_creds.unwrap().api_key, "test_key"); assert_eq!(client.api_creds.unwrap().api_key, "test_key");
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_from_config_rejects_invalid_api_secret() {
let api_creds = ApiCredentials {
api_key: "test_key".to_string(),
secret: "not valid base64!".to_string(),
passphrase: "test_passphrase".to_string(),
};
let err = match ClobClient::from_config(ClientConfig {
base_url: "https://test.example.com".to_string(),
chain: 137,
private_key: Some(
"0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
),
api_credentials: Some(api_creds),
..ClientConfig::default()
}) {
Ok(_) => panic!("expected invalid API credentials to fail"),
Err(err) => err,
};
assert!(err.to_string().contains("Failed to decode base64 secret"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_set_api_creds_rejects_invalid_api_secret() {
let mut client = create_test_client("https://test.example.com");
let api_creds = ApiCredentials {
api_key: "test_key".to_string(),
secret: "not valid base64!".to_string(),
passphrase: "test_passphrase".to_string(),
};
let err = client.set_api_creds(api_creds).unwrap_err();
assert!(client.api_creds.is_none());
assert!(err.to_string().contains("Failed to decode base64 secret"));
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_get_sampling_markets_success() { async fn test_get_sampling_markets_success() {
let mut server = Server::new_async().await; let mut server = Server::new_async().await;