@meddleware/walrus-ui 0.1.7 → 0.1.9
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 +5 -5
- package/src/components/MyBlobs.vue +13 -29
- package/src/components/WalrusView.vue +15 -5
- package/src/composables/useOwnedBlobs.ts +54 -0
- package/src/wallet.ts +13 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs \u2014 wallet-connected, NFT-gated relay support.",
|
|
5
5
|
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
6
6
|
"license": "0BSD",
|
|
7
7
|
"repository": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@meddleware/design-tokens": "^0.1.2",
|
|
44
44
|
"@meddleware/nft-gate-client": "^0.0.6",
|
|
45
|
-
"@meddleware/ui": "^0.1.
|
|
46
|
-
"@meddleware/wallet-adapter": "^0.0.
|
|
45
|
+
"@meddleware/ui": "^0.1.7",
|
|
46
|
+
"@meddleware/wallet-adapter": "^0.0.5",
|
|
47
47
|
"@meddleware/walrus-client": "^0.0.5",
|
|
48
|
-
"@meddleware/walrus-relay": "^0.1.
|
|
48
|
+
"@meddleware/walrus-relay": "^0.1.4",
|
|
49
49
|
"@mysten/sui": "^2.28.0",
|
|
50
50
|
"@mysten/wallet-standard": "^0.20.0",
|
|
51
51
|
"@mysten/walrus": "~1.1.7",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, watch } from 'vue'
|
|
2
|
+
import { onMounted, ref, watch } from 'vue'
|
|
3
3
|
import type { OwnedBlob } from '@meddleware/walrus-client'
|
|
4
4
|
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
5
|
-
import { getSuiClient } from '../wallet.js'
|
|
6
5
|
import type { Executor } from '../wallet.js'
|
|
7
6
|
import { NETWORK } from '../config.js'
|
|
7
|
+
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
8
8
|
|
|
9
9
|
const props = defineProps<{
|
|
10
10
|
/** Connected wallet address whose owned blobs to list; `null` when no wallet is connected. */
|
|
@@ -13,10 +13,8 @@ const props = defineProps<{
|
|
|
13
13
|
buildExecutor: () => Promise<Executor>
|
|
14
14
|
}>()
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const loading =
|
|
18
|
-
const error = ref<string | null>(null)
|
|
19
|
-
const currentEpoch = ref(0)
|
|
16
|
+
// Shared, session-persistent cache (survives tab switches and inline re-mounts).
|
|
17
|
+
const { blobs, currentEpoch, loading, error, load } = useOwnedBlobs()
|
|
20
18
|
const extending = ref<string | null>(null)
|
|
21
19
|
const extendStatus = ref<Record<string, string>>({})
|
|
22
20
|
|
|
@@ -30,26 +28,9 @@ function epochsToApproxDays(epochs: number): string {
|
|
|
30
28
|
return `${days} days`
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
error.value = null
|
|
37
|
-
blobs.value = []
|
|
38
|
-
try {
|
|
39
|
-
const { createWalrusClient, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
|
|
40
|
-
const suiClient = getSuiClient(NETWORK)
|
|
41
|
-
const walrusClient = createWalrusClient({ network: NETWORK, wasmUrl: walrusWasmUrl })
|
|
42
|
-
const [sys, fetched] = await Promise.all([
|
|
43
|
-
suiClient.getCurrentSystemState(),
|
|
44
|
-
fetchOwnedWalrusBlobs(suiClient, walrusClient, props.address),
|
|
45
|
-
])
|
|
46
|
-
currentEpoch.value = Number(sys.systemState.epoch)
|
|
47
|
-
blobs.value = fetched.sort((a: OwnedBlob, b: OwnedBlob) => a.endEpoch - b.endEpoch)
|
|
48
|
-
} catch (e) {
|
|
49
|
-
error.value = e instanceof Error ? e.message : String(e)
|
|
50
|
-
} finally {
|
|
51
|
-
loading.value = false
|
|
52
|
-
}
|
|
31
|
+
/** Force a fresh fetch (Refresh button + after an on-chain-affecting action). */
|
|
32
|
+
function refresh(): Promise<void> {
|
|
33
|
+
return load(props.address, { force: true })
|
|
53
34
|
}
|
|
54
35
|
|
|
55
36
|
async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
@@ -65,7 +46,7 @@ async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
|
65
46
|
await executor.waitForTransaction(digest)
|
|
66
47
|
extendStatus.value = { ...extendStatus.value, [blob.objectId]: `Extended ✓ (${digest.slice(0, 8)}…)` }
|
|
67
48
|
// Refresh the list so the new endEpoch is visible.
|
|
68
|
-
await
|
|
49
|
+
await refresh()
|
|
69
50
|
} catch (e) {
|
|
70
51
|
extendStatus.value = {
|
|
71
52
|
...extendStatus.value,
|
|
@@ -76,14 +57,17 @@ async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
|
76
57
|
}
|
|
77
58
|
}
|
|
78
59
|
|
|
79
|
-
|
|
60
|
+
// Load on first open (fixes "nothing shows until Refresh") and whenever the address changes.
|
|
61
|
+
// The composable no-ops when the list is already cached for this address, so re-mounting is cheap.
|
|
62
|
+
onMounted(() => void load(props.address))
|
|
63
|
+
watch(() => props.address, (addr) => void load(addr))
|
|
80
64
|
</script>
|
|
81
65
|
|
|
82
66
|
<template>
|
|
83
67
|
<section class="my-blobs">
|
|
84
68
|
<div class="toolbar">
|
|
85
69
|
<h2>My Blobs</h2>
|
|
86
|
-
<button type="button" :disabled="!address || loading" @click="
|
|
70
|
+
<button type="button" :disabled="!address || loading" @click="refresh">
|
|
87
71
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
|
88
72
|
</button>
|
|
89
73
|
</div>
|
|
@@ -17,6 +17,7 @@ import { WalletGuard } from '@meddleware/wallet-adapter'
|
|
|
17
17
|
import { useWallet, getSuiClient } from '../wallet.js'
|
|
18
18
|
import { NETWORK, relayHosts, accessGate, uploadRelayMaxTipMist } from '../config.js'
|
|
19
19
|
import { runBlobUpload } from '../upload-flow.js'
|
|
20
|
+
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
20
21
|
import MyBlobs from './MyBlobs.vue'
|
|
21
22
|
|
|
22
23
|
type Tab = 'upload' | 'blobs'
|
|
@@ -25,7 +26,7 @@ const activeTab = ref<Tab>('upload')
|
|
|
25
26
|
const { account, signPersonalMessage, buildExecutor } = useWallet()
|
|
26
27
|
|
|
27
28
|
const gate = accessGate(NETWORK)
|
|
28
|
-
const gateState = useAccessGate({ gate, getClient: () => getSuiClient(
|
|
29
|
+
const gateState = useAccessGate({ gate, getClient: () => getSuiClient() })
|
|
29
30
|
const purchasing = ref(false)
|
|
30
31
|
const result = ref<UploadResult | null>(null)
|
|
31
32
|
|
|
@@ -41,7 +42,7 @@ async function onPurchase(): Promise<void> {
|
|
|
41
42
|
if (!account.value) return
|
|
42
43
|
purchasing.value = true
|
|
43
44
|
try {
|
|
44
|
-
const executor = await buildExecutor(
|
|
45
|
+
const executor = await buildExecutor()
|
|
45
46
|
await gateState.purchase(executor, account.value.address)
|
|
46
47
|
} finally {
|
|
47
48
|
purchasing.value = false
|
|
@@ -56,7 +57,7 @@ async function performUpload(
|
|
|
56
57
|
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
57
58
|
): Promise<UploadResult> {
|
|
58
59
|
if (!account.value) throw new Error('Connect your wallet first.')
|
|
59
|
-
const executor = await buildExecutor(
|
|
60
|
+
const executor = await buildExecutor()
|
|
60
61
|
|
|
61
62
|
// If the relay is NFT-gated and we hold access, attach a signed proof token.
|
|
62
63
|
let authToken: string | undefined
|
|
@@ -77,15 +78,23 @@ async function performUpload(
|
|
|
77
78
|
maxTipMist: uploadRelayMaxTipMist(),
|
|
78
79
|
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
79
80
|
executor,
|
|
80
|
-
suiClient: getSuiClient(
|
|
81
|
+
suiClient: getSuiClient(),
|
|
81
82
|
authToken,
|
|
82
83
|
onStatus: opts.onStatus,
|
|
83
84
|
})
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
const ownedBlobs = useOwnedBlobs()
|
|
88
|
+
|
|
86
89
|
function onUploaded(r: UploadResult): void {
|
|
87
90
|
result.value = r
|
|
88
91
|
}
|
|
92
|
+
|
|
93
|
+
// Fires after every upload attempt (success or failure) — a failed UI run may still have landed
|
|
94
|
+
// on-chain, so force-refresh the owned-blobs cache in the background regardless of outcome.
|
|
95
|
+
function onSettled(): void {
|
|
96
|
+
if (account.value) void ownedBlobs.load(account.value.address, { force: true })
|
|
97
|
+
}
|
|
89
98
|
</script>
|
|
90
99
|
|
|
91
100
|
<template>
|
|
@@ -127,6 +136,7 @@ function onUploaded(r: UploadResult): void {
|
|
|
127
136
|
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
128
137
|
:perform-upload="performUpload"
|
|
129
138
|
@uploaded="onUploaded"
|
|
139
|
+
@settled="onSettled"
|
|
130
140
|
/>
|
|
131
141
|
|
|
132
142
|
<section v-if="result" class="result">
|
|
@@ -143,7 +153,7 @@ function onUploaded(r: UploadResult): void {
|
|
|
143
153
|
<MyBlobs
|
|
144
154
|
v-if="activeTab === 'blobs'"
|
|
145
155
|
:address="account?.address ?? null"
|
|
146
|
-
:build-executor="() => buildExecutor(
|
|
156
|
+
:build-executor="() => buildExecutor()"
|
|
147
157
|
/>
|
|
148
158
|
</WalletGuard>
|
|
149
159
|
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Module-singleton cache of the connected wallet's owned Walrus blobs.
|
|
2
|
+
//
|
|
3
|
+
// State lives at module scope (not inside the composable factory) so it is shared across every
|
|
4
|
+
// `MyBlobs` mount in the session: switching the Upload/My-Blobs tabs — or navigating the whole
|
|
5
|
+
// tool view in and out inline in the dashboard — no longer loses the list or forces a refetch.
|
|
6
|
+
// `load()` is a no-op when the list is already loaded for the given address, unless `force` is set
|
|
7
|
+
// (used after an upload/extend, which can create or mutate on-chain blobs).
|
|
8
|
+
import { ref } from 'vue'
|
|
9
|
+
import type { OwnedBlob } from '@meddleware/walrus-client'
|
|
10
|
+
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client eagerly).
|
|
11
|
+
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
12
|
+
import { getSuiClient } from '../wallet.js'
|
|
13
|
+
import { NETWORK } from '../config.js'
|
|
14
|
+
|
|
15
|
+
const blobs = ref<OwnedBlob[]>([])
|
|
16
|
+
const currentEpoch = ref(0)
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
const error = ref<string | null>(null)
|
|
19
|
+
/** Address the current `blobs` were fetched for; `null` until a successful load. */
|
|
20
|
+
const loadedFor = ref<string | null>(null)
|
|
21
|
+
|
|
22
|
+
async function load(address: string | null, opts: { force?: boolean } = {}): Promise<void> {
|
|
23
|
+
if (!address) {
|
|
24
|
+
blobs.value = []
|
|
25
|
+
loadedFor.value = null
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
// Skip refetch when we already have this address's list and the last load succeeded.
|
|
29
|
+
if (!opts.force && loadedFor.value === address && error.value === null) return
|
|
30
|
+
|
|
31
|
+
loading.value = true
|
|
32
|
+
error.value = null
|
|
33
|
+
try {
|
|
34
|
+
const { createWalrusClient, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
|
|
35
|
+
const suiClient = getSuiClient()
|
|
36
|
+
const walrusClient = createWalrusClient({ network: NETWORK, wasmUrl: walrusWasmUrl })
|
|
37
|
+
const [sys, fetched] = await Promise.all([
|
|
38
|
+
suiClient.getCurrentSystemState(),
|
|
39
|
+
fetchOwnedWalrusBlobs(suiClient, walrusClient, address),
|
|
40
|
+
])
|
|
41
|
+
currentEpoch.value = Number(sys.systemState.epoch)
|
|
42
|
+
blobs.value = fetched.sort((a: OwnedBlob, b: OwnedBlob) => a.endEpoch - b.endEpoch)
|
|
43
|
+
loadedFor.value = address
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
46
|
+
} finally {
|
|
47
|
+
loading.value = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Reactive access to the shared owned-blobs cache and its loader. */
|
|
52
|
+
export function useOwnedBlobs() {
|
|
53
|
+
return { blobs, currentEpoch, loading, error, loadedFor, load }
|
|
54
|
+
}
|
package/src/wallet.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
// Thin walrus-ui shim over the shared @meddleware/wallet-adapter singleton.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// RPC URL is resolved from the runtime useNetwork() singleton so network switching
|
|
4
|
+
// in the dashboard propagates immediately to all tool views. Because the adapter is a
|
|
5
|
+
// module singleton, the wallet connection is shared with any other tool view rendered
|
|
6
|
+
// in the same window (e.g. the dashboard).
|
|
7
7
|
import {
|
|
8
8
|
useWallet as useWalletBase,
|
|
9
9
|
getSuiClient as getSuiClientBase,
|
|
10
10
|
buildExecutor as buildExecutorBase,
|
|
11
|
+
useNetwork,
|
|
11
12
|
} from '@meddleware/wallet-adapter'
|
|
12
13
|
import type { Executor } from '@meddleware/wallet-adapter'
|
|
13
|
-
import type { WalrusNetwork } from '@meddleware/walrus-relay'
|
|
14
|
-
import { RPC_URLS } from './config.js'
|
|
15
14
|
|
|
16
15
|
export type { Executor }
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const { network, rpcUrl } = useNetwork()
|
|
18
|
+
|
|
19
|
+
/** Memoised Sui gRPC client for the currently selected network. */
|
|
20
|
+
export function getSuiClient() {
|
|
21
|
+
return getSuiClientBase(network.value, rpcUrl.value)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/** Build a transaction executor bound to the connected wallet
|
|
24
|
-
export function buildExecutor(
|
|
25
|
-
return buildExecutorBase(network,
|
|
24
|
+
/** Build a transaction executor bound to the connected wallet and current network RPC URL. */
|
|
25
|
+
export function buildExecutor(): Promise<Executor> {
|
|
26
|
+
return buildExecutorBase(network.value, rpcUrl.value)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|