@meddleware/walrus-ui 0.1.57 → 0.1.58
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/CHANGELOG.md +47 -0
- package/README.md +0 -2
- package/package.json +5 -6
- package/src/components/MyBlobs.vue +41 -36
- package/src/components/WalrusView.vue +25 -4
- package/src/composables/useExtendQuotes.ts +69 -0
- package/src/composables/useOwnedBlobs.ts +33 -3
- package/src/config.ts +6 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@meddleware/walrus-ui` are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/). Before 0.2.0, breaking changes ship as patch releases.
|
|
6
|
+
|
|
7
|
+
## [0.1.58] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Changed (breaking, pre-v0.2)
|
|
10
|
+
|
|
11
|
+
- **Adopts `@meddleware/walrus-relay` 0.1.31.** `useAccessGate` is given the network and a client getter
|
|
12
|
+
that serves the active network only; the access gate is still built by `relayGateConfig`, so it names the
|
|
13
|
+
published `access_gate` deployment. The purchase prompt (`AccessGateCta`) now shows the price, payees,
|
|
14
|
+
network and pass kind read from the chain, with a "Read the price again" button when they cannot be read.
|
|
15
|
+
- **`VITE_ACCESS_GATE_PRICE_MIST_TESTNET` is removed** (code, `.env.example`, Dockerfile build argument,
|
|
16
|
+
release workflow and README). The price is read from the gate on the chain; a build that still sets the
|
|
17
|
+
variable ignores it.
|
|
18
|
+
- `WalrusUpload` gets `sessionKey` (`<network>:<address>`), so a change of account or network discards the
|
|
19
|
+
chosen file and any held retry; the upload approvals hint follows the held pass (`singleUse`).
|
|
20
|
+
- Dependencies: `@meddleware/walrus-client` ^0.0.28, `@meddleware/walrus-relay` ^0.1.31,
|
|
21
|
+
`@meddleware/design-tokens` ^0.1.10, `@meddleware/access-gate-client` ^0.0.9. The unused
|
|
22
|
+
`@mysten/wallet-standard` dependency is dropped.
|
|
23
|
+
- Image base: `static-server` 0.1.8 (`sha256:e32cf42b…4000`).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **A slower owned-blob load no longer overwrites a newer one** after a wallet-account or network switch,
|
|
28
|
+
a disconnect, or a switch back to a list that is already held (a stale result, error or `loading` flag is
|
|
29
|
+
dropped); a cleared list no longer reads as loaded for the previous account.
|
|
30
|
+
- **Extend shows its cost before the wallet opens.** The seeded +10 epochs is priced on the first press
|
|
31
|
+
(the button reads "Show cost"), the amount and `≈ WAL` cost are shown, and the wallet opens on the next
|
|
32
|
+
press (`Extend`). An amount that cannot be priced says "price unavailable".
|
|
33
|
+
- **My Blobs text meets AA contrast in both themes.** It read `--mw-color-*` custom properties that
|
|
34
|
+
design-tokens does not define, so literal fallbacks (`#888`, `#c00`, `#ddd`, `#f90`) ignored theme and
|
|
35
|
+
season; it now uses `--muted`, `--danger`, `--border`, `--warning` (tint only) and `--accent`.
|
|
36
|
+
A test fails if a component reads a custom property design-tokens does not define.
|
|
37
|
+
|
|
38
|
+
### Security
|
|
39
|
+
|
|
40
|
+
- `.env*` is git-ignored (except `.env.example`).
|
|
41
|
+
- `SECURITY.md` invariant 1 names the real wiring of the tip ceiling (`maxTipMist` to `runBlobUpload`).
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- `CHANGELOG.md` (it was listed in the package `files` without existing).
|
|
46
|
+
- Tests: stale-load guard (`useOwnedBlobs.stale`), extend quotes, the custom-property check, and the
|
|
47
|
+
gate-config/`useAccessGate` pairing.
|
package/README.md
CHANGED
|
@@ -36,7 +36,6 @@ All `VITE_*` vars are baked into the static bundle at build time.
|
|
|
36
36
|
| `VITE_WALRUS_RELAY_MAINNET` | Public Mysten relay | Operator relay URL for mainnet |
|
|
37
37
|
| `VITE_ACCESS_GATE_ID_TESTNET` | — | Gate object ID (testnet; unset = no gate) |
|
|
38
38
|
| `VITE_ACCESS_GATE_SOULBOUND_TESTNET` | `false` | `true` if NFTs are soulbound |
|
|
39
|
-
| `VITE_ACCESS_GATE_PRICE_MIST_TESTNET` | `0` | Purchase price in MIST |
|
|
40
39
|
| `VITE_ACCESS_GATE_ID_MAINNET` | — | Gate object ID (mainnet) |
|
|
41
40
|
| `VITE_UPLOAD_RELAY_MAX_TIP_MIST` | `50000000` | Max relay tip cap in MIST (0.05 SUI) |
|
|
42
41
|
|
|
@@ -54,7 +53,6 @@ docker build \
|
|
|
54
53
|
--build-arg VITE_NETWORK=testnet \
|
|
55
54
|
--build-arg VITE_WALRUS_RELAY_TESTNET=https://sui-walrus-relay.example.com \
|
|
56
55
|
--build-arg VITE_ACCESS_GATE_ID_TESTNET=0x... \
|
|
57
|
-
--build-arg VITE_ACCESS_GATE_PRICE_MIST_TESTNET=100000000 \
|
|
58
56
|
-t walrus-ui:latest .
|
|
59
57
|
```
|
|
60
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"homepage": "https://docs.meddleware.co.uk/blockchain/sui/walrus-storage/",
|
|
5
5
|
"description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs — wallet-connected, NFT-gated relay support.",
|
|
6
6
|
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
@@ -47,12 +47,11 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@meddleware/design-tokens": "^0.1.
|
|
50
|
+
"@meddleware/design-tokens": "^0.1.10",
|
|
51
51
|
"@meddleware/ui": "^0.1.31",
|
|
52
|
-
"@meddleware/walrus-client": "^0.0.
|
|
53
|
-
"@meddleware/walrus-relay": "^0.1.
|
|
52
|
+
"@meddleware/walrus-client": "^0.0.28",
|
|
53
|
+
"@meddleware/walrus-relay": "^0.1.31",
|
|
54
54
|
"@mysten/sui": "^2.33.2",
|
|
55
|
-
"@mysten/wallet-standard": "^0.21.0",
|
|
56
55
|
"@mysten/walrus": "~1.2.32",
|
|
57
56
|
"@mysten/walrus-wasm": "~0.3.1",
|
|
58
57
|
"vue": "^3.5.43"
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"@meddleware/wallet-adapter": ">=0.0.12 <0.2.0"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@meddleware/access-gate-client": "^0.0.
|
|
63
|
+
"@meddleware/access-gate-client": "^0.0.9",
|
|
65
64
|
"@meddleware/eslint-config": "^0.0.2",
|
|
66
65
|
"@meddleware/wallet-adapter": "^0.0.17",
|
|
67
66
|
"@playwright/test": "~1.63.0",
|
|
@@ -7,6 +7,7 @@ import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
|
7
7
|
import type { Executor } from '../wallet.js'
|
|
8
8
|
import { walrusNetwork, walruscanBlobUrl } from '../config.js'
|
|
9
9
|
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
10
|
+
import { useExtendQuotes } from '../composables/useExtendQuotes.js'
|
|
10
11
|
import { groupBlobs, expiryLabel, maxExtendableEpochs, type BlobGroup } from '../blob-groups.js'
|
|
11
12
|
import {
|
|
12
13
|
pendingCertifyKey,
|
|
@@ -49,7 +50,6 @@ const pending = ref<Record<string, PendingCertify>>({})
|
|
|
49
50
|
const busy = ref<string | null>(null)
|
|
50
51
|
const actionStatus = ref<Record<string, string>>({})
|
|
51
52
|
const extendAmount = ref<Record<string, number>>({})
|
|
52
|
-
const extendCostFrost = ref<Record<string, bigint | null>>({})
|
|
53
53
|
|
|
54
54
|
const MAX = MAX_SINGLE_RESERVATION_EPOCHS
|
|
55
55
|
|
|
@@ -104,35 +104,39 @@ function refreshPending(): void {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// ── Extend ──────────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
107
|
+
// Extend adds storage only (no one-time write cost), so the quote prices the `storageCost` component.
|
|
108
|
+
async function estimateExtendCost(size: number, epochs: number): Promise<bigint | null> {
|
|
109
|
+
const { createWalrusClient, estimateStorageCost } = await import('@meddleware/walrus-client')
|
|
110
|
+
const walrusClient = createWalrusClient({ network: currentNetwork(), wasmUrl: walrusWasmUrl })
|
|
111
|
+
return (await estimateStorageCost(walrusClient, size, epochs)).storageCost
|
|
112
|
+
}
|
|
113
|
+
const extendQuotes = useExtendQuotes(estimateExtendCost)
|
|
108
114
|
|
|
109
|
-
// User picked an amount → clamp and (re)price it.
|
|
110
|
-
// on load, to avoid spinning up a Walrus client per blob
|
|
115
|
+
// User picked an amount → clamp and (re)price it. Only the picked amount is priced here, not every
|
|
116
|
+
// row on load, to avoid spinning up a Walrus client per blob; a row that was never touched is priced
|
|
117
|
+
// when Extend is first pressed (see `extendBlob`), before the wallet opens.
|
|
111
118
|
function setExtendAmount(blob: OwnedBlob, value: number | string): void {
|
|
112
119
|
const max = maxAddable(blob.endEpoch)
|
|
113
120
|
const v = Math.min(max, Math.max(1, Math.floor(Number(value) || 1)))
|
|
114
121
|
extendAmount.value = { ...extendAmount.value, [blob.objectId]: v }
|
|
115
|
-
void
|
|
122
|
+
void extendQuotes.price(blob.objectId, blob.size, v)
|
|
116
123
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (extendReq[id] === token) {
|
|
126
|
-
extendCostFrost.value = { ...extendCostFrost.value, [id]: cost.storageCost }
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
if (extendReq[id] === token) extendCostFrost.value = { ...extendCostFrost.value, [id]: null }
|
|
130
|
-
}
|
|
124
|
+
|
|
125
|
+
/** What the row shows beside the Extend button for its current amount. */
|
|
126
|
+
function extendQuoteLabel(blob: OwnedBlob): string {
|
|
127
|
+
const epochs = extendAmount.value[blob.objectId]
|
|
128
|
+
const q = extendQuotes.quoteFor(blob.objectId, epochs)
|
|
129
|
+
if (q?.state === 'ready') return `≈${formatCoinAmount(q.costFrost, 'WAL')} for +${q.epochs} epochs`
|
|
130
|
+
if (q?.state === 'unavailable') return 'price unavailable'
|
|
131
|
+
return extendQuotes.pricing.value[blob.objectId] === epochs ? 'pricing…' : ''
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
134
135
|
const epochs = extendAmount.value[blob.objectId]
|
|
135
136
|
if (!epochs || busy.value) return
|
|
137
|
+
// The wallet opens only for an amount whose cost the user has been shown. The seeded default has
|
|
138
|
+
// no quote yet: price it now, show it, and let the user press Extend again to confirm.
|
|
139
|
+
if (!(await extendQuotes.shownBeforeSigning(blob.objectId, blob.size, epochs))) return
|
|
136
140
|
busy.value = blob.objectId
|
|
137
141
|
actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Building transaction…' }
|
|
138
142
|
try {
|
|
@@ -143,6 +147,7 @@ async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
|
143
147
|
actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Approve in wallet…' }
|
|
144
148
|
const { digest } = await executor.signAndExecute(tx)
|
|
145
149
|
await executor.waitForTransaction(digest)
|
|
150
|
+
extendQuotes.clear(blob.objectId)
|
|
146
151
|
actionStatus.value = { ...actionStatus.value, [blob.objectId]: `Extended ✓ (${digest.slice(0, 8)}…)` }
|
|
147
152
|
await refresh()
|
|
148
153
|
} catch (e) {
|
|
@@ -298,10 +303,10 @@ watch(
|
|
|
298
303
|
>
|
|
299
304
|
Max
|
|
300
305
|
</button>
|
|
301
|
-
<button type="button" :disabled="!!busy" @click="extendBlob(g.representative)">
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
</span>
|
|
306
|
+
<button type="button" :disabled="!!busy" @click="extendBlob(g.representative)">
|
|
307
|
+
{{ extendQuotes.quoteFor(g.representative.objectId, extendAmount[g.representative.objectId]) ? 'Extend' : 'Show cost' }}
|
|
308
|
+
</button>
|
|
309
|
+
<span class="est" aria-live="polite">{{ extendQuoteLabel(g.representative) }}</span>
|
|
305
310
|
</span>
|
|
306
311
|
</template>
|
|
307
312
|
</td>
|
|
@@ -355,11 +360,11 @@ watch(
|
|
|
355
360
|
font-size: 1.2rem;
|
|
356
361
|
}
|
|
357
362
|
.hint {
|
|
358
|
-
color: var(--
|
|
363
|
+
color: var(--muted);
|
|
359
364
|
font-size: 0.9rem;
|
|
360
365
|
}
|
|
361
366
|
.err {
|
|
362
|
-
color: var(--
|
|
367
|
+
color: var(--danger);
|
|
363
368
|
font-size: 0.9rem;
|
|
364
369
|
}
|
|
365
370
|
.blob-table {
|
|
@@ -371,21 +376,21 @@ watch(
|
|
|
371
376
|
.blob-table td {
|
|
372
377
|
text-align: left;
|
|
373
378
|
padding: 0.4rem 0.6rem;
|
|
374
|
-
border-bottom: 1px solid var(--
|
|
379
|
+
border-bottom: 1px solid var(--border);
|
|
375
380
|
vertical-align: top;
|
|
376
381
|
}
|
|
377
382
|
.blob-table th {
|
|
378
383
|
font-weight: 600;
|
|
379
|
-
color: var(--
|
|
384
|
+
color: var(--muted);
|
|
380
385
|
}
|
|
381
386
|
.blob-table tr.warn td {
|
|
382
|
-
background: color-mix(in srgb, var(--
|
|
387
|
+
background: color-mix(in srgb, var(--warning) 8%, transparent);
|
|
383
388
|
}
|
|
384
389
|
.blob-table tr.highlight td {
|
|
385
|
-
background: color-mix(in srgb, var(--accent
|
|
390
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
386
391
|
}
|
|
387
392
|
.copy-row td {
|
|
388
|
-
background: color-mix(in srgb, var(--
|
|
393
|
+
background: color-mix(in srgb, var(--muted) 6%, transparent);
|
|
389
394
|
font-size: 0.82rem;
|
|
390
395
|
}
|
|
391
396
|
.copies-toggle {
|
|
@@ -415,23 +420,23 @@ watch(
|
|
|
415
420
|
}
|
|
416
421
|
.est {
|
|
417
422
|
font-size: 0.78rem;
|
|
418
|
-
color: var(--
|
|
423
|
+
color: var(--muted);
|
|
419
424
|
}
|
|
420
425
|
.at-max {
|
|
421
426
|
font-size: 0.8rem;
|
|
422
|
-
color: var(--
|
|
427
|
+
color: var(--muted);
|
|
423
428
|
}
|
|
424
429
|
.act-status {
|
|
425
430
|
font-size: 0.85rem;
|
|
426
|
-
color: var(--
|
|
431
|
+
color: var(--muted);
|
|
427
432
|
}
|
|
428
433
|
.certify-btn {
|
|
429
|
-
border-color: var(--accent
|
|
430
|
-
color: var(--accent
|
|
434
|
+
border-color: var(--accent);
|
|
435
|
+
color: var(--accent);
|
|
431
436
|
}
|
|
432
437
|
.pending-badge {
|
|
433
438
|
font-size: 0.78rem;
|
|
434
|
-
color: var(--accent
|
|
439
|
+
color: var(--accent);
|
|
435
440
|
font-weight: 600;
|
|
436
441
|
}
|
|
437
442
|
</style>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
WalrusUpload,
|
|
10
10
|
AccessGateCta,
|
|
11
11
|
useAccessGate,
|
|
12
|
+
type WalrusNetwork,
|
|
12
13
|
} from '@meddleware/walrus-relay'
|
|
13
14
|
import { AppTabNav, CopyableAddress, ExplorerLink, UiNotice, UiTabPanel, UiToolIntro, suiExplorerUrl, safeHref, type AppTab } from '@meddleware/ui'
|
|
14
15
|
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
@@ -45,9 +46,16 @@ const { account, signPersonalMessage, buildExecutor } = useWallet()
|
|
|
45
46
|
|
|
46
47
|
// One access-gate state per Walrus network (the gate is a per-network setting); the active
|
|
47
48
|
// network's is used. Each is inert until an ownership check runs.
|
|
49
|
+
// `useAccessGate` throws for a gate that is not the published deployment on its network;
|
|
50
|
+
// `accessGate()` only returns such configs (or null, ungated). The shared client serves the active
|
|
51
|
+
// network only, so a state for another network refuses to read rather than reading the wrong chain.
|
|
52
|
+
function gateClient(net: WalrusNetwork) {
|
|
53
|
+
if (net !== network.value) throw new Error(`No Sui client for ${net} while ${network.value} is active.`)
|
|
54
|
+
return getSuiClient()
|
|
55
|
+
}
|
|
48
56
|
const gateStates = {
|
|
49
|
-
testnet: useAccessGate({ gate: accessGate('testnet'), getClient:
|
|
50
|
-
mainnet: useAccessGate({ gate: accessGate('mainnet'), getClient:
|
|
57
|
+
testnet: useAccessGate({ network: 'testnet', gate: accessGate('testnet'), getClient: gateClient }),
|
|
58
|
+
mainnet: useAccessGate({ network: 'mainnet', gate: accessGate('mainnet'), getClient: gateClient }),
|
|
51
59
|
}
|
|
52
60
|
/** The active network's gate state (testnet's while the view is hidden on a non-Walrus network). */
|
|
53
61
|
const gateState = computed(() => gateStates[walrusNetwork.value ?? 'testnet'])
|
|
@@ -75,6 +83,8 @@ async function onPurchase(): Promise<void> {
|
|
|
75
83
|
try {
|
|
76
84
|
const executor = await buildExecutor()
|
|
77
85
|
await gateState.value.purchase(executor, account.value.address)
|
|
86
|
+
} catch {
|
|
87
|
+
// The reason is published in `purchaseError` and shown by the prompt.
|
|
78
88
|
} finally {
|
|
79
89
|
purchasing.value = false
|
|
80
90
|
}
|
|
@@ -237,9 +247,19 @@ function onSettled(): void {
|
|
|
237
247
|
:gate-configured="gateState.gateConfigured"
|
|
238
248
|
:has-access="gateState.hasAccess.value"
|
|
239
249
|
:busy="purchasing"
|
|
240
|
-
:
|
|
250
|
+
:terms="gateState.terms.value"
|
|
251
|
+
:terms-error="gateState.termsError.value"
|
|
252
|
+
:purchase-error="gateState.purchaseError.value"
|
|
241
253
|
@purchase="onPurchase"
|
|
242
254
|
/>
|
|
255
|
+
<button
|
|
256
|
+
v-if="gateState.termsError.value && !gateState.termsLoading.value"
|
|
257
|
+
type="button"
|
|
258
|
+
class="retry-terms"
|
|
259
|
+
@click="gateState.loadTerms()"
|
|
260
|
+
>
|
|
261
|
+
Read the price again
|
|
262
|
+
</button>
|
|
243
263
|
</div>
|
|
244
264
|
|
|
245
265
|
<!-- Ownership check still in flight. -->
|
|
@@ -266,7 +286,8 @@ function onSettled(): void {
|
|
|
266
286
|
<WalrusUpload
|
|
267
287
|
:hosts="relayHosts(walrusNetwork)"
|
|
268
288
|
:connected="!!account"
|
|
269
|
-
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
289
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess, singleUse: gateState.singleUse }"
|
|
290
|
+
:session-key="`${walrusNetwork}:${account?.address ?? ''}`"
|
|
270
291
|
:perform-upload="performUpload"
|
|
271
292
|
:estimate-storage-cost="estimateUploadStorageCost"
|
|
272
293
|
@uploaded="onUploaded"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Prices an Extend before the wallet opens (audit F12). A row's quote is the WAL cost of adding
|
|
2
|
+
// `epochs`; Extend opens the wallet only for an amount whose quote the user has been shown. A quote is
|
|
3
|
+
// an estimate (labelled `≈`): the wallet and the chain still decide the amount actually paid.
|
|
4
|
+
import { ref } from 'vue'
|
|
5
|
+
|
|
6
|
+
export type ExtendQuote =
|
|
7
|
+
| { epochs: number; state: 'ready'; costFrost: bigint }
|
|
8
|
+
| { epochs: number; state: 'unavailable'; costFrost: null }
|
|
9
|
+
|
|
10
|
+
/** Cost in FROST of storing `size` bytes for `epochs`, or `null` when it cannot be priced. */
|
|
11
|
+
export type EstimateExtendCost = (size: number, epochs: number) => Promise<bigint | null>
|
|
12
|
+
|
|
13
|
+
export function useExtendQuotes(estimate: EstimateExtendCost) {
|
|
14
|
+
/** Latest quote per Blob object id (one per row). */
|
|
15
|
+
const quotes = ref<Record<string, ExtendQuote>>({})
|
|
16
|
+
const pricing = ref<Record<string, number>>({})
|
|
17
|
+
/** Per-row request counter: an older answer never replaces a newer amount's quote. */
|
|
18
|
+
const tokens: Record<string, number> = {}
|
|
19
|
+
|
|
20
|
+
/** Price `epochs` for the row; resolves to its quote, or `null` if a newer request replaced this one. */
|
|
21
|
+
async function price(id: string, size: number, epochs: number): Promise<ExtendQuote | null> {
|
|
22
|
+
const token = (tokens[id] = (tokens[id] ?? 0) + 1)
|
|
23
|
+
pricing.value = { ...pricing.value, [id]: epochs }
|
|
24
|
+
let cost: bigint | null
|
|
25
|
+
try {
|
|
26
|
+
cost = await estimate(size, epochs)
|
|
27
|
+
} catch {
|
|
28
|
+
cost = null
|
|
29
|
+
}
|
|
30
|
+
if (tokens[id] !== token) return null
|
|
31
|
+
const rest = { ...pricing.value }
|
|
32
|
+
delete rest[id]
|
|
33
|
+
pricing.value = rest
|
|
34
|
+
const quote: ExtendQuote =
|
|
35
|
+
cost === null ? { epochs, state: 'unavailable', costFrost: null } : { epochs, state: 'ready', costFrost: cost }
|
|
36
|
+
quotes.value = { ...quotes.value, [id]: quote }
|
|
37
|
+
return quote
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The row's quote, only if it is for exactly `epochs` (an older amount's quote is not shown). */
|
|
41
|
+
function quoteFor(id: string, epochs: number | undefined): ExtendQuote | null {
|
|
42
|
+
const q = quotes.value[id]
|
|
43
|
+
return q && q.epochs === epochs ? q : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether Extend may open the wallet now: only when the quote for `epochs` has been shown. If it has
|
|
48
|
+
* not (the seeded default, or a price still loading), it is fetched now, so the user sees the cost
|
|
49
|
+
* and presses Extend again — never a wallet prompt for an unpriced amount.
|
|
50
|
+
*/
|
|
51
|
+
async function shownBeforeSigning(id: string, size: number, epochs: number): Promise<boolean> {
|
|
52
|
+
if (quoteFor(id, epochs)) return true
|
|
53
|
+
await price(id, size, epochs)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Forget the row's quote (after its blob was extended, the next Extend is priced afresh). */
|
|
58
|
+
function clear(id: string): void {
|
|
59
|
+
tokens[id] = (tokens[id] ?? 0) + 1
|
|
60
|
+
const q = { ...quotes.value }
|
|
61
|
+
delete q[id]
|
|
62
|
+
quotes.value = q
|
|
63
|
+
const p = { ...pricing.value }
|
|
64
|
+
delete p[id]
|
|
65
|
+
pricing.value = p
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { quotes, pricing, price, quoteFor, shownBeforeSigning, clear }
|
|
69
|
+
}
|
|
@@ -20,18 +20,41 @@ const error = ref<string | null>(null)
|
|
|
20
20
|
/** `<network>|<address>` the current `blobs` were fetched for; `null` until a successful load. */
|
|
21
21
|
const loadedFor = ref<string | null>(null)
|
|
22
22
|
|
|
23
|
+
/** Counts calls that decide what the list shows; a result is applied only if it is still the latest. */
|
|
24
|
+
let latest = 0
|
|
25
|
+
/** Key of the load in flight (the latest one), or `null` when none is. */
|
|
26
|
+
let inflightKey: string | null = null
|
|
27
|
+
|
|
28
|
+
/** Drop whatever load is in flight: its result will be discarded and it no longer owns `loading`. */
|
|
29
|
+
function supersede(): void {
|
|
30
|
+
latest++
|
|
31
|
+
inflightKey = null
|
|
32
|
+
loading.value = false
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
async function load(address: string | null, opts: { force?: boolean } = {}): Promise<void> {
|
|
24
36
|
const net = walrusNetwork.value
|
|
25
37
|
if (!address || !net) {
|
|
38
|
+
supersede()
|
|
26
39
|
blobs.value = []
|
|
27
40
|
loadedFor.value = null
|
|
28
41
|
return
|
|
29
42
|
}
|
|
30
43
|
// Skip refetch when we already have this address's list and the last load succeeded.
|
|
31
44
|
const key = `${net}|${address}`
|
|
32
|
-
if (!opts.force && loadedFor.value === key && error.value === null)
|
|
33
|
-
|
|
45
|
+
if (!opts.force && loadedFor.value === key && error.value === null) {
|
|
46
|
+
// Switching back to a list we hold: a slower load for another account or network must not land.
|
|
47
|
+
if (inflightKey !== null && inflightKey !== key) supersede()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (loadedFor.value !== key) {
|
|
51
|
+
// Never show another network's or wallet's list; and a cleared list must not read as loaded.
|
|
52
|
+
blobs.value = []
|
|
53
|
+
loadedFor.value = null
|
|
54
|
+
}
|
|
34
55
|
|
|
56
|
+
const mine = ++latest
|
|
57
|
+
inflightKey = key
|
|
35
58
|
loading.value = true
|
|
36
59
|
error.value = null
|
|
37
60
|
try {
|
|
@@ -45,13 +68,20 @@ async function load(address: string | null, opts: { force?: boolean } = {}): Pro
|
|
|
45
68
|
walrusClient.walrus.systemState(),
|
|
46
69
|
fetchOwnedWalrusBlobs(suiClient, walrusClient, address),
|
|
47
70
|
])
|
|
71
|
+
// A newer load (or a switch) took over while this one was in flight: its answer is for another
|
|
72
|
+
// account or network, so drop it without touching the list, the error or `loading`.
|
|
73
|
+
if (mine !== latest) return
|
|
48
74
|
currentEpoch.value = Number(sys.committee.epoch)
|
|
49
75
|
blobs.value = fetched.sort((a: OwnedBlob, b: OwnedBlob) => a.endEpoch - b.endEpoch)
|
|
50
76
|
loadedFor.value = key
|
|
51
77
|
} catch (e) {
|
|
78
|
+
if (mine !== latest) return
|
|
52
79
|
error.value = e instanceof Error ? e.message : String(e)
|
|
53
80
|
} finally {
|
|
54
|
-
|
|
81
|
+
if (mine === latest) {
|
|
82
|
+
loading.value = false
|
|
83
|
+
inflightKey = null
|
|
84
|
+
}
|
|
55
85
|
}
|
|
56
86
|
}
|
|
57
87
|
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// selects VITE_NETWORK in main.ts; embedded, the host's selector rules). Operator settings are
|
|
3
3
|
// build-time VITE_* per network: relay hostnames default to the public Mysten relay (set
|
|
4
4
|
// VITE_WALRUS_RELAY_* to your operator relay to collect the tip); the access gate is optional
|
|
5
|
-
// (unset ⇒ ungated), and its package + PlatformConfig come from the published deployment
|
|
5
|
+
// (unset ⇒ ungated), and its package + PlatformConfig come from the published deployment; the
|
|
6
|
+
// purchase price and payees are read from the chain, never configured.
|
|
6
7
|
import { computed } from 'vue'
|
|
7
8
|
import type { WalrusNetwork, RelayGateConfig } from '@meddleware/walrus-relay'
|
|
8
9
|
import { relayGateConfig } from '@meddleware/walrus-relay'
|
|
@@ -64,9 +65,10 @@ export function uploadRelayMaxTipMist(envSource: EnvSource = env): number {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
* The optional NFT access gate for a network: the operator supplies only the Gate id
|
|
68
|
-
* flag
|
|
69
|
-
*
|
|
68
|
+
* The optional NFT access gate for a network: the operator supplies only the Gate id and soulbound
|
|
69
|
+
* flag (`VITE_ACCESS_GATE_*_{NET}`); `relayGateConfig` fixes the package and `PlatformConfig` to the
|
|
70
|
+
* published deployment (commission enforcement), and the price is read from the gate on the chain.
|
|
71
|
+
* `null` — an ungated
|
|
70
72
|
* relay — when no gate id is set, or when no access_gate deployment is recorded for the network.
|
|
71
73
|
*/
|
|
72
74
|
export function accessGate(network: WalrusNetwork, envSource: EnvSource = env): RelayGateConfig | null {
|
|
@@ -77,7 +79,6 @@ export function accessGate(network: WalrusNetwork, envSource: EnvSource = env):
|
|
|
77
79
|
return relayGateConfig(network, {
|
|
78
80
|
gateId,
|
|
79
81
|
soulbound: envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true',
|
|
80
|
-
priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
|
|
81
82
|
})
|
|
82
83
|
} catch (e) {
|
|
83
84
|
console.warn(`[walrus-ui] access gate for ${network} disabled: ${e instanceof Error ? e.message : String(e)}`)
|