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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +179 -0
- package/package.json +29 -0
- package/template/CLAUDE.md +160 -0
- package/template/README.md +102 -0
- package/template/_gitignore +5 -0
- package/template/biome.json +40 -0
- package/template/index.html +13 -0
- package/template/package.json +30 -0
- package/template/rust/README.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
- package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
- package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
- package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
- package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
- package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
- package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
- package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
- package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
- package/template/rust/sia-sdk-rs/LICENSE +21 -0
- package/template/rust/sia-sdk-rs/README.md +30 -0
- package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
- package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
- package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
- package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
- package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
- package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
- package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
- package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
- package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
- package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
- package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
- package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
- package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
- package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
- package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
- package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
- package/template/rust/sia-sdk-rs/knope.toml +54 -0
- package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
- package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
- package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
- package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
- package/template/scripts/setup-rust.js +29 -0
- package/template/src/App.tsx +13 -0
- package/template/src/components/DevNote.tsx +21 -0
- package/template/src/components/auth/ApproveScreen.tsx +84 -0
- package/template/src/components/auth/AuthFlow.tsx +77 -0
- package/template/src/components/auth/ConnectScreen.tsx +214 -0
- package/template/src/components/auth/LoadingScreen.tsx +8 -0
- package/template/src/components/auth/RecoveryScreen.tsx +182 -0
- package/template/src/components/upload/UploadZone.tsx +314 -0
- package/template/src/index.css +9 -0
- package/template/src/lib/constants.ts +8 -0
- package/template/src/lib/format.ts +35 -0
- package/template/src/lib/hex.ts +13 -0
- package/template/src/lib/sdk.ts +25 -0
- package/template/src/lib/wasm-env.ts +5 -0
- package/template/src/main.tsx +12 -0
- package/template/src/stores/auth.ts +86 -0
- package/template/tsconfig.app.json +31 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +18 -0
- package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
- package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
- package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
- 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())
|