@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 +7 -2
- package/src/App.vue +14 -35
- package/src/config.ts +22 -5
- package/src/upload-flow.ts +100 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
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
|
-
|
|
67
|
+
return runBlobUpload({
|
|
68
|
+
bytes,
|
|
67
69
|
network: NETWORK,
|
|
70
|
+
relayHost: opts.relayHost,
|
|
71
|
+
address: account.value.address,
|
|
68
72
|
wasmUrl: walrusWasmUrl,
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
69
|
+
const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
|
|
53
70
|
if (!packageId || !platformConfigId || !gateId) return null
|
|
54
|
-
const soulbound =
|
|
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(
|
|
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
|
+
}
|