@meddleware/token-deployer-ui 0.0.8 → 0.0.10
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/.env.example +8 -11
- package/.github/workflows/e2e-realchain.yml +1 -1
- package/.github/workflows/node-ci.yml +9 -0
- package/.github/workflows/npm-publish.yml +16 -2
- package/.htmlvalidate.mjs +30 -0
- package/CLAUDE.md +14 -7
- package/Dockerfile +1 -1
- package/LICENSE +11 -118
- package/README.md +3 -5
- package/eslint.config.ts +63 -0
- package/index.html +11 -2
- package/package.json +32 -10
- package/postcss.config.js +8 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +1 -1
- package/public/sitemap.xml +1 -1
- package/scripts/e2e-deploy.mjs +22 -17
- package/scripts/e2e-localnet.mjs +14 -12
- package/scripts/e2e-walrus-browser.mjs +1 -4
- package/scripts/gen-og-image.mjs +2 -2
- package/scripts/publish-localnet.mjs +5 -4
- package/src/App.vue +8 -6
- package/src/component-styles.css +5 -38
- package/src/components/ConfigForm.vue +74 -28
- package/src/components/DeployProgress.vue +2 -4
- package/src/components/GithubPush.vue +22 -7
- package/src/components/IconPicker.vue +103 -42
- package/src/components/LicensePicker.vue +66 -66
- package/src/components/MyTokens.vue +125 -0
- package/src/components/ResultPanel.vue +2 -4
- package/src/components/ReviewPanel.vue +1 -1
- package/src/components/TermsDialog.vue +28 -27
- package/src/components/TokenDeployerView.vue +109 -40
- package/src/components/WalletBar.vue +67 -78
- package/src/components/WalrusBlobBrowser.vue +33 -25
- package/src/composables/useAccessGate.ts +7 -2
- package/src/composables/useWalrusRelay.ts +1 -1
- package/src/config.ts +10 -13
- package/src/lib/accessGate.ts +28 -14
- package/src/lib/buildPublishTx.ts +1 -1
- package/src/lib/deploy.ts +36 -0
- package/src/lib/deployExecutor.ts +14 -12
- package/src/lib/generatePackage.ts +18 -13
- package/src/lib/licenses.ts +4 -3
- package/src/lib/listMyTokens.ts +41 -0
- package/src/lib/readClient.ts +15 -7
- package/src/lib/types.ts +1 -1
- package/src/lib/validation.ts +8 -4
- package/src/lib/walrus-constants.ts +1 -1
- package/src/lib/walrus.ts +22 -11
- package/src/main.ts +46 -58
- package/src/styles.css +13 -5
- package/src/template-src/files.json +7 -7
- package/stylelint.config.js +33 -0
- package/test-results/.last-run.json +1 -1
- package/tests/accessGate.test.ts +26 -21
- package/tests/deploy.test.ts +70 -2
- package/tests/generatePackage.test.ts +11 -0
- package/tests/licenses.test.ts +6 -3
- package/tests/listMyTokens.test.ts +30 -0
- package/tests/useAccessGate.test.ts +15 -19
- package/typedoc.json +2 -2
- package/vite.config.ts +9 -1
- package/src/components/WalrusIconUpload.vue +0 -205
|
@@ -6,7 +6,16 @@ import ReviewPanel from './ReviewPanel.vue'
|
|
|
6
6
|
import DeployProgress from './DeployProgress.vue'
|
|
7
7
|
import ResultPanel from './ResultPanel.vue'
|
|
8
8
|
import TermsDialog from './TermsDialog.vue'
|
|
9
|
-
import
|
|
9
|
+
import MyTokens from './MyTokens.vue'
|
|
10
|
+
import {
|
|
11
|
+
AppTabNav,
|
|
12
|
+
UiNotice,
|
|
13
|
+
UiStepper,
|
|
14
|
+
UiTabPanel,
|
|
15
|
+
UiToolIntro,
|
|
16
|
+
type AppTab,
|
|
17
|
+
type StepperStep,
|
|
18
|
+
} from '@meddleware/ui'
|
|
10
19
|
import { emptyForm, toTokenConfig } from '../lib/form.js'
|
|
11
20
|
import { validateForm } from '../lib/validation.js'
|
|
12
21
|
import type { FormErrors } from '../lib/validation.js'
|
|
@@ -32,6 +41,14 @@ const CONFIG_STEPS: ConfigFormStep[] = ['identity', 'token', 'settings']
|
|
|
32
41
|
const { account } = useWallet()
|
|
33
42
|
const { network } = useNetwork()
|
|
34
43
|
|
|
44
|
+
// Top-level tabs — Deploy (the wizard) and My Tokens (coins this wallet has deployed), so the
|
|
45
|
+
// layout matches the other Sui tools (intro → tabs → content).
|
|
46
|
+
const TABS: AppTab[] = [
|
|
47
|
+
{ id: 'deploy', label: 'Deploy' },
|
|
48
|
+
{ id: 'my-tokens', label: 'My Tokens' },
|
|
49
|
+
]
|
|
50
|
+
const activeTab = ref('deploy')
|
|
51
|
+
|
|
35
52
|
const form = reactive(emptyForm())
|
|
36
53
|
const step = ref<Step>('identity')
|
|
37
54
|
const deployStep = ref<DeployStep | null>(null)
|
|
@@ -55,10 +72,14 @@ function stepHasErrors(keys: (keyof FormErrors)[]): boolean {
|
|
|
55
72
|
const canProceed = computed((): boolean => {
|
|
56
73
|
if (!connected.value) return false
|
|
57
74
|
switch (step.value) {
|
|
58
|
-
case 'identity':
|
|
59
|
-
|
|
60
|
-
case '
|
|
61
|
-
|
|
75
|
+
case 'identity':
|
|
76
|
+
return !stepHasErrors(IDENTITY_KEYS)
|
|
77
|
+
case 'token':
|
|
78
|
+
return !stepHasErrors(TOKEN_KEYS)
|
|
79
|
+
case 'settings':
|
|
80
|
+
return !stepHasErrors(SETTINGS_KEYS)
|
|
81
|
+
default:
|
|
82
|
+
return true
|
|
62
83
|
}
|
|
63
84
|
})
|
|
64
85
|
|
|
@@ -90,18 +111,36 @@ function onStepperBack(i: number): void {
|
|
|
90
111
|
|
|
91
112
|
function goNext(): void {
|
|
92
113
|
if (!canProceed.value) return
|
|
93
|
-
if (step.value === 'identity') {
|
|
94
|
-
|
|
114
|
+
if (step.value === 'identity') {
|
|
115
|
+
step.value = 'token'
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (step.value === 'token') {
|
|
119
|
+
step.value = 'settings'
|
|
120
|
+
return
|
|
121
|
+
}
|
|
95
122
|
if (step.value === 'settings') {
|
|
96
|
-
if (!termsAccepted.value) {
|
|
123
|
+
if (!termsAccepted.value) {
|
|
124
|
+
showTerms.value = true
|
|
125
|
+
return
|
|
126
|
+
}
|
|
97
127
|
step.value = 'review'
|
|
98
128
|
}
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
function goBack(): void {
|
|
102
|
-
if (step.value === 'token') {
|
|
103
|
-
|
|
104
|
-
|
|
132
|
+
if (step.value === 'token') {
|
|
133
|
+
step.value = 'identity'
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (step.value === 'settings') {
|
|
137
|
+
step.value = 'token'
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (step.value === 'review') {
|
|
141
|
+
step.value = 'settings'
|
|
142
|
+
return
|
|
143
|
+
}
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
function onTermsAccept(): void {
|
|
@@ -117,7 +156,10 @@ function onTermsCancel(): void {
|
|
|
117
156
|
|
|
118
157
|
async function confirmDeploy(): Promise<void> {
|
|
119
158
|
const sender = account.value?.address
|
|
120
|
-
if (!sender) {
|
|
159
|
+
if (!sender) {
|
|
160
|
+
deployError.value = 'Connect a wallet first.'
|
|
161
|
+
return
|
|
162
|
+
}
|
|
121
163
|
deployError.value = null
|
|
122
164
|
step.value = 'deploying'
|
|
123
165
|
deployStep.value = null
|
|
@@ -149,34 +191,61 @@ function restart(): void {
|
|
|
149
191
|
</script>
|
|
150
192
|
|
|
151
193
|
<template>
|
|
194
|
+
<UiToolIntro
|
|
195
|
+
>Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
|
|
196
|
+
network
|
|
197
|
+
}}).</UiToolIntro
|
|
198
|
+
>
|
|
199
|
+
|
|
200
|
+
<AppTabNav
|
|
201
|
+
v-model="activeTab"
|
|
202
|
+
:tabs="TABS"
|
|
203
|
+
id-prefix="token-deployer"
|
|
204
|
+
aria-label="Sections"
|
|
205
|
+
class="deployer-tabs"
|
|
206
|
+
/>
|
|
207
|
+
|
|
152
208
|
<WalletGuard message="Connect a Sui wallet to deploy your token.">
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
<UiTabPanel id-prefix="token-deployer" :tab="activeTab">
|
|
210
|
+
<template v-if="activeTab === 'deploy'">
|
|
211
|
+
<UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
|
|
212
|
+
<ConfigForm
|
|
213
|
+
v-if="step === 'identity' || step === 'token' || step === 'settings'"
|
|
214
|
+
:form="form"
|
|
215
|
+
:errors="errors"
|
|
216
|
+
:can-proceed="canProceed"
|
|
217
|
+
:network="network"
|
|
218
|
+
:connected="connected"
|
|
219
|
+
:form-step="formStep"
|
|
220
|
+
@next="goNext"
|
|
221
|
+
@back="goBack"
|
|
222
|
+
/>
|
|
223
|
+
<ReviewPanel
|
|
224
|
+
v-if="step === 'review'"
|
|
225
|
+
:config="config"
|
|
226
|
+
:network="network"
|
|
227
|
+
@back="goBack"
|
|
228
|
+
@confirm="confirmDeploy"
|
|
229
|
+
/>
|
|
230
|
+
<DeployProgress v-if="step === 'deploying'" :step="deployStep" />
|
|
231
|
+
<ResultPanel
|
|
232
|
+
v-if="step === 'done' && result"
|
|
233
|
+
:result="result"
|
|
234
|
+
:config="config"
|
|
235
|
+
@restart="restart"
|
|
236
|
+
/>
|
|
237
|
+
<UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<MyTokens v-else :owner="account?.address ?? null" :network="network" />
|
|
241
|
+
</UiTabPanel>
|
|
181
242
|
</WalletGuard>
|
|
243
|
+
|
|
244
|
+
<TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
|
|
182
245
|
</template>
|
|
246
|
+
|
|
247
|
+
<style scoped>
|
|
248
|
+
.deployer-tabs {
|
|
249
|
+
margin: 0 0 1rem;
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -3,12 +3,12 @@ import { computed, ref, watch } from 'vue'
|
|
|
3
3
|
import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
|
|
4
4
|
import { SELECTABLE_NETWORKS } from '../config.js'
|
|
5
5
|
import type { Network } from '../lib/types.js'
|
|
6
|
-
import {
|
|
6
|
+
import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
|
|
7
7
|
|
|
8
8
|
const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
|
|
9
9
|
const { network, setNetwork } = useNetwork()
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const dialogOpen = ref(false)
|
|
12
12
|
|
|
13
13
|
const shortAddress = computed(() => {
|
|
14
14
|
const a = account.value?.address
|
|
@@ -20,88 +20,77 @@ const networkModel = computed({
|
|
|
20
20
|
set: (v: string) => setNetwork(v as Network),
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
watch(() => account.value, (acc) => {
|
|
32
|
-
if (acc) {
|
|
33
|
-
closeDialog()
|
|
34
|
-
}
|
|
35
|
-
})
|
|
23
|
+
// Close the connect dialog once a wallet connects.
|
|
24
|
+
watch(
|
|
25
|
+
() => account.value,
|
|
26
|
+
(acc) => {
|
|
27
|
+
if (acc) dialogOpen.value = false
|
|
28
|
+
},
|
|
29
|
+
)
|
|
36
30
|
</script>
|
|
37
31
|
|
|
38
32
|
<template>
|
|
39
|
-
<
|
|
33
|
+
<section class="card wallet-bar" aria-label="Wallet and network">
|
|
40
34
|
<template v-if="account">
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<span class="
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</template>
|
|
57
|
-
<template v-else>
|
|
58
|
-
<div class="row">
|
|
59
|
-
<div v-if="!wallets.length" style="width: 100%">
|
|
60
|
-
<p class="hint" style="margin: 0">
|
|
61
|
-
No Sui wallet detected. Install a Sui wallet extension to continue.
|
|
62
|
-
</p>
|
|
63
|
-
</div>
|
|
64
|
-
<button v-else type="button" class="primary" @click="openDialog">
|
|
65
|
-
Connect Wallet
|
|
66
|
-
</button>
|
|
67
|
-
</div>
|
|
35
|
+
<label class="row wallet-bar__network">
|
|
36
|
+
Network
|
|
37
|
+
<UiSelect v-model="networkModel">
|
|
38
|
+
<option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
|
|
39
|
+
</UiSelect>
|
|
40
|
+
</label>
|
|
41
|
+
<p class="row wallet-bar__account">
|
|
42
|
+
<span class="badge">
|
|
43
|
+
<span class="mw-visually-hidden">Connected wallet:</span>
|
|
44
|
+
{{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
|
|
45
|
+
</span>
|
|
46
|
+
<button type="button" @click="disconnect">Disconnect</button>
|
|
47
|
+
</p>
|
|
68
48
|
</template>
|
|
49
|
+
<p v-else-if="!wallets.length" class="hint wallet-bar__none">
|
|
50
|
+
No Sui wallet detected. Install a Sui wallet extension to continue.
|
|
51
|
+
</p>
|
|
52
|
+
<button v-else type="button" class="primary" @click="dialogOpen = true">Connect Wallet</button>
|
|
69
53
|
|
|
70
|
-
<UiNotice v-if="error" type="error"
|
|
71
|
-
</
|
|
54
|
+
<UiNotice v-if="error" type="error" class="wallet-bar__error">{{ error }}</UiNotice>
|
|
55
|
+
</section>
|
|
72
56
|
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
>
|
|
82
|
-
×
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
57
|
+
<UiDialog v-model:open="dialogOpen" title="Connect Wallet">
|
|
58
|
+
<UiFormField id="dialog-network-select" label="Network">
|
|
59
|
+
<template #default="{ attrs }">
|
|
60
|
+
<UiSelect v-bind="attrs" v-model="networkModel">
|
|
61
|
+
<option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
|
|
62
|
+
</UiSelect>
|
|
63
|
+
</template>
|
|
64
|
+
</UiFormField>
|
|
85
65
|
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
v-for="w in wallets"
|
|
96
|
-
:key="w.name"
|
|
97
|
-
type="button"
|
|
98
|
-
class="primary"
|
|
99
|
-
:disabled="connecting"
|
|
100
|
-
@click="connect(w)"
|
|
101
|
-
>
|
|
102
|
-
<img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
|
|
103
|
-
{{ w.name }}
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
</dialog>
|
|
66
|
+
<menu class="wallet-list">
|
|
67
|
+
<li v-for="w in wallets" :key="w.name">
|
|
68
|
+
<button type="button" class="primary" :disabled="connecting" @click="connect(w)">
|
|
69
|
+
<img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
|
|
70
|
+
{{ w.name }}
|
|
71
|
+
</button>
|
|
72
|
+
</li>
|
|
73
|
+
</menu>
|
|
74
|
+
</UiDialog>
|
|
107
75
|
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.wallet-bar {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
gap: 0.75rem;
|
|
84
|
+
}
|
|
85
|
+
.wallet-bar__network,
|
|
86
|
+
.wallet-bar__account,
|
|
87
|
+
.wallet-bar__none {
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
.wallet-bar__none {
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
.wallet-bar__error {
|
|
94
|
+
flex-basis: 100%;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -46,9 +46,8 @@ async function load(): Promise<void> {
|
|
|
46
46
|
blobs.value = []
|
|
47
47
|
hasLoaded.value = false
|
|
48
48
|
try {
|
|
49
|
-
const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
|
|
50
|
-
'../lib/walrus.js'
|
|
51
|
-
)
|
|
49
|
+
const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
|
|
50
|
+
await import('../lib/walrus.js')
|
|
52
51
|
const suiClient = getReadClient(props.network as Network)
|
|
53
52
|
const client = createWalrusClient(props.network)
|
|
54
53
|
const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
|
|
@@ -75,9 +74,13 @@ function selectBlob(item: BlobItem): void {
|
|
|
75
74
|
type="text"
|
|
76
75
|
placeholder="0x… or leave blank for your wallet"
|
|
77
76
|
aria-label="Wallet address to browse"
|
|
78
|
-
|
|
77
|
+
class="blob-browser__address"
|
|
79
78
|
/>
|
|
80
|
-
<button
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
:disabled="loading || (!connected && !addressInput.trim())"
|
|
82
|
+
@click="load"
|
|
83
|
+
>
|
|
81
84
|
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
|
82
85
|
Load
|
|
83
86
|
</button>
|
|
@@ -90,26 +93,26 @@ function selectBlob(item: BlobItem): void {
|
|
|
90
93
|
No Walrus blobs found at this address.
|
|
91
94
|
</p>
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</
|
|
112
|
-
</
|
|
96
|
+
<!-- A list of toggle buttons (native keyboard behaviour); aria-pressed marks the chosen blob. -->
|
|
97
|
+
<ul v-if="blobs.length > 0" role="list" class="blob-grid" aria-label="Walrus blobs">
|
|
98
|
+
<li v-for="b in blobs" :key="b.objectId">
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
class="blob-thumb"
|
|
102
|
+
:class="{ selected: selected === b.blobId }"
|
|
103
|
+
:aria-pressed="selected === b.blobId"
|
|
104
|
+
@click="selectBlob(b)"
|
|
105
|
+
>
|
|
106
|
+
<img
|
|
107
|
+
:src="b.url"
|
|
108
|
+
:alt="`Blob ${b.blobId.slice(0, 8)}…`"
|
|
109
|
+
loading="lazy"
|
|
110
|
+
@error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
|
|
111
|
+
/>
|
|
112
|
+
<span class="blob-meta">{{ formatSize(b.size) }}</span>
|
|
113
|
+
</button>
|
|
114
|
+
</li>
|
|
115
|
+
</ul>
|
|
113
116
|
</div>
|
|
114
117
|
</template>
|
|
115
118
|
|
|
@@ -127,7 +130,12 @@ function selectBlob(item: BlobItem): void {
|
|
|
127
130
|
margin-top: 0.25rem;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
.blob-browser__address {
|
|
134
|
+
flex: 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
130
137
|
.blob-thumb {
|
|
138
|
+
width: 100%;
|
|
131
139
|
display: flex;
|
|
132
140
|
flex-direction: column;
|
|
133
141
|
align-items: center;
|
|
@@ -22,7 +22,7 @@ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature
|
|
|
22
22
|
* When no gate is configured for the network (`ACCESS_GATE[network] == null`), the relay is
|
|
23
23
|
* treated as OPEN (`hasAccess === true`) and this composable is inert — behaviour is
|
|
24
24
|
* unchanged from before gating existed. When a gate IS configured, `checkOwnership` decides
|
|
25
|
-
* access with a single `
|
|
25
|
+
* access with a single `listOwnedObjects` call (the same cheap pattern as the relay health
|
|
26
26
|
* check), and `purchase`/`buildRelayAccessToken` drive the buy + prove flows.
|
|
27
27
|
*/
|
|
28
28
|
export function useAccessGate(
|
|
@@ -53,7 +53,12 @@ export function useAccessGate(
|
|
|
53
53
|
checking.value = true
|
|
54
54
|
error.value = null
|
|
55
55
|
try {
|
|
56
|
-
const nfts = await fetchAccessNfts(
|
|
56
|
+
const nfts = await fetchAccessNfts(
|
|
57
|
+
deps.getClient(network),
|
|
58
|
+
address,
|
|
59
|
+
gate.nftType,
|
|
60
|
+
gate.gateId,
|
|
61
|
+
)
|
|
57
62
|
hasAccess.value = nfts.length > 0
|
|
58
63
|
usesRemaining.value = nfts.length ? nfts[0].usesRemaining : null
|
|
59
64
|
nftId.value = nfts.length ? nfts[0].objectId : null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Thin wrapper around the shared `@meddleware/walrus-relay-ui` composable: injects
|
|
2
2
|
// this app's per-network relay host pair so the relay-selection + tip-estimation logic
|
|
3
3
|
// lives in exactly one place (DRY). The return shape is unchanged, so consumers
|
|
4
|
-
// (`
|
|
4
|
+
// (`IconPicker.vue`) and tests are unaffected.
|
|
5
5
|
import { useWalrusRelay as useSharedWalrusRelay } from '@meddleware/walrus-relay'
|
|
6
6
|
import type { RelayAccessOptions, RelayOption } from '@meddleware/walrus-relay'
|
|
7
7
|
import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
|
package/src/config.ts
CHANGED
|
@@ -7,16 +7,16 @@ import type { AccessGateConfig } from './lib/accessGate.js'
|
|
|
7
7
|
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Public
|
|
10
|
+
* Public gRPC fullnode URLs per network (overridable via `VITE_RPC_*`).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* These are **gRPC(-web)** endpoints for `SuiGrpcClient` — public fullnodes have
|
|
13
|
+
* deprecated JSON-RPC, so a JSON-RPC-only URL will not work here. Operators should
|
|
14
|
+
* set `VITE_RPC_*` to their preferred (often paid) gRPC endpoint for production to
|
|
15
|
+
* avoid public rate limits.
|
|
16
16
|
*/
|
|
17
17
|
export const RPC_URLS: Record<Network, string> = {
|
|
18
18
|
mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
|
|
19
|
-
testnet: env.VITE_RPC_TESTNET || 'https://
|
|
19
|
+
testnet: env.VITE_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
|
|
20
20
|
localnet: env.VITE_RPC_LOCALNET || 'http://127.0.0.1:9000',
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -89,15 +89,12 @@ export function isOperatorRelayConfigured(network: WalrusNetwork): boolean {
|
|
|
89
89
|
/**
|
|
90
90
|
* gRPC fullnode URL for the Walrus icon-upload client (a `SuiGrpcClient`).
|
|
91
91
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* are not interchangeable — pointing the Walrus gRPC client at a JSON-RPC URL breaks it —
|
|
95
|
-
* so this has its own `VITE_WALRUS_RPC_*` override rather than reusing `VITE_RPC_*`.
|
|
96
|
-
* Defaults to the public gRPC fullnodes (rate-limited); set your own for production.
|
|
92
|
+
* Defaults to {@link RPC_URLS} (also gRPC); `VITE_WALRUS_RPC_*` overrides it only when
|
|
93
|
+
* Walrus traffic should use a different fullnode from the deploy path.
|
|
97
94
|
*/
|
|
98
95
|
export const WALRUS_RPC_URLS: Record<WalrusNetwork, string> = {
|
|
99
|
-
testnet: env.VITE_WALRUS_RPC_TESTNET ||
|
|
100
|
-
mainnet: env.VITE_WALRUS_RPC_MAINNET ||
|
|
96
|
+
testnet: env.VITE_WALRUS_RPC_TESTNET || RPC_URLS.testnet,
|
|
97
|
+
mainnet: env.VITE_WALRUS_RPC_MAINNET || RPC_URLS.mainnet,
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
/**
|
package/src/lib/accessGate.ts
CHANGED
|
@@ -30,27 +30,41 @@ export interface OwnedAccessNft {
|
|
|
30
30
|
usesRemaining: number | null
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Minimal structural subset of the
|
|
33
|
+
/** Minimal structural subset of the core (gRPC) client used here. */
|
|
34
34
|
export interface OwnedObjectsClient {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
core: {
|
|
36
|
+
listOwnedObjects(params: {
|
|
37
|
+
owner: string
|
|
38
|
+
type?: string
|
|
39
|
+
include?: { json?: boolean }
|
|
40
|
+
}): Promise<{ objects: unknown[] }>
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
|
|
46
|
+
* fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both.
|
|
47
|
+
*/
|
|
48
|
+
function structFields(v: unknown): Record<string, any> | undefined {
|
|
49
|
+
if (!v || typeof v !== 'object') return undefined
|
|
50
|
+
const o = v as Record<string, any>
|
|
51
|
+
const nested = o.fields
|
|
52
|
+
return nested && typeof nested === 'object' ? (nested as Record<string, any>) : o
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
function parseUsesRemaining(variant: any): number | null {
|
|
43
56
|
if (variant == null) return null
|
|
57
|
+
const tag: string | undefined = variant.variant ?? variant.type ?? variant.$kind
|
|
58
|
+
if (tag === 'UnlimitedPass') return null
|
|
44
59
|
const fields = variant.fields ?? variant
|
|
45
60
|
const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
|
|
46
61
|
return ur != null ? Number(ur) : null
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
/** Parse a `
|
|
64
|
+
/** Parse a `listOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
|
|
50
65
|
export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const inner = obj?.content?.fields?.data?.fields
|
|
66
|
+
const objectId: string | undefined = entry?.objectId
|
|
67
|
+
const inner = structFields(structFields(entry?.json)?.data)
|
|
54
68
|
const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
|
|
55
69
|
if (!objectId || !gateId) return null
|
|
56
70
|
return { objectId, gateId, usesRemaining: parseUsesRemaining(inner?.variant) }
|
|
@@ -63,12 +77,12 @@ export async function fetchAccessNfts(
|
|
|
63
77
|
nftType: string,
|
|
64
78
|
gateId?: string,
|
|
65
79
|
): Promise<OwnedAccessNft[]> {
|
|
66
|
-
const {
|
|
80
|
+
const { objects } = await client.core.listOwnedObjects({
|
|
67
81
|
owner,
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
type: nftType,
|
|
83
|
+
include: { json: true },
|
|
70
84
|
})
|
|
71
|
-
const parsed = (
|
|
85
|
+
const parsed = (objects ?? [])
|
|
72
86
|
.map(parseOwnedAccessNft)
|
|
73
87
|
.filter((n): n is OwnedAccessNft => n !== null)
|
|
74
88
|
return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
|
|
@@ -103,7 +103,7 @@ export function buildFinalizeTransaction(args: BuildFinalizeArgs): Transaction {
|
|
|
103
103
|
target: `${SUI_FRAMEWORK}::coin_registry::finalize_registration`,
|
|
104
104
|
typeArguments: [coinType],
|
|
105
105
|
arguments: [
|
|
106
|
-
tx.object('0xc'),
|
|
106
|
+
tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
|
|
107
107
|
tx.receivingRef(args.currencyRef), // Receiving<Currency<T>> — pending object at @0xc
|
|
108
108
|
],
|
|
109
109
|
})
|