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,398 @@
1
+ use std::collections::HashMap;
2
+ use std::future::Future;
3
+ use std::pin::Pin;
4
+ use std::sync::{Arc, RwLock};
5
+ use std::task::{Context, Poll};
6
+ use tokio::sync::Semaphore;
7
+
8
+ use bytes::Bytes;
9
+ use chrono::Utc;
10
+ use js_sys::{Reflect, Uint8Array};
11
+ use log::debug;
12
+ use sia::encoding_async::{AsyncDecoder, AsyncEncoder};
13
+ use sia::rhp::{self, AccountToken, HostPrices, RPCReadSector, RPCSettings, RPCWriteSector, Transport};
14
+ use sia::signing::{PrivateKey, PublicKey};
15
+ use sia::types::Hash256;
16
+ use sia::types::v2::Protocol;
17
+ use wasm_bindgen::prelude::*;
18
+ use wasm_bindgen_futures::JsFuture;
19
+ use web_sys::{ReadableStreamDefaultReader, WritableStreamDefaultWriter};
20
+
21
+ use sia::rhp::HostSettings;
22
+
23
+ use crate::rhp4::Error;
24
+ use crate::{Hosts, RHP4Client};
25
+
26
+ /// Wraps a `!Send` future to satisfy `Send` bounds.
27
+ ///
28
+ /// # Safety
29
+ /// Only used on WASM where execution is single-threaded and `Send` is meaningless.
30
+ struct SendFuture<F>(F);
31
+
32
+ // SAFETY: WASM is single-threaded.
33
+ unsafe impl<F> Send for SendFuture<F> {}
34
+
35
+ impl<F: Future> Future for SendFuture<F> {
36
+ type Output = F::Output;
37
+
38
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
39
+ // SAFETY: We never move the inner future after pinning. The outer
40
+ // Pin guarantees structural pinning for the single field.
41
+ unsafe { self.map_unchecked_mut(|s| &mut s.0).poll(cx) }
42
+ }
43
+ }
44
+
45
+ /// Wraps the browser's WebTransport bidirectional stream for use with
46
+ /// the sia RHP4 protocol.
47
+ struct Stream {
48
+ reader: ReadableStreamDefaultReader,
49
+ writer: WritableStreamDefaultWriter,
50
+ read_buf: Vec<u8>,
51
+ }
52
+
53
+
54
+ impl Stream {
55
+ /// Read exactly `buf.len()` bytes from the stream, buffering as needed.
56
+ async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> {
57
+ let mut filled = 0;
58
+ while filled < buf.len() {
59
+ // drain internal buffer first
60
+ if !self.read_buf.is_empty() {
61
+ let n = std::cmp::min(self.read_buf.len(), buf.len() - filled);
62
+ buf[filled..filled + n].copy_from_slice(&self.read_buf[..n]);
63
+ self.read_buf.drain(..n);
64
+ filled += n;
65
+ continue;
66
+ }
67
+
68
+ // read a chunk from the JS ReadableStream
69
+ let result = JsFuture::from(self.reader.read())
70
+ .await
71
+ .map_err(|e| Error::Transport(format!("read error: {:?}", e)))?;
72
+
73
+ let done = Reflect::get(&result, &JsValue::from_str("done"))
74
+ .map_err(|e| Error::Transport(format!("reflect error: {:?}", e)))?
75
+ .as_bool()
76
+ .unwrap_or(true);
77
+
78
+ if done {
79
+ return Err(Error::Transport("stream closed unexpectedly".into()));
80
+ }
81
+
82
+ let value = Reflect::get(&result, &JsValue::from_str("value"))
83
+ .map_err(|e| Error::Transport(format!("reflect error: {:?}", e)))?;
84
+
85
+ let chunk = Uint8Array::new(&value);
86
+ let mut data = vec![0u8; chunk.length() as usize];
87
+ chunk.copy_to(&mut data);
88
+ self.read_buf.extend_from_slice(&data);
89
+ }
90
+ Ok(())
91
+ }
92
+
93
+ /// Write all bytes to the JS WritableStream.
94
+ async fn write_all(&mut self, data: &[u8]) -> Result<(), Error> {
95
+ let array = Uint8Array::from(data);
96
+ JsFuture::from(self.writer.write_with_chunk(&array))
97
+ .await
98
+ .map_err(|e| Error::Transport(format!("write error: {:?}", e)))?;
99
+ Ok(())
100
+ }
101
+ }
102
+
103
+ impl AsyncEncoder for Stream {
104
+ type Error = Error;
105
+
106
+ async fn encode_buf(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
107
+ self.write_all(buf).await
108
+ }
109
+ }
110
+
111
+ impl AsyncDecoder for Stream {
112
+ type Error = Error;
113
+
114
+ async fn decode_buf(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
115
+ self.read_exact(buf).await
116
+ }
117
+ }
118
+
119
+ impl Transport for Stream {
120
+ type Error = Error;
121
+
122
+ async fn write_request<R: rhp::RPCRequest>(&mut self, req: &R) -> Result<(), Self::Error> {
123
+ req.encode_request(self).await?;
124
+ Ok(())
125
+ }
126
+
127
+ async fn write_bytes(&mut self, data: Bytes) -> Result<(), Self::Error> {
128
+ self.write_all(&data).await
129
+ }
130
+
131
+ async fn read_response<R: rhp::RPCResponse>(&mut self) -> Result<R, Self::Error> {
132
+ R::decode_response(self).await
133
+ }
134
+
135
+ async fn write_response<RR: rhp::RPCResponse>(
136
+ &mut self,
137
+ resp: &RR,
138
+ ) -> Result<(), Self::Error> {
139
+ resp.encode_response(self).await?;
140
+ Ok(())
141
+ }
142
+ }
143
+
144
+ /// Suppresses the unhandled promise rejection from `WebTransport.closed`.
145
+ ///
146
+ /// When a WebTransport connection is rejected, both the `ready` and
147
+ /// `closed` promises reject. If nobody catches `closed`, the browser
148
+ /// logs an unhandled rejection warning.
149
+ fn suppress_closed_rejection(wt: &web_sys::WebTransport) {
150
+ let closed = wt.closed();
151
+ let handler: Closure<dyn FnMut(JsValue)> = Closure::once(|_: JsValue| {});
152
+ let _ = closed.catch(&handler);
153
+ handler.forget(); // leak intentionally — called at most once per connection attempt
154
+ }
155
+
156
+ /// The WebTransport URL path for the RHP4 protocol.
157
+ const RHP4_PATH: &str = "/sia/rhp/v4";
158
+
159
+ /// Opens a WebTransport connection to the given address and creates
160
+ /// a bidirectional stream. If the address is a bare `host:port`, the
161
+ /// RHP4 path (`/sia/rhp/v4`) is appended automatically.
162
+ async fn connect_stream(address: &str) -> Result<Stream, Error> {
163
+ let url = if address.starts_with("https://") {
164
+ address.to_string()
165
+ } else if address.contains('/') {
166
+ // Already has a path component (e.g. host:port/sia/rhp/v4)
167
+ format!("https://{address}")
168
+ } else {
169
+ // Bare host:port — append the RHP4 path
170
+ format!("https://{address}{RHP4_PATH}")
171
+ };
172
+ debug!("connecting via WebTransport at {url}");
173
+
174
+ let options = web_sys::WebTransportOptions::new();
175
+ let wt = web_sys::WebTransport::new_with_options(&url, &options)
176
+ .map_err(|e| Error::Transport(format!("WebTransport constructor error: {:?}", e)))?;
177
+
178
+ suppress_closed_rejection(&wt);
179
+
180
+ JsFuture::from(wt.ready())
181
+ .await
182
+ .map_err(|e| Error::Transport(format!("WebTransport ready error: {:?}", e)))?;
183
+
184
+ debug!("WebTransport connected to {url}");
185
+
186
+ let bidi_stream = JsFuture::from(wt.create_bidirectional_stream())
187
+ .await
188
+ .map_err(|e| Error::Transport(format!("createBidirectionalStream error: {:?}", e)))?;
189
+
190
+ let bidi = web_sys::WebTransportBidirectionalStream::from(bidi_stream);
191
+ let reader = bidi
192
+ .readable()
193
+ .get_reader()
194
+ .dyn_into::<ReadableStreamDefaultReader>()
195
+ .map_err(|e| Error::Transport(format!("get_reader error: {:?}", e)))?;
196
+ let writer = bidi
197
+ .writable()
198
+ .get_writer()
199
+ .map_err(|e| Error::Transport(format!("get_writer error: {:?}", e)))?;
200
+
201
+ Ok(Stream {
202
+ reader,
203
+ writer,
204
+ read_buf: Vec::new(),
205
+ })
206
+ }
207
+
208
+ /// Connects to a host at the given address via WebTransport and fetches
209
+ /// its settings using the RHP4 settings RPC.
210
+ pub async fn fetch_host_settings(address: &str) -> Result<HostSettings, Error> {
211
+ let stream = connect_stream(address).await?;
212
+ let resp = RPCSettings::send_request(stream)
213
+ .await?
214
+ .complete()
215
+ .await?;
216
+ Ok(resp.settings)
217
+ }
218
+
219
+ #[derive(Clone, Debug)]
220
+ pub struct Client {
221
+ hosts: Hosts,
222
+ cached_prices: std::sync::Arc<RwLock<HashMap<PublicKey, HostPrices>>>,
223
+ cached_tokens: std::sync::Arc<RwLock<HashMap<PublicKey, AccountToken>>>,
224
+ price_fetch_semaphore: std::sync::Arc<Semaphore>,
225
+ }
226
+
227
+ impl Client {
228
+ pub fn new(hosts: Hosts, max_price_fetches: usize) -> Client {
229
+ Client {
230
+ hosts,
231
+ cached_prices: std::sync::Arc::new(RwLock::new(HashMap::new())),
232
+ cached_tokens: std::sync::Arc::new(RwLock::new(HashMap::new())),
233
+ price_fetch_semaphore: std::sync::Arc::new(Semaphore::new(max_price_fetches)),
234
+ }
235
+ }
236
+
237
+ fn get_cached_prices(&self, host_key: &PublicKey) -> Option<HostPrices> {
238
+ let cache = self.cached_prices.read().unwrap();
239
+ match cache.get(host_key) {
240
+ Some(prices) if prices.valid_until > Utc::now() => Some(prices.clone()),
241
+ _ => None,
242
+ }
243
+ }
244
+
245
+ fn set_cached_prices(&self, host_key: &PublicKey, prices: HostPrices) {
246
+ self.cached_prices
247
+ .write()
248
+ .unwrap()
249
+ .insert(*host_key, prices);
250
+ }
251
+
252
+ fn account_token(&self, account_key: &PrivateKey, host_key: PublicKey) -> AccountToken {
253
+ let cached = {
254
+ let cache = self.cached_tokens.read().unwrap();
255
+ cache.get(&host_key).cloned()
256
+ };
257
+ match cached {
258
+ Some(token) if token.valid_until > Utc::now() => token,
259
+ _ => {
260
+ let token = AccountToken::new(account_key, host_key);
261
+ self.cached_tokens
262
+ .write()
263
+ .unwrap()
264
+ .insert(host_key, token.clone());
265
+ token
266
+ }
267
+ }
268
+ }
269
+
270
+ /// Connects to a host via WebTransport and opens a bidirectional stream.
271
+ /// Tries all QUIC addresses for a host before giving up.
272
+ async fn host_stream(&self, host_key: PublicKey) -> Result<Stream, Error> {
273
+ let addresses = self
274
+ .hosts
275
+ .addresses(&host_key)
276
+ .ok_or_else(|| Error::Transport(format!("unknown host: {host_key}")))?;
277
+
278
+ let mut last_err = None;
279
+ for addr in addresses {
280
+ if addr.protocol != Protocol::QUIC {
281
+ continue;
282
+ }
283
+
284
+ match connect_stream(&addr.address).await {
285
+ Ok(stream) => return Ok(stream),
286
+ Err(e) => {
287
+ debug!("connection to {host_key} at {} failed: {e}", addr.address);
288
+ last_err = Some(e);
289
+ }
290
+ }
291
+ }
292
+
293
+ Err(last_err.unwrap_or_else(|| {
294
+ Error::Transport(format!(
295
+ "no QUIC/WebTransport address found for host {host_key}"
296
+ ))
297
+ }))
298
+ }
299
+ }
300
+
301
+ /// Manual `RHP4Client` implementation that wraps `!Send` WASM futures in
302
+ /// [`SendFuture`] so they satisfy the `Send` bound required by `#[async_trait]`.
303
+ impl RHP4Client for Client {
304
+ fn host_prices<'life0, 'async_trait>(
305
+ &'life0 self,
306
+ host_key: PublicKey,
307
+ refresh: bool,
308
+ ) -> Pin<Box<dyn Future<Output = Result<HostPrices, Error>> + Send + 'async_trait>>
309
+ where
310
+ 'life0: 'async_trait,
311
+ Self: 'async_trait,
312
+ {
313
+ Box::pin(SendFuture(async move {
314
+ if !refresh {
315
+ if let Some(prices) = self.get_cached_prices(&host_key) {
316
+ debug!("host_prices: using cached prices for {host_key}");
317
+ return Ok(prices);
318
+ }
319
+ }
320
+
321
+ // Acquire semaphore permit to limit concurrent price fetches
322
+ let _permit = self.price_fetch_semaphore.acquire().await
323
+ .map_err(|e| Error::Transport(format!("semaphore error: {}", e)))?;
324
+
325
+ // Check cache again in case another task fetched while we were waiting
326
+ if !refresh {
327
+ if let Some(prices) = self.get_cached_prices(&host_key) {
328
+ debug!("host_prices: using cached prices for {host_key} (fetched by other task)");
329
+ return Ok(prices);
330
+ }
331
+ }
332
+
333
+ debug!("host_prices: fetching prices from {host_key}");
334
+ let stream = self.host_stream(host_key).await?;
335
+ let resp = RPCSettings::send_request(stream)
336
+ .await?
337
+ .complete()
338
+ .await?;
339
+
340
+ debug!("host_prices: got prices from {host_key}");
341
+ self.set_cached_prices(&host_key, resp.settings.prices.clone());
342
+ Ok(resp.settings.prices)
343
+ }))
344
+ }
345
+
346
+ fn write_sector<'life0, 'life1, 'async_trait>(
347
+ &'life0 self,
348
+ host_key: PublicKey,
349
+ account_key: &'life1 PrivateKey,
350
+ sector: Bytes,
351
+ ) -> Pin<Box<dyn Future<Output = Result<Hash256, Error>> + Send + 'async_trait>>
352
+ where
353
+ 'life0: 'async_trait,
354
+ 'life1: 'async_trait,
355
+ Self: 'async_trait,
356
+ {
357
+ Box::pin(SendFuture(async move {
358
+ debug!("write_sector: getting prices for {host_key}");
359
+ let prices = self.host_prices(host_key, false).await?;
360
+ debug!("write_sector: connecting to {host_key}");
361
+ let stream = self.host_stream(host_key).await?;
362
+ let token = self.account_token(account_key, host_key);
363
+ debug!("write_sector: sending {} bytes to {host_key}", sector.len());
364
+ let resp = RPCWriteSector::send_request(stream, prices, token, sector)
365
+ .await?
366
+ .complete()
367
+ .await?;
368
+ debug!("write_sector: completed for {host_key}");
369
+ Ok(resp.root)
370
+ }))
371
+ }
372
+
373
+ fn read_sector<'life0, 'life1, 'async_trait>(
374
+ &'life0 self,
375
+ host_key: PublicKey,
376
+ account_key: &'life1 PrivateKey,
377
+ root: Hash256,
378
+ offset: usize,
379
+ length: usize,
380
+ ) -> Pin<Box<dyn Future<Output = Result<Bytes, Error>> + Send + 'async_trait>>
381
+ where
382
+ 'life0: 'async_trait,
383
+ 'life1: 'async_trait,
384
+ Self: 'async_trait,
385
+ {
386
+ Box::pin(SendFuture(async move {
387
+ let prices = self.host_prices(host_key, false).await?;
388
+ let stream = self.host_stream(host_key).await?;
389
+ let token = self.account_token(account_key, host_key);
390
+ let resp =
391
+ RPCReadSector::send_request(stream, prices, token, root, offset, length)
392
+ .await?
393
+ .complete()
394
+ .await?;
395
+ Ok(resp.data)
396
+ }))
397
+ }
398
+ }
@@ -0,0 +1,76 @@
1
+ ## 0.3.0 (2026-01-28)
2
+
3
+ ### Breaking Changes
4
+
5
+ - Implemented new `indexd` authentication.
6
+ - Merge SlabSlice and Slab types.
7
+ - Reduced size of shared object URLs by using base64 URL encoding for the encryption key.
8
+ - Reduced size of signed urls by shortening query parameter names and using base64 URL encoding instead of hex.
9
+ - Renamed `key` to `id` in object event and cursor.
10
+
11
+ ### Features
12
+
13
+ - Add optional host filters (offset/limit/service-account/protocol/country) plus distance sort
14
+ - Fixed an issue with downloaded data not always being flushed to the passed in writer.
15
+ - Implement upload packing
16
+ - Track RPC failure rate when selecting hosts rather than raw RPCs.
17
+ - Unified the SDK logic where possible.
18
+
19
+ ### Fixes
20
+
21
+ - Added missing updated_at field.
22
+ - Fix decoding failing when encrypted metadata is null or missing
23
+ - Fixed an issue with uploads stalling after resuming on some platforms.
24
+ - Fixed progress callback not being called immediately leading to incorrect reporting.
25
+ - Fixed signing when URLs have port number.
26
+ - Improved upload performance by 75%.
27
+ - Make use of goodForUpload field
28
+ - Remove service account fields
29
+ - Update object listing endpoints to use events
30
+
31
+ ## 0.2.2 (2025-10-04)
32
+
33
+ ### Features
34
+
35
+ - Add JSON serialization to ChainState
36
+
37
+ ## 0.2.1 (2025-10-04)
38
+
39
+ ### Features
40
+
41
+ - Add JSON serialization to ChainState
42
+
43
+ ### Fixes
44
+
45
+ - Fix path dependency versions.
46
+
47
+ ## 0.2.0 (2025-10-04)
48
+
49
+ ### Breaking Changes
50
+
51
+ - Publish to cargo
52
+
53
+ ### Features
54
+
55
+ - Add JSON serialization to ChainState
56
+
57
+ ## 0.1.1 (2025-10-04)
58
+
59
+ ### Features
60
+
61
+ - Add JSON serialization to ChainState
62
+ - Add account API endpoint to app_client and FFI implementation.
63
+ - Add object sharing.
64
+ - Add pin_slab and unpin_slab to FFI.
65
+ - Add progress callback.
66
+ - Add slab metadata to SDK
67
+ - Add slab pruning
68
+ - Remove separate range methods.
69
+ - Use randomly generated encryption keys.
70
+ - Add method for pinning multiple slabs
71
+
72
+ ### Fixes
73
+
74
+ - Enable replacing log hook.
75
+ - Swap out 'time' dependency for 'chrono'.
76
+ - Use PublicKey for account key type.
@@ -0,0 +1,47 @@
1
+ [package]
2
+ name = "indexd_ffi"
3
+ version = "0.3.0"
4
+ edition = "2024"
5
+ repository = "https://github.com/SiaFoundation/sia-sdk-rs"
6
+ license = "MIT"
7
+ description = "FFI SDK for interacting with a Sia network indexer"
8
+ authors = ["The Sia Foundation"]
9
+ categories = ["cryptography::cryptocurrencies"]
10
+ keywords = ["sia", "decentralized", "blockchain", "depin", "storage"]
11
+
12
+ [lib]
13
+ crate-type = ["rlib", "cdylib", "staticlib"]
14
+
15
+ [dependencies]
16
+ async-trait = "0.1.89"
17
+ base64 = "0.22.1"
18
+ bytes = "1.11.1"
19
+ indexd = { version = "0.2.0", path = "../indexd" }
20
+ log = "0.4.29"
21
+ rand = "0.10.0"
22
+ rustls = { version = "0.23.36", default-features = false, features = ["ring", "logging", "std", "tls12"] }
23
+ sia_sdk = { version = "0.2.0", path = "../sia_sdk" }
24
+ thiserror = "2.0.18"
25
+ tokio = { version = "1.49.0", features = ["rt-multi-thread", "sync"] }
26
+ tokio-stream = "0.1.18"
27
+ tokio-util = "0.7.18"
28
+ uniffi = { version = "=0.29.4", features = ["cli", "tokio"] } # be careful updating as it may break uniffi-react-native-bindgen
29
+ zeroize = "1.8.1"
30
+
31
+ [target.'cfg(target_os = "android")'.dependencies]
32
+ webpki-roots = "1.0"
33
+
34
+ [target.'cfg(not(target_os = "android"))'.dependencies]
35
+ rustls-platform-verifier = "0.6"
36
+
37
+ [build-dependencies]
38
+ uniffi = { version = "0.30.0", features = ["build"] }
39
+
40
+ [[bin]]
41
+ name = "uniffi-bindgen"
42
+
43
+ [dev-dependencies]
44
+ pretty_env_logger = "0.5.0"
45
+
46
+ [features]
47
+ mock = []
@@ -0,0 +1,10 @@
1
+ # Running
2
+
3
+ From the repo root, run the following:
4
+
5
+ ```sh
6
+ cargo build --release --package=indexd_ffi
7
+ cargo run --package=indexd_ffi --bin uniffi-bindgen generate --library target/release/libindexd_ffi.dylib --language python --out-dir indexd_ffi/examples/python
8
+ mv target/release/libindexd_ffi.dylib indexd_ffi/examples/python
9
+ (cd indexd_ffi/examples/python && python3 example.py)
10
+ ```
@@ -0,0 +1,130 @@
1
+ import asyncio
2
+ import json
3
+ from datetime import datetime, timedelta, timezone
4
+ from logging import fatal
5
+ from os import urandom
6
+ from random import randint
7
+ from sys import stdin
8
+
9
+ from indexd_ffi import (
10
+ AppMeta,
11
+ Builder,
12
+ DownloadOptions,
13
+ Logger,
14
+ Reader,
15
+ UploadOptions,
16
+ Writer,
17
+ generate_recovery_phrase,
18
+ set_logger,
19
+ uniffi_set_event_loop,
20
+ )
21
+
22
+
23
+ class PrintLogger(Logger):
24
+ def debug(self, msg):
25
+ print("DEBUG", msg)
26
+
27
+ def info(self, msg):
28
+ print("INFO", msg)
29
+
30
+ def warning(self, msg):
31
+ print("WARNING", msg)
32
+
33
+ def error(self, msg):
34
+ print("ERROR", msg)
35
+
36
+
37
+ from io import BytesIO
38
+
39
+
40
+ class BytesReader(Reader):
41
+ def __init__(self, data: bytes, chunk_size: int = 65536):
42
+ self.buffer = BytesIO(data)
43
+ self.chunk_size = chunk_size
44
+
45
+ async def read(self) -> bytes:
46
+ return self.buffer.read(self.chunk_size)
47
+
48
+
49
+ class BytesWriter(Writer):
50
+ def __init__(self):
51
+ self.buffer = BytesIO()
52
+
53
+ async def write(self, data: bytes) -> None:
54
+ if len(data) > 0:
55
+ self.buffer.write(data)
56
+
57
+ def get_data(self) -> bytes:
58
+ return self.buffer.getvalue()
59
+
60
+
61
+ set_logger(PrintLogger(), "debug")
62
+
63
+
64
+ async def main():
65
+ uniffi_set_event_loop(asyncio.get_running_loop()) # type: ignore[arg-type]
66
+ app_id = b"\x01" * 32
67
+
68
+ builder = Builder("https://app.sia.storage")
69
+
70
+ await builder.request_connection(
71
+ AppMeta(
72
+ id=app_id,
73
+ name="python example",
74
+ description="an example app",
75
+ service_url="https://example.com",
76
+ logo_url=None,
77
+ callback_url=None,
78
+ )
79
+ )
80
+
81
+ print(f"Please approve connection {builder.response_url()}")
82
+ await builder.wait_for_approval()
83
+
84
+ print("Enter mnemonic (or leave empty to generate new):")
85
+ mnemonic = stdin.readline().strip()
86
+ if not mnemonic:
87
+ mnemonic = generate_recovery_phrase()
88
+ print("mnemonic:", mnemonic)
89
+
90
+ sdk = await builder.register(mnemonic)
91
+
92
+ # Store the app key for later use
93
+ app_key = sdk.app_key()
94
+ print("App registered", app_key.export())
95
+
96
+ print("Connected to indexd")
97
+
98
+ start = datetime.now(timezone.utc)
99
+ upload = await sdk.upload_packed(UploadOptions())
100
+
101
+ i = 0
102
+ data = None
103
+ while upload.slabs() < 4:
104
+ data = urandom(randint(1024, 1024 * 1024))
105
+ reader = BytesReader(data)
106
+ add_start = datetime.now(timezone.utc)
107
+ size = await upload.add(reader)
108
+ elapsed = datetime.now(timezone.utc) - add_start
109
+ print(
110
+ f"upload {i} added {size} bytes ({upload.length()} bytes, {upload.remaining()} remaining, {upload.slabs()} slab) in {elapsed}"
111
+ )
112
+ i += 1
113
+
114
+ objects = await upload.finalize()
115
+ elapsed = datetime.now(timezone.utc) - start
116
+ print(f"Upload finished {len(objects)} objects in {elapsed}")
117
+
118
+ start = datetime.now(timezone.utc)
119
+ writer = BytesWriter()
120
+ print(f"Downloading object {objects[-1].id()} {objects[-1].size()} bytes")
121
+ await sdk.download(writer, objects[-1], DownloadOptions())
122
+ if writer.get_data() != data:
123
+ print("Downloaded data does not match uploaded data")
124
+ elapsed = datetime.now(timezone.utc) - start
125
+ print(
126
+ f"Downloaded object {objects[-1].id()} with {len(writer.get_data())} bytes in {elapsed}"
127
+ )
128
+
129
+
130
+ asyncio.run(main())
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ uniffi::uniffi_bindgen_main()
3
+ }