@meddleware/token-deployer-ui 0.0.8 → 0.0.9
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/.github/workflows/node-ci.yml +9 -0
- package/.github/workflows/npm-publish.yml +16 -2
- package/.htmlvalidate.mjs +30 -0
- package/CLAUDE.md +1 -1
- package/eslint.config.ts +63 -0
- package/package.json +31 -10
- package/postcss.config.js +8 -0
- 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 +65 -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 +6 -1
- package/src/composables/useWalrusRelay.ts +1 -1
- package/src/lib/buildPublishTx.ts +1 -1
- package/src/lib/generatePackage.ts +6 -1
- package/src/lib/listMyTokens.ts +45 -0
- package/src/lib/validation.ts +8 -4
- package/src/lib/walrus-constants.ts +1 -1
- package/src/main.ts +8 -3
- package/src/styles.css +13 -5
- package/stylelint.config.js +33 -0
- package/test-results/.last-run.json +1 -1
- package/typedoc.json +2 -2
- 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;
|
|
@@ -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'
|
|
@@ -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
|
})
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
import { zipSync, strToU8 } from 'fflate'
|
|
12
12
|
import templateFiles from '../template-src/files.json'
|
|
13
13
|
import type { PublishResult, TokenConfig } from './types.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
SAFE_TEXT,
|
|
16
|
+
deriveStructName,
|
|
17
|
+
validateIdentifier,
|
|
18
|
+
hasAllowedIconScheme,
|
|
19
|
+
} from './validation.js'
|
|
15
20
|
|
|
16
21
|
const files = templateFiles as Record<string, string>
|
|
17
22
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Discover coins the connected wallet has deployed, by listing the TreasuryCap<T> objects it
|
|
2
|
+
// owns (the deploy sends the TreasuryCap to the sender). Read-only; mirrors access-gate's
|
|
3
|
+
// AdminCap-based "My Gates" discovery.
|
|
4
|
+
import { getReadClient } from './readClient.js'
|
|
5
|
+
import type { Network } from './types.js'
|
|
6
|
+
|
|
7
|
+
/** One deployed coin the wallet controls (holds the TreasuryCap for). */
|
|
8
|
+
export interface DeployedToken {
|
|
9
|
+
/** Full coin type, e.g. `0x<pkg>::mytoken::MYTOKEN`. */
|
|
10
|
+
coinType: string
|
|
11
|
+
/** The published package id (first segment of the coin type). */
|
|
12
|
+
packageId: string
|
|
13
|
+
/** Object id of the owned `TreasuryCap<T>`. */
|
|
14
|
+
treasuryCapId: string
|
|
15
|
+
/** The one-time-witness struct name (last segment) — a compact display label. */
|
|
16
|
+
label: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
|
|
20
|
+
export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
|
|
21
|
+
const client = getReadClient(network)
|
|
22
|
+
// The StructType filter without type params matches every TreasuryCap<T> instantiation.
|
|
23
|
+
const res = await client.getOwnedObjects({
|
|
24
|
+
owner,
|
|
25
|
+
filter: { StructType: '0x2::coin::TreasuryCap' },
|
|
26
|
+
options: { showType: true },
|
|
27
|
+
})
|
|
28
|
+
const seen = new Set<string>()
|
|
29
|
+
const out: DeployedToken[] = []
|
|
30
|
+
for (const item of res.data ?? []) {
|
|
31
|
+
const data = item.data
|
|
32
|
+
const m = String(data?.type ?? '').match(/^0x2::coin::TreasuryCap<(.+)>$/)
|
|
33
|
+
if (!m || !data) continue
|
|
34
|
+
const coinType = m[1]
|
|
35
|
+
if (seen.has(coinType)) continue
|
|
36
|
+
seen.add(coinType)
|
|
37
|
+
out.push({
|
|
38
|
+
coinType,
|
|
39
|
+
packageId: coinType.split('::')[0] ?? '',
|
|
40
|
+
treasuryCapId: data.objectId,
|
|
41
|
+
label: coinType.split('::').pop() ?? coinType,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
package/src/lib/validation.ts
CHANGED
|
@@ -123,18 +123,22 @@ export function validateForm(form: ValidatableForm): FormErrors {
|
|
|
123
123
|
|
|
124
124
|
if (!form.symbol.trim()) errors.symbol = 'Required'
|
|
125
125
|
else if (form.symbol.length > 32) errors.symbol = 'Maximum 32 characters'
|
|
126
|
-
else if (!SAFE_TEXT.test(form.symbol))
|
|
126
|
+
else if (!SAFE_TEXT.test(form.symbol))
|
|
127
|
+
errors.symbol = 'Double quotes and backslashes are not supported'
|
|
127
128
|
|
|
128
129
|
if (!form.name.trim()) errors.name = 'Required'
|
|
129
130
|
else if (form.name.length > 64) errors.name = 'Maximum 64 characters'
|
|
130
|
-
else if (!SAFE_TEXT.test(form.name))
|
|
131
|
+
else if (!SAFE_TEXT.test(form.name))
|
|
132
|
+
errors.name = 'Double quotes and backslashes are not supported'
|
|
131
133
|
|
|
132
134
|
if (form.description.length > 256) errors.description = 'Maximum 256 characters'
|
|
133
|
-
else if (!SAFE_TEXT.test(form.description))
|
|
135
|
+
else if (!SAFE_TEXT.test(form.description))
|
|
136
|
+
errors.description = 'Double quotes and backslashes are not supported'
|
|
134
137
|
|
|
135
138
|
if (form.iconUrl) {
|
|
136
139
|
if (form.iconUrl.length > 512) errors.iconUrl = 'Maximum 512 characters'
|
|
137
|
-
else if (!SAFE_TEXT.test(form.iconUrl))
|
|
140
|
+
else if (!SAFE_TEXT.test(form.iconUrl))
|
|
141
|
+
errors.iconUrl = 'Double quotes and backslashes are not supported'
|
|
138
142
|
else if (!hasAllowedIconScheme(form.iconUrl)) errors.iconUrl = 'Use an https:// or ipfs:// URL'
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Lightweight Walrus constants with NO heavy imports, so both the (lazily loaded)
|
|
2
|
-
// `lib/walrus.ts` client and the eagerly rendered `
|
|
2
|
+
// `lib/walrus.ts` client and the eagerly rendered `IconPicker.vue` component
|
|
3
3
|
// can share a single source of truth without pulling `@mysten/walrus` into the
|
|
4
4
|
// component's chunk.
|
|
5
5
|
|
package/src/main.ts
CHANGED
|
@@ -6,12 +6,18 @@ import App from './App.vue'
|
|
|
6
6
|
import './styles.css'
|
|
7
7
|
import './component-styles.css'
|
|
8
8
|
import { configureWasm } from './lib/template.js'
|
|
9
|
-
|
|
9
|
+
// Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
|
|
10
|
+
// import it), so a dynamic import in the E2E block below could not split it out anyway.
|
|
11
|
+
import { getReadClient } from './lib/readClient.js'
|
|
12
|
+
import { useColorMode, useSeason } from '@meddleware/ui'
|
|
10
13
|
|
|
11
14
|
// Apply the colour mode before mount so there is no theme flash. Defaults to
|
|
12
15
|
// `system` (follows the OS) while enabling the in-app light/dark/system control.
|
|
13
16
|
useColorMode('system')
|
|
14
17
|
|
|
18
|
+
// Enable seasonal theming (sets data-season on <html>; seasons.css is imported above).
|
|
19
|
+
useSeason()
|
|
20
|
+
|
|
15
21
|
// Vite serves the wasm as an asset URL; wasm-bindgen's init loads it on demand.
|
|
16
22
|
import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_bg.wasm?url'
|
|
17
23
|
|
|
@@ -25,7 +31,6 @@ configureWasm(wasmUrl)
|
|
|
25
31
|
;(async () => {
|
|
26
32
|
if (import.meta.env.VITE_E2E === '1') {
|
|
27
33
|
const { getWallets } = await import('@mysten/wallet-standard')
|
|
28
|
-
const { getReadClient: getSuiClient } = await import('./lib/readClient.js')
|
|
29
34
|
|
|
30
35
|
// Mock fetch globally for RPC requests so Cypress intercepts are bypassed entirely
|
|
31
36
|
const originalFetch = globalThis.fetch
|
|
@@ -151,7 +156,7 @@ configureWasm(wasmUrl)
|
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
// Expose getSuiClient for tests to build transactions
|
|
154
|
-
w.__getSuiClient =
|
|
159
|
+
w.__getSuiClient = getReadClient
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
createApp(App).mount('#app')
|