@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meddleware/walrus-ui",
3
- "version": "0.1.7",
4
- "description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs wallet-connected, NFT-gated relay support.",
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.6",
46
- "@meddleware/wallet-adapter": "^0.0.4",
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.3",
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
- const blobs = ref<OwnedBlob[]>([])
17
- const loading = ref(false)
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
- async function loadBlobs(): Promise<void> {
34
- if (!props.address) return
35
- loading.value = true
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 loadBlobs()
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
- watch(() => props.address, loadBlobs)
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="loadBlobs">
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(NETWORK) })
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(NETWORK)
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(NETWORK)
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(NETWORK),
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(NETWORK)"
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
- // The adapter is network-agnostic (RPC URL passed per call); this shim binds walrus-ui's
4
- // RPC_URLS so call sites keep the one-arg ergonomics (`getSuiClient(NETWORK)` /
5
- // `buildExecutor(NETWORK)`). Because the adapter is a module singleton, the wallet connection
6
- // is shared with any other tool view rendered in the same window (e.g. the dashboard).
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
- /** Memoised Sui JSON-RPC client for the network, using walrus-ui's configured RPC URL. */
19
- export function getSuiClient(network: WalrusNetwork) {
20
- return getSuiClientBase(network, RPC_URLS[network])
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 + walrus-ui's RPC URL. */
24
- export function buildExecutor(network: WalrusNetwork): Promise<Executor> {
25
- return buildExecutorBase(network, RPC_URLS[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
  /**