@meddleware/walrus-ui 0.1.2 → 0.1.3
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 +13 -1
- package/src/App.vue +9 -224
- package/src/components/WalrusView.vue +221 -0
- package/src/index.ts +10 -0
- package/src/wallet.ts +30 -144
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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 <dev@meddleware.co.uk>",
|
|
6
6
|
"license": "0BSD",
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
"url": "git+https://github.com/meddleware-org/walrus-ui.git"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"sideEffects": [
|
|
13
|
+
"*.css",
|
|
14
|
+
"*.vue"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./src/index.ts",
|
|
19
|
+
"default": "./src/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
12
23
|
"files": [
|
|
13
24
|
"src",
|
|
14
25
|
"index.html",
|
|
@@ -32,6 +43,7 @@
|
|
|
32
43
|
"@meddleware/design-tokens": "^0.1.2",
|
|
33
44
|
"@meddleware/nft-gate-client": "^0.0.3",
|
|
34
45
|
"@meddleware/ui": "^0.1.6",
|
|
46
|
+
"@meddleware/wallet-adapter": "^0.0.1",
|
|
35
47
|
"@meddleware/walrus-client": "^0.0.3",
|
|
36
48
|
"@meddleware/walrus-relay": "^0.1.1",
|
|
37
49
|
"@mysten/sui": "~2.17.0",
|
package/src/App.vue
CHANGED
|
@@ -1,96 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
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'
|
|
2
|
+
// Standalone shell for the Walrus SPA: app header + footer wrapping the core tool view.
|
|
3
|
+
// The core UI lives in WalrusView.vue (also exported for inline embedding in the dashboard).
|
|
13
4
|
import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
|
|
14
|
-
import {
|
|
15
|
-
import { NETWORK, OPERATOR_RELAY_HOSTS
|
|
16
|
-
import
|
|
17
|
-
import MyBlobs from './components/MyBlobs.vue'
|
|
5
|
+
import { TipConfigBadge } from '@meddleware/walrus-relay'
|
|
6
|
+
import { NETWORK, OPERATOR_RELAY_HOSTS } from './config.js'
|
|
7
|
+
import WalrusView from './components/WalrusView.vue'
|
|
18
8
|
|
|
19
|
-
const isEmbedded = new URLSearchParams(window.location.search).has('embedded')
|
|
20
9
|
const { mode, set } = useColorMode('dark')
|
|
21
|
-
|
|
22
|
-
type Tab = 'upload' | 'blobs'
|
|
23
|
-
const activeTab = ref<Tab>('upload')
|
|
24
|
-
|
|
25
|
-
const { wallets, account, connect, disconnect, signPersonalMessage, buildExecutor } = useWallet()
|
|
26
|
-
|
|
27
|
-
const gate = accessGate(NETWORK)
|
|
28
|
-
const gateState = useAccessGate({ gate, getClient: () => getSuiClient(NETWORK) })
|
|
29
|
-
const purchasing = ref(false)
|
|
30
|
-
const result = ref<UploadResult | null>(null)
|
|
31
|
-
|
|
32
|
-
async function onConnectFirst(): Promise<void> {
|
|
33
|
-
const w = wallets.value[0]
|
|
34
|
-
if (w) {
|
|
35
|
-
await connect(w)
|
|
36
|
-
if (account.value) await gateState.checkOwnership(account.value.address)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function onPurchase(): Promise<void> {
|
|
41
|
-
if (!account.value) return
|
|
42
|
-
purchasing.value = true
|
|
43
|
-
try {
|
|
44
|
-
const executor = await buildExecutor(NETWORK)
|
|
45
|
-
await gateState.purchase(executor, account.value.address)
|
|
46
|
-
} finally {
|
|
47
|
-
purchasing.value = false
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
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.
|
|
54
|
-
async function performUpload(
|
|
55
|
-
bytes: Uint8Array,
|
|
56
|
-
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
57
|
-
): Promise<UploadResult> {
|
|
58
|
-
if (!account.value) throw new Error('Connect your wallet first.')
|
|
59
|
-
const executor = await buildExecutor(NETWORK)
|
|
60
|
-
|
|
61
|
-
// If the relay is NFT-gated and we hold access, attach a signed proof token.
|
|
62
|
-
let authToken: string | undefined
|
|
63
|
-
if (gate && gateState.hasAccess.value === true) {
|
|
64
|
-
authToken = await gateState.buildRelayAccessToken({
|
|
65
|
-
relayHost: opts.relayHost,
|
|
66
|
-
address: account.value.address,
|
|
67
|
-
sign: signPersonalMessage,
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return runBlobUpload({
|
|
72
|
-
bytes,
|
|
73
|
-
network: NETWORK,
|
|
74
|
-
relayHost: opts.relayHost,
|
|
75
|
-
address: account.value.address,
|
|
76
|
-
wasmUrl: walrusWasmUrl,
|
|
77
|
-
maxTipMist: uploadRelayMaxTipMist(),
|
|
78
|
-
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
79
|
-
executor,
|
|
80
|
-
suiClient: getSuiClient(NETWORK),
|
|
81
|
-
authToken,
|
|
82
|
-
onStatus: opts.onStatus,
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function onUploaded(r: UploadResult): void {
|
|
87
|
-
result.value = r
|
|
88
|
-
}
|
|
89
10
|
</script>
|
|
90
11
|
|
|
91
12
|
<template>
|
|
92
|
-
<div class="app"
|
|
93
|
-
<AppHeader
|
|
13
|
+
<div class="app">
|
|
14
|
+
<AppHeader variant="dark">
|
|
94
15
|
<template #brand>
|
|
95
16
|
<span>Walrus Assets</span>
|
|
96
17
|
</template>
|
|
@@ -100,76 +21,9 @@ function onUploaded(r: UploadResult): void {
|
|
|
100
21
|
</template>
|
|
101
22
|
</AppHeader>
|
|
102
23
|
|
|
103
|
-
<
|
|
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>
|
|
136
|
-
|
|
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
|
-
/>
|
|
24
|
+
<WalrusView />
|
|
153
25
|
|
|
154
|
-
|
|
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>
|
|
164
|
-
|
|
165
|
-
<MyBlobs
|
|
166
|
-
v-if="activeTab === 'blobs'"
|
|
167
|
-
:address="account?.address ?? null"
|
|
168
|
-
:build-executor="() => buildExecutor(NETWORK)"
|
|
169
|
-
/>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<AppFooter v-if="!isEmbedded" />
|
|
26
|
+
<AppFooter />
|
|
173
27
|
</div>
|
|
174
28
|
</template>
|
|
175
29
|
|
|
@@ -179,73 +33,4 @@ function onUploaded(r: UploadResult): void {
|
|
|
179
33
|
display: flex;
|
|
180
34
|
flex-direction: column;
|
|
181
35
|
}
|
|
182
|
-
|
|
183
|
-
.app--embedded {
|
|
184
|
-
min-height: 100%;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.page {
|
|
188
|
-
max-width: 640px;
|
|
189
|
-
margin: 0 auto;
|
|
190
|
-
padding: 2rem 1.25rem 4rem;
|
|
191
|
-
flex: 1;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.app--embedded .page {
|
|
195
|
-
padding-top: 1rem;
|
|
196
|
-
padding-bottom: 1rem;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.sub {
|
|
200
|
-
color: var(--muted);
|
|
201
|
-
margin: 0.25rem 0 0;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.wallet {
|
|
205
|
-
display: flex;
|
|
206
|
-
align-items: center;
|
|
207
|
-
gap: 0.75rem;
|
|
208
|
-
margin: 1.25rem 0;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.addr {
|
|
212
|
-
font-family: monospace;
|
|
213
|
-
font-size: 0.9rem;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.result {
|
|
217
|
-
margin-top: 1.5rem;
|
|
218
|
-
padding: 1rem;
|
|
219
|
-
border: 1px solid var(--border);
|
|
220
|
-
border-radius: 10px;
|
|
221
|
-
word-break: break-all;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.result code {
|
|
225
|
-
font-size: 0.85rem;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.tabs {
|
|
229
|
-
display: flex;
|
|
230
|
-
gap: 0.25rem;
|
|
231
|
-
margin: 1rem 0 0.5rem;
|
|
232
|
-
border-bottom: 2px solid var(--border);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.tab {
|
|
236
|
-
background: none;
|
|
237
|
-
border: none;
|
|
238
|
-
border-bottom: 2px solid transparent;
|
|
239
|
-
padding: 0.5rem 1rem;
|
|
240
|
-
margin-bottom: -2px;
|
|
241
|
-
cursor: pointer;
|
|
242
|
-
font-size: 0.95rem;
|
|
243
|
-
color: var(--muted);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.tab.active {
|
|
247
|
-
border-bottom-color: var(--accent);
|
|
248
|
-
color: var(--text);
|
|
249
|
-
font-weight: 600;
|
|
250
|
-
}
|
|
251
36
|
</style>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Core Walrus tool UI (upload + owned-blob management), free of any app shell (header/footer).
|
|
3
|
+
// Rendered standalone by walrus-ui's App.vue and inline by the dashboard. Wallet state comes
|
|
4
|
+
// from the shared @meddleware/wallet-adapter singleton (via ./wallet.js), so connecting here or
|
|
5
|
+
// in any other inline tool view reflects everywhere.
|
|
6
|
+
import { ref } from 'vue'
|
|
7
|
+
import {
|
|
8
|
+
WalrusUpload,
|
|
9
|
+
AccessGateCta,
|
|
10
|
+
useAccessGate,
|
|
11
|
+
MAX_SINGLE_RESERVATION_EPOCHS,
|
|
12
|
+
} from '@meddleware/walrus-relay'
|
|
13
|
+
import type { UploadResult } from '@meddleware/walrus-relay'
|
|
14
|
+
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
15
|
+
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
16
|
+
import { useWallet, getSuiClient } from '../wallet.js'
|
|
17
|
+
import { NETWORK, relayHosts, accessGate, uploadRelayMaxTipMist } from '../config.js'
|
|
18
|
+
import { runBlobUpload } from '../upload-flow.js'
|
|
19
|
+
import MyBlobs from './MyBlobs.vue'
|
|
20
|
+
|
|
21
|
+
type Tab = 'upload' | 'blobs'
|
|
22
|
+
const activeTab = ref<Tab>('upload')
|
|
23
|
+
|
|
24
|
+
const { wallets, account, connect, disconnect, signPersonalMessage, buildExecutor } = useWallet()
|
|
25
|
+
|
|
26
|
+
const gate = accessGate(NETWORK)
|
|
27
|
+
const gateState = useAccessGate({ gate, getClient: () => getSuiClient(NETWORK) })
|
|
28
|
+
const purchasing = ref(false)
|
|
29
|
+
const result = ref<UploadResult | null>(null)
|
|
30
|
+
|
|
31
|
+
async function onConnectFirst(): Promise<void> {
|
|
32
|
+
const w = wallets.value[0]
|
|
33
|
+
if (w) {
|
|
34
|
+
await connect(w)
|
|
35
|
+
if (account.value) await gateState.checkOwnership(account.value.address)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function onPurchase(): Promise<void> {
|
|
40
|
+
if (!account.value) return
|
|
41
|
+
purchasing.value = true
|
|
42
|
+
try {
|
|
43
|
+
const executor = await buildExecutor(NETWORK)
|
|
44
|
+
await gateState.purchase(executor, account.value.address)
|
|
45
|
+
} finally {
|
|
46
|
+
purchasing.value = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
|
|
51
|
+
// register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure only
|
|
52
|
+
// gathers the wallet-bound inputs (executor, sui client, gated proof token) and delegates.
|
|
53
|
+
async function performUpload(
|
|
54
|
+
bytes: Uint8Array,
|
|
55
|
+
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
56
|
+
): Promise<UploadResult> {
|
|
57
|
+
if (!account.value) throw new Error('Connect your wallet first.')
|
|
58
|
+
const executor = await buildExecutor(NETWORK)
|
|
59
|
+
|
|
60
|
+
// If the relay is NFT-gated and we hold access, attach a signed proof token.
|
|
61
|
+
let authToken: string | undefined
|
|
62
|
+
if (gate && gateState.hasAccess.value === true) {
|
|
63
|
+
authToken = await gateState.buildRelayAccessToken({
|
|
64
|
+
relayHost: opts.relayHost,
|
|
65
|
+
address: account.value.address,
|
|
66
|
+
sign: signPersonalMessage,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return runBlobUpload({
|
|
71
|
+
bytes,
|
|
72
|
+
network: NETWORK,
|
|
73
|
+
relayHost: opts.relayHost,
|
|
74
|
+
address: account.value.address,
|
|
75
|
+
wasmUrl: walrusWasmUrl,
|
|
76
|
+
maxTipMist: uploadRelayMaxTipMist(),
|
|
77
|
+
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
78
|
+
executor,
|
|
79
|
+
suiClient: getSuiClient(NETWORK),
|
|
80
|
+
authToken,
|
|
81
|
+
onStatus: opts.onStatus,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onUploaded(r: UploadResult): void {
|
|
86
|
+
result.value = r
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div class="page">
|
|
92
|
+
<p class="sub">Upload and manage blobs on Walrus decentralised storage ({{ NETWORK }}).</p>
|
|
93
|
+
|
|
94
|
+
<nav class="tabs" aria-label="Feature tabs">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="tab"
|
|
98
|
+
:class="{ active: activeTab === 'upload' }"
|
|
99
|
+
@click="activeTab = 'upload'"
|
|
100
|
+
>
|
|
101
|
+
Upload
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="tab"
|
|
106
|
+
:class="{ active: activeTab === 'blobs' }"
|
|
107
|
+
@click="activeTab = 'blobs'"
|
|
108
|
+
>
|
|
109
|
+
My Blobs
|
|
110
|
+
</button>
|
|
111
|
+
</nav>
|
|
112
|
+
|
|
113
|
+
<section class="wallet">
|
|
114
|
+
<template v-if="account">
|
|
115
|
+
<span class="addr">{{ account.address.slice(0, 8) }}…{{ account.address.slice(-4) }}</span>
|
|
116
|
+
<button type="button" @click="disconnect">Disconnect</button>
|
|
117
|
+
</template>
|
|
118
|
+
<template v-else>
|
|
119
|
+
<button type="button" :disabled="!wallets.length" @click="onConnectFirst">
|
|
120
|
+
{{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
|
|
121
|
+
</button>
|
|
122
|
+
</template>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<template v-if="activeTab === 'upload'">
|
|
126
|
+
<AccessGateCta
|
|
127
|
+
:gate-configured="gateState.gateConfigured"
|
|
128
|
+
:has-access="gateState.hasAccess.value"
|
|
129
|
+
:busy="purchasing"
|
|
130
|
+
:price-mist="gate?.priceMist ?? null"
|
|
131
|
+
@purchase="onPurchase"
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<WalrusUpload
|
|
135
|
+
:hosts="relayHosts(NETWORK)"
|
|
136
|
+
:connected="!!account"
|
|
137
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
138
|
+
:perform-upload="performUpload"
|
|
139
|
+
@uploaded="onUploaded"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<section v-if="result" class="result">
|
|
143
|
+
<h2>Uploaded ✓</h2>
|
|
144
|
+
<p><strong>Blob ID:</strong> <code>{{ result.blobId }}</code></p>
|
|
145
|
+
<p>
|
|
146
|
+
<strong>URL:</strong>
|
|
147
|
+
<a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
|
|
148
|
+
</p>
|
|
149
|
+
<p v-if="result.digest"><strong>Certify tx:</strong> <code>{{ result.digest }}</code></p>
|
|
150
|
+
</section>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<MyBlobs
|
|
154
|
+
v-if="activeTab === 'blobs'"
|
|
155
|
+
:address="account?.address ?? null"
|
|
156
|
+
:build-executor="() => buildExecutor(NETWORK)"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<style scoped>
|
|
162
|
+
.page {
|
|
163
|
+
max-width: 640px;
|
|
164
|
+
margin: 0 auto;
|
|
165
|
+
padding: 2rem 1.25rem 4rem;
|
|
166
|
+
flex: 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.sub {
|
|
170
|
+
color: var(--muted);
|
|
171
|
+
margin: 0.25rem 0 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.wallet {
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 0.75rem;
|
|
178
|
+
margin: 1.25rem 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.addr {
|
|
182
|
+
font-family: monospace;
|
|
183
|
+
font-size: 0.9rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.result {
|
|
187
|
+
margin-top: 1.5rem;
|
|
188
|
+
padding: 1rem;
|
|
189
|
+
border: 1px solid var(--border);
|
|
190
|
+
border-radius: 10px;
|
|
191
|
+
word-break: break-all;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.result code {
|
|
195
|
+
font-size: 0.85rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.tabs {
|
|
199
|
+
display: flex;
|
|
200
|
+
gap: 0.25rem;
|
|
201
|
+
margin: 1rem 0 0.5rem;
|
|
202
|
+
border-bottom: 2px solid var(--border);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.tab {
|
|
206
|
+
background: none;
|
|
207
|
+
border: none;
|
|
208
|
+
border-bottom: 2px solid transparent;
|
|
209
|
+
padding: 0.5rem 1rem;
|
|
210
|
+
margin-bottom: -2px;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
font-size: 0.95rem;
|
|
213
|
+
color: var(--muted);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.tab.active {
|
|
217
|
+
border-bottom-color: var(--accent);
|
|
218
|
+
color: var(--text);
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
}
|
|
221
|
+
</style>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @meddleware/walrus-ui — library entry.
|
|
2
|
+
//
|
|
3
|
+
// Exports the core Walrus tool view (no app shell) for inline embedding in the dashboard.
|
|
4
|
+
// The standalone SPA (App.vue + main.ts) is unaffected and still builds/deploys as before.
|
|
5
|
+
//
|
|
6
|
+
// Consumers must import the shared design tokens + UI base once at their entry:
|
|
7
|
+
// import '@meddleware/design-tokens/tokens.css'
|
|
8
|
+
// import '@meddleware/ui/base.css'
|
|
9
|
+
|
|
10
|
+
export { default as WalrusView } from './components/WalrusView.vue'
|
package/src/wallet.ts
CHANGED
|
@@ -1,160 +1,46 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Discovers wallets, connects, signs personal messages (for the NFT-gate proof), and
|
|
3
|
-
// adapts the connected wallet into a transaction executor. Module singleton.
|
|
1
|
+
// Thin walrus-ui shim over the shared @meddleware/wallet-adapter singleton.
|
|
4
2
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// the
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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).
|
|
7
|
+
import {
|
|
8
|
+
useWallet as useWalletBase,
|
|
9
|
+
getSuiClient as getSuiClientBase,
|
|
10
|
+
buildExecutor as buildExecutorBase,
|
|
11
|
+
} from '@meddleware/wallet-adapter'
|
|
12
|
+
import type { Executor } from '@meddleware/wallet-adapter'
|
|
13
13
|
import type { WalrusNetwork } from '@meddleware/walrus-relay'
|
|
14
14
|
import { RPC_URLS } from './config.js'
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
export type { Executor }
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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>
|
|
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])
|
|
104
21
|
}
|
|
105
22
|
|
|
106
|
-
/** Build
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}
|
|
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])
|
|
140
26
|
}
|
|
141
27
|
|
|
142
28
|
/**
|
|
143
|
-
* Wallet composable
|
|
144
|
-
*
|
|
145
|
-
* all callers share one wallet/account state.
|
|
29
|
+
* Wallet composable bound to walrus-ui's network config. Delegates to the shared adapter
|
|
30
|
+
* singleton; walrus needs `sui:signTransaction` (upload certify) so it's requested for discovery.
|
|
146
31
|
*/
|
|
147
32
|
export function useWallet() {
|
|
148
|
-
|
|
33
|
+
const base = useWalletBase({ requiredFeatures: ['sui:signTransaction'] })
|
|
149
34
|
return {
|
|
150
|
-
wallets:
|
|
151
|
-
currentWallet:
|
|
152
|
-
account:
|
|
153
|
-
connecting:
|
|
154
|
-
error:
|
|
155
|
-
connect,
|
|
156
|
-
disconnect,
|
|
157
|
-
signPersonalMessage,
|
|
35
|
+
wallets: base.wallets,
|
|
36
|
+
currentWallet: base.currentWallet,
|
|
37
|
+
account: base.account,
|
|
38
|
+
connecting: base.connecting,
|
|
39
|
+
error: base.error,
|
|
40
|
+
connect: base.connect,
|
|
41
|
+
disconnect: base.disconnect,
|
|
42
|
+
signPersonalMessage: base.signPersonalMessage,
|
|
43
|
+
getSuiClient,
|
|
158
44
|
buildExecutor,
|
|
159
45
|
}
|
|
160
46
|
}
|