@meddleware/token-deployer-ui 0.0.3 → 0.0.4
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/token-deployer-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": { "default": "./src/index.ts" }
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@meddleware/design-tokens": "^0.1.5",
|
|
34
|
-
"@meddleware/ui": "^0.1.
|
|
34
|
+
"@meddleware/ui": "^0.1.23",
|
|
35
35
|
"@meddleware/wallet-adapter": "^0.0.7",
|
|
36
36
|
"@meddleware/walrus-relay": "^0.1.16",
|
|
37
37
|
"@mysten/bcs": "~2.1.0",
|
|
@@ -8,8 +8,7 @@ import LicensePicker from './LicensePicker.vue'
|
|
|
8
8
|
import IconPicker from './IconPicker.vue'
|
|
9
9
|
import { UiFormField, UiSelect, UiFieldHint } from '@meddleware/ui'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export type ConfigFormStep = 'identity' | 'token' | 'settings'
|
|
13
12
|
|
|
14
13
|
const props = defineProps<{
|
|
15
14
|
form: FormModel
|
|
@@ -17,8 +16,9 @@ const props = defineProps<{
|
|
|
17
16
|
canProceed: boolean
|
|
18
17
|
network: Network
|
|
19
18
|
connected: boolean
|
|
19
|
+
formStep: ConfigFormStep
|
|
20
20
|
}>()
|
|
21
|
-
const emit = defineEmits<{ (e: '
|
|
21
|
+
const emit = defineEmits<{ (e: 'next'): void; (e: 'back'): void }>()
|
|
22
22
|
|
|
23
23
|
const structPreview = computed(() => {
|
|
24
24
|
const m = props.form.moduleName.trim()
|
|
@@ -42,68 +42,75 @@ function onBlur(key: string): void {
|
|
|
42
42
|
</script>
|
|
43
43
|
|
|
44
44
|
<template>
|
|
45
|
-
<form novalidate @submit.prevent="emit('
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
45
|
+
<form novalidate @submit.prevent="emit('next')">
|
|
46
|
+
|
|
47
|
+
<!-- Step 1: Identity -->
|
|
48
|
+
<template v-if="formStep === 'identity'">
|
|
49
|
+
<fieldset>
|
|
50
|
+
<legend>Package identity <span class="permanent">(permanent)</span></legend>
|
|
51
|
+
<div class="grid2">
|
|
52
|
+
<UiFormField id="package" label="Package name" :error="visibleError('packageName')" v-slot="{ attrs }">
|
|
53
|
+
<input v-bind="attrs" v-model="form.packageName" placeholder="my_token" autocomplete="off" @blur="onBlur('packageName')" />
|
|
54
|
+
</UiFormField>
|
|
55
|
+
<UiFormField id="module" label="Module name" :error="visibleError('moduleName')" v-slot="{ attrs }">
|
|
56
|
+
<input v-bind="attrs" v-model="form.moduleName" placeholder="mytoken" autocomplete="off" @blur="onBlur('moduleName')" />
|
|
57
|
+
</UiFormField>
|
|
58
|
+
</div>
|
|
59
|
+
<p class="hint" aria-live="polite">
|
|
60
|
+
<template v-if="identifierError">
|
|
61
|
+
Fix the package and module names to preview the coin type.
|
|
62
|
+
</template>
|
|
63
|
+
<template v-else>
|
|
64
|
+
Coin type will be <span class="mono">{{ coinTypePreview }}</span
|
|
65
|
+
>. The witness struct <span class="mono">{{ structPreview }}</span> is derived from
|
|
66
|
+
the module name (a Sui requirement).
|
|
67
|
+
</template>
|
|
68
|
+
</p>
|
|
69
|
+
</fieldset>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<!-- Step 2: Token -->
|
|
73
|
+
<template v-if="formStep === 'token'">
|
|
74
|
+
<fieldset>
|
|
75
|
+
<legend>Token details</legend>
|
|
76
|
+
|
|
77
|
+
<div class="grid2">
|
|
78
|
+
<UiFormField id="name" label="Token name" :error="visibleError('name')" v-slot="{ attrs }">
|
|
79
|
+
<input v-bind="attrs" v-model="form.name" autocomplete="off" @blur="onBlur('name')" />
|
|
80
|
+
</UiFormField>
|
|
81
|
+
<UiFormField id="symbol" label="Symbol" :error="visibleError('symbol')" v-slot="{ attrs }">
|
|
82
|
+
<input v-bind="attrs" v-model="form.symbol" autocomplete="off" @blur="onBlur('symbol')" />
|
|
83
|
+
</UiFormField>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<UiFormField id="description" label="Description" :error="visibleError('description')">
|
|
87
|
+
<template #label-suffix> <span class="hint">(optional)</span></template>
|
|
88
|
+
<template #default="{ attrs }">
|
|
89
|
+
<textarea v-bind="attrs" v-model="form.description" @blur="onBlur('description')" />
|
|
90
|
+
</template>
|
|
74
91
|
</UiFormField>
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
|
|
93
|
+
<UiFormField id="decimals" label="Decimals" :error="visibleError('decimals')">
|
|
94
|
+
<template #label-suffix> <span class="permanent">(permanent)</span></template>
|
|
95
|
+
<template #default="{ attrs }">
|
|
96
|
+
<input v-bind="attrs" v-model="form.decimals" inputmode="numeric" @blur="onBlur('decimals')" />
|
|
97
|
+
</template>
|
|
77
98
|
</UiFormField>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</template>
|
|
92
|
-
</UiFormField>
|
|
93
|
-
|
|
94
|
-
<IconPicker
|
|
95
|
-
v-model="form.iconUrl"
|
|
96
|
-
:network="network"
|
|
97
|
-
:connected="connected"
|
|
98
|
-
:error="visibleError('iconUrl')"
|
|
99
|
-
@blur="onBlur('iconUrl')"
|
|
100
|
-
/>
|
|
101
|
-
</fieldset>
|
|
102
|
-
|
|
103
|
-
<details>
|
|
104
|
-
<summary>Advanced options</summary>
|
|
99
|
+
|
|
100
|
+
<IconPicker
|
|
101
|
+
v-model="form.iconUrl"
|
|
102
|
+
:network="network"
|
|
103
|
+
:connected="connected"
|
|
104
|
+
:error="visibleError('iconUrl')"
|
|
105
|
+
@blur="onBlur('iconUrl')"
|
|
106
|
+
/>
|
|
107
|
+
</fieldset>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<!-- Step 3: Settings -->
|
|
111
|
+
<template v-if="formStep === 'settings'">
|
|
105
112
|
<fieldset>
|
|
106
|
-
<legend
|
|
113
|
+
<legend>Supply & policies</legend>
|
|
107
114
|
|
|
108
115
|
<UiFormField id="supply" label="Initial supply" :error="visibleError('initialSupply')">
|
|
109
116
|
<template #label-suffix>
|
|
@@ -184,10 +191,13 @@ function onBlur(key: string): void {
|
|
|
184
191
|
|
|
185
192
|
<LicensePicker v-model="form.license" @name="form.licenseName = $event" />
|
|
186
193
|
</fieldset>
|
|
187
|
-
</
|
|
194
|
+
</template>
|
|
188
195
|
|
|
189
196
|
<div class="row" style="margin-top: 1rem">
|
|
190
|
-
<button
|
|
197
|
+
<button v-if="formStep !== 'identity'" type="button" @click="emit('back')">Back</button>
|
|
198
|
+
<button type="submit" class="primary" :disabled="!canProceed">
|
|
199
|
+
{{ formStep === 'settings' ? 'Review & deploy' : 'Next' }}
|
|
200
|
+
</button>
|
|
191
201
|
</div>
|
|
192
202
|
</form>
|
|
193
203
|
</template>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../lib/accessGate.js'
|
|
15
15
|
import type { WalrusNetwork } from '../lib/walrus.js'
|
|
16
16
|
import type { Network } from '../lib/types.js'
|
|
17
|
-
import { UiNotice, UiFieldHint } from '@meddleware/ui'
|
|
17
|
+
import { UiNotice, UiFieldHint, UiSegmentedControl } from '@meddleware/ui'
|
|
18
18
|
import WalrusBlobBrowser from './WalrusBlobBrowser.vue'
|
|
19
19
|
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
20
20
|
|
|
@@ -76,7 +76,7 @@ async function purchaseAccess(): Promise<void> {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const tab = ref
|
|
79
|
+
const tab = ref('url')
|
|
80
80
|
|
|
81
81
|
// Upload state
|
|
82
82
|
const uploading = ref(false)
|
|
@@ -228,26 +228,16 @@ function onBrowseSelect(url: string): void {
|
|
|
228
228
|
<fieldset class="icon-picker">
|
|
229
229
|
<legend>Icon <span class="hint">(optional)</span></legend>
|
|
230
230
|
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
URL
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Upload to Walrus
|
|
242
|
-
</label>
|
|
243
|
-
<label
|
|
244
|
-
:class="{ active: tab === 'browse', disabled: !isWalrusNetwork }"
|
|
245
|
-
:title="isWalrusNetwork ? undefined : 'Not available on localnet'"
|
|
246
|
-
>
|
|
247
|
-
<input type="radio" v-model="tab" value="browse" :disabled="!isWalrusNetwork" />
|
|
248
|
-
My Walrus blobs
|
|
249
|
-
</label>
|
|
250
|
-
</div>
|
|
231
|
+
<UiSegmentedControl
|
|
232
|
+
v-model="tab"
|
|
233
|
+
:options="[
|
|
234
|
+
{ id: 'url', label: 'URL' },
|
|
235
|
+
{ id: 'upload', label: 'Upload to Walrus', disabled: !isWalrusNetwork, title: isWalrusNetwork ? undefined : 'Not available on localnet' },
|
|
236
|
+
{ id: 'browse', label: 'My Walrus blobs', disabled: !isWalrusNetwork, title: isWalrusNetwork ? undefined : 'Not available on localnet' },
|
|
237
|
+
]"
|
|
238
|
+
aria-label="Icon source"
|
|
239
|
+
style="margin-bottom: 0.75rem"
|
|
240
|
+
/>
|
|
251
241
|
|
|
252
242
|
<!-- URL panel -->
|
|
253
243
|
<div v-show="tab === 'url'" class="icon-panel">
|
|
@@ -347,49 +337,6 @@ function onBrowseSelect(url: string): void {
|
|
|
347
337
|
</template>
|
|
348
338
|
|
|
349
339
|
<style scoped>
|
|
350
|
-
.icon-tabs {
|
|
351
|
-
display: flex;
|
|
352
|
-
border: 1px solid var(--border);
|
|
353
|
-
border-radius: 6px;
|
|
354
|
-
overflow: hidden;
|
|
355
|
-
width: fit-content;
|
|
356
|
-
margin-bottom: 0.75rem;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.icon-tabs label {
|
|
360
|
-
display: flex;
|
|
361
|
-
align-items: center;
|
|
362
|
-
padding: 0.4rem 0.75rem;
|
|
363
|
-
cursor: pointer;
|
|
364
|
-
font-size: 0.875rem;
|
|
365
|
-
background: transparent;
|
|
366
|
-
border-right: 1px solid var(--border);
|
|
367
|
-
transition: background 0.15s, color 0.15s;
|
|
368
|
-
user-select: none;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.icon-tabs label:last-child {
|
|
372
|
-
border-right: none;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
.icon-tabs label.active {
|
|
376
|
-
background: var(--accent);
|
|
377
|
-
color: var(--accent-contrast);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
.icon-tabs label.disabled {
|
|
381
|
-
opacity: 0.5;
|
|
382
|
-
cursor: not-allowed;
|
|
383
|
-
pointer-events: none;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
.icon-tabs input[type='radio'] {
|
|
387
|
-
position: absolute;
|
|
388
|
-
opacity: 0;
|
|
389
|
-
width: 0;
|
|
390
|
-
height: 0;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
340
|
.icon-panel {
|
|
394
341
|
padding-top: 0.5rem;
|
|
395
342
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, reactive, ref } from 'vue'
|
|
3
3
|
import { useWallet, useNetwork, WalletGuard } from '@meddleware/wallet-adapter'
|
|
4
|
-
import ConfigForm from './ConfigForm.vue'
|
|
4
|
+
import ConfigForm, { type ConfigFormStep } from './ConfigForm.vue'
|
|
5
5
|
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 { UiNotice } from '@meddleware/ui'
|
|
9
|
+
import { UiNotice, UiStepper, type StepperStep } from '@meddleware/ui'
|
|
10
10
|
import { emptyForm, toTokenConfig } from '../lib/form.js'
|
|
11
|
-
import { validateForm
|
|
11
|
+
import { validateForm } from '../lib/validation.js'
|
|
12
|
+
import type { FormErrors } from '../lib/validation.js'
|
|
12
13
|
import { deployToken } from '../lib/deploy.js'
|
|
13
14
|
import type { DeployStep } from '../lib/deploy.js'
|
|
14
15
|
import { buildDeployExecutor } from '../lib/deployExecutor.js'
|
|
@@ -16,13 +17,23 @@ import { FEE_MIST, FEE_TREASURY, PUBLISH_GAS_BUDGET, isFeeConfigured } from '../
|
|
|
16
17
|
import { extractErrorMessage } from '../lib/errors.js'
|
|
17
18
|
import type { Network, PublishResult } from '../lib/types.js'
|
|
18
19
|
|
|
19
|
-
type Step = '
|
|
20
|
+
type Step = 'identity' | 'token' | 'settings' | 'review' | 'deploying' | 'done'
|
|
21
|
+
|
|
22
|
+
const STEPS: StepperStep[] = [
|
|
23
|
+
{ id: 'identity', label: 'Identity' },
|
|
24
|
+
{ id: 'token', label: 'Token' },
|
|
25
|
+
{ id: 'settings', label: 'Settings' },
|
|
26
|
+
{ id: 'review', label: 'Review' },
|
|
27
|
+
{ id: 'done', label: 'Done' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const CONFIG_STEPS: ConfigFormStep[] = ['identity', 'token', 'settings']
|
|
20
31
|
|
|
21
32
|
const { account } = useWallet()
|
|
22
33
|
const { network } = useNetwork()
|
|
23
34
|
|
|
24
35
|
const form = reactive(emptyForm())
|
|
25
|
-
const step = ref<Step>('
|
|
36
|
+
const step = ref<Step>('identity')
|
|
26
37
|
const deployStep = ref<DeployStep | null>(null)
|
|
27
38
|
const deployError = ref<string | null>(null)
|
|
28
39
|
const result = ref<PublishResult | null>(null)
|
|
@@ -32,12 +43,65 @@ const showTerms = ref(false)
|
|
|
32
43
|
const errors = computed(() => validateForm(form))
|
|
33
44
|
const config = computed(() => toTokenConfig(form))
|
|
34
45
|
const connected = computed(() => Boolean(account.value))
|
|
35
|
-
const canProceed = computed(() => isValid(errors.value) && connected.value)
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
const IDENTITY_KEYS: (keyof FormErrors)[] = ['packageName', 'moduleName']
|
|
48
|
+
const TOKEN_KEYS: (keyof FormErrors)[] = ['name', 'symbol', 'description', 'iconUrl', 'decimals']
|
|
49
|
+
const SETTINGS_KEYS: (keyof FormErrors)[] = ['initialSupply', 'recipient']
|
|
50
|
+
|
|
51
|
+
function stepHasErrors(keys: (keyof FormErrors)[]): boolean {
|
|
52
|
+
return keys.some((k) => Boolean(errors.value[k]))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const canProceed = computed((): boolean => {
|
|
56
|
+
if (!connected.value) return false
|
|
57
|
+
switch (step.value) {
|
|
58
|
+
case 'identity': return !stepHasErrors(IDENTITY_KEYS)
|
|
59
|
+
case 'token': return !stepHasErrors(TOKEN_KEYS)
|
|
60
|
+
case 'settings': return !stepHasErrors(SETTINGS_KEYS)
|
|
61
|
+
default: return true
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const formStep = computed((): ConfigFormStep => {
|
|
66
|
+
if (step.value === 'token') return 'token'
|
|
67
|
+
if (step.value === 'settings') return 'settings'
|
|
68
|
+
return 'identity'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const stepIndex = computed((): number => {
|
|
72
|
+
const map: Record<Step, number> = {
|
|
73
|
+
identity: 0,
|
|
74
|
+
token: 1,
|
|
75
|
+
settings: 2,
|
|
76
|
+
review: 3,
|
|
77
|
+
deploying: 3,
|
|
78
|
+
done: 4,
|
|
79
|
+
}
|
|
80
|
+
return map[step.value]
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function onStepperBack(i: number): void {
|
|
84
|
+
const configStepAt = CONFIG_STEPS[i]
|
|
85
|
+
if (configStepAt && step.value === 'review') step.value = configStepAt
|
|
86
|
+
if (i < stepIndex.value && CONFIG_STEPS.includes(step.value as ConfigFormStep)) {
|
|
87
|
+
step.value = CONFIG_STEPS[i] ?? 'identity'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function goNext(): void {
|
|
38
92
|
if (!canProceed.value) return
|
|
39
|
-
if (
|
|
40
|
-
step.value = '
|
|
93
|
+
if (step.value === 'identity') { step.value = 'token'; return }
|
|
94
|
+
if (step.value === 'token') { step.value = 'settings'; return }
|
|
95
|
+
if (step.value === 'settings') {
|
|
96
|
+
if (!termsAccepted.value) { showTerms.value = true; return }
|
|
97
|
+
step.value = 'review'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function goBack(): void {
|
|
102
|
+
if (step.value === 'token') { step.value = 'identity'; return }
|
|
103
|
+
if (step.value === 'settings') { step.value = 'token'; return }
|
|
104
|
+
if (step.value === 'review') { step.value = 'settings'; return }
|
|
41
105
|
}
|
|
42
106
|
|
|
43
107
|
function onTermsAccept(): void {
|
|
@@ -80,26 +144,29 @@ async function confirmDeploy(): Promise<void> {
|
|
|
80
144
|
function restart(): void {
|
|
81
145
|
result.value = null
|
|
82
146
|
deployError.value = null
|
|
83
|
-
step.value = '
|
|
147
|
+
step.value = 'identity'
|
|
84
148
|
}
|
|
85
149
|
</script>
|
|
86
150
|
|
|
87
151
|
<template>
|
|
88
152
|
<WalletGuard message="Connect a Sui wallet to deploy your token.">
|
|
153
|
+
<UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
|
|
89
154
|
<ConfigForm
|
|
90
|
-
v-if="step === '
|
|
155
|
+
v-if="step === 'identity' || step === 'token' || step === 'settings'"
|
|
91
156
|
:form="form"
|
|
92
157
|
:errors="errors"
|
|
93
158
|
:can-proceed="canProceed"
|
|
94
159
|
:network="network"
|
|
95
160
|
:connected="connected"
|
|
96
|
-
|
|
161
|
+
:form-step="formStep"
|
|
162
|
+
@next="goNext"
|
|
163
|
+
@back="goBack"
|
|
97
164
|
/>
|
|
98
165
|
<ReviewPanel
|
|
99
166
|
v-if="step === 'review'"
|
|
100
167
|
:config="config"
|
|
101
168
|
:network="network"
|
|
102
|
-
@back="
|
|
169
|
+
@back="goBack"
|
|
103
170
|
@confirm="confirmDeploy"
|
|
104
171
|
/>
|
|
105
172
|
<DeployProgress v-if="step === 'deploying'" :step="deployStep" />
|