create-sia-app 0.1.16 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "create-sia-app",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
+ "description": "Scaffold a private, encrypted storage app on the Sia network.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/SiaFoundation/create-sia-app.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/SiaFoundation/create-sia-app/issues"
12
+ },
13
+ "homepage": "https://github.com/SiaFoundation/create-sia-app#readme",
4
14
  "type": "module",
5
15
  "bin": {
6
16
  "create-sia-app": "./dist/index.js"
@@ -1,157 +1,249 @@
1
1
  # Sia Starter — AI Assistant Guide
2
2
 
3
- ## Overview
3
+ This is a starter for apps backed by the [Sia](https://sia.tech) storage network. Read this before writing code: it contains the mental model, the canonical patterns, and the footguns. If you're about to invent a pattern that isn't here, the odds are you shouldn't.
4
4
 
5
- A starter template for building decentralized storage apps on the Sia network. Uses [`@siafoundation/sia-storage`](https://www.npmjs.com/package/@siafoundation/sia-storage) — a TypeScript SDK that ships a pre-compiled WASM binary for encryption, uploads, downloads, and key management. WASM runs on the main thread (Rust async — no workers required).
5
+ ## Stack
6
6
 
7
- **Tech stack:** React 19, TypeScript, Vite, Tailwind CSS 4, Zustand, `@siafoundation/sia-storage`
7
+ React 19, TypeScript, Vite, Tailwind CSS 4, Zustand, [`@siafoundation/sia-storage`](https://www.npmjs.com/package/@siafoundation/sia-storage).
8
8
 
9
- ## Architecture
9
+ ## Types are the source of truth
10
10
 
11
- ### Auth Flow State Machine
11
+ The SDK's shape lives in its `.d.ts` files — more current and precise than any prose. Before calling an unfamiliar method, read:
12
12
 
13
- The app uses a step-based auth flow managed by Zustand (`src/stores/auth.ts`):
13
+ - `node_modules/@siafoundation/sia-storage/dist/index.d.ts` — top-level exports.
14
+ - `node_modules/@siafoundation/sia-storage/wasm/sia_storage_wasm.d.ts` — WASM-bound classes with full method signatures.
14
15
 
15
- ```
16
- loading → connect → approve → recovery → connected
17
- ```
16
+ Don't hallucinate methods. If a method isn't in those files, it doesn't exist.
17
+
18
+ ## Core concepts
19
+
20
+ **Indexer** — A service that coordinates storage: it tracks which hosts hold which encrypted shards, handles payments, and repairs slabs when hosts disappear. **It sees only ciphertext.** Trusted for availability and correctness of the repair/payment flow, *not* for data privacy. The indexer URL lives in `src/lib/constants.ts`.
21
+
22
+ **Hosts** — The actual storage providers. The browser talks to them directly over WebTransport for uploads and downloads. Erasure coding means any sufficient subset of hosts is enough to reconstruct a file.
23
+
24
+ **App** — Identified to the indexer by `APP_KEY` (32-byte hex) + `APP_META` in `src/lib/constants.ts`. Apps are namespaces: objects stored under one `APP_KEY` aren't visible to another.
25
+
26
+ **User key** — An `AppKey` instance derived from the user's 12-word BIP-39 recovery phrase. It's the encryption key and the indexer-auth identity for that user *within* the app. Persisted as hex in `localStorage` so users don't re-enter the phrase every session.
27
+
28
+ > **`APP_KEY` vs `AppKey`** — `APP_KEY` (constant, screaming snake) is the *app's* identity in `APP_META.appId`. `AppKey` (class, PascalCase) is the *user's* ed25519 key. They are not the same thing.
29
+
30
+ **Object** — A file or blob you upload. Represented at rest by a `PinnedObject` handle. Has an ID, a size, one or more slabs, and encrypted metadata.
31
+
32
+ **Slab / shard** — A file is split into slabs (default ~40 MB each), and each slab is erasure-coded into shards (10 data + 20 parity by default — see `DATA_SHARDS` / `PARITY_SHARDS` in `src/lib/constants.ts`). Shards are what actually ship to hosts.
33
+
34
+ **Pin** — "This object should persist." An unpinned object is transient. Always `await sdk.pinObject(obj)` after a successful upload, or the indexer will garbage-collect it.
18
35
 
19
- - **loading**: WASM initializes, checks for stored app key
20
- - **connect**: User enters indexer URL, app constructs a `Builder` and calls `requestConnection()`
21
- - **approve**: User visits approval URL in another tab; app polls `builder.waitForApproval()`
22
- - **recovery**: User generates or enters a 12-word BIP-39 recovery phrase; `builder.register(phrase)` returns the `Sdk`
23
- - **connected**: `Sdk` is ready, main app UI renders
36
+ **Metadata** — Encrypted app-defined bytes attached to an object (filename, MIME type, tags, etc.). Call `event.object.metadata()` to read, `pinnedObject.updateMetadata(bytes)` + `sdk.updateObjectMetadata(pinnedObject)` to write. Keep it under a few KB — it's a descriptor, not a payload.
24
37
 
25
- ### Returning users
38
+ ## Auth flow
26
39
 
27
- Returning users skip `requestConnection`/`waitForApproval`/`register` entirely. `AuthFlow` constructs a `Builder` and calls `builder.connected(appKey)` with the persisted key — it returns an `Sdk` if the key is still valid, or `undefined` to fall back to the `connect` step.
40
+ Step-based, managed by Zustand (`src/stores/auth.ts`):
28
41
 
29
- ### SDK
42
+ ```
43
+ loading → connect → approve → recovery → connected
44
+ ```
30
45
 
31
- `@siafoundation/sia-storage` handles:
32
- - Encrypted file uploads/downloads (erasure coding + encryption)
33
- - Key derivation from recovery phrases (BIP-39)
34
- - Object pinning and metadata management
35
- - Connection auth with indexers
36
- - Direct streaming to/from Sia hosts (no worker pool needed)
46
+ - **loading** — `initSia()` loads WASM; `AuthFlow` checks for a stored user key.
47
+ - **connect** User enters indexer URL. App constructs `new Builder(url, APP_META)` and calls `requestConnection()`.
48
+ - **approve** User visits `builder.responseUrl()` in another tab; the app polls `builder.waitForApproval()`.
49
+ - **recovery** User generates or enters a BIP-39 phrase; `builder.register(phrase)` returns the `Sdk`.
50
+ - **connected** `Sdk` is ready; main UI renders.
37
51
 
38
- ### Zustand Persistence
52
+ **Returning users** skip connect/approve/recovery entirely: `AuthFlow` constructs a `Builder` and calls `builder.connected(appKey)` with the persisted key. Returns an `Sdk` if valid, `undefined` to fall back to `connect`.
39
53
 
40
- Auth state persists to localStorage via Zustand's `persist` middleware. The storage key is `{app-name}-auth`. Persisted fields: `storedKeyHex`, `indexerUrl`. The app key is stored as hex via the TC39 `Uint8Array.prototype.toHex` method.
54
+ **Persistence**: Zustand `persist` middleware writes to `localStorage` under `sia-auth-<first-16-of-APP_KEY>` (keyed by app so two scaffolds on `localhost:5173` don't collide). Persisted: `storedKeyHex`, `indexerUrl`. The live `Sdk` is **not** persisted it's rehydrated by calling `builder.connected(appKey)` on mount.
41
55
 
42
- ## Key Files
56
+ ## Key files
43
57
 
44
- | File | Description |
45
- |------|-------------|
46
- | `src/lib/constants.ts` | App key, app name, indexer URL, app metadata (typed `AppMetadata`) |
47
- | `src/stores/auth.ts` | Auth state machine (Zustand + persist), holds the `Sdk` |
48
- | `src/stores/toast.ts` | Toast notification store (auto-dismiss) |
49
- | `src/components/Navbar.tsx` | App navbar with title, public key, sign out |
50
- | `src/components/Toast.tsx` | Toast overlay component |
51
- | `src/components/CopyButton.tsx` | Copy-to-clipboard button with toast |
52
- | `src/components/auth/AuthFlow.tsx` | Auth orchestrator `initSia()`, returning-user reconnect via `Builder.connected` |
53
- | `src/components/auth/ConnectScreen.tsx` | Indexer URL form; constructs `new Builder(url, APP_META)` and calls `requestConnection()` |
54
- | `src/components/auth/ApproveScreen.tsx` | Approval polling (auto-polls on mount) |
55
- | `src/components/auth/RecoveryScreen.tsx` | Recovery phrase generation/import; `builder.register(phrase)` `Sdk` |
56
- | `src/components/upload/UploadZone.tsx` | File upload/download dropzone + file list |
57
- | `src/components/DevNote.tsx` | Developer callout component |
58
- | `src/types/uint8array-hex.d.ts` | Ambient types for TC39 `Uint8Array.toHex`/`fromHex` (drop once TS lib ships them) |
58
+ | File | Role |
59
+ |---|---|
60
+ | `src/lib/constants.ts` | `APP_KEY`, `APP_NAME`, `APP_META` (`AppMetadata`), indexer default, erasure-coding constants |
61
+ | `src/stores/auth.ts` | Zustand store: holds the `Sdk`, persists `storedKeyHex` + `indexerUrl` |
62
+ | `src/stores/toast.ts` | Toast notifications (auto-dismiss) |
63
+ | `src/components/auth/AuthFlow.tsx` | Orchestrator: `initSia()`, returning-user reconnect |
64
+ | `src/components/auth/ConnectScreen.tsx` | `new Builder(url, APP_META).requestConnection()` |
65
+ | `src/components/auth/ApproveScreen.tsx` | Polls `builder.waitForApproval()` |
66
+ | `src/components/auth/RecoveryScreen.tsx` | Generate / validate phrase → `builder.register()` `Sdk` |
67
+ | `src/components/upload/UploadZone.tsx` | **Reference implementation.** Full cycle: dropzone upload pin metadata → list → download. Read this first when building new features. |
68
+ | `src/components/Navbar.tsx` | Public key + sign out |
69
+ | `src/components/DevNote.tsx` | Amber callout remove or replace for production |
70
+ | `src/types/uint8array-hex.d.ts` | Ambient types for TC39 `Uint8Array.{toHex,fromHex}` (drop once TS lib ships them) |
59
71
 
60
- ## SDK Usage Patterns
72
+ ## SDK usage patterns
61
73
 
62
- ### Upload a file
74
+ ### Upload pin → metadata
63
75
 
64
76
  ```ts
65
77
  import { PinnedObject } from '@siafoundation/sia-storage'
78
+ import { DATA_SHARDS, PARITY_SHARDS } from '../../lib/constants'
66
79
 
67
80
  const object = new PinnedObject()
68
- const pinnedObject = await sdk.upload(object, file.stream(), {
81
+ const pinned = await sdk.upload(object, file.stream(), {
69
82
  maxInflight: 10,
70
- onShardUploaded: (progress) => {
71
- // progress: { hostKey, shardSize, shardIndex, slabIndex, elapsedMs }
72
- console.log(`shard ${progress.shardIndex} uploaded (${progress.shardSize}b)`)
83
+ dataShards: DATA_SHARDS,
84
+ parityShards: PARITY_SHARDS,
85
+ onShardUploaded: (p) => {
86
+ // p: { hostKey, shardSize, shardIndex, slabIndex, elapsedMs }
87
+ // shardSize is post-erasure-coding bytes, not source bytes.
73
88
  },
74
89
  })
75
90
 
76
- pinnedObject.updateMetadata(new TextEncoder().encode(JSON.stringify({
77
- name: 'file.txt',
78
- type: 'text/plain',
79
- size: file.size,
80
- hash: '...',
81
- })))
82
- await sdk.pinObject(pinnedObject)
83
- await sdk.updateObjectMetadata(pinnedObject)
91
+ pinned.updateMetadata(
92
+ new TextEncoder().encode(JSON.stringify({ name: file.name, type: file.type, size: file.size })),
93
+ )
94
+ await sdk.pinObject(pinned)
95
+ await sdk.updateObjectMetadata(pinned)
84
96
  ```
85
97
 
86
- ### Download a file
98
+ All three calls matter:
99
+ 1. `upload` writes the encrypted shards to hosts.
100
+ 2. `pinObject` tells the indexer to keep it (without this, it's eventually GC'd).
101
+ 3. `updateObjectMetadata` persists the descriptor so other sessions can find it.
87
102
 
88
- `sdk.download` returns a `ReadableStream` of `Uint8Array` chunks. Buffer it, or stream it directly into a `Response`/`Blob`:
103
+ ### Byte progress (source units, not on-wire units)
104
+
105
+ `onShardUploaded.shardSize` is on-wire bytes (encoded). To show a progress bar in source-file units, use `encodedSize` as the denominator:
89
106
 
90
107
  ```ts
91
- const stream = sdk.download(pinnedObject, {
92
- maxInflight: 10,
93
- onShardDownloaded: (progress) => {
94
- console.log(`shard ${progress.shardIndex} downloaded`)
95
- },
96
- })
97
- const blob = await new Response(stream).blob()
108
+ import { encodedSize } from '@siafoundation/sia-storage'
109
+ const encodedTotal = encodedSize(file.size, DATA_SHARDS, PARITY_SHARDS)
110
+ const sourceProgress = (bytesUploaded / encodedTotal) * file.size
98
111
  ```
99
112
 
100
- ### List files
113
+ ### Download
114
+
115
+ Returns a `ReadableStream<Uint8Array>`. Buffer or pipe:
101
116
 
102
117
  ```ts
103
- const events = await sdk.objectEvents(undefined, 100)
104
- for (const event of events) {
105
- if (!event.deleted && event.object) {
106
- const meta = JSON.parse(new TextDecoder().decode(event.object.metadata()))
107
- console.log(meta.name, event.object.size())
108
- }
109
- }
118
+ const stream = sdk.download(pinnedObject, { maxInflight: 10 })
119
+ const blob = await new Response(stream).blob()
110
120
  ```
111
121
 
112
- ### Delete a file
122
+ ### Delete
113
123
 
114
124
  ```ts
115
125
  await sdk.deleteObject(objectId)
116
126
  ```
117
127
 
118
- ### Share a file
128
+ ### Share / consume a share URL
119
129
 
120
130
  ```ts
121
- const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
122
- const shareUrl = sdk.shareObject(pinnedObject, validUntil)
131
+ const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
132
+ const url = sdk.shareObject(pinnedObject, validUntil)
133
+ // On the recipient side (can be a different app / no auth needed):
134
+ const obj = await sdk.sharedObject(url)
135
+ const stream = sdk.download(obj)
123
136
  ```
124
137
 
125
- ### Download a shared file
138
+ Share URLs embed the decryption key in the fragment (`#...`) — never sent to the indexer.
139
+
140
+ ### Pack many small files
141
+
142
+ `sdk.uploadPacked()` batches small files into shared slabs to avoid wasting storage:
126
143
 
127
144
  ```ts
128
- const object = await sdk.sharedObject(shareUrl)
129
- const stream = sdk.download(object)
145
+ const packed = sdk.uploadPacked({ maxInflight: 10 })
146
+ await packed.add(fileA.stream())
147
+ await packed.add(fileB.stream())
148
+ for (const obj of await packed.finalize()) await sdk.pinObject(obj)
130
149
  ```
131
150
 
132
- ## Customization
151
+ ## Syncing with the indexer
133
152
 
134
- ### Change app key
153
+ `sdk.objectEvents(cursor, limit)` is the one sync primitive. Each `ObjectEvent` has `id`, `updatedAt: Date`, `deleted: boolean`, `object: PinnedObject | null`.
135
154
 
136
- Edit `src/lib/constants.ts`. The app key is a 32-byte hex string that identifies your app to the indexer. Generate one with:
155
+ Cursor is `{ id: string, after: Date }`. Passing it returns events strictly after that point.
156
+
157
+ ### `updatedAt` bumps
158
+
159
+ - **User actions from any device using this app key**: `updateObjectMetadata`, `deleteObject`, re-pin.
160
+ - **Indexer repairs**: when a host goes offline the indexer migrates shards to healthy hosts and bumps `updatedAt`. Your next poll picks up the repaired state automatically.
161
+
162
+ Both surface through the same stream, which is why polling `objectEvents` is the pattern every Sia app should implement.
163
+
164
+ ### Cross-device sync
137
165
 
138
166
  ```ts
139
- crypto.getRandomValues(new Uint8Array(32)).toHex()
167
+ // Fresh session: no cursor, pull latest N.
168
+ const events = await sdk.objectEvents(undefined, 500)
169
+ events.forEach(apply)
170
+ persistCursor(latest(events))
171
+
172
+ // Periodic tick: only what changed.
173
+ const cursor = loadCursor() // { id, after: Date } or undefined
174
+ const events = await sdk.objectEvents(cursor, 200)
175
+ events.forEach(apply)
176
+ if (events.length) persistCursor(latest(events))
140
177
  ```
141
178
 
142
- ### Replace the upload UI
179
+ Operational tips:
180
+ - Persist the cursor in `localStorage` keyed by app key.
181
+ - Poll only while the tab is visible (`document.visibilityState === 'visible'`).
182
+ - Advance the cursor only after local merge succeeds.
183
+ - `event.deleted === true` → evict from local store.
184
+
185
+ If a specific SDK build rejects the cursor shape (edge cases with `Date` serialization happen), fall back to `undefined` + client-side filter on `event.updatedAt.getTime() > watermarkMs`.
186
+
187
+ ## Gotchas
188
+
189
+ Things that look right but aren't:
190
+
191
+ - **Don't persist `Sdk` to storage.** It's a live WASM handle; rehydrate it via `Builder.connected(appKey)` on mount.
192
+ - **Don't forget `pinObject`.** A successful `upload` that isn't pinned is a transient object — the indexer will eventually drop it.
193
+ - **Don't conflate `APP_KEY` and `AppKey`.** `APP_KEY` is the app identity constant; `AppKey` is the user's key class.
194
+ - **Don't stuff large payloads into metadata.** It's a descriptor. Put file bytes in the object, not in metadata.
195
+ - **Don't re-bundle or wrap the WASM.** Vite dev needs `optimizeDeps: { exclude: ['@siafoundation/sia-storage'] }` (already set in `vite.config.ts`) because the SDK's `import.meta.url`-relative WASM path breaks under pre-bundling. If you add another bundler (Webpack, Rollup), check the SDK README for the equivalent.
196
+ - **Don't call `initSia()` more than once per mount.** It's already called in `AuthFlow`; additional call sites create race conditions.
197
+ - **Don't call `builder.waitForApproval()` twice.** React strict mode will remount — `ApproveScreen` guards this with a `pollStarted` ref. Follow that pattern.
198
+ - **`onShardUploaded.shardSize` is encoded bytes, not source bytes.** If you sum it, you're measuring on-wire traffic. Use `encodedSize()` for the matching denominator, or scale to source via `(bytes / encodedTotal) * file.size`.
199
+ - **Numeric types differ on Node vs browser.** Browser uses `number` (~9 PB safe); Node uses `bigint`. Template is browser-only, so `number` is correct here.
200
+ - **Sign-out should clear localStorage.** `useAuthStore.getState().reset()` + `window.location.reload()` is the pattern in `Navbar.tsx`.
201
+
202
+ ## Extending the starter
203
+
204
+ ### Swap out `UploadZone`
205
+
206
+ `src/App.tsx` renders `<UploadZone />` after auth. Replace it with your own post-auth component. Read `UploadZone.tsx` first — it shows the full upload → pin → metadata → list cycle that most apps will want to reuse in some form.
207
+
208
+ Access the SDK:
143
209
 
144
- The post-auth UI is rendered in `src/App.tsx`. Replace `<UploadZone />` with your own component. The `Sdk` is available via `useAuthStore((s) => s.sdk)`. All SDK methods live directly on `Sdk` — `sdk.upload()`, `sdk.download()`, `sdk.objectEvents()`, etc.
210
+ ```tsx
211
+ const sdk = useAuthStore((s) => s.sdk)
212
+ if (!sdk) return null
213
+ ```
145
214
 
146
215
  ### Add routes
147
216
 
148
- Install `react-router-dom` and wrap your app. The auth flow should gate all routes.
217
+ Install `react-router-dom`. Gate routes on `step === 'connected'`; render `<AuthFlow />` otherwise.
218
+
219
+ ### Add fields to file metadata
220
+
221
+ Extend the `FileMetadata` type in `UploadZone.tsx`, write the extra fields in the upload handler, read them back in `loadFiles`. Schema is app-owned — do whatever makes sense. Just keep it small.
222
+
223
+ ### Search / filter
224
+
225
+ Metadata is encrypted at rest but decrypted client-side, so once you've hydrated from `objectEvents` you can filter/sort/search it in memory like any local list. The indexer can't do this for you (it sees ciphertext) — search is always client-side.
226
+
227
+ ### Multi-device updates
149
228
 
150
- ## Common Commands
229
+ Implement the polling pattern from **Syncing with the indexer**. That's how uploads on one device appear on another.
230
+
231
+ ### Change erasure-coding parameters
232
+
233
+ Edit `DATA_SHARDS` / `PARITY_SHARDS` in `src/lib/constants.ts`. More parity = survives more host failures at the cost of more on-wire bytes. Keep `UploadZone`'s `encodedSize()` call in sync (it already reads from the same constants).
234
+
235
+ ### Change the app key
236
+
237
+ `APP_KEY` in `src/lib/constants.ts`. Generate with `crypto.getRandomValues(new Uint8Array(32)).toHex()`. **Changing it makes all previously uploaded data invisible to the app** — app key is the namespace.
238
+
239
+ ## Commands
151
240
 
152
241
  ```bash
153
- bun install # Install dependencies
154
- bun dev # Start dev server
155
- bun run build # Production build
156
- bun run check # Lint with Biome
242
+ bun install # Install deps
243
+ bun dev # Vite dev server (WASM loads lazily, ~100ms)
244
+ bun run build # tsc + Vite production build
245
+ bun run check # Biome lint + format check (use --write to fix)
246
+ bun x playwright test e2e/smoke.spec.ts # App-loads-without-errors smoke
157
247
  ```
248
+
249
+ After any substantive change, run `bun run check` (auto-fix with `bun x biome check . --write`) and `bun run build` before committing.
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "react": "^19.2.0",
14
14
  "react-dom": "^19.2.0",
15
- "@siafoundation/sia-storage": "^0.0.8",
15
+ "@siafoundation/sia-storage": "^0.0.9",
16
16
  "zustand": "^5.0.11"
17
17
  },
18
18
  "devDependencies": {
@@ -1,5 +1,5 @@
1
- import { useEffect, useRef, useState } from 'react'
2
1
  import type { Builder } from '@siafoundation/sia-storage'
2
+ import { useEffect, useRef, useState } from 'react'
3
3
  import { useAuthStore } from '../../stores/auth'
4
4
  import { CopyButton } from '../CopyButton'
5
5
  import { DevNote } from '../DevNote'
@@ -1,5 +1,5 @@
1
- import { useEffect, useRef } from 'react'
2
1
  import { AppKey, Builder, initSia } from '@siafoundation/sia-storage'
2
+ import { useEffect, useRef } from 'react'
3
3
  import { APP_META } from '../../lib/constants'
4
4
  import { useAuthStore } from '../../stores/auth'
5
5
  import { ApproveScreen } from './ApproveScreen'
@@ -1,5 +1,5 @@
1
- import { useState } from 'react'
2
1
  import { Builder } from '@siafoundation/sia-storage'
2
+ import { useState } from 'react'
3
3
  import { APP_META, DEFAULT_INDEXER_URL } from '../../lib/constants'
4
4
  import { useAuthStore } from '../../stores/auth'
5
5
  import { DevNote } from '../DevNote'
@@ -1,9 +1,9 @@
1
- import { useState } from 'react'
2
1
  import {
3
2
  type Builder,
4
3
  generateRecoveryPhrase,
5
4
  validateRecoveryPhrase,
6
5
  } from '@siafoundation/sia-storage'
6
+ import { useState } from 'react'
7
7
  import { useAuthStore } from '../../stores/auth'
8
8
  import { CopyButton } from '../CopyButton'
9
9
  import { DevNote } from '../DevNote'
@@ -1,5 +1,9 @@
1
+ import {
2
+ encodedSize,
3
+ PinnedObject,
4
+ type ShardProgress,
5
+ } from '@siafoundation/sia-storage'
1
6
  import { useCallback, useEffect, useRef, useState } from 'react'
2
- import { encodedSize, PinnedObject, type ShardProgress } from '@siafoundation/sia-storage'
3
7
  import { APP_KEY, DATA_SHARDS, PARITY_SHARDS } from '../../lib/constants'
4
8
  import { useAuthStore } from '../../stores/auth'
5
9
  import { DevNote } from '../DevNote'