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,182 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { toHex } from '../../lib/hex'
|
|
3
|
+
import {
|
|
4
|
+
type Builder,
|
|
5
|
+
generateRecoveryPhrase,
|
|
6
|
+
validateRecoveryPhrase,
|
|
7
|
+
} from '../../lib/sdk'
|
|
8
|
+
import { useAuthStore } from '../../stores/auth'
|
|
9
|
+
import { DevNote } from '../DevNote'
|
|
10
|
+
|
|
11
|
+
export function RecoveryScreen({
|
|
12
|
+
builder,
|
|
13
|
+
}: {
|
|
14
|
+
builder: React.RefObject<Builder | null>
|
|
15
|
+
}) {
|
|
16
|
+
const { setSdk, setStoredKeyHex, setError } = useAuthStore()
|
|
17
|
+
const [mode, setMode] = useState<'choose' | 'generate' | 'import'>('choose')
|
|
18
|
+
const [phrase, setPhrase] = useState('')
|
|
19
|
+
const [generatedPhrase, setGeneratedPhrase] = useState('')
|
|
20
|
+
const [loading, setLoading] = useState(false)
|
|
21
|
+
const [phraseError, setPhraseError] = useState<string | null>(null)
|
|
22
|
+
|
|
23
|
+
function handleGenerate() {
|
|
24
|
+
const mnemonic = generateRecoveryPhrase()
|
|
25
|
+
setGeneratedPhrase(mnemonic)
|
|
26
|
+
setPhrase(mnemonic)
|
|
27
|
+
setMode('generate')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleValidatePhrase(value: string) {
|
|
31
|
+
setPhrase(value)
|
|
32
|
+
setPhraseError(null)
|
|
33
|
+
if (value.trim()) {
|
|
34
|
+
try {
|
|
35
|
+
validateRecoveryPhrase(value.trim())
|
|
36
|
+
} catch {
|
|
37
|
+
setPhraseError('Invalid recovery phrase')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handleRegister() {
|
|
43
|
+
const b = builder.current
|
|
44
|
+
if (!b) {
|
|
45
|
+
setError('No builder instance')
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mnemonic = phrase.trim()
|
|
50
|
+
try {
|
|
51
|
+
validateRecoveryPhrase(mnemonic)
|
|
52
|
+
} catch {
|
|
53
|
+
setPhraseError('Invalid recovery phrase')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setLoading(true)
|
|
58
|
+
try {
|
|
59
|
+
const sdk = await b.register(mnemonic)
|
|
60
|
+
const appKey = sdk.appKey()
|
|
61
|
+
const exported = appKey.export()
|
|
62
|
+
setStoredKeyHex(toHex(exported))
|
|
63
|
+
setSdk(sdk)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
setError(e instanceof Error ? e.message : 'Registration failed')
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mode === 'choose') {
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex flex-col items-center justify-center min-h-screen px-4">
|
|
74
|
+
<div className="w-full max-w-md space-y-6">
|
|
75
|
+
<div className="text-center space-y-2">
|
|
76
|
+
<h1 className="text-2xl font-semibold text-white">
|
|
77
|
+
Recovery Phrase
|
|
78
|
+
</h1>
|
|
79
|
+
<p className="text-neutral-400 text-sm">
|
|
80
|
+
Generate a new recovery phrase or enter an existing one.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<DevNote title="Recovery Phrases & Key Derivation">
|
|
85
|
+
<p>
|
|
86
|
+
The 12-word BIP-39 recovery phrase deterministically derives the
|
|
87
|
+
user's cryptographic keys. The same phrase always produces
|
|
88
|
+
the same keys, enabling account recovery. The derived app key is
|
|
89
|
+
exported and stored in localStorage (via Zustand persist) so users
|
|
90
|
+
don't need to re-enter it.
|
|
91
|
+
</p>
|
|
92
|
+
</DevNote>
|
|
93
|
+
|
|
94
|
+
<div className="space-y-3">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={handleGenerate}
|
|
98
|
+
className="w-full py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"
|
|
99
|
+
>
|
|
100
|
+
Generate New Phrase
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => setMode('import')}
|
|
105
|
+
className="w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-white font-medium rounded-lg transition-colors"
|
|
106
|
+
>
|
|
107
|
+
Enter Existing Phrase
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex flex-col items-center justify-center min-h-screen px-4">
|
|
117
|
+
<div className="w-full max-w-md space-y-6">
|
|
118
|
+
<div className="text-center space-y-2">
|
|
119
|
+
<h1 className="text-2xl font-semibold text-white">
|
|
120
|
+
{mode === 'generate'
|
|
121
|
+
? 'Save Your Recovery Phrase'
|
|
122
|
+
: 'Enter Recovery Phrase'}
|
|
123
|
+
</h1>
|
|
124
|
+
<p className="text-neutral-400 text-sm">
|
|
125
|
+
{mode === 'generate'
|
|
126
|
+
? 'Write down these 12 words in order. You will need them to recover your account.'
|
|
127
|
+
: 'Enter your 12-word recovery phrase.'}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{mode === 'generate' ? (
|
|
132
|
+
<div className="grid grid-cols-3 gap-2 p-4 bg-neutral-900 rounded-lg border border-neutral-700">
|
|
133
|
+
{generatedPhrase.split(' ').map((word, i) => (
|
|
134
|
+
<div
|
|
135
|
+
key={`${word}-${i}`}
|
|
136
|
+
className="text-center py-2 bg-neutral-800 rounded text-sm"
|
|
137
|
+
>
|
|
138
|
+
<span className="text-neutral-500 mr-1">{i + 1}.</span>
|
|
139
|
+
<span className="text-white">{word}</span>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<textarea
|
|
146
|
+
value={phrase}
|
|
147
|
+
onChange={(e) => handleValidatePhrase(e.target.value)}
|
|
148
|
+
placeholder="Enter your 12-word recovery phrase..."
|
|
149
|
+
rows={3}
|
|
150
|
+
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"
|
|
151
|
+
/>
|
|
152
|
+
{phraseError && (
|
|
153
|
+
<p className="text-red-400 text-sm">{phraseError}</p>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={handleRegister}
|
|
161
|
+
disabled={loading || !phrase.trim()}
|
|
162
|
+
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"
|
|
163
|
+
>
|
|
164
|
+
{loading ? 'Registering...' : 'Complete Setup'}
|
|
165
|
+
</button>
|
|
166
|
+
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setMode('choose')
|
|
171
|
+
setPhrase('')
|
|
172
|
+
setGeneratedPhrase('')
|
|
173
|
+
setPhraseError(null)
|
|
174
|
+
}}
|
|
175
|
+
className="w-full py-2 text-neutral-400 hover:text-neutral-300 text-sm transition-colors"
|
|
176
|
+
>
|
|
177
|
+
Back
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { APP_KEY } from '../../lib/constants'
|
|
3
|
+
import { type FileMetadata, formatBytes } from '../../lib/format'
|
|
4
|
+
import { toHex } from '../../lib/hex'
|
|
5
|
+
import { useAuthStore } from '../../stores/auth'
|
|
6
|
+
import { DevNote } from '../DevNote'
|
|
7
|
+
|
|
8
|
+
type UploadedFile = {
|
|
9
|
+
id: string
|
|
10
|
+
metadata: FileMetadata
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type UploadProgress = {
|
|
14
|
+
fileName: string
|
|
15
|
+
current: number
|
|
16
|
+
total: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Detect unconfigured template placeholder. The string is split so the
|
|
20
|
+
// scaffolder's {{APP_KEY}} replacement doesn't rewrite this check.
|
|
21
|
+
const isPlaceholderKey = APP_KEY.startsWith('{' + '{')
|
|
22
|
+
|
|
23
|
+
export function UploadZone() {
|
|
24
|
+
const sdk = useAuthStore((s) => s.sdk)
|
|
25
|
+
const reset = useAuthStore((s) => s.reset)
|
|
26
|
+
const [files, setFiles] = useState<UploadedFile[]>([])
|
|
27
|
+
const [uploading, setUploading] = useState(false)
|
|
28
|
+
const [progress, setProgress] = useState<UploadProgress | null>(null)
|
|
29
|
+
const [dragOver, setDragOver] = useState(false)
|
|
30
|
+
const [error, setError] = useState<string | null>(null)
|
|
31
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
32
|
+
|
|
33
|
+
const publicKey = useMemo(() => {
|
|
34
|
+
try {
|
|
35
|
+
return sdk?.appKey().publicKey() ?? null
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}, [sdk])
|
|
40
|
+
|
|
41
|
+
const loadFiles = useCallback(async () => {
|
|
42
|
+
if (!sdk) return
|
|
43
|
+
try {
|
|
44
|
+
const events = await sdk.objectEvents(null, 100)
|
|
45
|
+
const loaded: UploadedFile[] = []
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (event.deleted || !event.object) continue
|
|
48
|
+
try {
|
|
49
|
+
const metaBytes = event.object.metadata()
|
|
50
|
+
const meta = JSON.parse(
|
|
51
|
+
new TextDecoder().decode(metaBytes),
|
|
52
|
+
) as FileMetadata
|
|
53
|
+
loaded.push({ id: event.id, metadata: meta })
|
|
54
|
+
} catch {
|
|
55
|
+
// skip objects with invalid metadata
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
setFiles(loaded)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('Failed to load files:', e)
|
|
61
|
+
}
|
|
62
|
+
}, [sdk])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
loadFiles()
|
|
66
|
+
}, [loadFiles])
|
|
67
|
+
|
|
68
|
+
async function uploadFile(file: File) {
|
|
69
|
+
if (!sdk) return
|
|
70
|
+
setUploading(true)
|
|
71
|
+
setError(null)
|
|
72
|
+
setProgress({ fileName: file.name, current: 0, total: 0 })
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const arrayBuffer = await file.arrayBuffer()
|
|
76
|
+
const data = new Uint8Array(arrayBuffer)
|
|
77
|
+
|
|
78
|
+
// Compute SHA-256 hash for metadata
|
|
79
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
80
|
+
const hash = toHex(new Uint8Array(hashBuffer))
|
|
81
|
+
|
|
82
|
+
const pinnedObject = await sdk.uploadWithProgress(
|
|
83
|
+
data,
|
|
84
|
+
(current: number, total: number) => {
|
|
85
|
+
setProgress({ fileName: file.name, current, total })
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const metadata: FileMetadata = {
|
|
90
|
+
name: file.name,
|
|
91
|
+
type: file.type || 'application/octet-stream',
|
|
92
|
+
size: file.size,
|
|
93
|
+
hash,
|
|
94
|
+
createdAt: Date.now(),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pinnedObject.updateMetadata(
|
|
98
|
+
new TextEncoder().encode(JSON.stringify(metadata)),
|
|
99
|
+
)
|
|
100
|
+
await sdk.pinObject(pinnedObject)
|
|
101
|
+
await sdk.updateObjectMetadata(pinnedObject)
|
|
102
|
+
|
|
103
|
+
setFiles((prev) => [{ id: pinnedObject.id(), metadata }, ...prev])
|
|
104
|
+
} catch (e) {
|
|
105
|
+
setError(e instanceof Error ? e.message : 'Upload failed')
|
|
106
|
+
} finally {
|
|
107
|
+
setUploading(false)
|
|
108
|
+
setProgress(null)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleFiles(fileList: FileList) {
|
|
113
|
+
for (const file of Array.from(fileList)) {
|
|
114
|
+
await uploadFile(file)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleDrop(e: React.DragEvent) {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
setDragOver(false)
|
|
121
|
+
if (e.dataTransfer.files.length > 0) {
|
|
122
|
+
handleFiles(e.dataTransfer.files)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleDisconnect() {
|
|
127
|
+
reset()
|
|
128
|
+
window.location.reload()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const progressPercent =
|
|
132
|
+
progress && progress.total > 0
|
|
133
|
+
? Math.round((progress.current / progress.total) * 100)
|
|
134
|
+
: 0
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="min-h-screen flex flex-col">
|
|
138
|
+
{/* Nav */}
|
|
139
|
+
<header className="flex items-center justify-between px-6 py-4 border-b border-neutral-800/60">
|
|
140
|
+
<div className="flex items-center gap-3">
|
|
141
|
+
<h1 className="text-sm font-semibold text-white tracking-tight">
|
|
142
|
+
Sia Storage
|
|
143
|
+
</h1>
|
|
144
|
+
<span className="text-xs text-neutral-600">
|
|
145
|
+
{files.length} file{files.length !== 1 ? 's' : ''}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex items-center gap-4">
|
|
149
|
+
{publicKey && (
|
|
150
|
+
<span
|
|
151
|
+
className="text-[11px] font-mono text-neutral-500 bg-neutral-900 border border-neutral-800 rounded px-2 py-1 cursor-default"
|
|
152
|
+
title={publicKey}
|
|
153
|
+
>
|
|
154
|
+
{publicKey.slice(0, 8)}...{publicKey.slice(-6)}
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={handleDisconnect}
|
|
160
|
+
className="text-xs text-neutral-500 hover:text-neutral-300 transition-colors"
|
|
161
|
+
>
|
|
162
|
+
Disconnect
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</header>
|
|
166
|
+
|
|
167
|
+
<div className="flex-1 p-6 space-y-5 max-w-2xl mx-auto w-full">
|
|
168
|
+
{/* Dev notes — remove these when shipping */}
|
|
169
|
+
{isPlaceholderKey && (
|
|
170
|
+
<DevNote title="Replace Your App Key">
|
|
171
|
+
<p>
|
|
172
|
+
You're using the template placeholder. Set your own key in{' '}
|
|
173
|
+
<code className="text-amber-300">src/lib/constants.ts</code> or
|
|
174
|
+
scaffold a fresh project with{' '}
|
|
175
|
+
<code className="text-amber-300">bunx create-sia-app</code>.
|
|
176
|
+
</p>
|
|
177
|
+
</DevNote>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<DevNote title="Upload Flow">
|
|
181
|
+
<p>
|
|
182
|
+
<code className="text-amber-300">sdk.uploadWithProgress()</code>{' '}
|
|
183
|
+
encrypts, erasure-codes, and uploads directly to Sia hosts. Metadata
|
|
184
|
+
is attached via{' '}
|
|
185
|
+
<code className="text-amber-300">
|
|
186
|
+
pinnedObject.updateMetadata()
|
|
187
|
+
</code>{' '}
|
|
188
|
+
then pinned with{' '}
|
|
189
|
+
<code className="text-amber-300">sdk.pinObject()</code>.
|
|
190
|
+
</p>
|
|
191
|
+
</DevNote>
|
|
192
|
+
|
|
193
|
+
{error && (
|
|
194
|
+
<div className="flex items-center justify-between px-4 py-2.5 bg-red-950/80 border border-red-900 rounded-lg text-red-300 text-sm">
|
|
195
|
+
<span>{error}</span>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={() => setError(null)}
|
|
199
|
+
className="text-red-500 hover:text-red-300 text-xs ml-4 shrink-0"
|
|
200
|
+
>
|
|
201
|
+
Dismiss
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Dropzone */}
|
|
207
|
+
<label
|
|
208
|
+
onDrop={handleDrop}
|
|
209
|
+
onDragOver={(e) => {
|
|
210
|
+
e.preventDefault()
|
|
211
|
+
setDragOver(true)
|
|
212
|
+
}}
|
|
213
|
+
onDragLeave={(e) => {
|
|
214
|
+
e.preventDefault()
|
|
215
|
+
setDragOver(false)
|
|
216
|
+
}}
|
|
217
|
+
className={`relative block border-2 border-dashed rounded-xl p-16 text-center transition-all duration-150 ${
|
|
218
|
+
uploading
|
|
219
|
+
? 'border-neutral-800 cursor-default'
|
|
220
|
+
: dragOver
|
|
221
|
+
? 'border-green-500 bg-green-500/5 cursor-pointer'
|
|
222
|
+
: 'border-neutral-800 hover:border-neutral-600 cursor-pointer'
|
|
223
|
+
}`}
|
|
224
|
+
>
|
|
225
|
+
<input
|
|
226
|
+
ref={fileInputRef}
|
|
227
|
+
type="file"
|
|
228
|
+
multiple
|
|
229
|
+
className="hidden"
|
|
230
|
+
disabled={uploading}
|
|
231
|
+
onChange={(e) => {
|
|
232
|
+
if (e.target.files) handleFiles(e.target.files)
|
|
233
|
+
e.target.value = ''
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{progress ? (
|
|
238
|
+
<div className="space-y-4">
|
|
239
|
+
<p className="text-neutral-300 text-sm">
|
|
240
|
+
Uploading{' '}
|
|
241
|
+
<span className="text-white">{progress.fileName}</span>
|
|
242
|
+
</p>
|
|
243
|
+
<div className="w-full max-w-xs mx-auto bg-neutral-800 rounded-full h-1.5 overflow-hidden">
|
|
244
|
+
<div
|
|
245
|
+
className="bg-green-500 h-full rounded-full transition-all duration-300"
|
|
246
|
+
style={{ width: `${progressPercent}%` }}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
<p className="text-neutral-600 text-xs font-mono">
|
|
250
|
+
{progress.current}/{progress.total} shards ·{' '}
|
|
251
|
+
{progressPercent}%
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
) : (
|
|
255
|
+
<div className="space-y-2">
|
|
256
|
+
<svg
|
|
257
|
+
className="w-8 h-8 mx-auto text-neutral-700"
|
|
258
|
+
viewBox="0 0 24 24"
|
|
259
|
+
fill="none"
|
|
260
|
+
stroke="currentColor"
|
|
261
|
+
strokeWidth="1.5"
|
|
262
|
+
aria-hidden="true"
|
|
263
|
+
>
|
|
264
|
+
<path d="M12 16V4m0 0l-4 4m4-4l4 4" />
|
|
265
|
+
<path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2" />
|
|
266
|
+
</svg>
|
|
267
|
+
<p className="text-neutral-400 text-sm">
|
|
268
|
+
Drop files here or click to browse
|
|
269
|
+
</p>
|
|
270
|
+
<p className="text-neutral-600 text-xs">
|
|
271
|
+
Encrypted end-to-end and stored on the Sia network
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</label>
|
|
276
|
+
|
|
277
|
+
{/* File list */}
|
|
278
|
+
{files.length > 0 && (
|
|
279
|
+
<div className="space-y-2">
|
|
280
|
+
<h2 className="text-xs font-medium text-neutral-500 uppercase tracking-wider">
|
|
281
|
+
Files
|
|
282
|
+
</h2>
|
|
283
|
+
<div className="divide-y divide-neutral-800/60">
|
|
284
|
+
{files.map((file) => (
|
|
285
|
+
<div
|
|
286
|
+
key={file.id}
|
|
287
|
+
className="flex items-center justify-between py-3 group"
|
|
288
|
+
>
|
|
289
|
+
<div className="flex-1 min-w-0 mr-4">
|
|
290
|
+
<p className="text-sm text-neutral-200 truncate">
|
|
291
|
+
{file.metadata.name}
|
|
292
|
+
</p>
|
|
293
|
+
<p className="text-xs text-neutral-600 mt-0.5">
|
|
294
|
+
{formatBytes(file.metadata.size)}
|
|
295
|
+
{file.metadata.type !== 'application/octet-stream' && (
|
|
296
|
+
<span> · {file.metadata.type}</span>
|
|
297
|
+
)}
|
|
298
|
+
</p>
|
|
299
|
+
</div>
|
|
300
|
+
<span
|
|
301
|
+
className="text-[11px] text-neutral-700 font-mono shrink-0 group-hover:text-neutral-500 transition-colors"
|
|
302
|
+
title={file.metadata.hash}
|
|
303
|
+
>
|
|
304
|
+
{file.metadata.hash.slice(0, 8)}...
|
|
305
|
+
</span>
|
|
306
|
+
</div>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type FileMetadata = {
|
|
2
|
+
name: string
|
|
3
|
+
type: string
|
|
4
|
+
size: number
|
|
5
|
+
hash: string
|
|
6
|
+
createdAt?: number
|
|
7
|
+
updatedAt?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatBytes(bytes: number): string {
|
|
11
|
+
if (bytes === 0) return '0 B'
|
|
12
|
+
const k = 1024
|
|
13
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
14
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
15
|
+
const value = bytes / k ** i
|
|
16
|
+
return `${value.toFixed(i === 0 ? 0 : 1)} ${sizes[i]}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatDate(epochMs: number): string {
|
|
20
|
+
return new Date(epochMs).toLocaleDateString(undefined, {
|
|
21
|
+
year: 'numeric',
|
|
22
|
+
month: 'short',
|
|
23
|
+
day: 'numeric',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function decodeMetadata(
|
|
28
|
+
metadataBytes: Uint8Array,
|
|
29
|
+
): Record<string, unknown> {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(new TextDecoder().decode(metadataBytes))
|
|
32
|
+
} catch {
|
|
33
|
+
return {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function toHex(bytes: Uint8Array): string {
|
|
2
|
+
return Array.from(bytes)
|
|
3
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
4
|
+
.join('')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function fromHex(hex: string): Uint8Array {
|
|
8
|
+
const bytes = new Uint8Array(hex.length / 2)
|
|
9
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
10
|
+
bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16)
|
|
11
|
+
}
|
|
12
|
+
return bytes
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import init, {
|
|
2
|
+
AppKey,
|
|
3
|
+
Builder,
|
|
4
|
+
generateRecoveryPhrase,
|
|
5
|
+
PinnedObject,
|
|
6
|
+
type SDK,
|
|
7
|
+
validateRecoveryPhrase,
|
|
8
|
+
} from 'indexd_wasm'
|
|
9
|
+
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
12
|
+
export async function initWasm(): Promise<void> {
|
|
13
|
+
if (initialized) return
|
|
14
|
+
await init()
|
|
15
|
+
initialized = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
AppKey,
|
|
20
|
+
Builder,
|
|
21
|
+
PinnedObject,
|
|
22
|
+
type SDK,
|
|
23
|
+
generateRecoveryPhrase,
|
|
24
|
+
validateRecoveryPhrase,
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import App from './App.tsx'
|
|
4
|
+
import './index.css'
|
|
5
|
+
|
|
6
|
+
const root = document.getElementById('root')
|
|
7
|
+
if (!root) throw new Error('Root element not found')
|
|
8
|
+
createRoot(root).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<App />
|
|
11
|
+
</StrictMode>,
|
|
12
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
import type { Builder, SDK } from '../lib/sdk'
|
|
4
|
+
|
|
5
|
+
export type AuthStep =
|
|
6
|
+
| 'loading'
|
|
7
|
+
| 'connect'
|
|
8
|
+
| 'approve'
|
|
9
|
+
| 'recovery'
|
|
10
|
+
| 'connected'
|
|
11
|
+
|
|
12
|
+
export type PerformancePreset = 'conservative' | 'balanced' | 'fast'
|
|
13
|
+
|
|
14
|
+
export const PRESETS: Record<
|
|
15
|
+
PerformancePreset,
|
|
16
|
+
{ priceFetches: number; downloads: number; uploads: number }
|
|
17
|
+
> = {
|
|
18
|
+
conservative: { priceFetches: 1, downloads: 2, uploads: 2 },
|
|
19
|
+
balanced: { priceFetches: 5, downloads: 10, uploads: 8 },
|
|
20
|
+
fast: { priceFetches: 10, downloads: 20, uploads: 16 },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function applyPresetToBuilder(
|
|
24
|
+
builder: Builder,
|
|
25
|
+
preset: PerformancePreset,
|
|
26
|
+
) {
|
|
27
|
+
const p = PRESETS[preset]
|
|
28
|
+
builder.withMaxPriceFetches(p.priceFetches)
|
|
29
|
+
builder.withMaxDownloads(p.downloads)
|
|
30
|
+
builder.withMaxUploads(p.uploads)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AuthState = {
|
|
34
|
+
sdk: SDK | null
|
|
35
|
+
storedKeyHex: string | null
|
|
36
|
+
indexerUrl: string
|
|
37
|
+
step: AuthStep
|
|
38
|
+
error: string | null
|
|
39
|
+
approvalUrl: string | null
|
|
40
|
+
performancePreset: PerformancePreset
|
|
41
|
+
setSdk: (sdk: SDK) => void
|
|
42
|
+
setStep: (step: AuthStep) => void
|
|
43
|
+
setError: (error: string | null) => void
|
|
44
|
+
setStoredKeyHex: (hex: string) => void
|
|
45
|
+
setIndexerUrl: (url: string) => void
|
|
46
|
+
setApprovalUrl: (url: string | null) => void
|
|
47
|
+
setPerformancePreset: (preset: PerformancePreset) => void
|
|
48
|
+
reset: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const useAuthStore = create<AuthState>()(
|
|
52
|
+
persist(
|
|
53
|
+
(set) => ({
|
|
54
|
+
sdk: null,
|
|
55
|
+
storedKeyHex: null,
|
|
56
|
+
indexerUrl: '',
|
|
57
|
+
step: 'loading',
|
|
58
|
+
error: null,
|
|
59
|
+
approvalUrl: null,
|
|
60
|
+
performancePreset: 'balanced',
|
|
61
|
+
setSdk: (sdk) => set({ sdk, step: 'connected', error: null }),
|
|
62
|
+
setStep: (step) => set({ step, error: null }),
|
|
63
|
+
setError: (error) => set({ error }),
|
|
64
|
+
setStoredKeyHex: (hex) => set({ storedKeyHex: hex }),
|
|
65
|
+
setIndexerUrl: (url) => set({ indexerUrl: url }),
|
|
66
|
+
setApprovalUrl: (url) => set({ approvalUrl: url }),
|
|
67
|
+
setPerformancePreset: (preset) => set({ performancePreset: preset }),
|
|
68
|
+
reset: () =>
|
|
69
|
+
set({
|
|
70
|
+
sdk: null,
|
|
71
|
+
storedKeyHex: null,
|
|
72
|
+
step: 'loading',
|
|
73
|
+
error: null,
|
|
74
|
+
approvalUrl: null,
|
|
75
|
+
}),
|
|
76
|
+
}),
|
|
77
|
+
{
|
|
78
|
+
name: '{{APP_NAME}}-auth',
|
|
79
|
+
partialize: (state) => ({
|
|
80
|
+
storedKeyHex: state.storedKeyHex,
|
|
81
|
+
indexerUrl: state.indexerUrl,
|
|
82
|
+
performancePreset: state.performancePreset,
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
)
|