@meddleware/walrus-ui 0.1.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/walrus-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs — wallet-connected, NFT-gated relay support.",
5
5
  "author": "MeddleWare <meddleware@proton.me>",
6
6
  "license": "0BSD",
@@ -20,7 +20,10 @@
20
20
  "dev": "vite",
21
21
  "build": "vue-tsc --noEmit && vite build",
22
22
  "preview": "vite preview",
23
- "type-check": "vue-tsc --noEmit"
23
+ "type-check": "vue-tsc --noEmit",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:e2e": "playwright test"
24
27
  },
25
28
  "publishConfig": {
26
29
  "access": "public"
@@ -37,10 +40,12 @@
37
40
  "vue": "^3.5.40"
38
41
  },
39
42
  "devDependencies": {
43
+ "@playwright/test": "~1.49.1",
40
44
  "@types/node": "~24.12.2",
41
45
  "@vitejs/plugin-vue": "^6.0.8",
42
46
  "typescript": "~6.0.0",
43
47
  "vite": "^8.1.5",
48
+ "vitest": "~2.1.9",
44
49
  "vue-tsc": "~3.3.0"
45
50
  },
46
51
  "engines": {
package/src/App.vue CHANGED
@@ -11,7 +11,8 @@ import type { UploadResult } from '@meddleware/walrus-relay'
11
11
  // Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
12
12
  import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
13
13
  import { useWallet, getSuiClient } from './wallet.js'
14
- import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate } from './config.js'
14
+ import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate, uploadRelayMaxTipMist } from './config.js'
15
+ import { runBlobUpload } from './upload-flow.js'
15
16
  import MyBlobs from './components/MyBlobs.vue'
16
17
 
17
18
  type Tab = 'upload' | 'blobs'
@@ -43,15 +44,15 @@ async function onPurchase(): Promise<void> {
43
44
  }
44
45
  }
45
46
 
46
- // Wire the shared WalrusUpload widget to @meddleware/walrus-client + the wallet.
47
+ // Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
48
+ // register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure only
49
+ // gathers the wallet-bound inputs (executor, sui client, gated proof token) and delegates.
47
50
  async function performUpload(
48
51
  bytes: Uint8Array,
49
52
  opts: { relayHost: string; onStatus: (s: string) => void },
50
53
  ): Promise<UploadResult> {
51
54
  if (!account.value) throw new Error('Connect your wallet first.')
52
- const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await import('@meddleware/walrus-client')
53
55
  const executor = await buildExecutor(NETWORK)
54
- const suiClient = getSuiClient(NETWORK)
55
56
 
56
57
  // If the relay is NFT-gated and we hold access, attach a signed proof token.
57
58
  let authToken: string | undefined
@@ -63,41 +64,19 @@ async function performUpload(
63
64
  })
64
65
  }
65
66
 
