use dynamic_waas_sdk_core::{
api::{ApiClient, ApiClientOpts, Auth},
Environment, Error, Result, ThresholdSignatureScheme, WalletProperties,
};
use tracing::{debug, instrument};
const DEFAULT_BASE_API_URL: &str = "https://app.dynamicauth.com";
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DynamicWalletClientOpts {
pub environment_id: String,
pub base_api_url: Option<String>,
pub base_mpc_relay_url: Option<String>,
}
impl DynamicWalletClientOpts {
pub fn new(environment_id: impl Into<String>) -> Self {
Self {
environment_id: environment_id.into(),
base_api_url: None,
base_mpc_relay_url: None,
}
}
#[must_use]
pub fn base_api_url(mut self, url: impl Into<String>) -> Self {
self.base_api_url = Some(url.into());
self
}
#[must_use]
pub fn base_mpc_relay_url(mut self, url: impl Into<String>) -> Self {
self.base_mpc_relay_url = Some(url.into());
self
}
}
pub struct DynamicWalletClient {
api: ApiClient,
base_api_url: String,
environment_id: String,
#[allow(dead_code)]
base_mpc_relay_url: String,
is_authenticated: bool,
}
impl DynamicWalletClient {
pub fn new(opts: DynamicWalletClientOpts) -> Result<Self> {
let base_api_url = opts
.base_api_url
.unwrap_or_else(|| DEFAULT_BASE_API_URL.to_string());
let env = Environment::detect(&base_api_url);
let base_mpc_relay_url = opts
.base_mpc_relay_url
.unwrap_or_else(|| env.mpc_relay_url().to_string());
let api = ApiClient::new(Self::build_api_opts(
&base_api_url,
&opts.environment_id,
env,
Auth::Unauthenticated,
))?;
Ok(Self {
api,
base_api_url,
environment_id: opts.environment_id,
base_mpc_relay_url,
is_authenticated: false,
})
}
pub(crate) fn build_api_opts(
base_api_url: &str,
environment_id: &str,
env: Environment,
auth: Auth,
) -> ApiClientOpts {
ApiClientOpts {
base_api_url: base_api_url.to_owned(),
environment_id: environment_id.to_owned(),
auth,
relay_base_url: Some(crate::mpc_config::keyshares_relay_url_for(env).to_owned()),
relay_app_id: crate::mpc_config::relay_app_id_for(env).map(str::to_owned),
relay_api_key: crate::mpc_config::relay_api_key_for(env).map(str::to_owned),
}
}
pub(crate) fn new_delegated(
opts: DynamicWalletClientOpts,
wallet_api_key: String,
) -> Result<Self> {
let base_api_url = opts
.base_api_url
.unwrap_or_else(|| DEFAULT_BASE_API_URL.to_string());
let env = Environment::detect(&base_api_url);
let base_mpc_relay_url = opts
.base_mpc_relay_url
.unwrap_or_else(|| env.mpc_relay_url().to_string());
let api = ApiClient::new(Self::build_api_opts(
&base_api_url,
&opts.environment_id,
env,
Auth::Delegated(wallet_api_key),
))?;
Ok(Self {
api,
base_api_url,
environment_id: opts.environment_id,
base_mpc_relay_url,
is_authenticated: true,
})
}
pub fn environment_id(&self) -> &str {
&self.environment_id
}
pub fn is_authenticated(&self) -> bool {
self.is_authenticated
}
#[instrument(skip(self, auth_token), level = "debug")]
pub async fn authenticate_api_token(&mut self, auth_token: &str) -> Result<()> {
let env = Environment::detect(&self.base_api_url);
let tmp = ApiClient::new(Self::build_api_opts(
&self.base_api_url,
&self.environment_id,
env,
Auth::Bearer(auth_token.to_owned()),
))?;
let response = tmp
.authenticate_api_token()
.await
.map_err(|e| Error::Authentication(format!("token exchange failed: {e}")))?;
self.api = ApiClient::new(Self::build_api_opts(
&self.base_api_url,
&self.environment_id,
env,
Auth::Bearer(response.encoded_jwts.minified_jwt),
))?;
self.is_authenticated = true;
debug!("authenticated successfully");
Ok(())
}
#[instrument(skip(self), level = "debug")]
pub async fn fetch_wallet_metadata(&self, account_address: &str) -> Result<WalletProperties> {
self.ensure_authenticated()?;
let raw = self.api.get_waas_wallet_by_address(account_address).await?;
let threshold = match raw.threshold_signature_scheme.as_deref() {
Some("TWO_OF_TWO") | None => ThresholdSignatureScheme::TwoOfTwo,
Some("TWO_OF_THREE") => ThresholdSignatureScheme::TwoOfThree,
Some(other) => {
return Err(Error::InvalidArgument(format!(
"unknown threshold signature scheme: {other}"
)))
}
};
let mut wp = WalletProperties::new(raw.chain_name, raw.wallet_id, raw.account_address)
.with_threshold(threshold);
if let Some(path) = raw.derivation_path {
wp = wp.with_derivation_path(path);
}
Ok(wp)
}
fn ensure_authenticated(&self) -> Result<()> {
if self.is_authenticated {
Ok(())
} else {
Err(Error::Authentication(
"client must be authenticated before making API calls — \
call authenticate_api_token first"
.into(),
))
}
}
pub(crate) fn api(&self) -> &ApiClient {
&self.api
}
pub(crate) fn base_mpc_relay_url(&self) -> &str {
&self.base_mpc_relay_url
}
}