@meddleware/walrus-ui 0.1.0 → 0.1.2
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 +12 -6
- package/src/App.vue +122 -127
- package/src/config.ts +22 -5
- package/src/main.ts +1 -0
- package/src/upload-flow.ts +100 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
-
"author": "
|
|
5
|
+
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
6
6
|
"license": "0BSD",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -20,16 +20,20 @@
|
|
|
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"
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@meddleware/design-tokens": "^0.1.
|
|
32
|
+
"@meddleware/design-tokens": "^0.1.2",
|
|
30
33
|
"@meddleware/nft-gate-client": "^0.0.3",
|
|
31
|
-
"@meddleware/
|
|
32
|
-
"@meddleware/walrus-
|
|
34
|
+
"@meddleware/ui": "^0.1.6",
|
|
35
|
+
"@meddleware/walrus-client": "^0.0.3",
|
|
36
|
+
"@meddleware/walrus-relay": "^0.1.1",
|
|
33
37
|
"@mysten/sui": "~2.17.0",
|
|
34
38
|
"@mysten/wallet-standard": "~0.19.9",
|
|
35
39
|
"@mysten/walrus": "~1.1.7",
|
|
@@ -37,10 +41,12 @@
|
|
|
37
41
|
"vue": "^3.5.40"
|
|
38
42
|
},
|
|
39
43
|
"devDependencies": {
|
|
44
|
+
"@playwright/test": "~1.49.1",
|
|
40
45
|
"@types/node": "~24.12.2",
|
|
41
46
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
42
47
|
"typescript": "~6.0.0",
|
|
43
48
|
"vite": "^8.1.5",
|
|
49
|
+
"vitest": "~2.1.9",
|
|
44
50
|
"vue-tsc": "~3.3.0"
|
|
45
51
|
},
|
|
46
52
|
"engines": {
|
package/src/App.vue
CHANGED
|
@@ -10,10 +10,15 @@ import {
|
|
|
10
10
|
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
|
+
import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
|
|
13
14
|
import { useWallet, getSuiClient } from './wallet.js'
|
|
14
|
-
import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate } from './config.js'
|
|
15
|
+
import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate, uploadRelayMaxTipMist } from './config.js'
|
|
16
|
+
import { runBlobUpload } from './upload-flow.js'
|
|
15
17
|
import MyBlobs from './components/MyBlobs.vue'
|
|
16
18
|
|
|
19
|
+
const isEmbedded = new URLSearchParams(window.location.search).has('embedded')
|
|
20
|
+
const { mode, set } = useColorMode('dark')
|
|
21
|
+
|
|
17
22
|
type Tab = 'upload' | 'blobs'
|
|
18
23
|
const activeTab = ref<Tab>('upload')
|
|
19
24
|
|
|
@@ -43,15 +48,15 @@ async function onPurchase(): Promise<void> {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
// Wire the shared WalrusUpload widget to
|
|
51
|
+
// Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
|
|
52
|
+
// register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure only
|
|
53
|
+
// gathers the wallet-bound inputs (executor, sui client, gated proof token) and delegates.
|
|
47
54
|
async function performUpload(
|
|
48
55
|
bytes: Uint8Array,
|
|
49
56
|
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
50
57
|
): Promise<UploadResult> {
|
|
51
58
|
if (!account.value) throw new Error('Connect your wallet first.')
|
|
52
|
-
const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await import('@meddleware/walrus-client')
|
|
53
59
|
const executor = await buildExecutor(NETWORK)
|
|
54
|
-
const suiClient = getSuiClient(NETWORK)
|
|
55
60
|
|
|
56
61
|
// If the relay is NFT-gated and we hold access, attach a signed proof token.
|
|
57
62
|
let authToken: string | undefined
|
|
@@ -63,41 +68,19 @@ async function performUpload(
|
|
|
63
68
|
})
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
return runBlobUpload({
|
|
72
|
+
bytes,
|
|
67
73
|
network: NETWORK,
|
|
74
|
+
relayHost: opts.relayHost,
|
|
75
|
+
address: account.value.address,
|
|
68
76
|
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,
|
|
77
|
+
maxTipMist: uploadRelayMaxTipMist(),
|
|
81
78
|
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
82
|
-
|
|
79
|
+
executor,
|
|
80
|
+
suiClient: getSuiClient(NETWORK),
|
|
81
|
+
authToken,
|
|
82
|
+
onStatus: opts.onStatus,
|
|
83
83
|
})
|
|
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
84
|
}
|
|
102
85
|
|
|
103
86
|
function onUploaded(r: UploadResult): void {
|
|
@@ -106,133 +89,149 @@ function onUploaded(r: UploadResult): void {
|
|
|
106
89
|
</script>
|
|
107
90
|
|
|
108
91
|
<template>
|
|
109
|
-
<div class="
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<p class="sub">Upload and manage blobs on Walrus decentralised storage ({{ NETWORK }}).</p>
|
|
114
|
-
</div>
|
|
115
|
-
<TipConfigBadge :host="OPERATOR_RELAY_HOSTS[NETWORK]" />
|
|
116
|
-
</header>
|
|
117
|
-
|
|
118
|
-
<nav class="tabs" aria-label="Feature tabs">
|
|
119
|
-
<button
|
|
120
|
-
type="button"
|
|
121
|
-
class="tab"
|
|
122
|
-
:class="{ active: activeTab === 'upload' }"
|
|
123
|
-
@click="activeTab = 'upload'"
|
|
124
|
-
>
|
|
125
|
-
Upload
|
|
126
|
-
</button>
|
|
127
|
-
<button
|
|
128
|
-
type="button"
|
|
129
|
-
class="tab"
|
|
130
|
-
:class="{ active: activeTab === 'blobs' }"
|
|
131
|
-
@click="activeTab = 'blobs'"
|
|
132
|
-
>
|
|
133
|
-
My Blobs
|
|
134
|
-
</button>
|
|
135
|
-
</nav>
|
|
136
|
-
|
|
137
|
-
<section class="wallet">
|
|
138
|
-
<template v-if="account">
|
|
139
|
-
<span class="addr">{{ account.address.slice(0, 8) }}…{{ account.address.slice(-4) }}</span>
|
|
140
|
-
<button type="button" @click="disconnect">Disconnect</button>
|
|
92
|
+
<div class="app" :class="{ 'app--embedded': isEmbedded }">
|
|
93
|
+
<AppHeader v-if="!isEmbedded" variant="dark">
|
|
94
|
+
<template #brand>
|
|
95
|
+
<span>Walrus Assets</span>
|
|
141
96
|
</template>
|
|
142
|
-
<template
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
</button>
|
|
97
|
+
<template #actions>
|
|
98
|
+
<TipConfigBadge :host="OPERATOR_RELAY_HOSTS[NETWORK]" />
|
|
99
|
+
<ColorModeControl :model-value="mode" @update:model-value="set" />
|
|
146
100
|
</template>
|
|
147
|
-
</
|
|
101
|
+
</AppHeader>
|
|
102
|
+
|
|
103
|
+
<div class="page">
|
|
104
|
+
<p class="sub">Upload and manage blobs on Walrus decentralised storage ({{ NETWORK }}).</p>
|
|
105
|
+
|
|
106
|
+
<nav class="tabs" aria-label="Feature tabs">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="tab"
|
|
110
|
+
:class="{ active: activeTab === 'upload' }"
|
|
111
|
+
@click="activeTab = 'upload'"
|
|
112
|
+
>
|
|
113
|
+
Upload
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="tab"
|
|
118
|
+
:class="{ active: activeTab === 'blobs' }"
|
|
119
|
+
@click="activeTab = 'blobs'"
|
|
120
|
+
>
|
|
121
|
+
My Blobs
|
|
122
|
+
</button>
|
|
123
|
+
</nav>
|
|
124
|
+
|
|
125
|
+
<section class="wallet">
|
|
126
|
+
<template v-if="account">
|
|
127
|
+
<span class="addr">{{ account.address.slice(0, 8) }}…{{ account.address.slice(-4) }}</span>
|
|
128
|
+
<button type="button" @click="disconnect">Disconnect</button>
|
|
129
|
+
</template>
|
|
130
|
+
<template v-else>
|
|
131
|
+
<button type="button" :disabled="!wallets.length" @click="onConnectFirst">
|
|
132
|
+
{{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
|
|
133
|
+
</button>
|
|
134
|
+
</template>
|
|
135
|
+
</section>
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
137
|
+
<template v-if="activeTab === 'upload'">
|
|
138
|
+
<AccessGateCta
|
|
139
|
+
:gate-configured="gateState.gateConfigured"
|
|
140
|
+
:has-access="gateState.hasAccess.value"
|
|
141
|
+
:busy="purchasing"
|
|
142
|
+
:price-mist="gate?.priceMist ?? null"
|
|
143
|
+
@purchase="onPurchase"
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<WalrusUpload
|
|
147
|
+
:hosts="relayHosts(NETWORK)"
|
|
148
|
+
:connected="!!account"
|
|
149
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
150
|
+
:perform-upload="performUpload"
|
|
151
|
+
@uploaded="onUploaded"
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<section v-if="result" class="result">
|
|
155
|
+
<h2>Uploaded ✓</h2>
|
|
156
|
+
<p><strong>Blob ID:</strong> <code>{{ result.blobId }}</code></p>
|
|
157
|
+
<p>
|
|
158
|
+
<strong>URL:</strong>
|
|
159
|
+
<a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
|
|
160
|
+
</p>
|
|
161
|
+
<p v-if="result.digest"><strong>Certify tx:</strong> <code>{{ result.digest }}</code></p>
|
|
162
|
+
</section>
|
|
163
|
+
</template>
|
|
157
164
|
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
:
|
|
161
|
-
:
|
|
162
|
-
:perform-upload="performUpload"
|
|
163
|
-
@uploaded="onUploaded"
|
|
165
|
+
<MyBlobs
|
|
166
|
+
v-if="activeTab === 'blobs'"
|
|
167
|
+
:address="account?.address ?? null"
|
|
168
|
+
:build-executor="() => buildExecutor(NETWORK)"
|
|
164
169
|
/>
|
|
170
|
+
</div>
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
<h2>Uploaded ✓</h2>
|
|
168
|
-
<p><strong>Blob ID:</strong> <code>{{ result.blobId }}</code></p>
|
|
169
|
-
<p>
|
|
170
|
-
<strong>URL:</strong>
|
|
171
|
-
<a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
|
|
172
|
-
</p>
|
|
173
|
-
<p v-if="result.digest"><strong>Certify tx:</strong> <code>{{ result.digest }}</code></p>
|
|
174
|
-
</section>
|
|
175
|
-
</template>
|
|
176
|
-
|
|
177
|
-
<MyBlobs
|
|
178
|
-
v-if="activeTab === 'blobs'"
|
|
179
|
-
:address="account?.address ?? null"
|
|
180
|
-
:build-executor="() => buildExecutor(NETWORK)"
|
|
181
|
-
/>
|
|
182
|
-
|
|
183
|
-
<footer class="foot">
|
|
184
|
-
<p>© MeddleWare · <a href="https://sui.meddleware.co.uk">more SUI tools</a></p>
|
|
185
|
-
</footer>
|
|
172
|
+
<AppFooter v-if="!isEmbedded" />
|
|
186
173
|
</div>
|
|
187
174
|
</template>
|
|
188
175
|
|
|
189
176
|
<style scoped>
|
|
177
|
+
.app {
|
|
178
|
+
min-height: 100vh;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.app--embedded {
|
|
184
|
+
min-height: 100%;
|
|
185
|
+
}
|
|
186
|
+
|
|
190
187
|
.page {
|
|
191
188
|
max-width: 640px;
|
|
192
189
|
margin: 0 auto;
|
|
193
190
|
padding: 2rem 1.25rem 4rem;
|
|
191
|
+
flex: 1;
|
|
194
192
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
gap: 1rem;
|
|
200
|
-
flex-wrap: wrap;
|
|
201
|
-
}
|
|
202
|
-
.head h1 {
|
|
203
|
-
margin: 0;
|
|
204
|
-
font-size: 1.8rem;
|
|
193
|
+
|
|
194
|
+
.app--embedded .page {
|
|
195
|
+
padding-top: 1rem;
|
|
196
|
+
padding-bottom: 1rem;
|
|
205
197
|
}
|
|
198
|
+
|
|
206
199
|
.sub {
|
|
207
|
-
color: var(--
|
|
200
|
+
color: var(--muted);
|
|
208
201
|
margin: 0.25rem 0 0;
|
|
209
202
|
}
|
|
203
|
+
|
|
210
204
|
.wallet {
|
|
211
205
|
display: flex;
|
|
212
206
|
align-items: center;
|
|
213
207
|
gap: 0.75rem;
|
|
214
208
|
margin: 1.25rem 0;
|
|
215
209
|
}
|
|
210
|
+
|
|
216
211
|
.addr {
|
|
217
212
|
font-family: monospace;
|
|
218
213
|
font-size: 0.9rem;
|
|
219
214
|
}
|
|
215
|
+
|
|
220
216
|
.result {
|
|
221
217
|
margin-top: 1.5rem;
|
|
222
218
|
padding: 1rem;
|
|
223
|
-
border: 1px solid var(--
|
|
219
|
+
border: 1px solid var(--border);
|
|
224
220
|
border-radius: 10px;
|
|
225
221
|
word-break: break-all;
|
|
226
222
|
}
|
|
223
|
+
|
|
227
224
|
.result code {
|
|
228
225
|
font-size: 0.85rem;
|
|
229
226
|
}
|
|
227
|
+
|
|
230
228
|
.tabs {
|
|
231
229
|
display: flex;
|
|
232
230
|
gap: 0.25rem;
|
|
233
231
|
margin: 1rem 0 0.5rem;
|
|
234
|
-
border-bottom: 2px solid var(--
|
|
232
|
+
border-bottom: 2px solid var(--border);
|
|
235
233
|
}
|
|
234
|
+
|
|
236
235
|
.tab {
|
|
237
236
|
background: none;
|
|
238
237
|
border: none;
|
|
@@ -241,16 +240,12 @@ function onUploaded(r: UploadResult): void {
|
|
|
241
240
|
margin-bottom: -2px;
|
|
242
241
|
cursor: pointer;
|
|
243
242
|
font-size: 0.95rem;
|
|
244
|
-
color: var(--
|
|
243
|
+
color: var(--muted);
|
|
245
244
|
}
|
|
245
|
+
|
|
246
246
|
.tab.active {
|
|
247
|
-
border-bottom-color: var(--
|
|
248
|
-
color: var(--
|
|
247
|
+
border-bottom-color: var(--accent);
|
|
248
|
+
color: var(--text);
|
|
249
249
|
font-weight: 600;
|
|
250
250
|
}
|
|
251
|
-
.foot {
|
|
252
|
-
margin-top: 3rem;
|
|
253
|
-
font-size: 0.85rem;
|
|
254
|
-
color: var(--mw-color-text-muted, #666);
|
|
255
|
-
}
|
|
256
251
|
</style>
|
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
|
}
|
package/src/main.ts
CHANGED
|
@@ -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
|
+
}
|