create-sia-app 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +179 -0
  3. package/package.json +29 -0
  4. package/template/CLAUDE.md +160 -0
  5. package/template/README.md +102 -0
  6. package/template/_gitignore +5 -0
  7. package/template/biome.json +40 -0
  8. package/template/index.html +13 -0
  9. package/template/package.json +30 -0
  10. package/template/rust/README.md +16 -0
  11. package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
  12. package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
  13. package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
  14. package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
  15. package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
  16. package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
  17. package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
  18. package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
  19. package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
  20. package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
  21. package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
  22. package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
  23. package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
  24. package/template/rust/sia-sdk-rs/LICENSE +21 -0
  25. package/template/rust/sia-sdk-rs/README.md +30 -0
  26. package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
  27. package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
  28. package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
  29. package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
  30. package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
  31. package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
  32. package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
  33. package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
  34. package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
  35. package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
  36. package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
  37. package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
  38. package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
  39. package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
  40. package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
  41. package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
  42. package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
  43. package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
  44. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
  45. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
  46. package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
  47. package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
  48. package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
  49. package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
  50. package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
  51. package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
  52. package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
  53. package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
  54. package/template/rust/sia-sdk-rs/knope.toml +54 -0
  55. package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
  56. package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
  57. package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
  58. package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
  59. package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
  60. package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
  61. package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
  62. package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
  63. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
  64. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
  65. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
  66. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
  67. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
  68. package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
  69. package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
  70. package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
  71. package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
  72. package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
  73. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
  74. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
  75. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
  76. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
  77. package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
  78. package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
  79. package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
  80. package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
  81. package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
  82. package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
  83. package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
  84. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
  85. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
  86. package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
  87. package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
  88. package/template/scripts/setup-rust.js +29 -0
  89. package/template/src/App.tsx +13 -0
  90. package/template/src/components/DevNote.tsx +21 -0
  91. package/template/src/components/auth/ApproveScreen.tsx +84 -0
  92. package/template/src/components/auth/AuthFlow.tsx +77 -0
  93. package/template/src/components/auth/ConnectScreen.tsx +214 -0
  94. package/template/src/components/auth/LoadingScreen.tsx +8 -0
  95. package/template/src/components/auth/RecoveryScreen.tsx +182 -0
  96. package/template/src/components/upload/UploadZone.tsx +314 -0
  97. package/template/src/index.css +9 -0
  98. package/template/src/lib/constants.ts +8 -0
  99. package/template/src/lib/format.ts +35 -0
  100. package/template/src/lib/hex.ts +13 -0
  101. package/template/src/lib/sdk.ts +25 -0
  102. package/template/src/lib/wasm-env.ts +5 -0
  103. package/template/src/main.tsx +12 -0
  104. package/template/src/stores/auth.ts +86 -0
  105. package/template/tsconfig.app.json +31 -0
  106. package/template/tsconfig.json +7 -0
  107. package/template/tsconfig.node.json +26 -0
  108. package/template/vite.config.ts +18 -0
  109. package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
  110. package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
  111. package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
  112. package/template/wasm/indexd_wasm/package.json +31 -0
