@meddleware/walrus-ui 0.1.0
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/LICENSE +14 -0
- package/README.md +76 -0
- package/index.html +13 -0
- package/package.json +49 -0
- package/src/App.vue +256 -0
- package/src/components/MyBlobs.vue +188 -0
- package/src/config.ts +63 -0
- package/src/env.d.ts +7 -0
- package/src/main.ts +5 -0
- package/src/wallet.ts +160 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeddleWare
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @meddleware/walrus-ui
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
A Vue 3 SPA for uploading and managing blobs on [Walrus](https://walrus.xyz) decentralised
|
|
6
|
+
storage on Sui. Supports NFT-gated operator relays (commission-enforced on-chain via
|
|
7
|
+
`access_gate`) with automatic fallback to the public Mysten relay.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Connect any Sui wallet (wallet-standard)
|
|
12
|
+
- Select between operator relay (supports the app) and public relay (free)
|
|
13
|
+
- Upload blobs through an NFT-gated or open relay
|
|
14
|
+
- View and extend your owned Walrus blobs
|
|
15
|
+
- Two-approval upload flow: register tx → upload → certify tx
|
|
16
|
+
|
|
17
|
+
## Local development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> `@meddleware/walrus-relay` is resolved from the npm registry. For local development against an
|
|
25
|
+
> unpublished library, use `npm link @meddleware/walrus-relay` or an `overrides` entry pointing at
|
|
26
|
+
> a local checkout.
|
|
27
|
+
|
|
28
|
+
## Environment variables
|
|
29
|
+
|
|
30
|
+
All `VITE_*` vars are baked into the static bundle at build time.
|
|
31
|
+
|
|
32
|
+
| Variable | Default | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `VITE_NETWORK` | `testnet` | `testnet` or `mainnet` |
|
|
35
|
+
| `VITE_WALRUS_RELAY_TESTNET` | Public Mysten relay | Operator relay URL for testnet |
|
|
36
|
+
| `VITE_WALRUS_RELAY_MAINNET` | Public Mysten relay | Operator relay URL for mainnet |
|
|
37
|
+
| `VITE_RPC_TESTNET` | `https://sui-testnet-rpc.publicnode.com` | Sui RPC for testnet |
|
|
38
|
+
| `VITE_RPC_MAINNET` | `https://fullnode.mainnet.sui.io:443` | Sui RPC for mainnet |
|
|
39
|
+
| `VITE_ACCESS_GATE_ID_TESTNET` | — | Gate object ID (testnet; unset = no gate) |
|
|
40
|
+
| `VITE_ACCESS_GATE_SOULBOUND_TESTNET` | `false` | `true` if NFTs are soulbound |
|
|
41
|
+
| `VITE_ACCESS_GATE_PRICE_MIST_TESTNET` | `0` | Purchase price in MIST |
|
|
42
|
+
| `VITE_ACCESS_GATE_ID_MAINNET` | — | Gate object ID (mainnet) |
|
|
43
|
+
| `VITE_UPLOAD_RELAY_MAX_TIP_MIST` | `50000000` | Max relay tip cap in MIST (0.05 SUI) |
|
|
44
|
+
|
|
45
|
+
`ACCESS_GATE_PACKAGE_ID` and `ACCESS_GATE_PLATFORM_CONFIG_ID` are hardcoded in
|
|
46
|
+
`@meddleware/walrus-relay` — operators only configure the values listed above.
|
|
47
|
+
|
|
48
|
+
## Docker build
|
|
49
|
+
|
|
50
|
+
The Docker context is this repo root. `@meddleware/walrus-relay` resolves from the npm registry,
|
|
51
|
+
so the library must be published before building the image.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docker build \
|
|
55
|
+
--build-arg VITE_NETWORK=testnet \
|
|
56
|
+
--build-arg VITE_WALRUS_RELAY_TESTNET=https://sui-walrus-relay.example.com \
|
|
57
|
+
--build-arg VITE_ACCESS_GATE_ID_TESTNET=0x... \
|
|
58
|
+
--build-arg VITE_ACCESS_GATE_PRICE_MIST_TESTNET=100000000 \
|
|
59
|
+
-t walrus-ui:latest .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Architecture
|
|
63
|
+
|
|
64
|
+
Thin SPA — no accounting logic, no chain state derivation. The app:
|
|
65
|
+
1. Reads config from env vars
|
|
66
|
+
2. Wires `@meddleware/walrus-relay` composables (`useAccessGate`, `useWalrusRelay`) to the
|
|
67
|
+
connected wallet
|
|
68
|
+
3. Passes a `performUpload` callback to `WalrusUpload`, which holds the `@mysten/walrus` client
|
|
69
|
+
and the upload flow logic
|
|
70
|
+
4. Displays results
|
|
71
|
+
|
|
72
|
+
All financial truth is on-chain. This UI is an orchestration shell.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
0BSD
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Walrus Uploader · MeddleWare</title>
|
|
7
|
+
<meta name="description" content="Upload blobs to Walrus decentralised storage on Sui through the MeddleWare relay. Client-side; your wallet signs and pays." />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meddleware/walrus-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
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": "MeddleWare <meddleware@proton.me>",
|
|
6
|
+
"license": "0BSD",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/meddleware-org/walrus-ui.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"index.html",
|
|
15
|
+
"vite.config.ts",
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "vite",
|
|
21
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"type-check": "vue-tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@meddleware/design-tokens": "^0.1.0",
|
|
30
|
+
"@meddleware/nft-gate-client": "^0.0.3",
|
|
31
|
+
"@meddleware/walrus-client": "^0.0.2",
|
|
32
|
+
"@meddleware/walrus-relay": "^0.1.0",
|
|
33
|
+
"@mysten/sui": "~2.17.0",
|
|
34
|
+
"@mysten/wallet-standard": "~0.19.9",
|
|
35
|
+
"@mysten/walrus": "~1.1.7",
|
|
36
|
+
"@mysten/walrus-wasm": "~0.2.2",
|
|
37
|
+
"vue": "^3.5.40"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "~24.12.2",
|
|
41
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
42
|
+
"typescript": "~6.0.0",
|
|
43
|
+
"vite": "^8.1.5",
|
|
44
|
+
"vue-tsc": "~3.3.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": "^22.18.0 || >=24.12.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
WalrusUpload,
|
|
5
|
+
TipConfigBadge,
|
|
6
|
+
AccessGateCta,
|
|
7
|
+
useAccessGate,
|
|
8
|
+
MAX_SINGLE_RESERVATION_EPOCHS,
|
|
9
|
+
} from '@meddleware/walrus-relay'
|
|
10
|
+
import type { UploadResult } from '@meddleware/walrus-relay'
|
|
11
|
+
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
12
|
+
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
13
|
+
import { useWallet, getSuiClient } from './wallet.js'
|
|
14
|
+
import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate } from './config.js'
|
|
15
|
+
import MyBlobs from './components/MyBlobs.vue'
|
|
16
|
+
|
|
17
|
+
type Tab = 'upload' | 'blobs'
|
|
18
|
+
const activeTab = ref<Tab>('upload')
|
|
19
|
+
|
|
20
|
+
const { wallets, account, connect, disconnect, signPersonalMessage, buildExecutor } = useWallet()
|
|
21
|
+
|
|
22
|
+
const gate = accessGate(NETWORK)
|
|
23
|
+
const gateState = useAccessGate({ gate, getClient: () => getSuiClient(NETWORK) })
|
|
24
|
+
const purchasing = ref(false)
|
|
25
|
+
const result = ref<UploadResult | null>(null)
|
|
26
|
+
|
|
27
|
+
async function onConnectFirst(): Promise<void> {
|
|
28
|
+
const w = wallets.value[0]
|
|
29
|
+
if (w) {
|
|
30
|
+
await connect(w)
|
|
31
|
+
if (account.value) await gateState.checkOwnership(account.value.address)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function onPurchase(): Promise<void> {
|
|
36
|
+
if (!account.value) return
|
|
37
|
+
purchasing.value = true
|
|
38
|
+
try {
|
|
39
|
+
const executor = await buildExecutor(NETWORK)
|
|
40
|
+
await gateState.purchase(executor, account.value.address)
|
|
41
|
+
} finally {
|
|
42
|
+
purchasing.value = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Wire the shared WalrusUpload widget to @meddleware/walrus-client + the wallet.
|
|
47
|
+
async function performUpload(
|
|
48
|
+
bytes: Uint8Array,
|
|
49
|
+
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
50
|
+
): Promise<UploadResult> {
|
|
51
|
+
if (!account.value) throw new Error('Connect your wallet first.')
|
|
52
|
+
const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await import('@meddleware/walrus-client')
|
|
53
|
+
const executor = await buildExecutor(NETWORK)
|
|
54
|
+
const suiClient = getSuiClient(NETWORK)
|
|
55
|
+
|
|
56
|
+
// If the relay is NFT-gated and we hold access, attach a signed proof token.
|
|
57
|
+
let authToken: string | undefined
|
|
58
|
+
if (gate && gateState.hasAccess.value === true) {
|
|
59
|
+
authToken = await gateState.buildRelayAccessToken({
|
|
60
|
+
relayHost: opts.relayHost,
|
|
61
|
+
address: account.value.address,
|
|
62
|
+
sign: signPersonalMessage,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = createWalrusClient({
|
|
67
|
+
network: NETWORK,
|
|
68
|
+
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,
|
|
81
|
+
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
82
|
+
deletable: false,
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
function onUploaded(r: UploadResult): void {
|
|
104
|
+
result.value = r
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div class="page">
|
|
110
|
+
<header class="head">
|
|
111
|
+
<div>
|
|
112
|
+
<h1>Walrus Assets</h1>
|
|
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>
|
|
141
|
+
</template>
|
|
142
|
+
<template v-else>
|
|
143
|
+
<button type="button" :disabled="!wallets.length" @click="onConnectFirst">
|
|
144
|
+
{{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
|
|
145
|
+
</button>
|
|
146
|
+
</template>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<template v-if="activeTab === 'upload'">
|
|
150
|
+
<AccessGateCta
|
|
151
|
+
:gate-configured="gateState.gateConfigured"
|
|
152
|
+
:has-access="gateState.hasAccess.value"
|
|
153
|
+
:busy="purchasing"
|
|
154
|
+
:price-mist="gate?.priceMist ?? null"
|
|
155
|
+
@purchase="onPurchase"
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<WalrusUpload
|
|
159
|
+
:hosts="relayHosts(NETWORK)"
|
|
160
|
+
:connected="!!account"
|
|
161
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
162
|
+
:perform-upload="performUpload"
|
|
163
|
+
@uploaded="onUploaded"
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<section v-if="result" class="result">
|
|
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>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<style scoped>
|
|
190
|
+
.page {
|
|
191
|
+
max-width: 640px;
|
|
192
|
+
margin: 0 auto;
|
|
193
|
+
padding: 2rem 1.25rem 4rem;
|
|
194
|
+
}
|
|
195
|
+
.head {
|
|
196
|
+
display: flex;
|
|
197
|
+
justify-content: space-between;
|
|
198
|
+
align-items: flex-start;
|
|
199
|
+
gap: 1rem;
|
|
200
|
+
flex-wrap: wrap;
|
|
201
|
+
}
|
|
202
|
+
.head h1 {
|
|
203
|
+
margin: 0;
|
|
204
|
+
font-size: 1.8rem;
|
|
205
|
+
}
|
|
206
|
+
.sub {
|
|
207
|
+
color: var(--mw-color-text-muted, #666);
|
|
208
|
+
margin: 0.25rem 0 0;
|
|
209
|
+
}
|
|
210
|
+
.wallet {
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 0.75rem;
|
|
214
|
+
margin: 1.25rem 0;
|
|
215
|
+
}
|
|
216
|
+
.addr {
|
|
217
|
+
font-family: monospace;
|
|
218
|
+
font-size: 0.9rem;
|
|
219
|
+
}
|
|
220
|
+
.result {
|
|
221
|
+
margin-top: 1.5rem;
|
|
222
|
+
padding: 1rem;
|
|
223
|
+
border: 1px solid var(--mw-color-border, #ddd);
|
|
224
|
+
border-radius: 10px;
|
|
225
|
+
word-break: break-all;
|
|
226
|
+
}
|
|
227
|
+
.result code {
|
|
228
|
+
font-size: 0.85rem;
|
|
229
|
+
}
|
|
230
|
+
.tabs {
|
|
231
|
+
display: flex;
|
|
232
|
+
gap: 0.25rem;
|
|
233
|
+
margin: 1rem 0 0.5rem;
|
|
234
|
+
border-bottom: 2px solid var(--mw-color-border, #ddd);
|
|
235
|
+
}
|
|
236
|
+
.tab {
|
|
237
|
+
background: none;
|
|
238
|
+
border: none;
|
|
239
|
+
border-bottom: 2px solid transparent;
|
|
240
|
+
padding: 0.5rem 1rem;
|
|
241
|
+
margin-bottom: -2px;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
font-size: 0.95rem;
|
|
244
|
+
color: var(--mw-color-text-muted, #666);
|
|
245
|
+
}
|
|
246
|
+
.tab.active {
|
|
247
|
+
border-bottom-color: var(--mw-color-primary, #6c3);
|
|
248
|
+
color: var(--mw-color-text, #111);
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
}
|
|
251
|
+
.foot {
|
|
252
|
+
margin-top: 3rem;
|
|
253
|
+
font-size: 0.85rem;
|
|
254
|
+
color: var(--mw-color-text-muted, #666);
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import type { OwnedBlob } from '@meddleware/walrus-client'
|
|
4
|
+
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
5
|
+
import { getSuiClient } from '../wallet.js'
|
|
6
|
+
import type { Executor } from '../wallet.js'
|
|
7
|
+
import { NETWORK } from '../config.js'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
/** Connected wallet address whose owned blobs to list; `null` when no wallet is connected. */
|
|
11
|
+
address: string | null
|
|
12
|
+
/** Factory that builds a transaction {@link Executor} bound to the connected wallet (for extend). */
|
|
13
|
+
buildExecutor: () => Promise<Executor>
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const blobs = ref<OwnedBlob[]>([])
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
const error = ref<string | null>(null)
|
|
19
|
+
const currentEpoch = ref(0)
|
|
20
|
+
const extending = ref<string | null>(null)
|
|
21
|
+
const extendStatus = ref<Record<string, string>>({})
|
|
22
|
+
|
|
23
|
+
const EXPIRY_WARN_EPOCHS = 10
|
|
24
|
+
const EPOCHS_PER_DAY = 1 / 0.038 // ~1 Walrus epoch ≈ 38 minutes on testnet
|
|
25
|
+
|
|
26
|
+
function epochsToApproxDays(epochs: number): string {
|
|
27
|
+
const days = Math.round(epochs * EPOCHS_PER_DAY)
|
|
28
|
+
if (days <= 0) return 'expired'
|
|
29
|
+
if (days < 2) return `${days} day`
|
|
30
|
+
return `${days} days`
|
|
31
|
+
}
|
|
32
|
+
|
|
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.getLatestSuiSystemState(),
|
|
44
|
+
fetchOwnedWalrusBlobs(suiClient, walrusClient, props.address),
|
|
45
|
+
])
|
|
46
|
+
currentEpoch.value = Number(sys.epoch)
|
|
47
|
+
blobs.value = fetched.sort((a, b) => a.endEpoch - b.endEpoch)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
50
|
+
} finally {
|
|
51
|
+
loading.value = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
56
|
+
extending.value = blob.objectId
|
|
57
|
+
extendStatus.value = { ...extendStatus.value, [blob.objectId]: 'Building transaction…' }
|
|
58
|
+
try {
|
|
59
|
+
const { createWalrusClient, extendBlobLifetimeTransaction } = await import('@meddleware/walrus-client')
|
|
60
|
+
const walrusClient = createWalrusClient({ network: NETWORK, wasmUrl: walrusWasmUrl })
|
|
61
|
+
const tx = await extendBlobLifetimeTransaction(walrusClient, blob.objectId, { epochs: 10 })
|
|
62
|
+
const executor = await props.buildExecutor()
|
|
63
|
+
extendStatus.value = { ...extendStatus.value, [blob.objectId]: 'Approve in wallet…' }
|
|
64
|
+
const { digest } = await executor.signAndExecute(tx)
|
|
65
|
+
await executor.waitForTransaction(digest)
|
|
66
|
+
extendStatus.value = { ...extendStatus.value, [blob.objectId]: `Extended ✓ (${digest.slice(0, 8)}…)` }
|
|
67
|
+
// Refresh the list so the new endEpoch is visible.
|
|
68
|
+
await loadBlobs()
|
|
69
|
+
} catch (e) {
|
|
70
|
+
extendStatus.value = {
|
|
71
|
+
...extendStatus.value,
|
|
72
|
+
[blob.objectId]: `Failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
extending.value = null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
watch(() => props.address, loadBlobs)
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<section class="my-blobs">
|
|
84
|
+
<div class="toolbar">
|
|
85
|
+
<h2>My Blobs</h2>
|
|
86
|
+
<button type="button" :disabled="!address || loading" @click="loadBlobs">
|
|
87
|
+
{{ loading ? 'Loading…' : 'Refresh' }}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<p v-if="!address" class="hint">Connect your wallet to list your Walrus blobs.</p>
|
|
92
|
+
<p v-else-if="error" class="err">{{ error }}</p>
|
|
93
|
+
<p v-else-if="!loading && blobs.length === 0" class="hint">No Walrus blobs found for this address.</p>
|
|
94
|
+
|
|
95
|
+
<table v-if="blobs.length" class="blob-table">
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<th>Blob ID</th>
|
|
99
|
+
<th>Size</th>
|
|
100
|
+
<th>Expires</th>
|
|
101
|
+
<th>Certified</th>
|
|
102
|
+
<th>Actions</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
<tr
|
|
107
|
+
v-for="blob in blobs"
|
|
108
|
+
:key="blob.objectId"
|
|
109
|
+
:class="{ warn: blob.endEpoch - currentEpoch < EXPIRY_WARN_EPOCHS }"
|
|
110
|
+
>
|
|
111
|
+
<td class="mono">{{ blob.blobId.slice(0, 12) }}…</td>
|
|
112
|
+
<td>{{ (blob.size / 1024).toFixed(1) }} KB</td>
|
|
113
|
+
<td>
|
|
114
|
+
epoch {{ blob.endEpoch }}
|
|
115
|
+
<span class="approx">(≈{{ epochsToApproxDays(blob.endEpoch - currentEpoch) }})</span>
|
|
116
|
+
</td>
|
|
117
|
+
<td>{{ blob.certified ? '✓' : '—' }}</td>
|
|
118
|
+
<td>
|
|
119
|
+
<span v-if="extendStatus[blob.objectId]" class="ext-status">
|
|
120
|
+
{{ extendStatus[blob.objectId] }}
|
|
121
|
+
</span>
|
|
122
|
+
<button
|
|
123
|
+
v-else
|
|
124
|
+
type="button"
|
|
125
|
+
:disabled="extending === blob.objectId"
|
|
126
|
+
@click="extendBlob(blob)"
|
|
127
|
+
>
|
|
128
|
+
+10 epochs
|
|
129
|
+
</button>
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
</tbody>
|
|
133
|
+
</table>
|
|
134
|
+
</section>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<style scoped>
|
|
138
|
+
.my-blobs {
|
|
139
|
+
margin-top: 1.5rem;
|
|
140
|
+
}
|
|
141
|
+
.toolbar {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 1rem;
|
|
145
|
+
margin-bottom: 0.75rem;
|
|
146
|
+
}
|
|
147
|
+
.toolbar h2 {
|
|
148
|
+
margin: 0;
|
|
149
|
+
font-size: 1.2rem;
|
|
150
|
+
}
|
|
151
|
+
.hint {
|
|
152
|
+
color: var(--mw-color-text-muted, #888);
|
|
153
|
+
font-size: 0.9rem;
|
|
154
|
+
}
|
|
155
|
+
.err {
|
|
156
|
+
color: var(--mw-color-danger, #c00);
|
|
157
|
+
font-size: 0.9rem;
|
|
158
|
+
}
|
|
159
|
+
.blob-table {
|
|
160
|
+
width: 100%;
|
|
161
|
+
border-collapse: collapse;
|
|
162
|
+
font-size: 0.88rem;
|
|
163
|
+
}
|
|
164
|
+
.blob-table th,
|
|
165
|
+
.blob-table td {
|
|
166
|
+
text-align: left;
|
|
167
|
+
padding: 0.4rem 0.6rem;
|
|
168
|
+
border-bottom: 1px solid var(--mw-color-border, #ddd);
|
|
169
|
+
}
|
|
170
|
+
.blob-table th {
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: var(--mw-color-text-muted, #888);
|
|
173
|
+
}
|
|
174
|
+
.blob-table tr.warn td {
|
|
175
|
+
background: color-mix(in srgb, var(--mw-color-warning, #f90) 8%, transparent);
|
|
176
|
+
}
|
|
177
|
+
.mono {
|
|
178
|
+
font-family: monospace;
|
|
179
|
+
}
|
|
180
|
+
.approx {
|
|
181
|
+
font-size: 0.8em;
|
|
182
|
+
color: var(--mw-color-text-muted, #888);
|
|
183
|
+
}
|
|
184
|
+
.ext-status {
|
|
185
|
+
font-size: 0.85rem;
|
|
186
|
+
color: var(--mw-color-text-muted, #888);
|
|
187
|
+
}
|
|
188
|
+
</style>
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Build-time configuration (Vite inlines VITE_*). The relay hostnames default to
|
|
2
|
+
// the public Mysten relay; set VITE_WALRUS_RELAY_* to your operator relay to collect
|
|
3
|
+
// the tip. Access-gate config is optional (unset ⇒ ungated).
|
|
4
|
+
import type { WalrusNetwork, RelayGateConfig } from '@meddleware/walrus-relay'
|
|
5
|
+
import {
|
|
6
|
+
ACCESS_GATE_PACKAGE_ID,
|
|
7
|
+
ACCESS_GATE_PLATFORM_CONFIG_ID,
|
|
8
|
+
accessGateNftType,
|
|
9
|
+
} from '@meddleware/walrus-relay'
|
|
10
|
+
|
|
11
|
+
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
|
|
12
|
+
|
|
13
|
+
/** Active Walrus network, from `VITE_NETWORK` (default `testnet`). */
|
|
14
|
+
export const NETWORK: WalrusNetwork = (env.VITE_NETWORK as WalrusNetwork) || 'testnet'
|
|
15
|
+
|
|
16
|
+
/** Public Mysten relays — the fallback when no operator relay is configured. */
|
|
17
|
+
export const PUBLIC_WALRUS_RELAY_HOSTS: Record<WalrusNetwork, string> = {
|
|
18
|
+
testnet: 'https://upload-relay.testnet.walrus.space',
|
|
19
|
+
mainnet: 'https://upload-relay.mainnet.walrus.space',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Operator relay host per network (env → public fallback). */
|
|
23
|
+
export const OPERATOR_RELAY_HOSTS: Record<WalrusNetwork, string> = {
|
|
24
|
+
testnet: env.VITE_WALRUS_RELAY_TESTNET || PUBLIC_WALRUS_RELAY_HOSTS.testnet,
|
|
25
|
+
mainnet: env.VITE_WALRUS_RELAY_MAINNET || PUBLIC_WALRUS_RELAY_HOSTS.mainnet,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The operator/public host pair for `useWalrusRelay`. */
|
|
29
|
+
export function relayHosts(network: WalrusNetwork): { operator: string; public: string } {
|
|
30
|
+
return { operator: OPERATOR_RELAY_HOSTS[network], public: PUBLIC_WALRUS_RELAY_HOSTS[network] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* JSON-RPC endpoint used to build + execute the register/certify transactions.
|
|
35
|
+
* NOTE: this is JSON-RPC (the public testnet fullnode serves gRPC only for JSON-RPC,
|
|
36
|
+
* so a JSON-RPC-capable endpoint is used for testnet).
|
|
37
|
+
*/
|
|
38
|
+
export const RPC_URLS: Record<WalrusNetwork, string> = {
|
|
39
|
+
testnet: env.VITE_RPC_TESTNET || 'https://sui-testnet-rpc.publicnode.com',
|
|
40
|
+
mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse the optional NFT access-gate config for a network from env.
|
|
45
|
+
* packageId and platformConfigId are hardcoded in the library — operators only
|
|
46
|
+
* need to supply their Gate object ID, soulbound flag, and purchase price.
|
|
47
|
+
*/
|
|
48
|
+
export function accessGate(network: WalrusNetwork): RelayGateConfig | null {
|
|
49
|
+
const NET = network.toUpperCase()
|
|
50
|
+
const packageId = ACCESS_GATE_PACKAGE_ID[network]
|
|
51
|
+
const platformConfigId = ACCESS_GATE_PLATFORM_CONFIG_ID[network]
|
|
52
|
+
const gateId = env[`VITE_ACCESS_GATE_ID_${NET}`]
|
|
53
|
+
if (!packageId || !platformConfigId || !gateId) return null
|
|
54
|
+
const soulbound = env[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true'
|
|
55
|
+
return {
|
|
56
|
+
packageId,
|
|
57
|
+
gateId,
|
|
58
|
+
platformConfigId,
|
|
59
|
+
nftType: accessGateNftType(network, soulbound),
|
|
60
|
+
soulbound,
|
|
61
|
+
priceMist: BigInt(env[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/env.d.ts
ADDED
package/src/main.ts
ADDED
package/src/wallet.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Minimal Sui wallet integration on @mysten/wallet-standard (dapp-kit is React-only).
|
|
2
|
+
// Discovers wallets, connects, signs personal messages (for the NFT-gate proof), and
|
|
3
|
+
// adapts the connected wallet into a transaction executor. Module singleton.
|
|
4
|
+
//
|
|
5
|
+
// NOTE: the signing path can only be exercised with a real wallet extension in a
|
|
6
|
+
// browser; it is intentionally thin and executes via a JSON-RPC client so we control
|
|
7
|
+
// the returned effects.
|
|
8
|
+
import { markRaw, readonly, ref, shallowRef } from 'vue'
|
|
9
|
+
import { getWallets, isWalletWithRequiredFeatureSet } from '@mysten/wallet-standard'
|
|
10
|
+
import type { Wallet, WalletAccount } from '@mysten/wallet-standard'
|
|
11
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
|
|
12
|
+
import type { Transaction } from '@mysten/sui/transactions'
|
|
13
|
+
import type { WalrusNetwork } from '@meddleware/walrus-relay'
|
|
14
|
+
import { RPC_URLS } from './config.js'
|
|
15
|
+
|
|
16
|
+
const REQUIRED_FEATURES = ['standard:connect', 'sui:signTransaction'] as const
|
|
17
|
+
|
|
18
|
+
const wallets = shallowRef<Wallet[]>([])
|
|
19
|
+
const currentWallet = shallowRef<Wallet | null>(null)
|
|
20
|
+
const account = shallowRef<WalletAccount | null>(null)
|
|
21
|
+
const connecting = ref(false)
|
|
22
|
+
const error = ref<string | null>(null)
|
|
23
|
+
|
|
24
|
+
const clients = new Map<WalrusNetwork, SuiJsonRpcClient>()
|
|
25
|
+
/** Return a memoised {@link SuiJsonRpcClient} for the network (one instance per network). */
|
|
26
|
+
export function getSuiClient(network: WalrusNetwork): SuiJsonRpcClient {
|
|
27
|
+
let c = clients.get(network)
|
|
28
|
+
if (!c) {
|
|
29
|
+
c = new SuiJsonRpcClient({ url: RPC_URLS[network], network })
|
|
30
|
+
clients.set(network, c)
|
|
31
|
+
}
|
|
32
|
+
return c
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function refreshWallets(): void {
|
|
36
|
+
// markRaw: extension Wallet objects expose name/icon as ES-private-field getters
|
|
37
|
+
// that throw when accessed through a Vue reactive Proxy.
|
|
38
|
+
wallets.value = getWallets()
|
|
39
|
+
.get()
|
|
40
|
+
.filter((w) => isWalletWithRequiredFeatureSet(w, [...REQUIRED_FEATURES]))
|
|
41
|
+
.map((w) => markRaw(w))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let initialised = false
|
|
45
|
+
function init(): void {
|
|
46
|
+
if (initialised) return
|
|
47
|
+
initialised = true
|
|
48
|
+
const api = getWallets()
|
|
49
|
+
refreshWallets()
|
|
50
|
+
api.on('register', refreshWallets)
|
|
51
|
+
api.on('unregister', refreshWallets)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function connect(wallet: Wallet): Promise<void> {
|
|
55
|
+
error.value = null
|
|
56
|
+
connecting.value = true
|
|
57
|
+
try {
|
|
58
|
+
const feature = wallet.features['standard:connect'] as {
|
|
59
|
+
connect: () => Promise<{ accounts: readonly WalletAccount[] }>
|
|
60
|
+
}
|
|
61
|
+
const { accounts } = await feature.connect()
|
|
62
|
+
if (!accounts.length) throw new Error('Wallet returned no accounts.')
|
|
63
|
+
currentWallet.value = markRaw(wallet)
|
|
64
|
+
account.value = markRaw(accounts[0])
|
|
65
|
+
} catch (e) {
|
|
66
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
67
|
+
throw e
|
|
68
|
+
} finally {
|
|
69
|
+
connecting.value = false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function disconnect(): void {
|
|
74
|
+
const disc = currentWallet.value?.features['standard:disconnect'] as
|
|
75
|
+
| { disconnect?: () => Promise<void> }
|
|
76
|
+
| undefined
|
|
77
|
+
void disc?.disconnect?.()
|
|
78
|
+
currentWallet.value = null
|
|
79
|
+
account.value = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Sign `nft-gate:access:<nonce>` to prove address control (returns base64 signature). */
|
|
83
|
+
async function signPersonalMessage(message: Uint8Array): Promise<{ signature: string }> {
|
|
84
|
+
const wallet = currentWallet.value
|
|
85
|
+
const acct = account.value
|
|
86
|
+
if (!wallet || !acct) throw new Error('Connect a wallet first.')
|
|
87
|
+
const feature = wallet.features['sui:signPersonalMessage'] as
|
|
88
|
+
| {
|
|
89
|
+
signPersonalMessage: (input: {
|
|
90
|
+
message: Uint8Array
|
|
91
|
+
account: WalletAccount
|
|
92
|
+
}) => Promise<{ bytes: string; signature: string }>
|
|
93
|
+
}
|
|
94
|
+
| undefined
|
|
95
|
+
if (!feature) throw new Error('This wallet cannot sign personal messages.')
|
|
96
|
+
const { signature } = await feature.signPersonalMessage({ message, account: acct })
|
|
97
|
+
return { signature }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Transaction executor bound to the connected wallet: sign+execute a PTB and await finality. */
|
|
101
|
+
export interface Executor {
|
|
102
|
+
signAndExecute(tx: Transaction): Promise<{ digest: string }>
|
|
103
|
+
waitForTransaction(digest: string): Promise<unknown>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Build an executor bound to the connected wallet + network. */
|
|
107
|
+
async function buildExecutor(network: WalrusNetwork): Promise<Executor> {
|
|
108
|
+
const wallet = currentWallet.value
|
|
109
|
+
const acct = account.value
|
|
110
|
+
if (!wallet || !acct) throw new Error('Connect a wallet first.')
|
|
111
|
+
const client = getSuiClient(network)
|
|
112
|
+
const chain = `sui:${network}` as const
|
|
113
|
+
|
|
114
|
+
const signFeature = wallet.features['sui:signTransaction'] as {
|
|
115
|
+
signTransaction: (input: {
|
|
116
|
+
transaction: Transaction
|
|
117
|
+
account: WalletAccount
|
|
118
|
+
chain: `sui:${string}`
|
|
119
|
+
}) => Promise<{ bytes: string; signature: string }>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
async signAndExecute(tx: Transaction): Promise<{ digest: string }> {
|
|
124
|
+
const { bytes, signature } = await signFeature.signTransaction({
|
|
125
|
+
transaction: tx,
|
|
126
|
+
account: acct,
|
|
127
|
+
chain,
|
|
128
|
+
})
|
|
129
|
+
const res = await client.executeTransactionBlock({
|
|
130
|
+
transactionBlock: bytes,
|
|
131
|
+
signature,
|
|
132
|
+
options: { showEffects: true },
|
|
133
|
+
})
|
|
134
|
+
return { digest: res.digest }
|
|
135
|
+
},
|
|
136
|
+
async waitForTransaction(digest: string): Promise<unknown> {
|
|
137
|
+
return client.waitForTransaction({ digest })
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wallet composable: discovers wallets, exposes reactive connection state, and provides
|
|
144
|
+
* `connect` / `disconnect` / `signPersonalMessage` / `buildExecutor`. Module singleton —
|
|
145
|
+
* all callers share one wallet/account state.
|
|
146
|
+
*/
|
|
147
|
+
export function useWallet() {
|
|
148
|
+
init()
|
|
149
|
+
return {
|
|
150
|
+
wallets: readonly(wallets),
|
|
151
|
+
currentWallet: readonly(currentWallet),
|
|
152
|
+
account: readonly(account),
|
|
153
|
+
connecting: readonly(connecting),
|
|
154
|
+
error: readonly(error),
|
|
155
|
+
connect,
|
|
156
|
+
disconnect,
|
|
157
|
+
signPersonalMessage,
|
|
158
|
+
buildExecutor,
|
|
159
|
+
}
|
|
160
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"types": ["node"],
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "vite.config.ts"]
|
|
18
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
// design-tokens and ui resolve from node_modules (published packages).
|
|
5
|
+
// @mysten/walrus is excluded from dep-optimization so its wasm `?url` import resolves.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [vue()],
|
|
8
|
+
optimizeDeps: {
|
|
9
|
+
exclude: ['@mysten/walrus', '@mysten/walrus-wasm'],
|
|
10
|
+
},
|
|
11
|
+
})
|