Skip to content

Commit 49f6f00

Browse files
author
jagdeep sidhu
committed
thread safe errors
1 parent 7c4c3d6 commit 49f6f00

File tree

3 files changed

+74
-65
lines changed

3 files changed

+74
-65
lines changed

src/bin/main.rs

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,41 @@
1-
use bitcoin_da_client::*;
1+
use bitcoin_da_client::SyscoinClient;
2+
3+
type Error = Box<dyn std::error::Error + Send + Sync>;
24

35
#[tokio::main]
4-
async fn main() -> Result<(), Box<dyn std::error::Error>> {
6+
async fn main() -> Result<(), Error> {
57
// Create a real RPC client
6-
println!("Starting") ;
8+
println!("Starting");
79
let rpc_url = "http://127.0.0.1:8370";
810
let rpc_user = "u";
911
let rpc_password = "p";
1012
let poda_url = "http://poda.tanenbaum.io/vh/";
1113
let timeout = Some(std::time::Duration::from_secs(30));
1214

13-
14-
let rpc_client = RealRpcClient::new(rpc_url, rpc_user, rpc_password, timeout)?;
15-
println!("Loading wallet") ;
16-
rpc_client.create_or_load_wallet("wallet12").await?;
17-
18-
19-
println!("Initializing client") ;
20-
let syscoin_client = SyscoinClient::new(
21-
"http://127.0.0.1:8370/wallet/wallet12",
22-
"u",
23-
"p",
15+
// Initialize the client
16+
let client = SyscoinClient::new(
17+
rpc_url,
18+
rpc_user,
19+
rpc_password,
2420
poda_url,
2521
timeout,
2622
)?;
27-
28-
println!("checking balance");
29-
let balance = syscoin_client.get_balance().await?;
30-
31-
println!("Balance: {}", balance);
32-
33-
println!("Sending blob data");
34-
let blob_hash = syscoin_client.create_blob(&[1, 2, 3, 4]).await?;
35-
println!("Created Blob Hash: {}", blob_hash);
36-
37-
let blob_data = syscoin_client.get_blob_from_cloud(&blob_hash).await?;
38-
39-
// TODO: script to get blob from rpc?
40-
// TODO: test for resilioence, how long can it be running?
41-
println!("Blob Data: {:?}", blob_data);
23+
24+
// Create/load wallet
25+
println!("Loading wallet");
26+
client.create_or_load_wallet("wallet12").await?;
27+
28+
println!("Checking balance");
29+
let balance = client.get_balance().await?;
30+
println!("Balance: {balance}");
31+
32+
println!("Uploading blob data");
33+
let blob_hash = client.create_blob(&[1, 2, 3, 4]).await?;
34+
println!("Created blob: {blob_hash}");
35+
36+
println!("Fetching it back (RPC → cloud fallback)");
37+
let blob_data = client.get_blob(&blob_hash).await?;
38+
println!("Blob data: {blob_data:?}");
4239

4340
Ok(())
4441
}

src/lib.rs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const DEFAULT_TIMEOUT_SECS: u64 = 30;
1212
/// Maximum payload accepted by the Syscoin PoDA endpoint (2 MiB).
1313
pub const MAX_BLOB_SIZE: usize = 2 * 1024 * 1024;
1414

15+
/// Thread-safe error type
16+
pub type SyscoinError = Box<dyn Error + Send + Sync + 'static>;
17+
1518
/// Response structure for JSON-RPC calls
1619
#[derive(Deserialize, Debug)]
1720
struct JsonRpcResponse<T> {
@@ -23,13 +26,13 @@ struct JsonRpcResponse<T> {
2326
#[async_trait]
2427
pub trait RpcClient {
2528
/// Make a generic RPC call with any method and parameters
26-
async fn call(&self, method: &str, params: &[Value]) -> Result<Value, Box<dyn Error>>;
29+
async fn call(&self, method: &str, params: &[Value]) -> Result<Value, SyscoinError>;
2730

2831
/// Get wallet balance with optional account and watchonly parameters
29-
async fn get_balance(&self, account: Option<&str>, include_watchonly: Option<bool>) -> Result<f64, Box<dyn Error>>;
32+
async fn get_balance(&self, account: Option<&str>, include_watchonly: Option<bool>) -> Result<f64, SyscoinError>;
3033

3134
/// Make an HTTP GET request to the specified URL
32-
async fn http_get(&self, url: &str) -> Result<Vec<u8>, Box<dyn Error>>;
35+
async fn http_get(&self, url: &str) -> Result<Vec<u8>, SyscoinError>;
3336
}
3437

3538
/// Production implementation of the RPC client for Syscoin
@@ -43,7 +46,7 @@ pub struct RealRpcClient {
4346

4447
impl RealRpcClient {
4548
/// Create a new RPC client with default timeout
46-
pub fn new(rpc_url: &str, rpc_user: &str, rpc_password: &str, timeout: Option<Duration>) -> Result<Self, Box<dyn Error>> {
49+
pub fn new(rpc_url: &str, rpc_user: &str, rpc_password: &str, timeout: Option<Duration>) -> Result<Self, SyscoinError> {
4750
Self::new_with_timeout(rpc_url, rpc_user, rpc_password, timeout)
4851
}
4952

@@ -53,7 +56,7 @@ impl RealRpcClient {
5356
rpc_user: &str,
5457
rpc_password: &str,
5558
timeout: Option<Duration>,
56-
) -> Result<Self, Box<dyn Error>> {
59+
) -> Result<Self, SyscoinError> {
5760
let timeout = timeout.unwrap_or_else(|| Duration::from_secs(DEFAULT_TIMEOUT_SECS));
5861

5962
let http_client = ClientBuilder::new()
@@ -70,7 +73,7 @@ impl RealRpcClient {
7073
}
7174

7275
/// Send a JSON-RPC request to the Syscoin node
73-
async fn rpc_request(&self, method: &str, params: &[Value]) -> Result<Value, Box<dyn Error>> {
76+
async fn rpc_request(&self, method: &str, params: &[Value]) -> Result<Value, SyscoinError> {
7477
let request_body = json!({
7578
"jsonrpc": "2.0",
7679
"id": 1,
@@ -100,7 +103,7 @@ impl RealRpcClient {
100103
}
101104

102105
/// Create or load a wallet by name
103-
pub async fn create_or_load_wallet(&self, wallet_name: &str) -> Result<(), Box<dyn Error>> {
106+
pub async fn create_or_load_wallet(&self, wallet_name: &str) -> Result<(), SyscoinError> {
104107
// First try to load the wallet
105108
match self.call("loadwallet", &[json!(wallet_name)]).await {
106109
Ok(_) => return Ok(()),
@@ -115,11 +118,11 @@ impl RealRpcClient {
115118

116119
#[async_trait]
117120
impl RpcClient for RealRpcClient {
118-
async fn call(&self, method: &str, params: &[Value]) -> Result<Value, Box<dyn Error>> {
121+
async fn call(&self, method: &str, params: &[Value]) -> Result<Value, SyscoinError> {
119122
self.rpc_request(method, params).await
120123
}
121124

122-
async fn get_balance(&self, account: Option<&str>, include_watchonly: Option<bool>) -> Result<f64, Box<dyn Error>> {
125+
async fn get_balance(&self, account: Option<&str>, include_watchonly: Option<bool>) -> Result<f64, SyscoinError> {
123126
let mut params = Vec::new();
124127

125128
if let Some(acct) = account {
@@ -136,7 +139,7 @@ impl RpcClient for RealRpcClient {
136139
Ok(balance)
137140
}
138141

139-
async fn http_get(&self, url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
142+
async fn http_get(&self, url: &str) -> Result<Vec<u8>, SyscoinError> {
140143
let response = self.http_client.get(url).send().await?;
141144

142145
if !response.status().is_success() {
@@ -160,7 +163,7 @@ impl SyscoinClient {
160163
rpc_password: &str,
161164
poda_url: &str,
162165
timeout: Option<Duration>,
163-
) -> Result<Self, Box<dyn Error>> {
166+
) -> Result<Self, SyscoinError> {
164167
let rpc_client = RealRpcClient::new_with_timeout(rpc_url, rpc_user, rpc_password, timeout)?;
165168

166169
Ok(Self {
@@ -169,8 +172,13 @@ impl SyscoinClient {
169172
})
170173
}
171174

175+
/// Return the maximum supported blob size
176+
pub fn max_blob_size(&self) -> usize {
177+
MAX_BLOB_SIZE
178+
}
179+
172180
/// Create a blob in BitcoinDA(FKA Poda) storage
173-
pub async fn create_blob(&self, data: &[u8]) -> Result<String, Box<dyn Error>> {
181+
pub async fn create_blob(&self, data: &[u8]) -> Result<String, SyscoinError> {
174182
if data.len() > MAX_BLOB_SIZE {
175183
return Err(format!(
176184
"blob size ({}) exceeds maximum allowed ({})",
@@ -193,12 +201,12 @@ impl SyscoinClient {
193201
}
194202

195203
/// Get wallet balance
196-
pub async fn get_balance(&self) -> Result<f64, Box<dyn Error>> {
204+
pub async fn get_balance(&self) -> Result<f64, SyscoinError> {
197205
self.rpc_client.get_balance(None, None).await
198206
}
199207

200208
/// Fetch a blob; tries RPC first, then falls back to PoDA cloud
201-
pub async fn get_blob(&self, blob_id: &str) -> Result<Vec<u8>, Box<dyn Error>> {
209+
pub async fn get_blob(&self, blob_id: &str) -> Result<Vec<u8>, SyscoinError> {
202210
match self.get_blob_from_rpc(blob_id).await {
203211
Ok(data) => Ok(data),
204212
Err(e) => {
@@ -209,7 +217,7 @@ impl SyscoinClient {
209217
}
210218

211219
/// Retrieve blob data from RPC node
212-
async fn get_blob_from_rpc(&self, blob_id: &str) -> Result<Vec<u8>, Box<dyn Error>> {
220+
async fn get_blob_from_rpc(&self, blob_id: &str) -> Result<Vec<u8>, SyscoinError> {
213221
// Strip any 0x prefix
214222
let actual_blob_id = if let Some(stripped) = blob_id.strip_prefix("0x") {
215223
stripped
@@ -241,20 +249,15 @@ impl SyscoinClient {
241249
}
242250

243251
/// Retrieve blob data from PODA cloud storage
244-
pub async fn get_blob_from_cloud(&self, version_hash: &str) -> Result<Vec<u8>, Box<dyn Error>> {
252+
pub async fn get_blob_from_cloud(&self, version_hash: &str) -> Result<Vec<u8>, SyscoinError> {
245253
let url = format!("{}/blob/{}", self.poda_url, version_hash);
246254
self.rpc_client.http_get(&url).await
247255
}
248256

249257
/// Create or load a wallet by name
250-
pub async fn create_or_load_wallet(&self, wallet_name: &str) -> Result<(), Box<dyn Error>> {
258+
pub async fn create_or_load_wallet(&self, wallet_name: &str) -> Result<(), SyscoinError> {
251259
self.rpc_client.create_or_load_wallet(wallet_name).await
252260
}
253-
254-
/// Return the maximum supported blob size
255-
pub fn max_blob_size(&self) -> usize {
256-
MAX_BLOB_SIZE
257-
}
258261
}
259262

260263
/// Mock implementation for testing
@@ -266,7 +269,7 @@ pub struct MockRpcClient {
266269
#[cfg(test)]
267270
#[async_trait]
268271
impl RpcClient for MockRpcClient {
269-
async fn call(&self, method: &str, _params: &[Value]) -> Result<Value, Box<dyn Error>> {
272+
async fn call(&self, method: &str, _params: &[Value]) -> Result<Value, SyscoinError> {
270273
// Return mock responses based on the method
271274
match method {
272275
"getbalance" => Ok(json!(10.5)),
@@ -278,11 +281,11 @@ impl RpcClient for MockRpcClient {
278281
}
279282
}
280283

281-
async fn get_balance(&self, _account: Option<&str>, _include_watchonly: Option<bool>) -> Result<f64, Box<dyn Error>> {
284+
async fn get_balance(&self, _account: Option<&str>, _include_watchonly: Option<bool>) -> Result<f64, SyscoinError> {
282285
Ok(10.5)
283286
}
284287

285-
async fn http_get(&self, _url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
288+
async fn http_get(&self, _url: &str) -> Result<Vec<u8>, SyscoinError> {
286289
Ok(b"mock_data".to_vec())
287290
}
288291
}

tests/lib_test.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,28 +103,37 @@ mod tests {
103103
let mut mock_server = std::thread::spawn(|| {
104104
Server::new()
105105
}).join().expect("Failed to create mock server");
106+
106107
let expected_data = b"retrieved data".to_vec();
107108
let version_hash = "deadbeef";
108109

109110
// Mock HTTP GET response
110111
let _m = mock_server
111-
.mock("GET", format!("/blob/{}", version_hash).as_str()) // Convert to &str
112+
.mock("GET", format!("/blob/{}", version_hash).as_str())
112113
.with_status(200)
113114
.with_body(&expected_data)
114115
.create();
115116

116117
let client = SyscoinClient::new(
117-
"http://localhost:8888",
118-
"user",
119-
"password",
120-
&mock_server.url(),
121-
None,
122-
)
123-
.unwrap();
124-
125-
let result = client.get_blob_from_cloud(version_hash).await;
118+
"http://localhost:8888", // RPC URL (won't be used)
119+
"user", // Username
120+
"password", // Password
121+
&mock_server.url(), // PODA cloud URL
122+
None // Timeout
123+
).unwrap();
126124

127-
assert!(result.is_ok());
125+
// Use get_blob with a non-existent RPC server to force fallback to cloud
126+
// First make sure RPC will fail by mocking it to return an error
127+
mock_server
128+
.mock("POST", "/")
129+
.with_status(500)
130+
.with_body("RPC error")
131+
.create();
132+
133+
// Then call get_blob which should fall back to the cloud endpoint
134+
let result = client.get_blob(version_hash).await;
135+
136+
assert!(result.is_ok(), "Error: {:?}", result.err());
128137
assert_eq!(result.unwrap(), expected_data);
129138
}
130139

0 commit comments

Comments
 (0)