synor/sdk/rust/src/bridge/client.rs
Gulshan Yadav 74b82d2bb2 Add Synor Storage and Wallet SDKs for Swift
- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management.
- Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig.
- Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries.
- Create supporting types and models for wallet operations such as Wallet, Address, and Transaction.
- Introduce error handling for both storage and wallet operations.
2026-01-27 01:56:45 +05:30

554 lines
18 KiB
Rust

//! Bridge client implementation
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::error::{BridgeError, Result};
use super::types::*;
/// Synor Bridge Client
pub struct BridgeClient {
config: BridgeConfig,
http_client: reqwest::Client,
closed: Arc<AtomicBool>,
}
impl BridgeClient {
/// Create a new bridge client
pub fn new(config: BridgeConfig) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", config.api_key)).unwrap(),
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert("X-SDK-Version", HeaderValue::from_static("rust/0.1.0"));
let http_client = reqwest::Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self {
config,
http_client,
closed: Arc::new(AtomicBool::new(false)),
}
}
// ==================== Chain Operations ====================
/// Get all supported chains
pub async fn get_supported_chains(&self) -> Result<Vec<Chain>> {
#[derive(Deserialize)]
struct Response {
chains: Vec<Chain>,
}
let resp: Response = self.request("GET", "/chains", Option::<()>::None).await?;
Ok(resp.chains)
}
/// Get chain by ID
pub async fn get_chain(&self, chain_id: ChainId) -> Result<Chain> {
self.request("GET", &format!("/chains/{:?}", chain_id).to_lowercase(), Option::<()>::None).await
}
/// Check if chain is supported
pub async fn is_chain_supported(&self, chain_id: ChainId) -> bool {
match self.get_chain(chain_id).await {
Ok(chain) => chain.supported,
Err(_) => false,
}
}
// ==================== Asset Operations ====================
/// Get supported assets for a chain
pub async fn get_supported_assets(&self, chain_id: ChainId) -> Result<Vec<Asset>> {
#[derive(Deserialize)]
struct Response {
assets: Vec<Asset>,
}
let resp: Response = self.request(
"GET",
&format!("/chains/{:?}/assets", chain_id).to_lowercase(),
Option::<()>::None,
).await?;
Ok(resp.assets)
}
/// Get asset by ID
pub async fn get_asset(&self, asset_id: &str) -> Result<Asset> {
self.request("GET", &format!("/assets/{}", urlencoding::encode(asset_id)), Option::<()>::None).await
}
/// Get wrapped asset mapping
pub async fn get_wrapped_asset(&self, original_asset_id: &str, target_chain: ChainId) -> Result<WrappedAsset> {
self.request(
"GET",
&format!("/assets/{}/wrapped/{:?}", urlencoding::encode(original_asset_id), target_chain).to_lowercase(),
Option::<()>::None,
).await
}
/// Get all wrapped assets for a chain
pub async fn get_wrapped_assets(&self, chain_id: ChainId) -> Result<Vec<WrappedAsset>> {
#[derive(Deserialize)]
struct Response {
assets: Vec<WrappedAsset>,
}
let resp: Response = self.request(
"GET",
&format!("/chains/{:?}/wrapped", chain_id).to_lowercase(),
Option::<()>::None,
).await?;
Ok(resp.assets)
}
// ==================== Fee & Rate Operations ====================
/// Estimate bridge fee
pub async fn estimate_fee(
&self,
asset: &str,
amount: &str,
source_chain: ChainId,
target_chain: ChainId,
) -> Result<FeeEstimate> {
#[derive(Serialize)]
struct Request {
asset: String,
amount: String,
#[serde(rename = "sourceChain")]
source_chain: ChainId,
#[serde(rename = "targetChain")]
target_chain: ChainId,
}
self.request("POST", "/fees/estimate", Some(Request {
asset: asset.to_string(),
amount: amount.to_string(),
source_chain,
target_chain,
})).await
}
/// Get exchange rate between assets
pub async fn get_exchange_rate(&self, from_asset: &str, to_asset: &str) -> Result<ExchangeRate> {
self.request(
"GET",
&format!("/rates/{}/{}", urlencoding::encode(from_asset), urlencoding::encode(to_asset)),
Option::<()>::None,
).await
}
// ==================== Lock-Mint Flow ====================
/// Lock assets on source chain
pub async fn lock(
&self,
asset: &str,
amount: &str,
target_chain: ChainId,
options: Option<LockOptions>,
) -> Result<LockReceipt> {
#[derive(Serialize)]
struct Request {
asset: String,
amount: String,
#[serde(rename = "targetChain")]
target_chain: ChainId,
#[serde(flatten)]
options: Option<LockOptions>,
}
self.request("POST", "/transfers/lock", Some(Request {
asset: asset.to_string(),
amount: amount.to_string(),
target_chain,
options,
})).await
}
/// Get lock proof for minting
pub async fn get_lock_proof(&self, lock_receipt_id: &str) -> Result<LockProof> {
self.request(
"GET",
&format!("/transfers/lock/{}/proof", urlencoding::encode(lock_receipt_id)),
Option::<()>::None,
).await
}
/// Wait for lock proof to be ready
pub async fn wait_for_lock_proof(
&self,
lock_receipt_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<LockProof> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5));
let max_wait = max_wait.unwrap_or(Duration::from_secs(600));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for lock proof")
.with_code("CONFIRMATIONS_PENDING"));
}
match self.get_lock_proof(lock_receipt_id).await {
Ok(proof) => return Ok(proof),
Err(e) if e.is_confirmations_pending() => {
tokio::time::sleep(poll_interval).await;
}
Err(e) => return Err(e),
}
}
}
/// Mint wrapped tokens on target chain
pub async fn mint(
&self,
proof: &LockProof,
target_address: &str,
options: Option<MintOptions>,
) -> Result<SignedTransaction> {
#[derive(Serialize)]
struct Request<'a> {
proof: &'a LockProof,
#[serde(rename = "targetAddress")]
target_address: String,
#[serde(flatten)]
options: Option<MintOptions>,
}
self.request("POST", "/transfers/mint", Some(Request {
proof,
target_address: target_address.to_string(),
options,
})).await
}
// ==================== Burn-Unlock Flow ====================
/// Burn wrapped tokens
pub async fn burn(
&self,
wrapped_asset: &str,
amount: &str,
options: Option<BurnOptions>,
) -> Result<BurnReceipt> {
#[derive(Serialize)]
struct Request {
#[serde(rename = "wrappedAsset")]
wrapped_asset: String,
amount: String,
#[serde(flatten)]
options: Option<BurnOptions>,
}
self.request("POST", "/transfers/burn", Some(Request {
wrapped_asset: wrapped_asset.to_string(),
amount: amount.to_string(),
options,
})).await
}
/// Get burn proof for unlocking
pub async fn get_burn_proof(&self, burn_receipt_id: &str) -> Result<BurnProof> {
self.request(
"GET",
&format!("/transfers/burn/{}/proof", urlencoding::encode(burn_receipt_id)),
Option::<()>::None,
).await
}
/// Wait for burn proof to be ready
pub async fn wait_for_burn_proof(
&self,
burn_receipt_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<BurnProof> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5));
let max_wait = max_wait.unwrap_or(Duration::from_secs(600));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for burn proof")
.with_code("CONFIRMATIONS_PENDING"));
}
match self.get_burn_proof(burn_receipt_id).await {
Ok(proof) => return Ok(proof),
Err(e) if e.is_confirmations_pending() => {
tokio::time::sleep(poll_interval).await;
}
Err(e) => return Err(e),
}
}
}
/// Unlock original tokens
pub async fn unlock(&self, proof: &BurnProof, options: Option<UnlockOptions>) -> Result<SignedTransaction> {
#[derive(Serialize)]
struct Request<'a> {
proof: &'a BurnProof,
#[serde(flatten)]
options: Option<UnlockOptions>,
}
self.request("POST", "/transfers/unlock", Some(Request { proof, options })).await
}
// ==================== Transfer Management ====================
/// Get transfer by ID
pub async fn get_transfer(&self, transfer_id: &str) -> Result<Transfer> {
self.request(
"GET",
&format!("/transfers/{}", urlencoding::encode(transfer_id)),
Option::<()>::None,
).await
}
/// Get transfer status
pub async fn get_transfer_status(&self, transfer_id: &str) -> Result<TransferStatus> {
let transfer = self.get_transfer(transfer_id).await?;
Ok(transfer.status)
}
/// List transfers
pub async fn list_transfers(&self, filter: Option<TransferFilter>) -> Result<Vec<Transfer>> {
let mut params = vec![];
if let Some(f) = &filter {
if let Some(status) = f.status {
params.push(format!("status={:?}", status).to_lowercase());
}
if let Some(chain) = f.source_chain {
params.push(format!("sourceChain={:?}", chain).to_lowercase());
}
if let Some(chain) = f.target_chain {
params.push(format!("targetChain={:?}", chain).to_lowercase());
}
if let Some(ref asset) = f.asset {
params.push(format!("asset={}", urlencoding::encode(asset)));
}
if let Some(ref sender) = f.sender {
params.push(format!("sender={}", urlencoding::encode(sender)));
}
if let Some(ref recipient) = f.recipient {
params.push(format!("recipient={}", urlencoding::encode(recipient)));
}
if let Some(from_date) = f.from_date {
params.push(format!("fromDate={}", from_date));
}
if let Some(to_date) = f.to_date {
params.push(format!("toDate={}", to_date));
}
if let Some(limit) = f.limit {
params.push(format!("limit={}", limit));
}
if let Some(offset) = f.offset {
params.push(format!("offset={}", offset));
}
}
let path = if params.is_empty() {
"/transfers".to_string()
} else {
format!("/transfers?{}", params.join("&"))
};
#[derive(Deserialize)]
struct Response {
transfers: Vec<Transfer>,
}
let resp: Response = self.request("GET", &path, Option::<()>::None).await?;
Ok(resp.transfers)
}
/// Wait for transfer to complete
pub async fn wait_for_transfer(
&self,
transfer_id: &str,
poll_interval: Option<Duration>,
max_wait: Option<Duration>,
) -> Result<Transfer> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(10));
let max_wait = max_wait.unwrap_or(Duration::from_secs(1800));
let deadline = std::time::Instant::now() + max_wait;
loop {
if std::time::Instant::now() >= deadline {
return Err(BridgeError::new("Timeout waiting for transfer completion"));
}
let transfer = self.get_transfer(transfer_id).await?;
match transfer.status {
TransferStatus::Completed | TransferStatus::Failed | TransferStatus::Refunded => {
return Ok(transfer);
}
_ => {
tokio::time::sleep(poll_interval).await;
}
}
}
}
// ==================== Convenience Methods ====================
/// Execute complete lock-mint transfer
pub async fn bridge_to(
&self,
asset: &str,
amount: &str,
target_chain: ChainId,
target_address: &str,
lock_options: Option<LockOptions>,
mint_options: Option<MintOptions>,
) -> Result<Transfer> {
// Lock on source chain
let lock_receipt = self.lock(asset, amount, target_chain, lock_options).await?;
if self.config.debug {
eprintln!("Locked: {}, waiting for confirmations...", lock_receipt.id);
}
// Wait for proof
let proof = self.wait_for_lock_proof(&lock_receipt.id, None, None).await?;
if self.config.debug {
eprintln!("Proof ready, minting on {:?}...", target_chain);
}
// Mint on target chain
self.mint(&proof, target_address, mint_options).await?;
// Return final transfer status
self.wait_for_transfer(&lock_receipt.id, None, None).await
}
/// Execute complete burn-unlock transfer
pub async fn bridge_back(
&self,
wrapped_asset: &str,
amount: &str,
burn_options: Option<BurnOptions>,
unlock_options: Option<UnlockOptions>,
) -> Result<Transfer> {
// Burn wrapped tokens
let burn_receipt = self.burn(wrapped_asset, amount, burn_options).await?;
if self.config.debug {
eprintln!("Burned: {}, waiting for confirmations...", burn_receipt.id);
}
// Wait for proof
let proof = self.wait_for_burn_proof(&burn_receipt.id, None, None).await?;
if self.config.debug {
eprintln!("Proof ready, unlocking on {:?}...", burn_receipt.target_chain);
}
// Unlock on original chain
self.unlock(&proof, unlock_options).await?;
// Return final transfer status
self.wait_for_transfer(&burn_receipt.id, None, None).await
}
// ==================== Lifecycle ====================
/// Close the client
pub fn close(&self) {
self.closed.store(true, Ordering::SeqCst);
}
/// Check if client is closed
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
/// Health check
pub async fn health_check(&self) -> bool {
#[derive(Deserialize)]
struct Response {
status: String,
}
match self.request::<_, Response>("GET", "/health", Option::<()>::None).await {
Ok(resp) => resp.status == "healthy",
Err(_) => false,
}
}
async fn request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<B>,
) -> Result<R> {
if self.is_closed() {
return Err(BridgeError::new("Client has been closed"));
}
let mut last_error = None;
for attempt in 0..self.config.retries {
match self.do_request(method, path, &body).await {
Ok(result) => return Ok(result),
Err(e) => {
if self.config.debug {
eprintln!("Attempt {} failed: {}", attempt + 1, e);
}
last_error = Some(e);
if attempt < self.config.retries - 1 {
tokio::time::sleep(Duration::from_secs((attempt + 1) as u64)).await;
}
}
}
}
Err(last_error.unwrap_or_else(|| BridgeError::new("Unknown error after retries")))
}
async fn do_request<B: Serialize, R: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: &Option<B>,
) -> Result<R> {
let url = format!("{}{}", self.config.endpoint, path);
let mut request = match method {
"GET" => self.http_client.get(&url),
"POST" => self.http_client.post(&url),
"PUT" => self.http_client.put(&url),
"PATCH" => self.http_client.patch(&url),
"DELETE" => self.http_client.delete(&url),
_ => return Err(BridgeError::new(format!("Unknown method: {}", method))),
};
if let Some(b) = body {
request = request.json(b);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_body: serde_json::Value = response.json().await.unwrap_or_default();
let message = error_body["message"]
.as_str()
.or_else(|| error_body["error"].as_str())
.unwrap_or(&format!("HTTP {}", status.as_u16()));
return Err(BridgeError::new(message)
.with_status(status.as_u16())
.with_code(error_body["code"].as_str().unwrap_or("").to_string()));
}
let result = response.json().await?;
Ok(result)
}
}