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,377 @@
|
|
|
1
|
+
use std::sync::{Arc, Mutex};
|
|
2
|
+
|
|
3
|
+
use indexd::app_client::RegisterAppRequest;
|
|
4
|
+
use indexd::{self, Url};
|
|
5
|
+
use sia::seed::{self, Seed};
|
|
6
|
+
use sia::signing::{PrivateKey, Signature};
|
|
7
|
+
use thiserror::Error;
|
|
8
|
+
|
|
9
|
+
use crate::{AppMeta, SDK, spawn, tls};
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Error, uniffi::Error)]
|
|
12
|
+
#[uniffi(flat_error)]
|
|
13
|
+
pub enum AppKeyError {
|
|
14
|
+
#[error("app keys must be 32 bytes")]
|
|
15
|
+
InvalidLength,
|
|
16
|
+
|
|
17
|
+
#[error("signatures must be 64 bytes")]
|
|
18
|
+
SignatureLength,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// An AppKey is used to sign requests to the indexer.
|
|
22
|
+
///
|
|
23
|
+
/// AppKeys can be registered with an indexer during
|
|
24
|
+
/// onboarding with a [Builder]. They are derived from
|
|
25
|
+
/// a BIP-32 recovery phrase, which can be generated
|
|
26
|
+
/// using [generate_recovery_phrase].
|
|
27
|
+
///
|
|
28
|
+
/// It must be stored securely by the application and
|
|
29
|
+
/// never shared publicly. If exposed, a user's data
|
|
30
|
+
/// is compromised.
|
|
31
|
+
///
|
|
32
|
+
/// Mishandling the app key will lead to data loss
|
|
33
|
+
/// and inability to access stored objects.
|
|
34
|
+
|
|
35
|
+
#[derive(uniffi::Object)]
|
|
36
|
+
pub struct AppKey(PrivateKey);
|
|
37
|
+
|
|
38
|
+
impl AppKey {
|
|
39
|
+
pub(crate) fn private_key(&self) -> &PrivateKey {
|
|
40
|
+
&self.0
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[uniffi::export]
|
|
45
|
+
impl AppKey {
|
|
46
|
+
/// Imports an AppKey from the provided byte array.
|
|
47
|
+
///
|
|
48
|
+
/// # Arguments
|
|
49
|
+
/// * `key` - A 32-byte array representing the app key.
|
|
50
|
+
#[uniffi::constructor]
|
|
51
|
+
pub fn new(key: Vec<u8>) -> Result<Self, AppKeyError> {
|
|
52
|
+
if key.len() != 32 {
|
|
53
|
+
return Err(AppKeyError::InvalidLength);
|
|
54
|
+
}
|
|
55
|
+
let mut seed = [0u8; 32];
|
|
56
|
+
seed.copy_from_slice(&key);
|
|
57
|
+
Ok(AppKey(PrivateKey::from_seed(&seed)))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Exports the AppKey. The app key can be re-imported later
|
|
61
|
+
/// using [AppKey::new].
|
|
62
|
+
///
|
|
63
|
+
/// AppKeys should be stored securely by the application in lieu of the
|
|
64
|
+
/// recovery phrase.
|
|
65
|
+
pub fn export(&self) -> Vec<u8> {
|
|
66
|
+
self.0.as_ref()[..32].to_vec()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Signs a message using the AppKey.
|
|
70
|
+
pub fn sign(&self, message: Vec<u8>) -> Vec<u8> {
|
|
71
|
+
let signature = self.0.sign(&message);
|
|
72
|
+
signature.as_ref().to_vec()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Returns the public key corresponding to the AppKey.
|
|
76
|
+
///
|
|
77
|
+
/// This can be safely shared with others.
|
|
78
|
+
pub fn public_key(&self) -> String {
|
|
79
|
+
self.0.public_key().to_string()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Verifies a signature for a given message using the AppKey.
|
|
83
|
+
pub fn verify_signature(
|
|
84
|
+
&self,
|
|
85
|
+
message: Vec<u8>,
|
|
86
|
+
signature: Vec<u8>,
|
|
87
|
+
) -> Result<bool, AppKeyError> {
|
|
88
|
+
if signature.len() != 64 {
|
|
89
|
+
return Err(AppKeyError::SignatureLength);
|
|
90
|
+
}
|
|
91
|
+
let mut sig_bytes = [0u8; 64];
|
|
92
|
+
sig_bytes.copy_from_slice(&signature);
|
|
93
|
+
Ok(self
|
|
94
|
+
.0
|
|
95
|
+
.public_key()
|
|
96
|
+
.verify(&message, &Signature::from(sig_bytes)))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl From<PrivateKey> for AppKey {
|
|
101
|
+
fn from(pk: PrivateKey) -> Self {
|
|
102
|
+
AppKey(pk)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[derive(uniffi::Error, Debug, Error)]
|
|
107
|
+
#[uniffi(flat_error)]
|
|
108
|
+
pub enum SeedError {
|
|
109
|
+
#[error(transparent)]
|
|
110
|
+
InvalidMnemonic(#[from] seed::SeedError),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Generates a new BIP-32 12-word recovery phrase.
|
|
114
|
+
#[uniffi::export]
|
|
115
|
+
pub fn generate_recovery_phrase() -> String {
|
|
116
|
+
let seed: [u8; 16] = rand::random();
|
|
117
|
+
Seed::from_seed(seed).to_string()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Validates a BIP-32 recovery phrase.
|
|
121
|
+
#[uniffi::export]
|
|
122
|
+
pub fn validate_recovery_phrase(phrase: &str) -> Result<(), SeedError> {
|
|
123
|
+
Seed::new(phrase)?;
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
enum BuilderState {
|
|
128
|
+
Disconnected(indexd::Builder<indexd::DisconnectedState>),
|
|
129
|
+
RequestingApproval(indexd::Builder<indexd::RequestingApprovalState>),
|
|
130
|
+
Approved(indexd::Builder<indexd::ApprovedState>),
|
|
131
|
+
Finalized,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[derive(uniffi::Object)]
|
|
135
|
+
pub struct Builder {
|
|
136
|
+
state: Arc<Mutex<Option<BuilderState>>>,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[derive(Debug, Error, uniffi::Error)]
|
|
140
|
+
#[uniffi(flat_error)]
|
|
141
|
+
pub enum BuilderError {
|
|
142
|
+
#[error(transparent)]
|
|
143
|
+
Error(#[from] indexd::BuilderError),
|
|
144
|
+
|
|
145
|
+
#[error("invalid state for this operation")]
|
|
146
|
+
InvalidState,
|
|
147
|
+
|
|
148
|
+
#[error("crypto error: {0}")]
|
|
149
|
+
Crypto(String),
|
|
150
|
+
|
|
151
|
+
#[error("join error: {0}")]
|
|
152
|
+
JoinError(#[from] tokio::task::JoinError),
|
|
153
|
+
|
|
154
|
+
#[error("{0}")]
|
|
155
|
+
Custom(String),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl Builder {
|
|
159
|
+
async fn with_state_transition<F, Fut, R>(&self, f: F) -> Result<R, BuilderError>
|
|
160
|
+
where
|
|
161
|
+
R: Send + 'static,
|
|
162
|
+
F: FnOnce(BuilderState) -> Fut + Send + 'static,
|
|
163
|
+
Fut: Future<Output = Result<(BuilderState, R), BuilderError>> + Send + 'static,
|
|
164
|
+
{
|
|
165
|
+
let state = {
|
|
166
|
+
self.state
|
|
167
|
+
.lock()
|
|
168
|
+
.map_err(|_| BuilderError::Custom("mutex poisoned".into()))?
|
|
169
|
+
.take()
|
|
170
|
+
};
|
|
171
|
+
match state {
|
|
172
|
+
Some(state) => {
|
|
173
|
+
let (next, result) = spawn(async move { f(state).await }).await??;
|
|
174
|
+
*self
|
|
175
|
+
.state
|
|
176
|
+
.lock()
|
|
177
|
+
.map_err(|_| BuilderError::Custom("mutex poisoned".into()))? = Some(next);
|
|
178
|
+
Ok(result)
|
|
179
|
+
}
|
|
180
|
+
_ => Err(BuilderError::InvalidState),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn with_state<F, R>(&self, f: F) -> Result<R, BuilderError>
|
|
185
|
+
where
|
|
186
|
+
F: FnOnce(&BuilderState) -> Result<R, BuilderError>,
|
|
187
|
+
{
|
|
188
|
+
let state = self
|
|
189
|
+
.state
|
|
190
|
+
.lock()
|
|
191
|
+
.map_err(|_| BuilderError::Custom("mutex poisoned".into()))?;
|
|
192
|
+
match state.as_ref() {
|
|
193
|
+
Some(state) => f(state),
|
|
194
|
+
None => Err(BuilderError::InvalidState),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[uniffi::export]
|
|
200
|
+
impl Builder {
|
|
201
|
+
/// Creates a new SDK builder with the provided indexer URL.
|
|
202
|
+
///
|
|
203
|
+
/// After creating the builder, call [Builder::connected] to attempt
|
|
204
|
+
/// to connect using an existing app key, or [Builder::request_connection]
|
|
205
|
+
/// to request a new connection.
|
|
206
|
+
#[uniffi::constructor]
|
|
207
|
+
pub fn new(indexer_url: String) -> Result<Self, BuilderError> {
|
|
208
|
+
let builder = indexd::Builder::new(indexer_url)?;
|
|
209
|
+
Ok(Builder {
|
|
210
|
+
state: Arc::new(Mutex::new(Some(BuilderState::Disconnected(builder)))),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Attempts to connect using the provided app key and TLS configuration.
|
|
215
|
+
/// If the app key is valid, returns Some([SDK]), otherwise returns None.
|
|
216
|
+
///
|
|
217
|
+
/// If you receive None, call [Builder::request_connection] to request a new connection.
|
|
218
|
+
///
|
|
219
|
+
/// # Arguments
|
|
220
|
+
/// * `app_key` - The application key used for authentication.
|
|
221
|
+
pub async fn connected(&self, app_key: Arc<AppKey>) -> Result<Option<Arc<SDK>>, BuilderError> {
|
|
222
|
+
self.with_state_transition(|state| async move {
|
|
223
|
+
match state {
|
|
224
|
+
BuilderState::Disconnected(builder) => {
|
|
225
|
+
// install crypto provider
|
|
226
|
+
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
|
227
|
+
rustls::crypto::ring::default_provider()
|
|
228
|
+
.install_default()
|
|
229
|
+
.map_err(|e| BuilderError::Crypto(format!("{:?}", e)))?;
|
|
230
|
+
}
|
|
231
|
+
let rustls_config = tls::tls_config();
|
|
232
|
+
|
|
233
|
+
match builder.connected(&app_key.0, rustls_config).await? {
|
|
234
|
+
Some(sdk) => {
|
|
235
|
+
Ok((BuilderState::Finalized, Some(Arc::new(SDK { inner: sdk }))))
|
|
236
|
+
}
|
|
237
|
+
None => Ok((BuilderState::Disconnected(builder), None)),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
_ => Err(BuilderError::InvalidState),
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
.await
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Requests a new connection for the application.
|
|
247
|
+
///
|
|
248
|
+
/// # Arguments
|
|
249
|
+
/// * `app` - Details of the application requesting connection.
|
|
250
|
+
pub async fn request_connection(&self, meta: AppMeta) -> Result<Self, BuilderError> {
|
|
251
|
+
self.with_state_transition(|state| async move {
|
|
252
|
+
if meta.id.len() != 32 {
|
|
253
|
+
return Err(BuilderError::Custom("app ID must be 32 bytes".to_string()));
|
|
254
|
+
}
|
|
255
|
+
let mut app_id = [0u8; 32];
|
|
256
|
+
app_id.copy_from_slice(&meta.id);
|
|
257
|
+
match state {
|
|
258
|
+
BuilderState::Disconnected(builder) => {
|
|
259
|
+
let builder = builder
|
|
260
|
+
.request_connection(&RegisterAppRequest {
|
|
261
|
+
app_id: app_id.into(),
|
|
262
|
+
name: meta.name,
|
|
263
|
+
description: meta.description,
|
|
264
|
+
service_url: Url::parse(&meta.service_url).map_err(|e| {
|
|
265
|
+
BuilderError::Custom(format!("invalid service url: {e}"))
|
|
266
|
+
})?,
|
|
267
|
+
logo_url: meta
|
|
268
|
+
.logo_url
|
|
269
|
+
.map(|s| {
|
|
270
|
+
Url::parse(&s).map_err(|e| {
|
|
271
|
+
BuilderError::Custom(format!("invalid logo url: {e}"))
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
.transpose()?,
|
|
275
|
+
callback_url: meta
|
|
276
|
+
.callback_url
|
|
277
|
+
.map(|s| {
|
|
278
|
+
Url::parse(&s).map_err(|e| {
|
|
279
|
+
BuilderError::Custom(format!("invalid callback url: {e}"))
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
.transpose()?,
|
|
283
|
+
})
|
|
284
|
+
.await?;
|
|
285
|
+
Ok((BuilderState::RequestingApproval(builder), ()))
|
|
286
|
+
}
|
|
287
|
+
_ => Err(BuilderError::InvalidState),
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
.await?;
|
|
291
|
+
|
|
292
|
+
Ok(Builder {
|
|
293
|
+
state: self.state.clone(),
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Retrieves the response URL for the connection request.
|
|
298
|
+
/// This URL can be used to approve the connection request.
|
|
299
|
+
/// It should be displayed to the user.
|
|
300
|
+
pub fn response_url(&self) -> Result<String, BuilderError> {
|
|
301
|
+
self.with_state(|state| match state {
|
|
302
|
+
BuilderState::RequestingApproval(builder) => Ok(builder.response_url().to_owned()),
|
|
303
|
+
_ => Err(BuilderError::InvalidState),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Waits for the connection request to be approved.
|
|
308
|
+
/// Once approved, the app can be registered and used to create an
|
|
309
|
+
/// SDK instance.
|
|
310
|
+
pub async fn wait_for_approval(&self) -> Result<Self, BuilderError> {
|
|
311
|
+
self.with_state_transition(|state| async move {
|
|
312
|
+
match state {
|
|
313
|
+
BuilderState::RequestingApproval(builder) => {
|
|
314
|
+
let builder = builder.wait_for_approval().await?;
|
|
315
|
+
// transition to approved state
|
|
316
|
+
Ok((BuilderState::Approved(builder), ()))
|
|
317
|
+
}
|
|
318
|
+
_ => Err(BuilderError::InvalidState),
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
.await?;
|
|
322
|
+
Ok(Builder {
|
|
323
|
+
state: self.state.clone(),
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Registers the application with the indexer using the provided mnemonic.
|
|
328
|
+
/// Once registered, returns an [SDK] instance that can be used to interact
|
|
329
|
+
/// with the indexer.
|
|
330
|
+
///
|
|
331
|
+
/// # Arguments
|
|
332
|
+
/// * `mnemonic` - The user's mnemonic phrase used to derive the application key.
|
|
333
|
+
pub async fn register(&self, mnemonic: String) -> Result<Arc<SDK>, BuilderError> {
|
|
334
|
+
self.with_state_transition(|state| async move {
|
|
335
|
+
match state {
|
|
336
|
+
BuilderState::Approved(builder) => {
|
|
337
|
+
// install crypto provider
|
|
338
|
+
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
|
339
|
+
rustls::crypto::ring::default_provider()
|
|
340
|
+
.install_default()
|
|
341
|
+
.map_err(|e| BuilderError::Crypto(format!("{:?}", e)))?;
|
|
342
|
+
}
|
|
343
|
+
let rustls_config = tls::tls_config();
|
|
344
|
+
let sdk = builder.register(&mnemonic, rustls_config).await?;
|
|
345
|
+
Ok((BuilderState::Finalized, Arc::new(SDK { inner: sdk })))
|
|
346
|
+
}
|
|
347
|
+
_ => Err(BuilderError::InvalidState),
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
.await
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[cfg(test)]
|
|
355
|
+
mod tests {
|
|
356
|
+
use super::*;
|
|
357
|
+
|
|
358
|
+
#[test]
|
|
359
|
+
fn test_generate_recovery_phrase() {
|
|
360
|
+
let phrase = generate_recovery_phrase();
|
|
361
|
+
assert!(validate_recovery_phrase(&phrase).is_ok());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#[test]
|
|
365
|
+
fn test_validate_recovery_phrase_invalid() {
|
|
366
|
+
let invalid_phrase = "invalid recovery phrase";
|
|
367
|
+
assert!(validate_recovery_phrase(invalid_phrase).is_err());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#[test]
|
|
371
|
+
fn test_app_key_export() {
|
|
372
|
+
let seed: [u8; 32] = rand::random();
|
|
373
|
+
let app_key = AppKey::new(seed.to_vec()).unwrap();
|
|
374
|
+
let exported = app_key.export();
|
|
375
|
+
assert_eq!(exported, seed.to_vec());
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
use std::io;
|
|
2
|
+
use std::pin::Pin;
|
|
3
|
+
use std::sync::Arc;
|
|
4
|
+
use std::task::{Context, Poll, ready};
|
|
5
|
+
|
|
6
|
+
use bytes::Bytes;
|
|
7
|
+
use thiserror::Error;
|
|
8
|
+
use tokio::io::{AsyncRead, AsyncWrite};
|
|
9
|
+
use tokio::sync::mpsc;
|
|
10
|
+
use tokio_stream::wrappers::ReceiverStream;
|
|
11
|
+
use tokio_util::io::StreamReader;
|
|
12
|
+
use tokio_util::sync::PollSender;
|
|
13
|
+
use tokio_util::task::AbortOnDropHandle;
|
|
14
|
+
|
|
15
|
+
use crate::spawn;
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Error, uniffi::Error)]
|
|
18
|
+
#[uniffi(flat_error)]
|
|
19
|
+
pub enum IOError {
|
|
20
|
+
#[error("i/o error: {0}")]
|
|
21
|
+
Io(String),
|
|
22
|
+
|
|
23
|
+
#[error("reader closed")]
|
|
24
|
+
Closed,
|
|
25
|
+
|
|
26
|
+
#[error("cancelled")]
|
|
27
|
+
Cancelled,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl From<IOError> for std::io::Error {
|
|
31
|
+
fn from(e: IOError) -> Self {
|
|
32
|
+
match e {
|
|
33
|
+
IOError::Closed => std::io::Error::new(std::io::ErrorKind::UnexpectedEof, e),
|
|
34
|
+
IOError::Cancelled => std::io::Error::new(std::io::ErrorKind::Interrupted, e),
|
|
35
|
+
IOError::Io(msg) => std::io::Error::other(msg),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// A foreign reader that can be used to transfer data across FFI boundaries.
|
|
41
|
+
///
|
|
42
|
+
/// Implementations should send an empty chunk to signal completion. It is recommended
|
|
43
|
+
/// that implementations chunk data into reasonably sized pieces (e.g. 64KiB) to avoid
|
|
44
|
+
/// excessive memory usage.
|
|
45
|
+
///
|
|
46
|
+
/// If an error is returned by `read`, the reader will be closed and no
|
|
47
|
+
/// further calls will be made.
|
|
48
|
+
#[uniffi::export(with_foreign)]
|
|
49
|
+
#[async_trait::async_trait]
|
|
50
|
+
pub trait Reader: Send + Sync {
|
|
51
|
+
async fn read(&self) -> Result<Vec<u8>, IOError>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub(crate) fn adapt_ffi_reader(reader: Arc<dyn Reader>) -> impl AsyncRead + Unpin {
|
|
55
|
+
let (tx, rx) = mpsc::channel(1);
|
|
56
|
+
|
|
57
|
+
// Spawn a task to pump data from foreign -> channel
|
|
58
|
+
tokio::spawn(async move {
|
|
59
|
+
loop {
|
|
60
|
+
match reader.read().await {
|
|
61
|
+
Ok(data) if data.is_empty() => break,
|
|
62
|
+
Ok(data) => {
|
|
63
|
+
if tx.send(Ok(Bytes::from(data))).await.is_err() {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
Err(e) => {
|
|
68
|
+
let _ = tx.send(Err(e)).await;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
StreamReader::new(ReceiverStream::new(rx))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// A foreign writer that can be used to transfer data across FFI boundaries.
|
|
79
|
+
/// The data may be sent in multiple chunks. The implementation should handle
|
|
80
|
+
/// buffering and writing the data as it is received.
|
|
81
|
+
#[uniffi::export(with_foreign)]
|
|
82
|
+
#[async_trait::async_trait]
|
|
83
|
+
pub trait Writer: Send + Sync {
|
|
84
|
+
async fn write(&self, data: Vec<u8>) -> Result<(), IOError>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub struct FFIWriter {
|
|
88
|
+
sender: PollSender<Bytes>,
|
|
89
|
+
join_handle: Option<AbortOnDropHandle<()>>,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl AsyncWrite for FFIWriter {
|
|
93
|
+
fn poll_write(
|
|
94
|
+
self: Pin<&mut Self>,
|
|
95
|
+
cx: &mut Context<'_>,
|
|
96
|
+
buf: &[u8],
|
|
97
|
+
) -> Poll<std::io::Result<usize>> {
|
|
98
|
+
let this = self.get_mut();
|
|
99
|
+
|
|
100
|
+
if ready!(this.sender.poll_reserve(cx)).is_err() {
|
|
101
|
+
return Poll::Ready(Err(io::Error::new(
|
|
102
|
+
io::ErrorKind::BrokenPipe,
|
|
103
|
+
"channel closed",
|
|
104
|
+
)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let n = buf.len();
|
|
108
|
+
let item = Bytes::copy_from_slice(buf);
|
|
109
|
+
|
|
110
|
+
if this.sender.send_item(item).is_err() {
|
|
111
|
+
return Poll::Ready(Err(io::Error::new(
|
|
112
|
+
io::ErrorKind::BrokenPipe,
|
|
113
|
+
"channel closed",
|
|
114
|
+
)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Poll::Ready(Ok(n))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
|
121
|
+
Poll::Ready(Ok(()))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
|
125
|
+
self.sender.close();
|
|
126
|
+
|
|
127
|
+
if let Some(join_handle) = self.join_handle.as_mut() {
|
|
128
|
+
match ready!(Pin::new(join_handle).poll(cx)) {
|
|
129
|
+
Ok(()) => {
|
|
130
|
+
self.join_handle = None;
|
|
131
|
+
}
|
|
132
|
+
Err(e) => {
|
|
133
|
+
self.join_handle = None;
|
|
134
|
+
return Poll::Ready(Err(io::Error::other(format!("join error: {}", e))));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
Poll::Ready(Ok(()))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pub(crate) fn adapt_ffi_writer(writer: Arc<dyn Writer>) -> FFIWriter {
|
|
143
|
+
let (tx, mut rx) = mpsc::channel::<Bytes>(8);
|
|
144
|
+
let join_handle = spawn(async move {
|
|
145
|
+
while let Some(buf) = rx.recv().await {
|
|
146
|
+
if writer.write(buf.to_vec()).await.is_err() {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
FFIWriter {
|
|
152
|
+
sender: PollSender::new(tx),
|
|
153
|
+
join_handle: Some(join_handle),
|
|
154
|
+
}
|
|
155
|
+
}
|