@meddleware/token-deployer-ui 0.0.22 → 0.0.24

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.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -49,10 +49,10 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@meddleware/design-tokens": "^0.1.7",
52
- "@meddleware/sui-token-client": "^0.0.3",
52
+ "@meddleware/sui-token-client": "^0.0.5",
53
53
  "@meddleware/ui": "^0.1.28",
54
- "@meddleware/walrus-client": "^0.0.22",
55
- "@meddleware/walrus-relay": "^0.1.23",
54
+ "@meddleware/walrus-client": "^0.0.24",
55
+ "@meddleware/walrus-relay": "^0.1.25",
56
56
  "@mysten/move-bytecode-template": "~0.4.1",
57
57
  "@mysten/sui": "^2.33.1",
58
58
  "@mysten/walrus": "~1.2.32",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@meddleware/access-gate-client": "^0.0.3",
66
- "@meddleware/wallet-adapter": "^0.0.12",
66
+ "@meddleware/wallet-adapter": "^0.0.13",
67
67
  "@playwright/test": "^1.56.0",
68
68
  "@types/node": "~24.12.2",
69
69
  "@vitejs/plugin-vue": "^6.0.8",
@@ -6,7 +6,7 @@ import { getReadClient } from '../wallet.js'
6
6
  import { useWalrusRelay } from '../composables/useWalrusRelay.js'
7
7
  import { useAccessGate } from '@meddleware/walrus-relay'
8
8
  // The flow subpath carries no wasm; it loads the Walrus client lazily when an upload starts.
9
- import { consumeStorageKey, createGatedAccess, runBlobUpload } from '@meddleware/walrus-client/flow'
9
+ import { browserStorage, consumeStorageKey, createGatedAccess, runBlobUpload } from '@meddleware/walrus-client/flow'
10
10
  import { ICON_EPOCHS } from '../lib/walrus-constants.js'
11
11
  import { ACCESS_GATE, ICON_MAX_BYTES, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS, validateIconFile } from '../config.js'
12
12
  import type { WalrusNetwork } from '../config.js'
