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,59 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use uint::construct_uint;
|
|
3
|
+
|
|
4
|
+
use crate::encoding::{self, SiaDecodable, SiaEncodable, V1SiaDecodable, V1SiaEncodable};
|
|
5
|
+
|
|
6
|
+
construct_uint! {
|
|
7
|
+
/// Work is a 256-bit unsigned integer.
|
|
8
|
+
pub struct Work(4);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl From<&[u8; 32]> for Work {
|
|
12
|
+
fn from(bytes: &[u8; 32]) -> Self {
|
|
13
|
+
Work::from_big_endian(bytes)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl SiaEncodable for Work {
|
|
18
|
+
fn encode<W: std::io::Write>(&self, w: &mut W) -> encoding::Result<()> {
|
|
19
|
+
self.to_big_endian().encode(w)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl SiaDecodable for Work {
|
|
24
|
+
fn decode<R: std::io::Read>(r: &mut R) -> encoding::Result<Self> {
|
|
25
|
+
Ok(Work::from_big_endian(&<[u8; 32]>::decode(r).map_err(
|
|
26
|
+
|_| encoding::Error::Custom("invalid work".to_string()),
|
|
27
|
+
)?))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl V1SiaEncodable for Work {
|
|
32
|
+
fn encode_v1<W: std::io::Write>(&self, w: &mut W) -> encoding::Result<()> {
|
|
33
|
+
self.to_big_endian().encode_v1(w)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl V1SiaDecodable for Work {
|
|
38
|
+
fn decode_v1<R: std::io::Read>(r: &mut R) -> encoding::Result<Self> {
|
|
39
|
+
Ok(Work::from_big_endian(&<[u8; 32]>::decode_v1(r)?))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl Serialize for Work {
|
|
44
|
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
45
|
+
where
|
|
46
|
+
S: serde::Serializer,
|
|
47
|
+
{
|
|
48
|
+
serializer.serialize_str(&self.to_string())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl<'de> Deserialize<'de> for Work {
|
|
53
|
+
fn deserialize<D>(deserializer: D) -> Result<Work, D::Error>
|
|
54
|
+
where
|
|
55
|
+
D: serde::Deserializer<'de>,
|
|
56
|
+
{
|
|
57
|
+
Work::from_dec_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mod common;
|
|
2
|
+
mod currency;
|
|
3
|
+
mod specifier;
|
|
4
|
+
mod spendpolicy; // exposed in v2 types
|
|
5
|
+
|
|
6
|
+
#[allow(clippy::manual_div_ceil)]
|
|
7
|
+
mod work;
|
|
8
|
+
|
|
9
|
+
pub use common::*;
|
|
10
|
+
pub use currency::*;
|
|
11
|
+
pub use specifier::*;
|
|
12
|
+
pub use work::*;
|
|
13
|
+
|
|
14
|
+
pub(crate) mod utils;
|
|
15
|
+
pub mod v1;
|
|
16
|
+
pub mod v2;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { resolve, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const targetDir = resolve(__dirname, '../rust/sia-sdk-rs')
|
|
8
|
+
|
|
9
|
+
if (existsSync(targetDir)) {
|
|
10
|
+
console.log('rust/sia-sdk-rs already exists, skipping clone.')
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const repo = 'https://github.com/alexfreska/sia-sdk-rs.git'
|
|
15
|
+
const branch = 'alex/wasm-experimental'
|
|
16
|
+
|
|
17
|
+
console.log(`Cloning ${repo} (branch: ${branch})...`)
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
execSync(
|
|
21
|
+
`git clone --branch ${branch} --single-branch --depth 1 ${repo} ${targetDir}`,
|
|
22
|
+
{ stdio: 'inherit' },
|
|
23
|
+
)
|
|
24
|
+
console.log('Rust SDK source cloned successfully.')
|
|
25
|
+
} catch {
|
|
26
|
+
console.warn(
|
|
27
|
+
'\nWarning: Failed to clone Rust SDK source. This is optional — the pre-built WASM binary in wasm/ is what the app uses at runtime. The Rust source is only needed if you want to rebuild the WASM module.\n',
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AuthFlow } from './components/auth/AuthFlow'
|
|
2
|
+
import { UploadZone } from './components/upload/UploadZone'
|
|
3
|
+
import { useAuthStore } from './stores/auth'
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
const step = useAuthStore((s) => s.step)
|
|
7
|
+
|
|
8
|
+
if (step === 'connected') {
|
|
9
|
+
return <UploadZone />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return <AuthFlow />
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export function DevNote({
|
|
4
|
+
title,
|
|
5
|
+
children,
|
|
6
|
+
}: {
|
|
7
|
+
title: string
|
|
8
|
+
children: ReactNode
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="border-l-4 border-amber-500 bg-amber-950/50 rounded-r-lg p-4 space-y-1">
|
|
12
|
+
<p className="text-amber-400 text-xs font-semibold uppercase tracking-wider">
|
|
13
|
+
Developer Note
|
|
14
|
+
</p>
|
|
15
|
+
<p className="text-amber-200 text-sm font-medium">{title}</p>
|
|
16
|
+
<div className="text-amber-200/80 text-xs leading-relaxed">
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { Builder } from '../../lib/sdk'
|
|
3
|
+
import { useAuthStore } from '../../stores/auth'
|
|
4
|
+
import { DevNote } from '../DevNote'
|
|
5
|
+
|
|
6
|
+
export function ApproveScreen({
|
|
7
|
+
builder,
|
|
8
|
+
}: {
|
|
9
|
+
builder: React.RefObject<Builder | null>
|
|
10
|
+
}) {
|
|
11
|
+
const { approvalUrl, setStep, setError } = useAuthStore()
|
|
12
|
+
const [waiting, setWaiting] = useState(false)
|
|
13
|
+
|
|
14
|
+
async function handleWaitForApproval() {
|
|
15
|
+
const b = builder.current
|
|
16
|
+
if (!b) {
|
|
17
|
+
setError('No builder instance')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setWaiting(true)
|
|
22
|
+
try {
|
|
23
|
+
await b.waitForApproval()
|
|
24
|
+
setStep('recovery')
|
|
25
|
+
} catch (e) {
|
|
26
|
+
setError(e instanceof Error ? e.message : 'Approval failed')
|
|
27
|
+
} finally {
|
|
28
|
+
setWaiting(false)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col items-center justify-center min-h-screen px-4">
|
|
34
|
+
<div className="w-full max-w-md space-y-6">
|
|
35
|
+
<div className="text-center space-y-2">
|
|
36
|
+
<h1 className="text-2xl font-semibold text-white">
|
|
37
|
+
Approve Connection
|
|
38
|
+
</h1>
|
|
39
|
+
<p className="text-neutral-400 text-sm">
|
|
40
|
+
Open the link below in a new tab to approve the connection, then
|
|
41
|
+
click "Check Approval".
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<DevNote title="Out-of-Band Approval">
|
|
46
|
+
<p>
|
|
47
|
+
The user must visit the approval URL in another tab (or on the
|
|
48
|
+
indexer's dashboard) to authorize your app. This is an
|
|
49
|
+
out-of-band step — your app polls for approval via{' '}
|
|
50
|
+
<code className="text-amber-300">builder.waitForApproval()</code>.
|
|
51
|
+
Once approved, the flow continues to recovery phrase setup.
|
|
52
|
+
</p>
|
|
53
|
+
</DevNote>
|
|
54
|
+
|
|
55
|
+
{approvalUrl && (
|
|
56
|
+
<a
|
|
57
|
+
href={approvalUrl}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
className="block w-full text-center py-3 bg-neutral-800 hover:bg-neutral-700 text-green-400 font-mono text-sm rounded-lg transition-colors break-all px-4"
|
|
61
|
+
>
|
|
62
|
+
{approvalUrl}
|
|
63
|
+
</a>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={handleWaitForApproval}
|
|
69
|
+
disabled={waiting}
|
|
70
|
+
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-700 disabled:text-neutral-500 text-white font-medium rounded-lg transition-colors"
|
|
71
|
+
>
|
|
72
|
+
{waiting ? (
|
|
73
|
+
<span className="flex items-center justify-center gap-2">
|
|
74
|
+
<span className="w-4 h-4 border-2 border-neutral-400 border-t-white rounded-full animate-spin" />
|
|
75
|
+
Waiting for approval...
|
|
76
|
+
</span>
|
|
77
|
+
) : (
|
|
78
|
+
'Check Approval'
|
|
79
|
+
)}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { fromHex } from '../../lib/hex'
|
|
3
|
+
import { AppKey, Builder, initWasm, type SDK } from '../../lib/sdk'
|
|
4
|
+
import { applyPresetToBuilder, useAuthStore } from '../../stores/auth'
|
|
5
|
+
import { ApproveScreen } from './ApproveScreen'
|
|
6
|
+
import { ConnectScreen } from './ConnectScreen'
|
|
7
|
+
import { LoadingScreen } from './LoadingScreen'
|
|
8
|
+
import { RecoveryScreen } from './RecoveryScreen'
|
|
9
|
+
|
|
10
|
+
export function AuthFlow() {
|
|
11
|
+
const step = useAuthStore((s) => s.step)
|
|
12
|
+
const error = useAuthStore((s) => s.error)
|
|
13
|
+
const setError = useAuthStore((s) => s.setError)
|
|
14
|
+
const builderRef = useRef<Builder | null>(null)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let cancelled = false
|
|
18
|
+
|
|
19
|
+
async function init() {
|
|
20
|
+
const { storedKeyHex, indexerUrl, performancePreset, setSdk, setStep } =
|
|
21
|
+
useAuthStore.getState()
|
|
22
|
+
try {
|
|
23
|
+
await initWasm()
|
|
24
|
+
|
|
25
|
+
if (storedKeyHex && indexerUrl) {
|
|
26
|
+
const keyBytes = fromHex(storedKeyHex)
|
|
27
|
+
const appKey = new AppKey(keyBytes)
|
|
28
|
+
const builder = new Builder(indexerUrl)
|
|
29
|
+
applyPresetToBuilder(builder, performancePreset)
|
|
30
|
+
const result: SDK | null = await builder.connected(appKey)
|
|
31
|
+
|
|
32
|
+
if (cancelled) return
|
|
33
|
+
if (result) {
|
|
34
|
+
setSdk(result)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!cancelled) {
|
|
40
|
+
setStep('connect')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (!cancelled) {
|
|
44
|
+
console.error('Init error:', e)
|
|
45
|
+
setStep('connect')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
init()
|
|
51
|
+
return () => {
|
|
52
|
+
cancelled = true
|
|
53
|
+
}
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{error && (
|
|
59
|
+
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 bg-red-900/90 border border-red-700 rounded-lg text-red-200 text-sm max-w-md text-center">
|
|
60
|
+
{error}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => setError(null)}
|
|
64
|
+
className="ml-2 text-red-400 hover:text-red-300"
|
|
65
|
+
>
|
|
66
|
+
Dismiss
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{step === 'loading' && <LoadingScreen />}
|
|
72
|
+
{step === 'connect' && <ConnectScreen builder={builderRef} />}
|
|
73
|
+
{step === 'approve' && <ApproveScreen builder={builderRef} />}
|
|
74
|
+
{step === 'recovery' && <RecoveryScreen builder={builderRef} />}
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { APP_KEY, APP_META, DEFAULT_INDEXER_URL } from '../../lib/constants'
|
|
3
|
+
import { Builder } from '../../lib/sdk'
|
|
4
|
+
import {
|
|
5
|
+
applyPresetToBuilder,
|
|
6
|
+
type PerformancePreset,
|
|
7
|
+
PRESETS,
|
|
8
|
+
useAuthStore,
|
|
9
|
+
} from '../../stores/auth'
|
|
10
|
+
import { DevNote } from '../DevNote'
|
|
11
|
+
|
|
12
|
+
const PRESET_OPTIONS: {
|
|
13
|
+
key: PerformancePreset
|
|
14
|
+
label: string
|
|
15
|
+
sublabel: string
|
|
16
|
+
color: string
|
|
17
|
+
}[] = [
|
|
18
|
+
{
|
|
19
|
+
key: 'conservative',
|
|
20
|
+
label: 'Conservative',
|
|
21
|
+
sublabel: 'Most Stable',
|
|
22
|
+
color: 'bg-green-600 border-green-500',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: 'balanced',
|
|
26
|
+
label: 'Balanced',
|
|
27
|
+
sublabel: 'Recommended',
|
|
28
|
+
color: 'bg-blue-600 border-blue-500',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'fast',
|
|
32
|
+
label: 'Fast',
|
|
33
|
+
sublabel: 'May Be Unstable',
|
|
34
|
+
color: 'bg-amber-600 border-amber-500',
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export function ConnectScreen({
|
|
39
|
+
builder,
|
|
40
|
+
}: {
|
|
41
|
+
builder: React.RefObject<Builder | null>
|
|
42
|
+
}) {
|
|
43
|
+
const {
|
|
44
|
+
indexerUrl,
|
|
45
|
+
performancePreset,
|
|
46
|
+
setIndexerUrl,
|
|
47
|
+
setPerformancePreset,
|
|
48
|
+
setStep,
|
|
49
|
+
setError,
|
|
50
|
+
setApprovalUrl,
|
|
51
|
+
} = useAuthStore()
|
|
52
|
+
const [url, setUrl] = useState(indexerUrl || DEFAULT_INDEXER_URL)
|
|
53
|
+
const [loading, setLoading] = useState(false)
|
|
54
|
+
const [showCurl, setShowCurl] = useState(false)
|
|
55
|
+
const [curlResponse, setCurlResponse] = useState('')
|
|
56
|
+
|
|
57
|
+
async function handleConnect() {
|
|
58
|
+
setLoading(true)
|
|
59
|
+
setError(null)
|
|
60
|
+
try {
|
|
61
|
+
const b = new Builder(url)
|
|
62
|
+
applyPresetToBuilder(b, performancePreset)
|
|
63
|
+
builder.current = b
|
|
64
|
+
setIndexerUrl(url)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await b.requestConnection(APP_META)
|
|
68
|
+
const approvalUrl = b.responseUrl()
|
|
69
|
+
setApprovalUrl(approvalUrl)
|
|
70
|
+
setStep('approve')
|
|
71
|
+
} catch {
|
|
72
|
+
setShowCurl(true)
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
setError(e instanceof Error ? e.message : 'Failed to connect')
|
|
76
|
+
} finally {
|
|
77
|
+
setLoading(false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleCurlSubmit() {
|
|
82
|
+
try {
|
|
83
|
+
const b = builder.current
|
|
84
|
+
if (!b) {
|
|
85
|
+
setError('No builder instance')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
b.setConnectionResponse(APP_KEY, curlResponse)
|
|
89
|
+
const approvalUrl = b.responseUrl()
|
|
90
|
+
setApprovalUrl(approvalUrl)
|
|
91
|
+
setStep('approve')
|
|
92
|
+
} catch (e) {
|
|
93
|
+
setError(e instanceof Error ? e.message : 'Invalid response')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const currentPreset = PRESETS[performancePreset]
|
|
98
|
+
|
|
99
|
+
const curlCommand = `curl -X POST ${url}/auth/connect \\
|
|
100
|
+
-H "Content-Type: application/json" \\
|
|
101
|
+
-d '${APP_META}'`
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-col items-center justify-center min-h-screen px-4">
|
|
105
|
+
<div className="w-full max-w-md space-y-6">
|
|
106
|
+
<div className="text-center space-y-2">
|
|
107
|
+
<h1 className="text-2xl font-semibold text-white">
|
|
108
|
+
Connect to Indexer
|
|
109
|
+
</h1>
|
|
110
|
+
<p className="text-neutral-400 text-sm">
|
|
111
|
+
Enter your Sia indexer URL to get started
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<DevNote title="Indexer URL & App Key">
|
|
116
|
+
<p>
|
|
117
|
+
The indexer URL points to your Sia storage provider. The default is{' '}
|
|
118
|
+
<code className="text-amber-300">https://app.sia.storage</code>.
|
|
119
|
+
Your app key (set in{' '}
|
|
120
|
+
<code className="text-amber-300">src/lib/constants.ts</code>)
|
|
121
|
+
uniquely identifies your app to the indexer.
|
|
122
|
+
</p>
|
|
123
|
+
<p className="mt-1">
|
|
124
|
+
If the direct connection fails (CORS), the app falls back to a
|
|
125
|
+
manual curl flow where the user pastes the response.
|
|
126
|
+
</p>
|
|
127
|
+
</DevNote>
|
|
128
|
+
|
|
129
|
+
<div className="space-y-4">
|
|
130
|
+
<input
|
|
131
|
+
type="url"
|
|
132
|
+
value={url}
|
|
133
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
134
|
+
placeholder="https://app.sia.storage"
|
|
135
|
+
className="w-full px-4 py-3 bg-neutral-900 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:border-green-500"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={handleConnect}
|
|
141
|
+
disabled={loading || !url}
|
|
142
|
+
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-700 disabled:text-neutral-500 text-white font-medium rounded-lg transition-colors"
|
|
143
|
+
>
|
|
144
|
+
{loading ? 'Connecting...' : 'Connect'}
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="space-y-3">
|
|
149
|
+
<p className="text-sm text-neutral-400">Performance</p>
|
|
150
|
+
<div className="grid grid-cols-3 gap-2">
|
|
151
|
+
{PRESET_OPTIONS.map((opt) => (
|
|
152
|
+
<button
|
|
153
|
+
key={opt.key}
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={() => setPerformancePreset(opt.key)}
|
|
156
|
+
className={`py-3 px-2 rounded-lg text-center transition-all border-2 ${
|
|
157
|
+
performancePreset === opt.key
|
|
158
|
+
? `${opt.color} text-white`
|
|
159
|
+
: 'bg-neutral-900 border-neutral-700 text-neutral-400 hover:border-neutral-500'
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
<div className="text-sm font-medium">{opt.label}</div>
|
|
163
|
+
<div className="text-xs opacity-80">{opt.sublabel}</div>
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
<DevNote title="Performance Presets">
|
|
168
|
+
<p>
|
|
169
|
+
These control how many concurrent operations the SDK performs.
|
|
170
|
+
Conservative is safest, Fast uses more connections but may hit
|
|
171
|
+
rate limits. See{' '}
|
|
172
|
+
<code className="text-amber-300">src/stores/auth.ts</code> to
|
|
173
|
+
customize values.
|
|
174
|
+
</p>
|
|
175
|
+
</DevNote>
|
|
176
|
+
<div className="text-xs text-neutral-500 space-y-1">
|
|
177
|
+
<p>
|
|
178
|
+
Price fetches: {currentPreset.priceFetches} · Downloads:{' '}
|
|
179
|
+
{currentPreset.downloads} · Uploads:{' '}
|
|
180
|
+
{currentPreset.uploads}
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{showCurl && (
|
|
186
|
+
<div className="space-y-4 p-4 bg-neutral-900 rounded-lg border border-neutral-700">
|
|
187
|
+
<p className="text-sm text-neutral-300">
|
|
188
|
+
Direct connection failed (CORS). Run this command and paste the
|
|
189
|
+
response:
|
|
190
|
+
</p>
|
|
191
|
+
<pre className="text-xs bg-neutral-950 p-3 rounded overflow-x-auto text-green-400">
|
|
192
|
+
{curlCommand}
|
|
193
|
+
</pre>
|
|
194
|
+
<textarea
|
|
195
|
+
value={curlResponse}
|
|
196
|
+
onChange={(e) => setCurlResponse(e.target.value)}
|
|
197
|
+
placeholder="Paste the JSON response here..."
|
|
198
|
+
rows={4}
|
|
199
|
+
className="w-full px-3 py-2 bg-neutral-950 border border-neutral-700 rounded text-sm text-white placeholder-neutral-500 focus:outline-none focus:border-green-500 font-mono"
|
|
200
|
+
/>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={handleCurlSubmit}
|
|
204
|
+
disabled={!curlResponse}
|
|
205
|
+
className="w-full py-2 bg-green-600 hover:bg-green-700 disabled:bg-neutral-700 disabled:text-neutral-500 text-white text-sm font-medium rounded-lg transition-colors"
|
|
206
|
+
>
|
|
207
|
+
Submit Response
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function LoadingScreen({ message }: { message?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
|
4
|
+
<div className="w-8 h-8 border-2 border-neutral-600 border-t-green-500 rounded-full animate-spin" />
|
|
5
|
+
<p className="text-neutral-400 text-sm">{message || 'Initializing...'}</p>
|
|
6
|
+
</div>
|
|
7
|
+
)
|
|
8
|
+
}
|