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,818 @@
|
|
|
1
|
+
use std::io::Cursor;
|
|
2
|
+
use std::pin::Pin;
|
|
3
|
+
use std::sync::{Arc, Mutex};
|
|
4
|
+
use std::task::{Context, Poll};
|
|
5
|
+
|
|
6
|
+
use indexd::app_client::{RegisterAppRequest, RegisterAppResponse};
|
|
7
|
+
use js_sys::Uint8Array;
|
|
8
|
+
use tokio::io::AsyncWrite;
|
|
9
|
+
use sia::seed::Seed;
|
|
10
|
+
use sia::signing::{PrivateKey, Signature};
|
|
11
|
+
use sia::types::Hash256;
|
|
12
|
+
use std::str::FromStr;
|
|
13
|
+
use std::time::SystemTime;
|
|
14
|
+
use chrono::{TimeZone, Utc};
|
|
15
|
+
use wasm_bindgen::prelude::*;
|
|
16
|
+
|
|
17
|
+
/// Install a panic hook and logging bridge so that Rust panics show a proper
|
|
18
|
+
/// stack trace and `log::debug!()` / `log::info!()` etc. appear in the browser
|
|
19
|
+
/// console.
|
|
20
|
+
#[wasm_bindgen(start)]
|
|
21
|
+
fn init_panic_hook() {
|
|
22
|
+
console_error_panic_hook::set_once();
|
|
23
|
+
console_log::init_with_level(log::Level::Trace).ok();
|
|
24
|
+
log::info!("indexd WASM initialized - logging at TRACE level");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Converts a JsValue error context into a JsError.
|
|
28
|
+
fn to_js_err(e: impl std::fmt::Display) -> JsError {
|
|
29
|
+
JsError::new(&e.to_string())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── AppKey ──────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
#[wasm_bindgen]
|
|
35
|
+
pub struct AppKey(PrivateKey);
|
|
36
|
+
|
|
37
|
+
#[wasm_bindgen]
|
|
38
|
+
impl AppKey {
|
|
39
|
+
/// Imports an AppKey from a 64-byte ed25519 keypair or a 32-byte seed.
|
|
40
|
+
#[wasm_bindgen(constructor)]
|
|
41
|
+
pub fn new(key: &[u8]) -> Result<AppKey, JsError> {
|
|
42
|
+
match key.len() {
|
|
43
|
+
64 => {
|
|
44
|
+
let mut keypair = [0u8; 64];
|
|
45
|
+
keypair.copy_from_slice(key);
|
|
46
|
+
Ok(AppKey(PrivateKey::from(keypair)))
|
|
47
|
+
}
|
|
48
|
+
32 => {
|
|
49
|
+
let mut seed = [0u8; 32];
|
|
50
|
+
seed.copy_from_slice(key);
|
|
51
|
+
Ok(AppKey(PrivateKey::from_seed(&seed)))
|
|
52
|
+
}
|
|
53
|
+
_ => Err(JsError::new("app key must be 64 bytes (keypair) or 32 bytes (seed)")),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Exports the full 64-byte ed25519 keypair.
|
|
58
|
+
pub fn export(&self) -> Uint8Array {
|
|
59
|
+
Uint8Array::from(self.0.as_ref())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the hex-encoded public key.
|
|
63
|
+
#[wasm_bindgen(js_name = "publicKey")]
|
|
64
|
+
pub fn public_key(&self) -> String {
|
|
65
|
+
self.0.public_key().to_string()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Signs a message, returning the 64-byte signature.
|
|
69
|
+
pub fn sign(&self, message: &[u8]) -> Uint8Array {
|
|
70
|
+
let sig = self.0.sign(message);
|
|
71
|
+
Uint8Array::from(sig.as_ref() as &[u8])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Verifies a signature against a message.
|
|
75
|
+
#[wasm_bindgen(js_name = "verifySignature")]
|
|
76
|
+
pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Result<bool, JsError> {
|
|
77
|
+
if signature.len() != 64 {
|
|
78
|
+
return Err(JsError::new("signatures must be 64 bytes"));
|
|
79
|
+
}
|
|
80
|
+
let mut sig_bytes = [0u8; 64];
|
|
81
|
+
sig_bytes.copy_from_slice(signature);
|
|
82
|
+
Ok(self
|
|
83
|
+
.0
|
|
84
|
+
.public_key()
|
|
85
|
+
.verify(message, &Signature::from(sig_bytes)))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── PinnedObject ────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
#[wasm_bindgen]
|
|
92
|
+
pub struct PinnedObject {
|
|
93
|
+
inner: Arc<Mutex<indexd::Object>>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[wasm_bindgen]
|
|
97
|
+
impl PinnedObject {
|
|
98
|
+
/// Opens a sealed object (JSON) using the provided app key.
|
|
99
|
+
pub fn open(app_key: &AppKey, sealed_json: &str) -> Result<PinnedObject, JsError> {
|
|
100
|
+
let sealed: indexd::SealedObject =
|
|
101
|
+
serde_json::from_str(sealed_json).map_err(to_js_err)?;
|
|
102
|
+
let obj = sealed.open(&app_key.0).map_err(to_js_err)?;
|
|
103
|
+
Ok(PinnedObject {
|
|
104
|
+
inner: Arc::new(Mutex::new(obj)),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Seals the object for offline storage, returning JSON.
|
|
109
|
+
pub fn seal(&self, app_key: &AppKey) -> Result<String, JsError> {
|
|
110
|
+
let inner = self.inner.lock().map_err(to_js_err)?;
|
|
111
|
+
let sealed = inner.seal(&app_key.0);
|
|
112
|
+
serde_json::to_string(&sealed).map_err(to_js_err)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Returns the object's ID as a hex string.
|
|
116
|
+
pub fn id(&self) -> Result<String, JsError> {
|
|
117
|
+
let inner = self.inner.lock().map_err(to_js_err)?;
|
|
118
|
+
Ok(inner.id().to_string())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Returns the total size of the object in bytes.
|
|
122
|
+
pub fn size(&self) -> Result<f64, JsError> {
|
|
123
|
+
let inner = self.inner.lock().map_err(to_js_err)?;
|
|
124
|
+
Ok(inner.size() as f64)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Returns the metadata as a Uint8Array.
|
|
128
|
+
pub fn metadata(&self) -> Result<Uint8Array, JsError> {
|
|
129
|
+
let inner = self.inner.lock().map_err(to_js_err)?;
|
|
130
|
+
Ok(Uint8Array::from(inner.metadata.as_slice()))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Updates the metadata.
|
|
134
|
+
#[wasm_bindgen(js_name = "updateMetadata")]
|
|
135
|
+
pub fn update_metadata(&self, metadata: &[u8]) -> Result<(), JsError> {
|
|
136
|
+
let mut inner = self.inner.lock().map_err(to_js_err)?;
|
|
137
|
+
inner.metadata = metadata.to_vec();
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── SDK ─────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
#[wasm_bindgen]
|
|
145
|
+
pub struct SDK {
|
|
146
|
+
inner: indexd::SDK,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[wasm_bindgen]
|
|
150
|
+
impl SDK {
|
|
151
|
+
/// Returns the app key used by this SDK instance.
|
|
152
|
+
#[wasm_bindgen(js_name = "appKey")]
|
|
153
|
+
pub fn app_key(&self) -> AppKey {
|
|
154
|
+
AppKey(self.inner.app_key().clone())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Uploads a Uint8Array to the Sia network.
|
|
158
|
+
///
|
|
159
|
+
/// Returns a PinnedObject containing the metadata needed to download the data.
|
|
160
|
+
pub async fn upload(&self, data: &[u8]) -> Result<PinnedObject, JsError> {
|
|
161
|
+
log::info!("upload: starting ({} bytes)", data.len());
|
|
162
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
163
|
+
.build()
|
|
164
|
+
.map_err(to_js_err)?;
|
|
165
|
+
let _guard = rt.enter();
|
|
166
|
+
let local = tokio::task::LocalSet::new();
|
|
167
|
+
let cursor = Cursor::new(data.to_vec());
|
|
168
|
+
let object: indexd::Object = local
|
|
169
|
+
.run_until(self.inner.upload(cursor, indexd::UploadOptions::default()))
|
|
170
|
+
.await
|
|
171
|
+
.map_err(to_js_err)?;
|
|
172
|
+
log::info!("upload: complete");
|
|
173
|
+
Ok(PinnedObject {
|
|
174
|
+
inner: Arc::new(Mutex::new(object)),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Downloads an object's data, returning a Uint8Array.
|
|
179
|
+
pub async fn download(&self, object: &PinnedObject) -> Result<Uint8Array, JsError> {
|
|
180
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
181
|
+
.build()
|
|
182
|
+
.map_err(to_js_err)?;
|
|
183
|
+
let _guard = rt.enter();
|
|
184
|
+
let local = tokio::task::LocalSet::new();
|
|
185
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
186
|
+
let size = obj.size() as usize;
|
|
187
|
+
let mut buf = vec![0u8; size];
|
|
188
|
+
local
|
|
189
|
+
.run_until(async {
|
|
190
|
+
self.inner.download(
|
|
191
|
+
&mut Cursor::new(&mut buf),
|
|
192
|
+
&obj,
|
|
193
|
+
indexd::DownloadOptions::default(),
|
|
194
|
+
).await
|
|
195
|
+
})
|
|
196
|
+
.await
|
|
197
|
+
.map_err(to_js_err)?;
|
|
198
|
+
Ok(Uint8Array::from(buf.as_slice()))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Uploads a Uint8Array with per-shard progress reporting.
|
|
202
|
+
///
|
|
203
|
+
/// The `on_progress` callback receives `(current_shards, total_shards)`.
|
|
204
|
+
#[wasm_bindgen(js_name = "uploadWithProgress")]
|
|
205
|
+
pub async fn upload_with_progress(
|
|
206
|
+
&self,
|
|
207
|
+
data: &[u8],
|
|
208
|
+
on_progress: &js_sys::Function,
|
|
209
|
+
) -> Result<PinnedObject, JsError> {
|
|
210
|
+
log::info!("upload_with_progress: starting ({} bytes)", data.len());
|
|
211
|
+
let slab_data_size = 10usize * 4_194_304; // data_shards(10) * SECTOR_SIZE(4 MiB)
|
|
212
|
+
let num_slabs = if data.is_empty() { 0 } else { data.len().div_ceil(slab_data_size) };
|
|
213
|
+
let total_shards = (num_slabs as u32) * 30; // 30 shards per slab (10 data + 20 parity)
|
|
214
|
+
|
|
215
|
+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
|
216
|
+
let options = indexd::UploadOptions {
|
|
217
|
+
shard_uploaded: Some(tx),
|
|
218
|
+
..self.inner.default_upload_options()
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
222
|
+
.build()
|
|
223
|
+
.map_err(to_js_err)?;
|
|
224
|
+
let _guard = rt.enter();
|
|
225
|
+
let local = tokio::task::LocalSet::new();
|
|
226
|
+
let cursor = Cursor::new(data.to_vec());
|
|
227
|
+
let on_progress = on_progress.clone();
|
|
228
|
+
let object: indexd::Object = local
|
|
229
|
+
.run_until(async {
|
|
230
|
+
tokio::task::spawn_local(async move {
|
|
231
|
+
let mut count: u32 = 0;
|
|
232
|
+
while rx.recv().await.is_some() {
|
|
233
|
+
count += 1;
|
|
234
|
+
let _ = on_progress.call2(
|
|
235
|
+
&JsValue::NULL,
|
|
236
|
+
&JsValue::from(count),
|
|
237
|
+
&JsValue::from(total_shards),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
self.inner.upload(cursor, options).await
|
|
242
|
+
})
|
|
243
|
+
.await
|
|
244
|
+
.map_err(to_js_err)?;
|
|
245
|
+
log::info!("upload_with_progress: complete");
|
|
246
|
+
Ok(PinnedObject {
|
|
247
|
+
inner: Arc::new(Mutex::new(object)),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Downloads an object's data with per-slab progress reporting.
|
|
252
|
+
///
|
|
253
|
+
/// The `on_progress` callback receives `(current_slabs, total_slabs)`.
|
|
254
|
+
#[wasm_bindgen(js_name = "downloadWithProgress")]
|
|
255
|
+
pub async fn download_with_progress(
|
|
256
|
+
&self,
|
|
257
|
+
object: &PinnedObject,
|
|
258
|
+
on_progress: &js_sys::Function,
|
|
259
|
+
) -> Result<Uint8Array, JsError> {
|
|
260
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
261
|
+
.build()
|
|
262
|
+
.map_err(to_js_err)?;
|
|
263
|
+
let _guard = rt.enter();
|
|
264
|
+
let local = tokio::task::LocalSet::new();
|
|
265
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
266
|
+
let size = obj.size() as usize;
|
|
267
|
+
let total_slabs = obj.slabs().len() as u32;
|
|
268
|
+
let mut buf = vec![0u8; size];
|
|
269
|
+
|
|
270
|
+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
|
271
|
+
let options = indexd::DownloadOptions {
|
|
272
|
+
slab_downloaded: Some(tx),
|
|
273
|
+
..self.inner.default_download_options()
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let on_progress = on_progress.clone();
|
|
277
|
+
local
|
|
278
|
+
.run_until(async {
|
|
279
|
+
tokio::task::spawn_local(async move {
|
|
280
|
+
let mut count: u32 = 0;
|
|
281
|
+
while rx.recv().await.is_some() {
|
|
282
|
+
count += 1;
|
|
283
|
+
let _ = on_progress.call2(
|
|
284
|
+
&JsValue::NULL,
|
|
285
|
+
&JsValue::from(count),
|
|
286
|
+
&JsValue::from(total_slabs),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
self.inner
|
|
291
|
+
.download(&mut Cursor::new(&mut buf), &obj, options)
|
|
292
|
+
.await
|
|
293
|
+
})
|
|
294
|
+
.await
|
|
295
|
+
.map_err(to_js_err)?;
|
|
296
|
+
Ok(Uint8Array::from(buf.as_slice()))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Downloads an object with streaming chunks.
|
|
300
|
+
/// Fires `on_chunk(bytes)` after each slab is decoded and `on_progress(current, total)` for progress.
|
|
301
|
+
#[wasm_bindgen(js_name = "downloadStreaming")]
|
|
302
|
+
pub async fn download_streaming(
|
|
303
|
+
&self,
|
|
304
|
+
object: &PinnedObject,
|
|
305
|
+
on_chunk: &js_sys::Function,
|
|
306
|
+
on_progress: &js_sys::Function,
|
|
307
|
+
) -> Result<(), JsError> {
|
|
308
|
+
/// Custom AsyncWrite that calls JS callback with each chunk
|
|
309
|
+
struct ChunkWriter {
|
|
310
|
+
callback: js_sys::Function,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
impl AsyncWrite for ChunkWriter {
|
|
314
|
+
fn poll_write(
|
|
315
|
+
self: Pin<&mut Self>,
|
|
316
|
+
_cx: &mut Context<'_>,
|
|
317
|
+
buf: &[u8],
|
|
318
|
+
) -> Poll<Result<usize, std::io::Error>> {
|
|
319
|
+
let array = Uint8Array::from(buf);
|
|
320
|
+
let _ = self.callback.call1(&JsValue::NULL, &array);
|
|
321
|
+
Poll::Ready(Ok(buf.len()))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn poll_flush(
|
|
325
|
+
self: Pin<&mut Self>,
|
|
326
|
+
_cx: &mut Context<'_>,
|
|
327
|
+
) -> Poll<Result<(), std::io::Error>> {
|
|
328
|
+
Poll::Ready(Ok(()))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fn poll_shutdown(
|
|
332
|
+
self: Pin<&mut Self>,
|
|
333
|
+
_cx: &mut Context<'_>,
|
|
334
|
+
) -> Poll<Result<(), std::io::Error>> {
|
|
335
|
+
Poll::Ready(Ok(()))
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
340
|
+
.build()
|
|
341
|
+
.map_err(to_js_err)?;
|
|
342
|
+
let _guard = rt.enter();
|
|
343
|
+
let local = tokio::task::LocalSet::new();
|
|
344
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
345
|
+
let total_slabs = obj.slabs().len() as u32;
|
|
346
|
+
|
|
347
|
+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
|
348
|
+
let options = indexd::DownloadOptions {
|
|
349
|
+
slab_downloaded: Some(tx),
|
|
350
|
+
..self.inner.default_download_options()
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let mut writer = ChunkWriter {
|
|
354
|
+
callback: on_chunk.clone(),
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
let on_progress = on_progress.clone();
|
|
358
|
+
local
|
|
359
|
+
.run_until(async {
|
|
360
|
+
tokio::task::spawn_local(async move {
|
|
361
|
+
let mut count: u32 = 0;
|
|
362
|
+
while rx.recv().await.is_some() {
|
|
363
|
+
count += 1;
|
|
364
|
+
let _ = on_progress.call2(
|
|
365
|
+
&JsValue::NULL,
|
|
366
|
+
&JsValue::from(count),
|
|
367
|
+
&JsValue::from(total_slabs),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
self.inner.download(&mut writer, &obj, options).await
|
|
372
|
+
})
|
|
373
|
+
.await
|
|
374
|
+
.map_err(to_js_err)?;
|
|
375
|
+
Ok(())
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Returns hosts as a JSON array.
|
|
379
|
+
pub async fn hosts(&self) -> Result<JsValue, JsError> {
|
|
380
|
+
let hosts = self
|
|
381
|
+
.inner
|
|
382
|
+
.hosts(Default::default())
|
|
383
|
+
.await
|
|
384
|
+
.map_err(to_js_err)?;
|
|
385
|
+
serde_wasm_bindgen::to_value(&hosts).map_err(to_js_err)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Returns account information as a JS object.
|
|
389
|
+
pub async fn account(&self) -> Result<JsValue, JsError> {
|
|
390
|
+
let a = self.inner.account().await.map_err(to_js_err)?;
|
|
391
|
+
let obj = serde_json::json!({
|
|
392
|
+
"accountKey": a.account_key.to_string(),
|
|
393
|
+
"connectKey": a.connect_key,
|
|
394
|
+
"maxPinnedData": a.max_pinned_data,
|
|
395
|
+
"pinnedData": a.pinned_data,
|
|
396
|
+
"app": {
|
|
397
|
+
"id": a.app.id.to_string(),
|
|
398
|
+
"description": a.app.description,
|
|
399
|
+
"serviceUrl": a.app.service_url,
|
|
400
|
+
"logoUrl": a.app.logo_url,
|
|
401
|
+
},
|
|
402
|
+
"lastUsed": a.last_used.timestamp_millis(),
|
|
403
|
+
});
|
|
404
|
+
serde_wasm_bindgen::to_value(&obj).map_err(to_js_err)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Retrieves a pinned object by its hex-encoded key.
|
|
408
|
+
pub async fn object(&self, key: &str) -> Result<PinnedObject, JsError> {
|
|
409
|
+
let key = Hash256::from_str(key).map_err(to_js_err)?;
|
|
410
|
+
let obj = self.inner.object(&key).await.map_err(to_js_err)?;
|
|
411
|
+
Ok(PinnedObject {
|
|
412
|
+
inner: Arc::new(Mutex::new(obj)),
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// Pins an object to the indexer.
|
|
417
|
+
#[wasm_bindgen(js_name = "pinObject")]
|
|
418
|
+
pub async fn pin_object(&self, object: &PinnedObject) -> Result<(), JsError> {
|
|
419
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
420
|
+
self.inner.pin_object(&obj).await.map_err(to_js_err)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Updates the metadata of an object already stored in the indexer.
|
|
424
|
+
#[wasm_bindgen(js_name = "updateObjectMetadata")]
|
|
425
|
+
pub async fn update_object_metadata(&self, object: &PinnedObject) -> Result<(), JsError> {
|
|
426
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
427
|
+
self.inner
|
|
428
|
+
.update_object_metadata(&obj)
|
|
429
|
+
.await
|
|
430
|
+
.map_err(to_js_err)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// Deletes an object from the indexer by its hex-encoded key.
|
|
434
|
+
#[wasm_bindgen(js_name = "deleteObject")]
|
|
435
|
+
pub async fn delete_object(&self, key: &str) -> Result<(), JsError> {
|
|
436
|
+
let key = Hash256::from_str(key).map_err(to_js_err)?;
|
|
437
|
+
self.inner.delete_object(&key).await.map_err(to_js_err)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// Prunes unused slabs from the indexer.
|
|
441
|
+
#[wasm_bindgen(js_name = "pruneSlabs")]
|
|
442
|
+
pub async fn prune_slabs(&self) -> Result<(), JsError> {
|
|
443
|
+
self.inner.prune_slabs().await.map_err(to_js_err)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Returns object events for syncing. Supports cursor-based pagination.
|
|
447
|
+
///
|
|
448
|
+
/// `cursor_json` is an optional JSON string: `{"id": "hex...", "after": <epoch_ms>}`
|
|
449
|
+
/// `limit` is the maximum number of events to return.
|
|
450
|
+
///
|
|
451
|
+
/// Returns a JS array of objects:
|
|
452
|
+
/// `[{ id: string, deleted: bool, updatedAt: number, object: PinnedObject | null }]`
|
|
453
|
+
#[wasm_bindgen(js_name = "objectEvents")]
|
|
454
|
+
pub async fn object_events(
|
|
455
|
+
&self,
|
|
456
|
+
cursor_json: Option<String>,
|
|
457
|
+
limit: u32,
|
|
458
|
+
) -> Result<JsValue, JsError> {
|
|
459
|
+
let cursor = match cursor_json {
|
|
460
|
+
Some(json) => {
|
|
461
|
+
let parsed: serde_json::Value = serde_json::from_str(&json).map_err(to_js_err)?;
|
|
462
|
+
let id_str = parsed["id"].as_str().ok_or_else(|| JsError::new("cursor missing 'id'"))?;
|
|
463
|
+
let after_ms = parsed["after"].as_i64().ok_or_else(|| JsError::new("cursor missing 'after'"))?;
|
|
464
|
+
let id = sia::types::Hash256::from_str(id_str).map_err(to_js_err)?;
|
|
465
|
+
let after = Utc.timestamp_millis_opt(after_ms)
|
|
466
|
+
.single()
|
|
467
|
+
.ok_or_else(|| JsError::new("invalid 'after' timestamp"))?;
|
|
468
|
+
Some(indexd::app_client::ObjectsCursor { id, after })
|
|
469
|
+
}
|
|
470
|
+
None => None,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
let events = self.inner.object_events(cursor, Some(limit as usize)).await.map_err(to_js_err)?;
|
|
474
|
+
|
|
475
|
+
let arr = js_sys::Array::new();
|
|
476
|
+
let js_err = |_| JsError::new("failed to set property on object");
|
|
477
|
+
for event in events {
|
|
478
|
+
let obj = js_sys::Object::new();
|
|
479
|
+
js_sys::Reflect::set(&obj, &"id".into(), &event.id.to_string().into())
|
|
480
|
+
.map_err(js_err)?;
|
|
481
|
+
js_sys::Reflect::set(&obj, &"deleted".into(), &event.deleted.into())
|
|
482
|
+
.map_err(js_err)?;
|
|
483
|
+
js_sys::Reflect::set(
|
|
484
|
+
&obj,
|
|
485
|
+
&"updatedAt".into(),
|
|
486
|
+
&JsValue::from_f64(event.updated_at.timestamp_millis() as f64),
|
|
487
|
+
)
|
|
488
|
+
.map_err(js_err)?;
|
|
489
|
+
|
|
490
|
+
let pinned_val = match event.object {
|
|
491
|
+
Some(o) => {
|
|
492
|
+
let pinned = PinnedObject {
|
|
493
|
+
inner: Arc::new(Mutex::new(o)),
|
|
494
|
+
};
|
|
495
|
+
pinned.into()
|
|
496
|
+
}
|
|
497
|
+
None => JsValue::NULL,
|
|
498
|
+
};
|
|
499
|
+
js_sys::Reflect::set(&obj, &"object".into(), &pinned_val)
|
|
500
|
+
.map_err(js_err)?;
|
|
501
|
+
|
|
502
|
+
arr.push(&obj);
|
|
503
|
+
}
|
|
504
|
+
Ok(arr.into())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/// Creates a share URL for an object, valid until the given timestamp (ms since epoch).
|
|
508
|
+
#[wasm_bindgen(js_name = "shareObject")]
|
|
509
|
+
pub fn share_object(
|
|
510
|
+
&self,
|
|
511
|
+
object: &PinnedObject,
|
|
512
|
+
valid_until_ms: f64,
|
|
513
|
+
) -> Result<String, JsError> {
|
|
514
|
+
let obj = object.inner.lock().map_err(to_js_err)?.clone();
|
|
515
|
+
let duration = std::time::Duration::from_millis(valid_until_ms as u64);
|
|
516
|
+
let valid_until = SystemTime::UNIX_EPOCH + duration;
|
|
517
|
+
let url = self
|
|
518
|
+
.inner
|
|
519
|
+
.share_object(&obj, valid_until.into())
|
|
520
|
+
.map_err(to_js_err)?;
|
|
521
|
+
// sia:// URLs can open the mobile app when clicked
|
|
522
|
+
// FIXME Alright - could leave this for the app to implement
|
|
523
|
+
let url_str = url.to_string();
|
|
524
|
+
Ok(if let Some(rest) = url_str.strip_prefix("https://") {
|
|
525
|
+
format!("sia://{rest}")
|
|
526
|
+
} else {
|
|
527
|
+
url_str
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// Retrieves a shared object from a signed share URL.
|
|
532
|
+
/// Accepts both `https://` and `sia://` schemes.
|
|
533
|
+
#[wasm_bindgen(js_name = "sharedObject")]
|
|
534
|
+
pub async fn shared_object(&self, share_url: &str) -> Result<PinnedObject, JsError> {
|
|
535
|
+
let url = if share_url.starts_with("sia://") {
|
|
536
|
+
format!("https://{}", &share_url[6..])
|
|
537
|
+
} else {
|
|
538
|
+
share_url.to_string()
|
|
539
|
+
};
|
|
540
|
+
let obj = self
|
|
541
|
+
.inner
|
|
542
|
+
.shared_object(url)
|
|
543
|
+
.await
|
|
544
|
+
.map_err(to_js_err)?;
|
|
545
|
+
Ok(PinnedObject {
|
|
546
|
+
inner: Arc::new(Mutex::new(obj)),
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Builder ─────────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
#[wasm_bindgen]
|
|
554
|
+
pub struct Builder {
|
|
555
|
+
state: Arc<Mutex<Option<BuilderState>>>,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
enum BuilderState {
|
|
559
|
+
Disconnected(indexd::Builder<indexd::DisconnectedState>),
|
|
560
|
+
RequestingApproval(indexd::Builder<indexd::RequestingApprovalState>),
|
|
561
|
+
Approved(indexd::Builder<indexd::ApprovedState>),
|
|
562
|
+
Finalized,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#[wasm_bindgen]
|
|
566
|
+
impl Builder {
|
|
567
|
+
/// Creates a new SDK builder for the given indexer URL.
|
|
568
|
+
#[wasm_bindgen(constructor)]
|
|
569
|
+
pub fn new(indexer_url: &str) -> Result<Builder, JsError> {
|
|
570
|
+
let builder = indexd::Builder::new(indexer_url).map_err(to_js_err)?;
|
|
571
|
+
Ok(Builder {
|
|
572
|
+
state: Arc::new(Mutex::new(Some(BuilderState::Disconnected(builder)))),
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/// Sets the maximum number of concurrent price fetches (default: 1).
|
|
577
|
+
/// Lower values = more stable, higher values = faster but may crash browser.
|
|
578
|
+
#[wasm_bindgen(js_name = "withMaxPriceFetches")]
|
|
579
|
+
pub fn with_max_price_fetches(&self, max: usize) -> Result<(), JsError> {
|
|
580
|
+
let mut state = self.state.lock().map_err(to_js_err)?;
|
|
581
|
+
if let Some(BuilderState::Disconnected(builder)) = state.take() {
|
|
582
|
+
let builder = builder.with_max_price_fetches(max);
|
|
583
|
+
*state = Some(BuilderState::Disconnected(builder));
|
|
584
|
+
Ok(())
|
|
585
|
+
} else {
|
|
586
|
+
Err(JsError::new("Can only set concurrency on disconnected builder"))
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/// Sets the maximum number of concurrent downloads (default: 2).
|
|
591
|
+
/// Lower values = more stable, higher values = faster but may crash browser.
|
|
592
|
+
#[wasm_bindgen(js_name = "withMaxDownloads")]
|
|
593
|
+
pub fn with_max_downloads(&self, max: usize) -> Result<(), JsError> {
|
|
594
|
+
let mut state = self.state.lock().map_err(to_js_err)?;
|
|
595
|
+
if let Some(BuilderState::Disconnected(builder)) = state.take() {
|
|
596
|
+
let builder = builder.with_max_downloads(max);
|
|
597
|
+
*state = Some(BuilderState::Disconnected(builder));
|
|
598
|
+
Ok(())
|
|
599
|
+
} else {
|
|
600
|
+
Err(JsError::new("Can only set concurrency on disconnected builder"))
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Sets the maximum number of concurrent uploads (default: 3).
|
|
605
|
+
/// Lower values = more stable, higher values = faster but may crash browser.
|
|
606
|
+
#[wasm_bindgen(js_name = "withMaxUploads")]
|
|
607
|
+
pub fn with_max_uploads(&self, max: usize) -> Result<(), JsError> {
|
|
608
|
+
let mut state = self.state.lock().map_err(to_js_err)?;
|
|
609
|
+
if let Some(BuilderState::Disconnected(builder)) = state.take() {
|
|
610
|
+
let builder = builder.with_max_uploads(max);
|
|
611
|
+
*state = Some(BuilderState::Disconnected(builder));
|
|
612
|
+
Ok(())
|
|
613
|
+
} else {
|
|
614
|
+
Err(JsError::new("Can only set concurrency on disconnected builder"))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/// Attempts to connect using an existing app key.
|
|
619
|
+
///
|
|
620
|
+
/// Returns the SDK if authenticated, or null if the key is not recognized.
|
|
621
|
+
/// Call `requestConnection` if null is returned.
|
|
622
|
+
pub async fn connected(&self, app_key: &AppKey) -> Result<JsValue, JsError> {
|
|
623
|
+
let state = self
|
|
624
|
+
.state
|
|
625
|
+
.lock()
|
|
626
|
+
.map_err(to_js_err)?
|
|
627
|
+
.take()
|
|
628
|
+
.ok_or_else(|| JsError::new("invalid builder state"))?;
|
|
629
|
+
|
|
630
|
+
match state {
|
|
631
|
+
BuilderState::Disconnected(builder) => {
|
|
632
|
+
match builder.connected(&app_key.0).await.map_err(to_js_err)? {
|
|
633
|
+
Some(sdk) => {
|
|
634
|
+
*self.state.lock().map_err(to_js_err)? = Some(BuilderState::Finalized);
|
|
635
|
+
Ok(SDK { inner: sdk }.into())
|
|
636
|
+
}
|
|
637
|
+
None => {
|
|
638
|
+
*self.state.lock().map_err(to_js_err)? =
|
|
639
|
+
Some(BuilderState::Disconnected(builder));
|
|
640
|
+
Ok(JsValue::NULL)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
other => {
|
|
645
|
+
*self.state.lock().map_err(to_js_err)? = Some(other);
|
|
646
|
+
Err(JsError::new("invalid state: expected Disconnected"))
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/// Requests a new app connection. Pass app metadata as a JSON object:
|
|
652
|
+
/// ```json
|
|
653
|
+
/// {
|
|
654
|
+
/// "app_id": [32 bytes as hex],
|
|
655
|
+
/// "name": "My App",
|
|
656
|
+
/// "description": "...",
|
|
657
|
+
/// "service_url": "https://...",
|
|
658
|
+
/// "logo_url": "https://..." (optional),
|
|
659
|
+
/// "callback_url": "https://..." (optional)
|
|
660
|
+
/// }
|
|
661
|
+
/// ```
|
|
662
|
+
#[wasm_bindgen(js_name = "requestConnection")]
|
|
663
|
+
pub async fn request_connection(&self, app_meta_json: &str) -> Result<(), JsError> {
|
|
664
|
+
let meta: RegisterAppRequest =
|
|
665
|
+
serde_json::from_str(app_meta_json).map_err(to_js_err)?;
|
|
666
|
+
|
|
667
|
+
let state = self
|
|
668
|
+
.state
|
|
669
|
+
.lock()
|
|
670
|
+
.map_err(to_js_err)?
|
|
671
|
+
.take()
|
|
672
|
+
.ok_or_else(|| JsError::new("invalid builder state"))?;
|
|
673
|
+
|
|
674
|
+
match state {
|
|
675
|
+
BuilderState::Disconnected(builder) => {
|
|
676
|
+
let builder = builder
|
|
677
|
+
.request_connection(&meta)
|
|
678
|
+
.await
|
|
679
|
+
.map_err(to_js_err)?;
|
|
680
|
+
*self.state.lock().map_err(to_js_err)? =
|
|
681
|
+
Some(BuilderState::RequestingApproval(builder));
|
|
682
|
+
Ok(())
|
|
683
|
+
}
|
|
684
|
+
other => {
|
|
685
|
+
*self.state.lock().map_err(to_js_err)? = Some(other);
|
|
686
|
+
Err(JsError::new("invalid state: expected Disconnected"))
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/// Transitions the builder using a pre-fetched connection response.
|
|
692
|
+
/// Use this when the `POST /auth/connect` call was made out-of-band
|
|
693
|
+
/// (e.g. via curl) to work around CORS restrictions.
|
|
694
|
+
///
|
|
695
|
+
/// `app_id_hex` is the hex-encoded app ID used in the request.
|
|
696
|
+
/// `response_json` is the JSON response from `POST /auth/connect`.
|
|
697
|
+
#[wasm_bindgen(js_name = "setConnectionResponse")]
|
|
698
|
+
pub fn set_connection_response(
|
|
699
|
+
&self,
|
|
700
|
+
app_id_hex: &str,
|
|
701
|
+
response_json: &str,
|
|
702
|
+
) -> Result<(), JsError> {
|
|
703
|
+
let app_id = Hash256::from_str(app_id_hex).map_err(to_js_err)?;
|
|
704
|
+
let response: RegisterAppResponse =
|
|
705
|
+
serde_json::from_str(response_json).map_err(to_js_err)?;
|
|
706
|
+
|
|
707
|
+
let state = self
|
|
708
|
+
.state
|
|
709
|
+
.lock()
|
|
710
|
+
.map_err(to_js_err)?
|
|
711
|
+
.take()
|
|
712
|
+
.ok_or_else(|| JsError::new("invalid builder state"))?;
|
|
713
|
+
|
|
714
|
+
match state {
|
|
715
|
+
BuilderState::Disconnected(builder) => {
|
|
716
|
+
let builder = builder
|
|
717
|
+
.with_connection_response(app_id, response)
|
|
718
|
+
.map_err(to_js_err)?;
|
|
719
|
+
*self.state.lock().map_err(to_js_err)? =
|
|
720
|
+
Some(BuilderState::RequestingApproval(builder));
|
|
721
|
+
Ok(())
|
|
722
|
+
}
|
|
723
|
+
other => {
|
|
724
|
+
*self.state.lock().map_err(to_js_err)? = Some(other);
|
|
725
|
+
Err(JsError::new("invalid state: expected Disconnected"))
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/// Returns the response URL the user must visit to authorize the connection.
|
|
731
|
+
#[wasm_bindgen(js_name = "responseUrl")]
|
|
732
|
+
pub fn response_url(&self) -> Result<String, JsError> {
|
|
733
|
+
let state = self.state.lock().map_err(to_js_err)?;
|
|
734
|
+
match state.as_ref() {
|
|
735
|
+
Some(BuilderState::RequestingApproval(builder)) => {
|
|
736
|
+
Ok(builder.response_url().to_owned())
|
|
737
|
+
}
|
|
738
|
+
_ => Err(JsError::new(
|
|
739
|
+
"invalid state: expected RequestingApproval",
|
|
740
|
+
)),
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/// Polls for approval. Resolves when the user approves.
|
|
745
|
+
#[wasm_bindgen(js_name = "waitForApproval")]
|
|
746
|
+
pub async fn wait_for_approval(&self) -> Result<(), JsError> {
|
|
747
|
+
let state = self
|
|
748
|
+
.state
|
|
749
|
+
.lock()
|
|
750
|
+
.map_err(to_js_err)?
|
|
751
|
+
.take()
|
|
752
|
+
.ok_or_else(|| JsError::new("invalid builder state"))?;
|
|
753
|
+
|
|
754
|
+
match state {
|
|
755
|
+
BuilderState::RequestingApproval(builder) => {
|
|
756
|
+
let builder = builder.wait_for_approval().await.map_err(to_js_err)?;
|
|
757
|
+
*self.state.lock().map_err(to_js_err)? = Some(BuilderState::Approved(builder));
|
|
758
|
+
Ok(())
|
|
759
|
+
}
|
|
760
|
+
other => {
|
|
761
|
+
*self.state.lock().map_err(to_js_err)? = Some(other);
|
|
762
|
+
Err(JsError::new(
|
|
763
|
+
"invalid state: expected RequestingApproval",
|
|
764
|
+
))
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// Registers the app using the user's recovery phrase and returns the SDK.
|
|
770
|
+
pub async fn register(&self, mnemonic: &str) -> Result<SDK, JsError> {
|
|
771
|
+
let state = self
|
|
772
|
+
.state
|
|
773
|
+
.lock()
|
|
774
|
+
.map_err(to_js_err)?
|
|
775
|
+
.take()
|
|
776
|
+
.ok_or_else(|| JsError::new("invalid builder state"))?;
|
|
777
|
+
|
|
778
|
+
match state {
|
|
779
|
+
BuilderState::Approved(builder) => {
|
|
780
|
+
let sdk = builder.register(mnemonic).await.map_err(to_js_err)?;
|
|
781
|
+
*self.state.lock().map_err(to_js_err)? = Some(BuilderState::Finalized);
|
|
782
|
+
Ok(SDK { inner: sdk })
|
|
783
|
+
}
|
|
784
|
+
other => {
|
|
785
|
+
*self.state.lock().map_err(to_js_err)? = Some(other);
|
|
786
|
+
Err(JsError::new("invalid state: expected Approved"))
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ── Free functions ──────────────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
/// Generates a new 12-word BIP-32 recovery phrase.
|
|
795
|
+
#[wasm_bindgen(js_name = "generateRecoveryPhrase")]
|
|
796
|
+
pub fn generate_recovery_phrase() -> String {
|
|
797
|
+
let seed: [u8; 16] = rand::random();
|
|
798
|
+
Seed::from_seed(seed).to_string()
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/// Validates a BIP-32 recovery phrase.
|
|
802
|
+
#[wasm_bindgen(js_name = "validateRecoveryPhrase")]
|
|
803
|
+
pub fn validate_recovery_phrase(phrase: &str) -> Result<(), JsError> {
|
|
804
|
+
Seed::new(phrase).map_err(to_js_err)?;
|
|
805
|
+
Ok(())
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/// Connects to a host via WebTransport and fetches its settings/prices.
|
|
809
|
+
///
|
|
810
|
+
/// `address` should be a host address like `host.example.com:9883`.
|
|
811
|
+
/// Returns the host settings as a JS object.
|
|
812
|
+
#[wasm_bindgen(js_name = "fetchHostSettings")]
|
|
813
|
+
pub async fn fetch_host_settings(address: &str) -> Result<JsValue, JsError> {
|
|
814
|
+
let settings = indexd::web_transport::fetch_host_settings(address)
|
|
815
|
+
.await
|
|
816
|
+
.map_err(to_js_err)?;
|
|
817
|
+
serde_wasm_bindgen::to_value(&settings).map_err(to_js_err)
|
|
818
|
+
}
|