@@ -157,7 +157,7 @@ async function upload(): Promise<void> {
157
157
  const access =
158
158
  gated && gate && nftId
159
159
  ? createGatedAccess({
160
- storage: window.localStorage,
160
+ storage: browserStorage(),
161
161
  key: consumeStorageKey(walrusNet, gate.gateId, address),
162
162
  relayHost: selectedRelayHost.value,
163
163
  address,
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, reactive, ref } from 'vue'
2
+ import { computed, reactive, ref, watch } from 'vue'
3
3
  import { useWallet, useNetwork, WalletGuard } from '@meddleware/wallet-adapter'
4
4
  import ConfigForm, { type ConfigFormStep } from './ConfigForm.vue'
5
5
  import ReviewPanel from './ReviewPanel.vue'
@@ -9,6 +9,7 @@ import TermsDialog from './TermsDialog.vue'
9
9
  import MyTokens from './MyTokens.vue'
10
10
  import {
11
11
  AppTabNav,
12
+ UiButton,
12
13
  UiNotice,
13
14
  UiStepper,
14
15
  UiTabPanel,
@@ -19,14 +20,12 @@ import {
19
20
  import { emptyForm, toTokenConfig } from '../lib/form.js'
20
21
  import { validateForm } from '../lib/validation.js'
21
22
  import type { FormErrors } from '../lib/validation.js'
22
- import { deployToken } from '@meddleware/sui-token-client/deploy'
23
- import type { DeployStep } from '@meddleware/sui-token-client/deploy'
24
- import { buildDeployExecutor } from '../lib/deployExecutor.js'
23
+ import { useDeploy } from '../composables/useDeploy.js'
24
+ import { browserStorage } from '@meddleware/walrus-client/flow'
25
25
  import { FEE_MIST, FEE_TREASURY, PUBLISH_GAS_BUDGET, isFeeConfigured } from '../config.js'
26
- import { extractErrorMessage } from '../lib/errors.js'
27
- import type { TokenNetwork as Network, PublishResult } from '@meddleware/sui-token-client'
26
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
28
27
 
29
- type Step = 'identity' | 'token' | 'settings' | 'review' | 'deploying' | 'done'
28
+ type Step = 'identity' | 'token' | 'settings' | 'review' | 'deploying' | 'incomplete' | 'published' | 'done'
30
29
 
31
30
  const STEPS: StepperStep[] = [
32
31
  { id: 'identity', label: 'Identity' },
@@ -51,10 +50,24 @@ const activeTab = ref('deploy')
51
50
 
52
51
  const form = reactive(emptyForm())
53
52
  const step = ref<Step>('identity')
54
- const deployStep = ref<DeployStep | null>(null)
55
- const deployError = ref<string | null>(null)
56
- const result = ref<PublishResult | null>(null)
57
- const termsAccepted = ref(localStorage.getItem('sui-deployer:terms-accepted') === '1')
53
+ const flow = useDeploy()
54
+ const deployStep = flow.step
55
+ const result = flow.result
56
+ // A wallet message ("Connect a wallet first.") or the flow's error.
57
+ const walletError = ref<string | null>(null)
58
+ const deployError = computed(() => walletError.value ?? flow.error.value)
59
+ // The flow decides the step once it leaves `deploying`: a failure before publishing returns to the
60
+ // review; anything after a publish never does (a second deploy would publish a second coin).
61
+ watch(flow.phase, (phase) => {
62
+ if (phase === 'deploying') step.value = 'deploying'
63
+ else if (phase === 'done') step.value = 'done'
64
+ else if (phase === 'incomplete') step.value = 'incomplete'
65
+ else if (phase === 'published') step.value = 'published'
66
+ else if (phase === 'failed') step.value = 'review'
67
+ })
68
+ // Storage that never throws: with site data blocked the terms are simply asked again next visit.
69
+ const storage = browserStorage()
70
+ const termsAccepted = ref(storage.getItem('sui-deployer:terms-accepted') === '1')
58
71
  const showTerms = ref(false)
59
72
 
60
73
  const errors = computed(() => validateForm(form))
@@ -96,6 +109,8 @@ const stepIndex = computed((): number => {
96
109
  settings: 2,
97
110
  review: 3,
98
111
  deploying: 3,
112
+ incomplete: 3,
113
+ published: 3,
99
114
  done: 4,
100
115
  }
101
116
  return map[step.value]
@@ -144,7 +159,7 @@ function goBack(): void {
144
159
  }
145
160
 
146
161
  function onTermsAccept(): void {
147
- localStorage.setItem('sui-deployer:terms-accepted', '1')
162
+ storage.setItem('sui-deployer:terms-accepted', '1')
148
163
  termsAccepted.value = true
149
164
  showTerms.value = false
150
165
  step.value = 'review'
@@ -157,35 +172,25 @@ function onTermsCancel(): void {
157
172
  async function confirmDeploy(): Promise<void> {
158
173
  const sender = account.value?.address
159
174
  if (!sender) {
160
- deployError.value = 'Connect a wallet first.'
175
+ walletError.value = 'Connect a wallet first.'
161
176
  return
162
177
  }
163
- deployError.value = null
164
- step.value = 'deploying'
165
- deployStep.value = null
166
- try {
167
- const net = network.value as Network
168
- const feeConfigured = isFeeConfigured(net)
169
- result.value = await deployToken({
170
- config: config.value,
171
- network: net,
172
- sender,
173
- feeMist: feeConfigured ? FEE_MIST : 0n,
174
- feeTreasury: FEE_TREASURY[net],
175
- gasBudget: PUBLISH_GAS_BUDGET,
176
- executor: await buildDeployExecutor(net),
177
- onStep: (s) => (deployStep.value = s),
178
- })
179
- step.value = 'done'
180
- } catch (e) {
181
- deployError.value = extractErrorMessage(e)
182
- step.value = 'review'
183
- }
178
+ walletError.value = null
179
+ const net = network.value as Network
180
+ const feeConfigured = isFeeConfigured(net)
181
+ await flow.deploy({
182
+ config: config.value,
183
+ network: net,
184
+ sender,
185
+ feeMist: feeConfigured ? FEE_MIST : 0n,
186
+ feeTreasury: FEE_TREASURY[net],
187
+ gasBudget: PUBLISH_GAS_BUDGET,
188
+ })
184
189
  }
185
190
 
186
191
  function restart(): void {
187
- result.value = null
188
- deployError.value = null
192
+ flow.reset()
193
+ walletError.value = null
189
194
  step.value = 'identity'
190
195
  }
191
196
  </script>
@@ -231,6 +236,25 @@ function restart(): void {
231
236
  @confirm="confirmDeploy"
232
237
  />
233
238
  <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
239
+ <section v-if="step === 'incomplete' && flow.pending.value" class="deploy-incomplete" aria-labelledby="incomplete-h">
240
+ <h2 id="incomplete-h">Your coin is published — finish its setup</h2>
241
+ <p>
242
+ <code>{{ flow.pending.value.result.coinType }}</code> exists on {{ flow.pending.value.result.network }},
243
+ but the second transaction did not complete: the currency is not registered, the initial supply
244
+ is not minted and your supply and metadata choices are not applied yet. The caps are in your
245
+ wallet.
246
+ </p>
247
+ <p>Do not deploy again — that would publish a second coin. Sign the setup transaction instead.</p>
248
+ <UiButton variant="primary" @click="flow.finish()">Finish setup (sign again)</UiButton>
249
+ </section>
250
+ <section v-if="step === 'published'" class="deploy-incomplete" aria-labelledby="published-h">
251
+ <h2 id="published-h">A coin was published, but the result could not be read</h2>
252
+ <p>
253
+ Publish transaction <code>{{ flow.publishDigest.value }}</code>. Check it in an explorer before
254
+ doing anything else; do not deploy again.
255
+ </p>
256
+ <UiButton variant="ghost" @click="restart">Start a new token</UiButton>
257
+ </section>
234
258
  <ResultPanel
235
259
  v-if="step === 'done' && result"
236
260
  :result="result"
@@ -252,4 +276,8 @@ function restart(): void {
252
276
  .deployer-tabs {
253
277
  margin: 0 0 1rem;
254
278
  }
279
+
280
+ .deploy-incomplete {
281
+ margin: 1rem 0;
282
+ }
255
283
  </style>
@@ -0,0 +1,105 @@
1
+ import { ref } from 'vue'
2
+ import {
3
+ DeployIncompleteError,
4
+ DeployUnconfirmedError,
5
+ PublishedError,
6
+ deployToken,
7
+ finalizeToken,
8
+ type DeployArgs,
9
+ type DeployStep,
10
+ type Executor,
11
+ type PendingFinalize,
12
+ } from '@meddleware/sui-token-client/deploy'
13
+ import type { PublishResult, TokenNetwork } from '@meddleware/sui-token-client'
14
+ import { buildDeployExecutor } from '../lib/deployExecutor.js'
15
+ import { extractErrorMessage } from '../lib/errors.js'
16
+
17
+ /**
18
+ * Where a deploy stands:
19
+ * - `failed` — nothing was published; the user may review and try again.
20
+ * - `incomplete` — the coin is published but its setup (second signature) is not finished. The UI
21
+ * must offer {@link finish}, never a fresh deploy, which would publish a second coin and charge the
22
+ * fee again.
23
+ * - `published` — a coin was published but the outcome could not be read; the UI shows the
24
+ * transaction and offers no deploy or retry.
25
+ * - `done` — finished (`error` may still say the last step was not confirmed).
26
+ */
27
+ export type DeployPhase = 'idle' | 'deploying' | 'failed' | 'incomplete' | 'published' | 'done'
28
+
29
+ /** The deploy flow's state machine, with the wallet executor injectable for tests. */
30
+ export function useDeploy(executorFor: (network: TokenNetwork) => Promise<Executor> = buildDeployExecutor) {
31
+ const phase = ref<DeployPhase>('idle')
32
+ const step = ref<DeployStep | null>(null)
33
+ const error = ref<string | null>(null)
34
+ const result = ref<PublishResult | null>(null)
35
+ const pending = ref<PendingFinalize | null>(null)
36
+ /** The publish transaction, once one executed (shown when its outcome is unclear). */
37
+ const publishDigest = ref<string | null>(null)
38
+
39
+ /** Route a failure: anything after the publish executed never goes back to a fresh deploy. */
40
+ function onFailure(e: unknown): void {
41
+ error.value = extractErrorMessage(e)
42
+ if (e instanceof DeployUnconfirmedError) {
43
+ result.value = e.result
44
+ pending.value = null
45
+ phase.value = 'done'
46
+ } else if (e instanceof DeployIncompleteError) {
47
+ pending.value = e.pending
48
+ publishDigest.value = e.publishDigest
49
+ phase.value = 'incomplete'
50
+ } else if (e instanceof PublishedError) {
51
+ publishDigest.value = e.publishDigest
52
+ phase.value = 'published'
53
+ } else if (pending.value) {
54
+ phase.value = 'incomplete'
55
+ } else {
56
+ phase.value = 'failed'
57
+ }
58
+ }
59
+
60
+ async function deploy(args: Omit<DeployArgs, 'executor' | 'onStep'>): Promise<void> {
61
+ phase.value = 'deploying'
62
+ step.value = null
63
+ error.value = null
64
+ try {
65
+ result.value = await deployToken({
66
+ ...args,
67
+ executor: await executorFor(args.network),
68
+ onStep: (s) => (step.value = s),
69
+ })
70
+ phase.value = 'done'
71
+ } catch (e) {
72
+ onFailure(e)
73
+ }
74
+ }
75
+
76
+ /** Finish a published coin's setup: one more signature, on the network it was published to. */
77
+ async function finish(): Promise<void> {
78
+ const p = pending.value
79
+ if (!p) return
80
+ phase.value = 'deploying'
81
+ error.value = null
82
+ try {
83
+ result.value = await finalizeToken({
84
+ pending: p,
85
+ executor: await executorFor(p.result.network),
86
+ onStep: (s) => (step.value = s),
87
+ })
88
+ pending.value = null
89
+ phase.value = 'done'
90
+ } catch (e) {
91
+ onFailure(e)
92
+ }
93
+ }
94
+
95
+ function reset(): void {
96
+ phase.value = 'idle'
97
+ step.value = null
98
+ error.value = null
99
+ result.value = null
100
+ pending.value = null
101
+ publishDigest.value = null
102
+ }
103
+
104
+ return { phase, step, error, result, pending, publishDigest, deploy, finish, reset }
105
+ }