use crate::{
error::Error,
models::{
common::ApiErrorBody,
signing::{
SignMessageRequest, SignTransactionRequest, SignTransactionResponse,
SignTypedDataRequest, SignatureResponse,
},
token::TokenResponse,
transaction::{
AccelerateTxRequest, CancelTxRequest, CreateContractExecutionTxRequest,
CreateTransferTxRequest, EstimateFeeResponse, EstimateTransferFeeRequest,
ListTransactionsParams, TransactionResponse, Transactions, ValidateAddressRequest,
ValidateAddressResponse,
},
wallet::{
Balances, CreateWalletsRequest, ListWalletBalancesParams, ListWalletNftsParams,
ListWalletsParams, Nfts, UpdateWalletRequest, WalletNftsParams, WalletResponse,
Wallets, WalletsWithBalances,
},
wallet_set::{
CreateWalletSetRequest, ListWalletSetsParams, UpdateWalletSetRequest,
WalletSetResponse, WalletSets,
},
},
};
pub struct DeveloperWalletsClient {
base_url: String,
api_key: String,
http: hpx::Client,
}
impl std::fmt::Debug for DeveloperWalletsClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeveloperWalletsClient")
.field("base_url", &self.base_url)
.field("api_key", &"<redacted>")
.finish_non_exhaustive()
}
}
impl DeveloperWalletsClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, "https://api.circle.com")
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self { base_url: base_url.into(), api_key: api_key.into(), http: hpx::Client::new() }
}
async fn get<T, P>(&self, path: &str, params: &P) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
P: serde::Serialize + ?Sized,
{
let url = format!("{}{}", self.base_url, path);
let resp = self
.http
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("X-Request-Id", uuid::Uuid::new_v4().to_string())
.query(params)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if resp.status().is_success() {
resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
} else {
let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
Err(Error::Api { code: err.code, message: err.message })
}
}
async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
B: serde::Serialize + ?Sized,
{
let url = format!("{}{}", self.base_url, path);
let resp = self
.http
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("X-Request-Id", uuid::Uuid::new_v4().to_string())
.json(body)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if resp.status().is_success() {
resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
} else {
let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
Err(Error::Api { code: err.code, message: err.message })
}
}
async fn put<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
B: serde::Serialize + ?Sized,
{
let url = format!("{}{}", self.base_url, path);
let resp = self
.http
.put(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("X-Request-Id", uuid::Uuid::new_v4().to_string())
.json(body)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if resp.status().is_success() {
resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
} else {
let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
Err(Error::Api { code: err.code, message: err.message })
}
}
pub async fn create_wallet_set(
&self,
req: &CreateWalletSetRequest,
) -> Result<WalletSetResponse, Error> {
self.post("/v1/w3s/developer/walletSets", req).await
}
pub async fn get_wallet_set(&self, id: &str) -> Result<WalletSetResponse, Error> {
let path = format!("/v1/w3s/developer/walletSets/{}", id);
self.get(&path, &[("", "")][..0]).await
}
pub async fn update_wallet_set(
&self,
id: &str,
req: &UpdateWalletSetRequest,
) -> Result<WalletSetResponse, Error> {
let path = format!("/v1/w3s/developer/walletSets/{}", id);
self.put(&path, req).await
}
pub async fn list_wallet_sets(
&self,
params: &ListWalletSetsParams,
) -> Result<WalletSets, Error> {
self.get("/v1/w3s/walletSets", params).await
}
pub async fn create_wallets(&self, req: &CreateWalletsRequest) -> Result<Wallets, Error> {
self.post("/v1/w3s/developer/wallets", req).await
}
pub async fn list_wallets(&self, params: &ListWalletsParams) -> Result<Wallets, Error> {
self.get("/v1/w3s/wallets", params).await
}
pub async fn get_wallet(&self, id: &str) -> Result<WalletResponse, Error> {
let path = format!("/v1/w3s/wallets/{}", id);
self.get(&path, &[("", "")][..0]).await
}
pub async fn update_wallet(
&self,
id: &str,
req: &UpdateWalletRequest,
) -> Result<WalletResponse, Error> {
let path = format!("/v1/w3s/wallets/{}", id);
self.put(&path, req).await
}
pub async fn list_wallet_balances(
&self,
params: &ListWalletBalancesParams,
) -> Result<WalletsWithBalances, Error> {
self.get("/v1/w3s/developer/wallets/balances", params).await
}
pub async fn list_wallet_token_balances(
&self,
wallet_id: &str,
params: &WalletNftsParams,
) -> Result<Balances, Error> {
let path = format!("/v1/w3s/wallets/{}/balances", wallet_id);
self.get(&path, params).await
}
pub async fn list_wallet_nfts(
&self,
wallet_id: &str,
params: &ListWalletNftsParams,
) -> Result<Nfts, Error> {
let path = format!("/v1/w3s/wallets/{}/nfts", wallet_id);
self.get(&path, params).await
}
pub async fn sign_message(&self, req: &SignMessageRequest) -> Result<SignatureResponse, Error> {
self.post("/v1/w3s/developer/sign/message", req).await
}
pub async fn sign_typed_data(
&self,
req: &SignTypedDataRequest,
) -> Result<SignatureResponse, Error> {
self.post("/v1/w3s/developer/sign/typedData", req).await
}
pub async fn sign_transaction(
&self,
req: &SignTransactionRequest,
) -> Result<SignTransactionResponse, Error> {
self.post("/v1/w3s/developer/sign/transaction", req).await
}
pub async fn list_transactions(
&self,
params: &ListTransactionsParams,
) -> Result<Transactions, Error> {
self.get("/v1/w3s/transactions", params).await
}
pub async fn get_transaction(&self, id: &str) -> Result<TransactionResponse, Error> {
let path = format!("/v1/w3s/transactions/{}", id);
self.get(&path, &[("", "")][..0]).await
}
pub async fn create_transfer_transaction(
&self,
req: &CreateTransferTxRequest,
) -> Result<TransactionResponse, Error> {
self.post("/v1/w3s/developer/transactions/transfer", req).await
}
pub async fn get_fee_parameters(
&self,
req: &CreateTransferTxRequest,
) -> Result<EstimateFeeResponse, Error> {
self.post("/v1/w3s/developer/transactions/feeParameters", req).await
}
pub async fn create_contract_execution_transaction(
&self,
req: &CreateContractExecutionTxRequest,
) -> Result<TransactionResponse, Error> {
self.post("/v1/w3s/developer/transactions/contractExecution", req).await
}
pub async fn cancel_transaction(
&self,
id: &str,
req: &CancelTxRequest,
) -> Result<TransactionResponse, Error> {
let path = format!("/v1/w3s/developer/transactions/{}/cancel", id);
self.post(&path, req).await
}
pub async fn accelerate_transaction(
&self,
id: &str,
req: &AccelerateTxRequest,
) -> Result<TransactionResponse, Error> {
let path = format!("/v1/w3s/developer/transactions/{}/accelerate", id);
self.post(&path, req).await
}
pub async fn get_token(&self, id: &str) -> Result<TokenResponse, Error> {
let path = format!("/v1/w3s/tokens/{}", id);
self.get(&path, &[("", "")][..0]).await
}
pub async fn estimate_transfer_fee(
&self,
req: &EstimateTransferFeeRequest,
) -> Result<EstimateFeeResponse, Error> {
self.post("/v1/w3s/transactions/transfer/estimateFee", req).await
}
pub async fn validate_address(
&self,
req: &ValidateAddressRequest,
) -> Result<ValidateAddressResponse, Error> {
self.post("/v1/w3s/transactions/validateAddress", req).await
}
}
#[cfg(test)]
mod tests {
use crate::models::transaction::TransactionState;
#[test]
fn transaction_state_serde_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
let s = serde_json::to_string(&TransactionState::Complete)?;
assert_eq!(s, "\"COMPLETE\"");
Ok(())
}
#[test]
fn cancel_tx_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
let req = crate::models::transaction::CancelTxRequest {
entity_secret_ciphertext: "cipher".to_string(),
};
let json = serde_json::to_string(&req)?;
assert!(json.contains("entitySecretCiphertext"));
Ok(())
}
#[test]
fn accelerate_tx_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
let req = crate::models::transaction::AccelerateTxRequest {
entity_secret_ciphertext: "cipher".to_string(),
};
let json = serde_json::to_string(&req)?;
assert!(json.contains("entitySecretCiphertext"));
Ok(())
}
}