66
- const client = createWalrusClient({
67
+ return runBlobUpload({
68
+ bytes,
67
69
  network: NETWORK,
70
+ relayHost: opts.relayHost,
71
+ address: account.value.address,
68
72
  wasmUrl: walrusWasmUrl,
69
- uploadRelayHost: opts.relayHost,
70
- uploadRelayAuthToken: authToken,
71
- uploadRelayMaxTipMist: Number(import.meta.env.VITE_UPLOAD_RELAY_MAX_TIP_MIST) || 50_000_000,
72
- })
73
- const flow = createBlobUploadFlow(client, bytes)
74
-
75
- opts.onStatus('Encoding…')
76
- await flow.encode()
77
-
78
- opts.onStatus('Registering blob (approve in wallet)…')
79
- const regTx = flow.register({
80
- owner: account.value.address,
73
+ maxTipMist: uploadRelayMaxTipMist(),
81
74
  epochs: MAX_SINGLE_RESERVATION_EPOCHS,
82
- deletable: false,
75
+ executor,
76
+ suiClient: getSuiClient(NETWORK),
77
+ authToken,
78
+ onStatus: opts.onStatus,
83
79
  })
84
- regTx.setSenderIfNotSet(account.value.address)
85
- await regTx.build({ client: suiClient })
86
- const reg = await executor.signAndExecute(regTx)
87
- await executor.waitForTransaction(reg.digest)
88
-
89
- opts.onStatus('Uploading to the relay…')
90
- await flow.upload({ digest: reg.digest })
91
-
92
- opts.onStatus('Certifying (approve in wallet)…')
93
- const certTx = flow.certify()
94
- certTx.setSenderIfNotSet(account.value.address)
95
- await certTx.build({ client: suiClient })
96
- const cert = await executor.signAndExecute(certTx)
97
- await executor.waitForTransaction(cert.digest)
98
-
99
- const blob = await flow.getBlob()
100
- return { blobId: blob.blobId, url: walrusBlobUrl(NETWORK, blob.blobId), digest: cert.digest }
101
80
  }
102
81
 
103
82
  function onUploaded(r: UploadResult): void {
package/src/config.ts CHANGED
@@ -8,7 +8,11 @@ import {
8
8
  accessGateNftType,
9
9
  } from '@meddleware/walrus-relay'
10
10
 
11
- const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
11
+ /** Build-time env bag. Injectable on the env-reading helpers below purely so unit tests can exercise
12
+ * each branch without depending on Vite's (frozen) `import.meta.env` inlining. */
13
+ export type EnvSource = Record<string, string | undefined>
14
+
15
+ const env: EnvSource = (import.meta as unknown as { env?: EnvSource }).env ?? {}
12
16
 
13
17
  /** Active Walrus network, from `VITE_NETWORK` (default `testnet`). */
14
18
  export const NETWORK: WalrusNetwork = (env.VITE_NETWORK as WalrusNetwork) || 'testnet'
@@ -30,6 +34,19 @@ export function relayHosts(network: WalrusNetwork): { operator: string; public:
30
34
  return { operator: OPERATOR_RELAY_HOSTS[network], public: PUBLIC_WALRUS_RELAY_HOSTS[network] }
31
35
  }
32
36
 
37
+ /** Default relay tip ceiling (MIST) when `VITE_UPLOAD_RELAY_MAX_TIP_MIST` is unset (0.05 SUI). */
38
+ export const DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST = 50_000_000
39
+
40
+ /**
41
+ * Cap on the relay tip payment in MIST, from `VITE_UPLOAD_RELAY_MAX_TIP_MIST`. This is a ceiling to
42
+ * prevent overpayment (the relay asks for the minimum), NOT a fixed charge. A non-positive or
43
+ * unparseable value falls back to {@link DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST}.
44
+ */
45
+ export function uploadRelayMaxTipMist(envSource: EnvSource = env): number {
46
+ const parsed = Number(envSource.VITE_UPLOAD_RELAY_MAX_TIP_MIST)
47
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST
48
+ }
49
+
33
50
  /**
34
51
  * JSON-RPC endpoint used to build + execute the register/certify transactions.
35
52
  * NOTE: this is JSON-RPC (the public testnet fullnode serves gRPC only for JSON-RPC,
@@ -45,19 +62,19 @@ export const RPC_URLS: Record<WalrusNetwork, string> = {
45
62
  * packageId and platformConfigId are hardcoded in the library — operators only
46
63
  * need to supply their Gate object ID, soulbound flag, and purchase price.
47
64
  */
48
- export function accessGate(network: WalrusNetwork): RelayGateConfig | null {
65
+ export function accessGate(network: WalrusNetwork, envSource: EnvSource = env): RelayGateConfig | null {
49
66
  const NET = network.toUpperCase()
50
67
  const packageId = ACCESS_GATE_PACKAGE_ID[network]
51
68
  const platformConfigId = ACCESS_GATE_PLATFORM_CONFIG_ID[network]
52
- const gateId = env[`VITE_ACCESS_GATE_ID_${NET}`]
69
+ const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
53
70
  if (!packageId || !platformConfigId || !gateId) return null
54
- const soulbound = env[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true'
71
+ const soulbound = envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true'
55
72
  return {
56
73
  packageId,
57
74
  gateId,
58
75
  platformConfigId,
59
76
  nftType: accessGateNftType(network, soulbound),
60
77
  soulbound,
61
- priceMist: BigInt(env[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
78
+ priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
62
79
  }
63
80
  }
@@ -0,0 +1,100 @@
1
+ // The multi-step Walrus blob upload orchestration (encode → register → upload → certify → resolve),
2
+ // extracted from App.vue so it can be unit-tested and reused by the e2e harness.
3
+ //
4
+ // The `@mysten/walrus` wasm client must NOT be pulled into the eager module graph (see CLAUDE.md),
5
+ // so this module never imports @meddleware/walrus-client at top level — it loads it lazily through
6
+ // the injectable `loadWalrusClient` (default: a dynamic import). Tests inject a fake loader.
7
+ import type { UploadResult } from '@meddleware/walrus-relay'
8
+
9
+ /** Minimal transaction executor — the structural subset App.vue's wallet executor already provides. */
10
+ export interface UploadExecutor {
11
+ signAndExecute(tx: unknown): Promise<{ digest: string }>
12
+ waitForTransaction(digest: string): Promise<unknown>
13
+ }
14
+
15
+ /** The slice of `@meddleware/walrus-client` this flow needs. */
16
+ export interface WalrusClientModule {
17
+ createWalrusClient: (opts: Record<string, unknown>) => unknown
18
+ createBlobUploadFlow: (client: unknown, bytes: Uint8Array) => BlobUploadFlow
19
+ walrusBlobUrl: (network: string, blobId: string) => string
20
+ }
21
+
22
+ interface BuiltTx {
23
+ setSenderIfNotSet(address: string): void
24
+ build(opts: { client: unknown }): Promise<unknown>
25
+ }
26
+
27
+ interface BlobUploadFlow {
28
+ encode(): Promise<void>
29
+ register(opts: { owner: string; epochs: number; deletable: boolean }): BuiltTx
30
+ upload(opts: { digest: string }): Promise<void>
31
+ certify(): BuiltTx
32
+ getBlob(): Promise<{ blobId: string }>
33
+ }
34
+
35
+ export interface RunBlobUploadDeps {
36
+ bytes: Uint8Array
37
+ network: string
38
+ relayHost: string
39
+ /** Owner/sender address (already-connected wallet account). */
40
+ address: string
41
+ /** Wasm bundle URL for the Walrus client. */
42
+ wasmUrl: string
43
+ /** Cap on the relay tip payment in MIST. */
44
+ maxTipMist: number
45
+ /** Blob storage reservation length in epochs. */
46
+ epochs: number
47
+ executor: UploadExecutor
48
+ /** A Sui client used to `build()` the register/certify transactions. */
49
+ suiClient: unknown
50
+ /** Optional Bearer proof token for an NFT-gated relay. */
51
+ authToken?: string
52
+ onStatus: (s: string) => void
53
+ /** Lazy loader for the Walrus client module (keeps wasm out of the eager bundle). */
54
+ loadWalrusClient?: () => Promise<WalrusClientModule>
55
+ }
56
+
57
+ /**
58
+ * Register → upload → certify a blob and resolve its id + public URL. Two wallet approvals
59
+ * (register, certify) are requested via `executor`; `onStatus` narrates each step.
60
+ */
61
+ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResult> {
62
+ // The real module's flow types are richer than the narrow structural subset we use here, so the
63
+ // default loader casts through `unknown`. Tests inject an exact-shape fake instead.
64
+ const load =
65
+ deps.loadWalrusClient ??
66
+ (async () => (await import('@meddleware/walrus-client')) as unknown as WalrusClientModule)
67
+ const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await load()
68
+
69
+ const client = createWalrusClient({
70
+ network: deps.network,
71
+ wasmUrl: deps.wasmUrl,
72
+ uploadRelayHost: deps.relayHost,
73
+ uploadRelayAuthToken: deps.authToken,
74
+ uploadRelayMaxTipMist: deps.maxTipMist,
75
+ })
76
+ const flow = createBlobUploadFlow(client, deps.bytes)
77
+
78
+ deps.onStatus('Encoding…')
79
+ await flow.encode()
80
+
81
+ deps.onStatus('Registering blob (approve in wallet)…')
82
+ const regTx = flow.register({ owner: deps.address, epochs: deps.epochs, deletable: false })
83
+ regTx.setSenderIfNotSet(deps.address)
84
+ await regTx.build({ client: deps.suiClient })
85
+ const reg = await deps.executor.signAndExecute(regTx)
86
+ await deps.executor.waitForTransaction(reg.digest)
87
+
88
+ deps.onStatus('Uploading to the relay…')
89
+ await flow.upload({ digest: reg.digest })
90
+
91
+ deps.onStatus('Certifying (approve in wallet)…')
92
+ const certTx = flow.certify()
93
+ certTx.setSenderIfNotSet(deps.address)
94
+ await certTx.build({ client: deps.suiClient })
95
+ const cert = await deps.executor.signAndExecute(certTx)
96
+ await deps.executor.waitForTransaction(cert.digest)
97
+
98
+ const blob = await flow.getBlob()
99
+ return { blobId: blob.blobId, url: walrusBlobUrl(deps.network, blob.blobId), digest: cert.digest }
100
+ }