@@ -0,0 +1,575 @@
1
+ use async_trait::async_trait;
2
+ use bytes::Bytes;
3
+ use chrono::Utc;
4
+ use core::fmt::Debug;
5
+ use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
6
+ use log::debug;
7
+ use quinn::crypto::rustls::QuicClientConfig;
8
+ use std::collections::HashMap;
9
+ use std::num::ParseIntError;
10
+ use std::sync::atomic::{AtomicU16, Ordering};
11
+ use std::sync::{Arc, RwLock};
12
+ use std::time::{Duration, Instant};
13
+ use thiserror::{self, Error};
14
+ use tokio::net::lookup_host;
15
+ use tokio::time::error::Elapsed;
16
+ use tokio::time::timeout;
17
+
18
+ use crate::rhp4::Error as RHP4Error;
19
+ use quinn::{ClientConfig, Connection, Endpoint, RecvStream, SendStream, VarInt};
20
+ use sia::encoding_async::AsyncDecoder;
21
+ use sia::rhp::{
22
+ self, AccountToken, HostPrices, RPCReadSector, RPCSettings, RPCWriteSector, Transport,
23
+ };
24
+ use sia::signing::{PrivateKey, PublicKey};
25
+ use sia::types::Hash256;
26
+ use sia::types::v2::Protocol;
27
+
28
+ use crate::{Hosts, RHP4Client};
29
+
30
+ struct Stream {
31
+ send: SendStream,
32
+ recv: RecvStream,
33
+ }
34
+
35
+ #[derive(Debug, Error)]
36
+ pub enum ConnectError {
37
+ #[error("connect error: {0}")]
38
+ Connect(#[from] quinn::ConnectError),
39
+
40
+ #[error("connection error: {0}")]
41
+ Connection(#[from] quinn::ConnectionError),
42
+
43
+ #[error("invalid address: {0}")]
44
+ InvalidAddress(String),
45
+
46
+ #[error("timeout error: {0}")]
47
+ Elapsed(#[from] Elapsed),
48
+
49
+ #[error("unknown host: {0}")]
50
+ UnknownHost(PublicKey),
51
+
52
+ #[error("I/O error: {0}")]
53
+ Io(#[from] std::io::Error),
54
+
55
+ #[error("invalid port: {0}")]
56
+ InvalidPort(#[from] ParseIntError),
57
+
58
+ #[error("no endpoint")]
59
+ NoEndpoint,
60
+ }
61
+
62
+ impl AsyncDecoder for Stream {
63
+ type Error = RHP4Error;
64
+ async fn decode_buf(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
65
+ self.recv
66
+ .read_exact(buf)
67
+ .await
68
+ .map_err(|e| RHP4Error::Transport(e.to_string()))
69
+ }
70
+ }
71
+
72
+ impl Transport for Stream {
73
+ type Error = RHP4Error;
74
+
75
+ async fn write_request<R: rhp::RPCRequest>(&mut self, req: &R) -> Result<(), Self::Error> {
76
+ req.encode_request(&mut self.send).await?;
77
+ Ok(())
78
+ }
79
+
80
+ async fn write_bytes(&mut self, data: Bytes) -> Result<(), Self::Error> {
81
+ self.send
82
+ .write_chunk(data)
83
+ .await
84
+ .map_err(|e| RHP4Error::Transport(e.to_string()))
85
+ }
86
+
87
+ async fn read_response<R: rhp::RPCResponse>(&mut self) -> Result<R, Self::Error> {
88
+ R::decode_response(self).await
89
+ }
90
+
91
+ async fn write_response<RR: rhp::RPCResponse>(&mut self, resp: &RR) -> Result<(), Self::Error> {
92
+ resp.encode_response(&mut self.send).await?;
93
+ Ok(())
94
+ }
95
+ }
96
+
97
+ /// A Client manages QUIC connections to Sia hosts.
98
+ /// Connections will be cached for reuse whenever possible.
99
+ #[derive(Debug)]
100
+ pub struct ClientInner {
101
+ hosts: Hosts,
102
+ /*
103
+ note: quinn's documentation (https://docs.rs/quinn/latest/quinn/struct.Endpoint.html#method.client) suggests
104
+ non-ideal fallback behavior when dual-stack is not supported. This effectively treats every platform as
105
+ single-stack instead since IPv4 is the preferred fallback.
106
+ */
107
+ endpoint_v4: RwLock<Option<Arc<Endpoint>>>,
108
+ endpoint_v6: RwLock<Option<Arc<Endpoint>>>,
109
+ consecutive_failures: AtomicU16,
110
+
111
+ client_config: ClientConfig,
112
+
113
+ open_conns: RwLock<HashMap<PublicKey, Connection>>,
114
+ cached_prices: RwLock<HashMap<PublicKey, HostPrices>>,
115
+ cached_tokens: RwLock<HashMap<PublicKey, AccountToken>>,
116
+ }
117
+
118
+ impl ClientInner {
119
+ const MAX_CONSECUTIVE_FAILURES: u16 = 5;
120
+
121
+ async fn connect_v4(
122
+ &self,
123
+ socket_addr: SocketAddr,
124
+ server_name: &str,
125
+ ) -> Result<Connection, ConnectError> {
126
+ if self.consecutive_failures.load(Ordering::Relaxed) > Self::MAX_CONSECUTIVE_FAILURES {
127
+ // The endpoint does not recover after resuming on some platforms (iOS). Effectively
128
+ // re-binds the socket if more than MAX_CONSECUTIVE_FAILURES failures occur.
129
+ self.init_quic_endpoints()?;
130
+ }
131
+ let endpoint = {
132
+ let endpoint = self.endpoint_v4.read().unwrap();
133
+ match endpoint.as_ref() {
134
+ None => return Err(ConnectError::NoEndpoint),
135
+ Some(endpoint) => endpoint.clone(),
136
+ }
137
+ };
138
+
139
+ let conn = endpoint
140
+ .connect(socket_addr, server_name)
141
+ .inspect_err(|e| {
142
+ self.consecutive_failures
143
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
144
+ debug!(
145
+ "failed to connect to {server_name} via {:?}: {e}",
146
+ endpoint.local_addr()
147
+ );
148
+ })?
149
+ .await
150
+ .inspect_err(|e| {
151
+ self.consecutive_failures
152
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
153
+ debug!(
154
+ "failed to establish connection to {server_name} via {:?}: {e}",
155
+ endpoint.local_addr()
156
+ );
157
+ })?;
158
+ debug!(
159
+ "established connection to {server_name} via {:?}",
160
+ endpoint.local_addr()
161
+ );
162
+ self.consecutive_failures.store(0, Ordering::Relaxed);
163
+ Ok(conn)
164
+ }
165
+
166
+ async fn connect_v6(
167
+ &self,
168
+ socket_addr: SocketAddr,
169
+ server_name: &str,
170
+ ) -> Result<Connection, ConnectError> {
171
+ if self.consecutive_failures.load(Ordering::Relaxed) > Self::MAX_CONSECUTIVE_FAILURES {
172
+ // The endpoint does not recover after resuming on some platforms (iOS). Effectively
173
+ // re-binds the socket if more than MAX_CONSECUTIVE_FAILURES failures occur.
174
+ self.init_quic_endpoints()?;
175
+ }
176
+ let endpoint = {
177
+ let endpoint = self.endpoint_v6.read().unwrap();
178
+ match endpoint.as_ref() {
179
+ None => return Err(ConnectError::NoEndpoint),
180
+ Some(endpoint) => endpoint.clone(),
181
+ }
182
+ };
183
+ let conn = endpoint
184
+ .connect(socket_addr, server_name)
185
+ .inspect_err(|e| {
186
+ self.consecutive_failures
187
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
188
+ debug!(
189
+ "failed to connect to {server_name} via {:?}: {e}",
190
+ endpoint.local_addr()
191
+ );
192
+ })?
193
+ .await
194
+ .inspect_err(|e| {
195
+ self.consecutive_failures
196
+ .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
197
+ debug!(
198
+ "failed to establish connection to {server_name} via {:?}: {e}",
199
+ endpoint.local_addr()
200
+ );
201
+ })?;
202
+ debug!(
203
+ "established connection to {server_name} via {:?}",
204
+ endpoint.local_addr()
205
+ );
206
+ self.consecutive_failures.store(0, Ordering::Relaxed);
207
+ Ok(conn)
208
+ }
209
+
210
+ fn get_cached_prices(&self, host_key: &PublicKey) -> Option<HostPrices> {
211
+ let cached_prices = {
212
+ let cache = self.cached_prices.read().unwrap();
213
+ cache.get(host_key).cloned()
214
+ };
215
+ match cached_prices {
216
+ Some(prices) if prices.valid_until > Utc::now() => Some(prices),
217
+ _ => None,
218
+ }
219
+ }
220
+
221
+ fn set_cached_prices(&self, host_key: &PublicKey, prices: HostPrices) {
222
+ self.cached_prices
223
+ .write()
224
+ .unwrap()
225
+ .insert(*host_key, prices);
226
+ }
227
+
228
+ fn existing_conn(&self, host: PublicKey) -> Option<Connection> {
229
+ let open_conn = {
230
+ let conn_cache = self.open_conns.read().unwrap();
231
+ conn_cache.get(&host).cloned()
232
+ };
233
+ match open_conn {
234
+ Some(conn) if conn.close_reason().is_none() => Some(conn),
235
+ _ => None,
236
+ }
237
+ }
238
+
239
+ fn account_token(&self, account_key: &PrivateKey, host_key: PublicKey) -> AccountToken {
240
+ let cached_token = {
241
+ let cache = self.cached_tokens.read().unwrap();
242
+ cache.get(&host_key).cloned()
243
+ };
244
+ match cached_token {
245
+ Some(token) if token.valid_until > Utc::now() => token.clone(),
246
+ _ => {
247
+ let token = AccountToken::new(account_key, host_key);
248
+ self.cached_tokens
249
+ .write()
250
+ .unwrap()
251
+ .insert(host_key, token.clone());
252
+ token
253
+ }
254
+ }
255
+ }
256
+
257
+ async fn connect_to_host(
258
+ &self,
259
+ socket_addr: SocketAddr,
260
+ server_name: &str,
261
+ ) -> Result<Connection, ConnectError> {
262
+ if socket_addr.is_ipv6() {
263
+ return self.connect_v6(socket_addr, server_name).await;
264
+ } else if socket_addr.is_ipv4() {
265
+ return self.connect_v4(socket_addr, server_name).await;
266
+ }
267
+ Err(ConnectError::InvalidAddress(socket_addr.to_string()))
268
+ }
269
+
270
+ async fn new_conn(&self, host: PublicKey) -> Result<Connection, ConnectError> {
271
+ let addresses = self
272
+ .hosts
273
+ .addresses(&host)
274
+ .ok_or(ConnectError::UnknownHost(host))?;
275
+ for addr in addresses {
276
+ if addr.protocol != Protocol::QUIC {
277
+ continue;
278
+ }
279
+ let (addr, port_str) = addr
280
+ .address
281
+ .rsplit_once(':')
282
+ .ok_or(ConnectError::InvalidAddress(addr.address.clone()))?;
283
+ let port: u16 = port_str.parse()?;
284
+ let resolved_addrs = lookup_host((addr, port)).await?;
285
+ for socket in resolved_addrs {
286
+ if let Ok(conn) = self.connect_to_host(socket, addr).await.inspect_err(|e| {
287
+ debug!("failed to connect to {addr}:{port} ({socket}) : {e}");
288
+ }) {
289
+ return Ok(conn);
290
+ }
291
+ }
292
+ }
293
+ Err(ConnectError::NoEndpoint)
294
+ }
295
+
296
+ async fn host_stream(&self, host: PublicKey) -> Result<Stream, ConnectError> {
297
+ let conn = match self.existing_conn(host) {
298
+ Some(conn) => {
299
+ debug!("reusing existing connection to {host}");
300
+ conn
301
+ }
302
+ None => {
303
+ let now = Instant::now();
304
+ let new_conn = timeout(Duration::from_secs(30), self.new_conn(host))
305
+ .await
306
+ .inspect_err(|e| {
307
+ debug!(
308
+ "new connection to {host} timed out in {:?}ms {e}",
309
+ now.elapsed().as_millis()
310
+ );
311
+ })??;
312
+ let mut open_conns = self.open_conns.write().unwrap();
313
+ open_conns.insert(host, new_conn.clone());
314
+ debug!("established new connection to {host}");
315
+ new_conn
316
+ }
317
+ };
318
+
319
+ let (send, recv) = conn.open_bi().await.inspect_err(|_| {
320
+ self.open_conns.write().unwrap().remove(&host);
321
+ })?;
322
+ Ok(Stream { send, recv })
323
+ }
324
+
325
+ async fn host_prices(&self, host_key: PublicKey) -> Result<HostPrices, RHP4Error> {
326
+ let stream = self
327
+ .host_stream(host_key)
328
+ .await
329
+ .map_err(|e| RHP4Error::Transport(e.to_string()))?;
330
+ let resp = RPCSettings::send_request(stream).await?.complete().await?;
331
+ self.set_cached_prices(&host_key, resp.settings.prices.clone());
332
+ Ok(resp.settings.prices)
333
+ }
334
+
335
+ async fn write_sector(
336
+ &self,
337
+ host_key: PublicKey,
338
+ account_key: &PrivateKey,
339
+ prices: HostPrices,
340
+ sector: Bytes,
341
+ ) -> Result<Hash256, RHP4Error> {
342
+ let stream = self
343
+ .host_stream(host_key)
344
+ .await
345
+ .map_err(|e| RHP4Error::Transport(e.to_string()))?;
346
+ let token = self.account_token(account_key, host_key);
347
+ let resp = RPCWriteSector::send_request(stream, prices, token, sector)
348
+ .await?
349
+ .complete()
350
+ .await?;
351
+ Ok(resp.root)
352
+ }
353
+
354
+ async fn read_sector(
355
+ &self,
356
+ host_key: PublicKey,
357
+ account_key: &PrivateKey,
358
+ prices: HostPrices,
359
+ root: Hash256,
360
+ offset: usize,
361
+ length: usize,
362
+ ) -> Result<Bytes, RHP4Error> {
363
+ let stream = self
364
+ .host_stream(host_key)
365
+ .await
366
+ .map_err(|e| RHP4Error::Transport(e.to_string()))?;
367
+ let token = self.account_token(account_key, host_key);
368
+ let resp = RPCReadSector::send_request(stream, prices, token, root, offset, length)
369
+ .await?
370
+ .complete()
371
+ .await?;
372
+ Ok(resp.data)
373
+ }
374
+
375
+ fn init_quic_endpoints(&self) -> Result<(), ConnectError> {
376
+ let endpoint_v4 = match quinn::Endpoint::client((Ipv4Addr::UNSPECIFIED, 0).into()) {
377
+ Ok(mut endpoint) => {
378
+ endpoint.set_default_client_config(self.client_config.clone());
379
+ Some(Arc::new(endpoint))
380
+ }
381
+ Err(e) => {
382
+ debug!("error opening IPv4 endpoint {:?}", e);
383
+ None
384
+ }
385
+ };
386
+ let endpoint_v6 = match quinn::Endpoint::client((Ipv6Addr::UNSPECIFIED, 0).into()) {
387
+ Ok(mut endpoint) => {
388
+ endpoint.set_default_client_config(self.client_config.clone());
389
+ Some(Arc::new(endpoint))
390
+ }
391
+ Err(e) => {
392
+ debug!("error opening IPv6 endpoint {:?}", e);
393
+ None
394
+ }
395
+ };
396
+
397
+ if endpoint_v4.is_none() && endpoint_v6.is_none() {
398
+ return Err(ConnectError::NoEndpoint);
399
+ }
400
+ debug!(
401
+ "initialized QUIC endpoints: v4={:?}, v6={:?}",
402
+ endpoint_v4.clone().map(|e| e.local_addr()),
403
+ endpoint_v6.clone().map(|e| e.local_addr())
404
+ );
405
+
406
+ // reset the endpoints, clear open connections, and reset failure count
407
+ let mut open_conns = self.open_conns.write().unwrap();
408
+ open_conns.clear();
409
+
410
+ let mut endpoint_v4_lock = self.endpoint_v4.write().unwrap();
411
+ *endpoint_v4_lock = endpoint_v4;
412
+ let mut endpoint_v6_lock = self.endpoint_v6.write().unwrap();
413
+ *endpoint_v6_lock = endpoint_v6;
414
+
415
+ self.consecutive_failures.store(0, Ordering::Relaxed);
416
+ Ok(())
417
+ }
418
+ }
419
+
420
+ #[derive(Debug, Clone)]
421
+ pub struct Client {
422
+ inner: Arc<ClientInner>,
423
+ }
424
+
425
+ impl Client {
426
+ pub fn new(
427
+ mut client_config: rustls::ClientConfig,
428
+ hosts: Hosts,
429
+ ) -> Result<Self, ConnectError> {
430
+ const MAX_STREAM_BANDWIDTH: u64 = 1024 * 1024 * 1024; // 1 GiB/s
431
+ const EXPECTED_RTT: u64 = 100; // ms
432
+ client_config.enable_early_data = true;
433
+ client_config.alpn_protocols = vec![b"sia/rhp4".to_vec()];
434
+
435
+ let mut transport_config = quinn::TransportConfig::default();
436
+ transport_config.max_concurrent_bidi_streams(VarInt::from_u32(0));
437
+ transport_config.max_concurrent_uni_streams(VarInt::from_u32(0));
438
+ transport_config.max_idle_timeout(Some(Duration::from_secs(15).try_into().unwrap()));
439
+ transport_config.keep_alive_interval(Some(Duration::from_secs(5)));
440
+ transport_config
441
+ .stream_receive_window(VarInt::from_u64(MAX_STREAM_BANDWIDTH * EXPECTED_RTT).unwrap());
442
+
443
+ let client_config = QuicClientConfig::try_from(client_config).unwrap();
444
+ let mut client_config = quinn::ClientConfig::new(Arc::new(client_config));
445
+ client_config.transport_config(Arc::new(transport_config));
446
+
447
+ let client = Self {
448
+ inner: Arc::new(ClientInner {
449
+ hosts,
450
+ endpoint_v4: RwLock::new(None),
451
+ endpoint_v6: RwLock::new(None),
452
+ consecutive_failures: AtomicU16::new(0),
453
+ client_config,
454
+ open_conns: RwLock::new(HashMap::new()),
455
+ cached_prices: RwLock::new(HashMap::new()),
456
+ cached_tokens: RwLock::new(HashMap::new()),
457
+ }),
458
+ };
459
+ client.inner.init_quic_endpoints()?;
460
+ Ok(client)
461
+ }
462
+ }
463
+
464
+ #[async_trait]
465
+ impl RHP4Client for Client {
466
+ async fn host_prices(
467
+ &self,
468
+ host_key: PublicKey,
469
+ refresh: bool,
470
+ ) -> Result<HostPrices, RHP4Error> {
471
+ if !refresh && let Some(prices) = self.inner.get_cached_prices(&host_key) {
472
+ return Ok(prices);
473
+ }
474
+ self.inner
475
+ .host_prices(host_key)
476
+ .await
477
+ .inspect_err(|_| self.inner.hosts.add_failure(&host_key))
478
+ }
479
+
480
+ async fn write_sector(
481
+ &self,
482
+ host_key: PublicKey,
483
+ account_key: &PrivateKey,
484
+ sector: Bytes,
485
+ ) -> Result<Hash256, RHP4Error> {
486
+ let prices = self.host_prices(host_key, false).await?;
487
+ let start = Instant::now();
488
+ let root = self
489
+ .inner
490
+ .write_sector(host_key, account_key, prices, sector)
491
+ .await
492
+ .inspect_err(|_| self.inner.hosts.add_failure(&host_key))?;
493
+ self.inner
494
+ .hosts
495
+ .add_write_sample(&host_key, start.elapsed());
496
+ Ok(root)
497
+ }
498
+
499
+ async fn read_sector(
500
+ &self,
501
+ host_key: PublicKey,
502
+ account_key: &PrivateKey,
503
+ root: Hash256,
504
+ offset: usize,
505
+ length: usize,
506
+ ) -> Result<Bytes, RHP4Error> {
507
+ let prices = self.host_prices(host_key, false).await?;
508
+ let start = Instant::now();
509
+ let data = self
510
+ .inner
511
+ .read_sector(host_key, account_key, prices, root, offset, length)
512
+ .await
513
+ .inspect_err(|_| self.inner.hosts.add_failure(&host_key))?;
514
+ self.inner.hosts.add_read_sample(&host_key, start.elapsed());
515
+ Ok(data)
516
+ }
517
+ }
518
+
519
+ #[cfg(test)]
520
+ mod test {
521
+ use std::time::Duration;
522
+
523
+ use super::*;
524
+ use rustls_platform_verifier::ConfigVerifierExt;
525
+ use sia::public_key;
526
+ use sia::rhp::Host;
527
+ use sia::types::v2::NetAddress;
528
+ use tokio::time::sleep;
529
+
530
+ #[tokio::test]
531
+ async fn test_dialer() {
532
+ if rustls::crypto::CryptoProvider::get_default().is_none() {
533
+ rustls::crypto::ring::default_provider()
534
+ .install_default()
535
+ .unwrap();
536
+ }
537
+
538
+ let host_key =
539
+ public_key!("ed25519:36c8b07e61548a57e16dfabdfcc07dc157974a75010ab1684643d933e83fa7b1");
540
+
541
+ let client_config =
542
+ rustls::ClientConfig::with_platform_verifier().expect("Failed to create client config");
543
+ let hosts = Hosts::new();
544
+ hosts.update(vec![Host {
545
+ public_key: host_key,
546
+ addresses: vec![NetAddress {
547
+ protocol: Protocol::QUIC,
548
+ address: "6r4b0vj1ai55fobdvauvpg3to5bpeijl045b2q268fcj7q1vkuog.sia.host:9984"
549
+ .into(),
550
+ }],
551
+ country_code: "US".into(),
552
+ latitude: 0.0,
553
+ longitude: 0.0,
554
+ good_for_upload: true,
555
+ }]);
556
+ let dialer = Client::new(client_config, hosts).expect("Failed to create dialer");
557
+
558
+ let prices = dialer
559
+ .host_prices(host_key, false)
560
+ .await
561
+ .expect("Failed to get host prices");
562
+ // check that they are cached
563
+ let prices2 = dialer
564
+ .host_prices(host_key, false)
565
+ .await
566
+ .expect("Failed to get host prices");
567
+ assert_eq!(prices, prices2);
568
+ sleep(Duration::from_secs(2)).await; // ensure the signature changes
569
+ let prices3 = dialer
570
+ .host_prices(host_key, true)
571
+ .await
572
+ .expect("Failed to get host prices");
573
+ assert_ne!(prices2, prices3);
574
+ }
575
+ }
@@ -0,0 +1,52 @@
1
+ use async_trait::async_trait;
2
+ use bytes::Bytes;
3
+ use sia::encoding;
4
+ use sia::rhp::{self, HostPrices};
5
+ use sia::signing::{PrivateKey, PublicKey};
6
+ use sia::types::Hash256;
7
+ use thiserror::Error;
8
+
9
+ #[derive(Debug, Error)]
10
+ pub enum Error {
11
+ #[error("i/o error: {0}")]
12
+ Io(#[from] std::io::Error),
13
+
14
+ #[error("encoding error: {0}")]
15
+ Encoding(#[from] encoding::Error),
16
+
17
+ #[error("rhp error: {0}")]
18
+ Rhp(#[from] rhp::Error),
19
+
20
+ #[error("invalid prices")]
21
+ InvalidPrices,
22
+
23
+ #[error("invalid signature")]
24
+ InvalidSignature,
25
+
26
+ #[cfg(not(target_arch = "wasm32"))]
27
+ #[error("timeout error: {0}")]
28
+ Timeout(#[from] tokio::time::error::Elapsed),
29
+
30
+ #[error("transport error: {0}")]
31
+ Transport(String),
32
+ }
33
+
34
+ /// Trait defining the operations that can be performed on a host.
35
+ #[async_trait]
36
+ pub(crate) trait RHP4Client: Send + Sync {
37
+ async fn host_prices(&self, host_key: PublicKey, refresh: bool) -> Result<HostPrices, Error>;
38
+ async fn write_sector(
39
+ &self,
40
+ host_key: PublicKey,
41
+ account_key: &PrivateKey,
42
+ sector: Bytes,
43
+ ) -> Result<Hash256, Error>;
44
+ async fn read_sector(
45
+ &self,
46
+ host_key: PublicKey,
47
+ account_key: &PrivateKey,
48
+ root: Hash256,
49
+ offset: usize,
50
+ length: usize,
51
+ ) -> Result<Bytes, Error>;
52
+ }