- 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.
554 lines
18 KiB
Rust
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)
|
|
}
|
|
}
|