@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.
Files changed (64) hide show
  1. package/.env.example +8 -11
  2. package/.github/workflows/e2e-realchain.yml +1 -1
  3. package/.github/workflows/node-ci.yml +9 -0
  4. package/.github/workflows/npm-publish.yml +16 -2
  5. package/.htmlvalidate.mjs +30 -0
  6. package/CLAUDE.md +14 -7
  7. package/Dockerfile +1 -1
  8. package/LICENSE +11 -118
  9. package/README.md +3 -5
  10. package/eslint.config.ts +63 -0
  11. package/index.html +11 -2
  12. package/package.json +32 -10
  13. package/postcss.config.js +8 -0
  14. package/public/og-image.png +0 -0
  15. package/public/robots.txt +1 -1
  16. package/public/sitemap.xml +1 -1
  17. package/scripts/e2e-deploy.mjs +22 -17
  18. package/scripts/e2e-localnet.mjs +14 -12
  19. package/scripts/e2e-walrus-browser.mjs +1 -4
  20. package/scripts/gen-og-image.mjs +2 -2
  21. package/scripts/publish-localnet.mjs +5 -4
  22. package/src/App.vue +8 -6
  23. package/src/component-styles.css +5 -38
  24. package/src/components/ConfigForm.vue +74 -28
  25. package/src/components/DeployProgress.vue +2 -4
  26. package/src/components/GithubPush.vue +22 -7
  27. package/src/components/IconPicker.vue +103 -42
  28. package/src/components/LicensePicker.vue +66 -66
  29. package/src/components/MyTokens.vue +125 -0
  30. package/src/components/ResultPanel.vue +2 -4
  31. package/src/components/ReviewPanel.vue +1 -1
  32. package/src/components/TermsDialog.vue +28 -27
  33. package/src/components/TokenDeployerView.vue +109 -40
  34. package/src/components/WalletBar.vue +67 -78
  35. package/src/components/WalrusBlobBrowser.vue +33 -25
  36. package/src/composables/useAccessGate.ts +7 -2
  37. package/src/composables/useWalrusRelay.ts +1 -1
  38. package/src/config.ts +10 -13
  39. package/src/lib/accessGate.ts +28 -14
  40. package/src/lib/buildPublishTx.ts +1 -1
  41. package/src/lib/deploy.ts +36 -0
  42. package/src/lib/deployExecutor.ts +14 -12
  43. package/src/lib/generatePackage.ts +18 -13
  44. package/src/lib/licenses.ts +4 -3
  45. package/src/lib/listMyTokens.ts +41 -0
  46. package/src/lib/readClient.ts +15 -7
  47. package/src/lib/types.ts +1 -1
  48. package/src/lib/validation.ts +8 -4
  49. package/src/lib/walrus-constants.ts +1 -1
  50. package/src/lib/walrus.ts +22 -11
  51. package/src/main.ts +46 -58
  52. package/src/styles.css +13 -5
  53. package/src/template-src/files.json +7 -7
  54. package/stylelint.config.js +33 -0
  55. package/test-results/.last-run.json +1 -1
  56. package/tests/accessGate.test.ts +26 -21
  57. package/tests/deploy.test.ts +70 -2
  58. package/tests/generatePackage.test.ts +11 -0
  59. package/tests/licenses.test.ts +6 -3
  60. package/tests/listMyTokens.test.ts +30 -0
  61. package/tests/useAccessGate.test.ts +15 -19
  62. package/typedoc.json +2 -2
  63. package/vite.config.ts +9 -1
  64. package/src/components/WalrusIconUpload.vue +0 -205
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { readFileSync, existsSync } from 'node:fs'
21
21
  import { chromium } from 'playwright'
22
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
22
+ import { SuiGrpcClient } from '@mysten/sui/grpc'
23
23
  import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet'
24
24
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
25
25
  import { Transaction } from '@mysten/sui/transactions'
@@ -37,7 +37,7 @@ const APP_NETWORK = NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
37
37
 
38
38
  const DEFAULT_RPC = {
39
39
  localnet: 'http://127.0.0.1:9000',
40
- testnet: 'https://sui-testnet-rpc.publicnode.com',
40
+ testnet: 'https://fullnode.testnet.sui.io:443',
41
41
  mainnet: 'https://fullnode.mainnet.sui.io:443',
42
42
  }
43
43
  const RPC = process.env.E2E_RPC || DEFAULT_RPC[NETWORK]
@@ -83,9 +83,15 @@ if (NETWORK === 'localnet') {
83
83
  }
84
84
  const address = keypair.toSuiAddress()
85
85
 
86
- // Node-side client points at the PHYSICAL RPC; its `network` label is the app network so the
87
- // built transaction's chain identifier matches what the wallet/app sign under.
88
- const client = new SuiJsonRpcClient({ url: RPC, network: APP_NETWORK })
86
+ // Node-side gRPC client points at the PHYSICAL RPC; its `network` label is the app network so
87
+ // the built transaction's chain identifier matches what the wallet/app sign under.
88
+ const client = new SuiGrpcClient({ baseUrl: RPC, network: APP_NETWORK })
89
+
90
+ /** Total SUI balance (MIST) of `owner`. */
91
+ async function suiBalance(owner) {
92
+ const { balance } = await client.getBalance({ owner })
93
+ return BigInt(balance.balance)
94
+ }
89
95
 
90
96
  const EXPECT_FEE = Boolean(FEE_ADDR) && FEE_ADDR !== ZERO_ADDR
91
97
 
@@ -95,19 +101,16 @@ async function main() {
95
101
  if (NETWORK === 'localnet') {
96
102
  await requestSuiFromFaucetV2({ host: FAUCET, recipient: address })
97
103
  for (let i = 0; i < 25; i++) {
98
- const b = await client.getBalance({ owner: address })
99
- if (BigInt(b.totalBalance) > 0n) break
104
+ if ((await suiBalance(address)) > 0n) break
100
105
  await new Promise((r) => setTimeout(r, 1000))
101
106
  }
102
107
  }
103
108
 
104
- const balance = BigInt((await client.getBalance({ owner: address })).totalBalance)
109
+ const balance = await suiBalance(address)
105
110
  console.log('sender:', address, 'balance:', balance.toString(), 'MIST')
106
111
  if (balance === 0n) fail(`sender ${address} has no SUI on ${NETWORK}`)
107
112
 
108
- const feeBefore = EXPECT_FEE
109
- ? BigInt((await client.getBalance({ owner: FEE_ADDR })).totalBalance)
110
- : 0n
113
+ const feeBefore = EXPECT_FEE ? await suiBalance(FEE_ADDR) : 0n
111
114
 
112
115
  // Browser resolution: explicit CHROME_PATH → a locally-installed Chrome → Playwright's
113
116
  // bundled Chromium (CI installs it via `npx playwright install chromium`).
@@ -167,7 +170,7 @@ async function main() {
167
170
 
168
171
  // Connect: open the dialog, select the app network if it differs from the testnet default,
169
172
  // then pick the injected wallet.
170
- await page.getByRole('button', { name: 'Connect Wallet' }).click()
173
+ await page.getByRole('button', { name: 'Connect Wallet', exact: true }).click()
171
174
  if (APP_NETWORK !== 'testnet') {
172
175
  await page.locator('#dialog-network-select').selectOption(APP_NETWORK)
173
176
  }
@@ -175,15 +178,17 @@ async function main() {
175
178
  await page.getByText(/Mock Test Wallet ·/).waitFor({ timeout: 10000 })
176
179
  console.log('wallet connected in UI')
177
180
 
178
- // Fill the form.
181
+ // Fill the wizard: identity → token → settings (defaults) → terms (first run) → review.
182
+ await page.locator('#package').fill('browser_coin')
183
+ await page.locator('#module').fill('browsercoin')
184
+ await page.getByRole('button', { name: 'Next', exact: true }).click()
179
185
  await page.locator('#name').fill('Browser Coin')
180
186
  await page.locator('#symbol').fill('BRWC')
181
187
  await page.locator('#description').fill('Deployed via headless browser e2e')
182
188
  await page.locator('#decimals').fill('6')
183
- await page.locator('#package').fill('browser_coin')
184
- await page.locator('#module').fill('browsercoin')
185
-
189
+ await page.getByRole('button', { name: 'Next', exact: true }).click()
186
190
  await page.getByRole('button', { name: /Review & deploy/ }).click()
191
+ await page.getByRole('button', { name: /I understand — continue/ }).click()
187
192
  await page.getByRole('button', { name: /Confirm & deploy/ }).click()
188
193
  console.log('confirmed; deploying (real publish + confirmation)…')
189
194
 
@@ -208,7 +213,7 @@ async function main() {
208
213
  if (!coinType.includes('::browsercoin::BROWSERCOIN')) fail(`unexpected coin type: ${coinType}`)
209
214
 
210
215
  if (EXPECT_FEE) {
211
- const feeAfter = BigInt((await client.getBalance({ owner: FEE_ADDR })).totalBalance)
216
+ const feeAfter = await suiBalance(FEE_ADDR)
212
217
  const feeDelta = feeAfter - feeBefore
213
218
  console.log('fee delivered this run:', feeDelta.toString(), 'MIST (to', FEE_ADDR + ')')
214
219
  if (feeDelta !== EXPECT_FEE_MIST) fail(`expected ${EXPECT_FEE_MIST} MIST fee this run, got ${feeDelta}`)
@@ -1,18 +1,21 @@
1
1
  // End-to-end: patch -> publish (fee + immutable) -> finalize (mint + fixed supply
2
2
  // + frozen metadata) against localnet, using the actual app lib.
3
3
  import init from '@mysten/move-bytecode-template';
4
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
4
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
5
5
  import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet';
6
6
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
7
7
  import { patchTemplateModule } from '../src/lib/template.ts';
8
8
  import { buildPublishTransaction, buildFinalizeTransaction, extractPublishResult, needsFinalize } from '../src/lib/buildPublishTx.ts';
9
+ import { toSuiTxResult } from '../src/lib/deploy.ts';
9
10
 
10
11
  await (init)();
11
- const client = new SuiJsonRpcClient({ url: 'http://127.0.0.1:9000' });
12
+ const client = new SuiGrpcClient({ baseUrl: 'http://127.0.0.1:9000', network: 'localnet' });
13
+ const balanceOf = async (owner, coinType) => BigInt((await client.getBalance({ owner, coinType })).balance.balance);
14
+ const include = { effects: true, objectTypes: true };
12
15
  const kp = new Ed25519Keypair(); const sender = kp.toSuiAddress();
13
16
  const feeRecipient = new Ed25519Keypair().toSuiAddress();
14
17
  await requestSuiFromFaucetV2({ host: 'http://127.0.0.1:9123', recipient: sender });
15
- for (let i=0;i<25;i++){ const b=await client.getBalance({owner:sender}); if(BigInt(b.totalBalance)>0n)break; await new Promise(r=>setTimeout(r,1000)); }
18
+ for (let i=0;i<25;i++){ if((await balanceOf(sender))>0n)break; await new Promise(r=>setTimeout(r,1000)); }
16
19
 
17
20
  const config = {
18
21
  packageName:'fixed_token', moduleName:'fixedtok', structName:'FIXEDTOK',
@@ -24,7 +27,7 @@ const config = {
24
27
  const moduleBytes = await patchTemplateModule(config);
25
28
  const feeMist = 250000000n;
26
29
  const pubTx = buildPublishTransaction({ moduleBytes, sender, feeMist, feeRecipient, gasBudget: 500000000n, packagePolicy: config.packagePolicy });
27
- const pub = await client.signAndExecuteTransaction({ signer: kp, transaction: pubTx, options: { showEffects:true, showObjectChanges:true, showBalanceChanges:true } });
30
+ const pub = toSuiTxResult(await client.signAndExecuteTransaction({ signer: kp, transaction: pubTx, include }));
28
31
  console.log('publish status:', pub.effects?.status?.status);
29
32
  if (pub.effects?.status?.status !== 'success') { console.log(JSON.stringify(pub.effects?.status)); process.exit(1); }
30
33
 
@@ -35,30 +38,29 @@ console.log('coinType:', result.coinType);
35
38
  console.log('caps:', { t: !!result.treasuryCapId, m: !!result.metadataCapId, upgrade: result.upgradeCapId ?? '(burned=immutable)' });
36
39
 
37
40
  // fee arrived?
38
- const feeBal = await client.getBalance({ owner: feeRecipient });
39
- console.log('fee recipient balance:', feeBal.totalBalance, feeBal.totalBalance === feeMist.toString() ? 'OK' : 'MISMATCH');
41
+ const feeBal = await balanceOf(feeRecipient);
42
+ console.log('fee recipient balance:', feeBal.toString(), feeBal === feeMist ? 'OK' : 'MISMATCH');
40
43
 
41
44
  // finalize
42
45
  console.log('needsFinalize:', needsFinalize(config, sender));
43
46
  const finTx = buildFinalizeTransaction({ config, coinType: result.coinType, treasuryCapId: result.treasuryCapId, metadataCapId: result.metadataCapId, sender, gasBudget: 500000000n });
44
- const fin = await client.signAndExecuteTransaction({ signer: kp, transaction: finTx, options: { showEffects:true, showObjectChanges:true } });
47
+ const fin = toSuiTxResult(await client.signAndExecuteTransaction({ signer: kp, transaction: finTx, include }));
45
48
  await client.waitForTransaction({ digest: fin.digest });
46
49
  console.log('finalize status:', fin.effects?.status?.status);
47
50
  if (fin.effects?.status?.status !== 'success') { console.log(JSON.stringify(fin.effects?.status)); process.exit(1); }
48
51
 
49
52
  // minted coin present?
50
- const coins = await client.getCoins({ owner: sender, coinType: result.coinType });
51
- const total = coins.data.reduce((a,c)=>a+BigInt(c.balance), 0n);
53
+ const total = await balanceOf(sender, result.coinType);
52
54
  const expected = 1000n * 10n**6n;
53
55
  console.log('minted balance:', total.toString(), total===expected ? 'OK (1000 * 10^6)' : 'MISMATCH exp '+expected);
54
56
 
55
57
  // treasury cap frozen (immutable owner)?
56
- const tcap = await client.getObject({ id: result.treasuryCapId, options:{ showOwner:true } });
57
- const owner = tcap.data?.owner ?? tcap.object?.owner ?? tcap.owner;
58
+ const { object: tcap } = await client.getObject({ objectId: result.treasuryCapId });
59
+ const owner = tcap.owner;
58
60
  console.log('treasuryCap owner after fixed-supply freeze:', JSON.stringify(owner));
59
61
 
60
62
  const ok = result.coinType.endsWith('::fixedtok::FIXEDTOK')
61
- && feeBal.totalBalance === feeMist.toString()
63
+ && feeBal === feeMist
62
64
  && !result.upgradeCapId
63
65
  && total === expected;
64
66
  console.log('\nE2E', ok ? 'PASS ✓' : 'FAIL ✗');
@@ -8,15 +8,12 @@
8
8
 
9
9
  import { readFileSync } from 'node:fs'
10
10
  import { chromium } from 'playwright'
11
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
12
11
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
13
12
  import { toBase64 } from '@mysten/sui/utils'
14
13
 
15
14
  const APP_URL = process.env.APP_URL || 'http://localhost:4173'
16
- const RPC = process.env.RPC || 'https://sui-testnet-rpc.publicnode.com'
17
15
  const ICON = '/tmp/test-icon.svg'
18
16
 
19
- const _client = new SuiJsonRpcClient({ url: RPC, network: 'testnet' })
20
17
  const keypair = Ed25519Keypair.fromSecretKey(process.env.SUI_PRIV)
21
18
  const address = keypair.toSuiAddress()
22
19
 
@@ -30,7 +27,7 @@ async function main() {
30
27
  const browser = await chromium.launch({
31
28
  headless: true,
32
29
  executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome',
33
- // Bypass CORS so the in-browser tx.build() can hit the testnet RPC.
30
+ // Bypass CORS so the in-browser tx.build() can hit the testnet gRPC endpoint.
34
31
  args: ['--disable-web-security', '--disable-features=IsolateOrigins,site-per-process'],
35
32
  })
36
33
  const page = await browser.newPage()
@@ -14,13 +14,13 @@ h1 .accent{color:#6ea8fe}
14
14
  p{font-size:34px;line-height:1.35;color:#aebfd6;max-width:960px}
15
15
  .row{display:flex;gap:18px;margin-top:52px}
16
16
  .chip{font-size:24px;color:#cfe0f7;background:#16233d;border:1px solid #26365a;border-radius:10px;padding:12px 20px}
17
- .url{position:absolute;bottom:56px;right:80px;font-size:26px;color:#6ea8fe;font-weight:600}
17
+ .url{position:absolute;top:84px;right:80px;font-size:26px;color:#6ea8fe;font-weight:600}
18
18
  </style></head><body>
19
19
  <div class="badge">Sui &middot; client-side</div>
20
20
  <h1>Create your own <span class="accent">Sui coin</span><br>in the browser</h1>
21
21
  <p>Your wallet signs and pays gas &mdash; nothing is compiled or signed on a server. Keep the full, verifiable source package.</p>
22
22
  <div class="row"><div class="chip">No server</div><div class="chip">No custody</div><div class="chip">Full source included</div></div>
23
- <div class="url">tokens.meddleware.co.uk</div>
23
+ <div class="url">sui-token-deployer.meddleware.co.uk</div>
24
24
  </body></html>`
25
25
 
26
26
  const browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome' })
@@ -1,16 +1,17 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { bcs } from '@mysten/bcs';
3
3
  import * as tpl from '@mysten/move-bytecode-template';
4
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
4
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
5
5
  import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet';
6
6
  import { Transaction } from '@mysten/sui/transactions';
7
7
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
8
8
  import { toBase64 } from '@mysten/sui/utils';
9
+ import { toSuiTxResult } from '../src/lib/deploy.ts';
9
10
 
10
11
  const init = tpl.default ?? tpl.init;
11
12
  try { if (typeof init === 'function') await init(); } catch {}
12
13
 
13
- const client = new SuiJsonRpcClient({ url: 'http://127.0.0.1:9000' });
14
+ const client = new SuiGrpcClient({ baseUrl: 'http://127.0.0.1:9000', network: 'localnet' });
14
15
 
15
16
  const orig = new Uint8Array(readFileSync(new URL('../src/move-template/sui_token_template.mv', import.meta.url)));
16
17
  const json = tpl.deserialize(orig);
@@ -29,7 +30,7 @@ const addr = kp.toSuiAddress();
29
30
  console.log('address:', addr);
30
31
  await requestSuiFromFaucetV2({ host: 'http://127.0.0.1:9123', recipient: addr });
31
32
  let bal = '0';
32
- for (let i=0;i<25;i++){ const b = await client.getBalance({ owner: addr }); if (BigInt(b.totalBalance) > 0n){ bal=b.totalBalance; break;} await new Promise(r=>setTimeout(r,1000)); }
33
+ for (let i=0;i<25;i++){ const { balance } = await client.getBalance({ owner: addr }); if (BigInt(balance.balance) > 0n){ bal=balance.balance; break;} await new Promise(r=>setTimeout(r,1000)); }
33
34
  console.log('balance:', bal);
34
35
 
35
36
  const tx = new Transaction();
@@ -39,7 +40,7 @@ tx.transferObjects([upgradeCap], addr);
39
40
  tx.setGasBudget(500000000);
40
41
 
41
42
  try {
42
- const res = await client.signAndExecuteTransaction({ signer: kp, transaction: tx, options: { showEffects: true, showObjectChanges: true } });
43
+ const res = toSuiTxResult(await client.signAndExecuteTransaction({ signer: kp, transaction: tx, include: { effects: true, objectTypes: true } }));
43
44
  const status = res.effects?.status?.status;
44
45
  console.log('\nstatus:', status);
45
46
  if (status !== 'success') { console.log('ERR:', JSON.stringify(res.effects?.status)); process.exit(1); }
package/src/App.vue CHANGED
@@ -9,12 +9,14 @@ const { mode: colorMode, set: setColorMode } = useColorMode('system')
9
9
  <template>
10
10
  <a class="skip-link" href="#main-content">Skip to main content</a>
11
11
  <main id="main-content">
12
- <div class="theme-bar">
13
- <ColorModeControl :model-value="colorMode" @update:model-value="setColorMode" />
14
- </div>
15
- <header v-once>
12
+ <header class="page-header">
13
+ <ColorModeControl
14
+ class="theme-toggle"
15
+ :model-value="colorMode"
16
+ @update:model-value="setColorMode"
17
+ />
16
18
  <h1>Create your own Sui coin</h1>
17
- <p class="lede">
19
+ <p v-once class="lede">
18
20
  Deploy a coin on Sui directly from your browser. Your wallet signs and pays gas — nothing is
19
21
  compiled or signed on a server — and you keep the full, verifiable source package.
20
22
  </p>
@@ -24,7 +26,7 @@ const { mode: colorMode, set: setColorMode } = useColorMode('system')
24
26
 
25
27
  <TokenDeployerView />
26
28
 
27
- <footer v-once style="margin-top: 3rem; color: var(--muted); font-size: 0.9rem">
29
+ <footer v-once class="page-footer">
28
30
  <p>
29
31
  Client-side token deployment. This tool never takes custody of your token or keys. Symbol,
30
32
  name, and decimals are permanent once published.
@@ -77,12 +77,16 @@ button:disabled {
77
77
  flex-wrap: wrap;
78
78
  align-items: center;
79
79
  }
80
+ /* A row of form/navigation actions below a block of content. */
81
+ .actions {
82
+ margin-top: 1rem;
83
+ }
80
84
  .grid2 {
81
85
  display: grid;
82
86
  grid-template-columns: 1fr;
83
87
  gap: 0 1rem;
84
88
  }
85
- @media (min-width: 520px) {
89
+ @media (width >= 520px) {
86
90
  .grid2 {
87
91
  grid-template-columns: 1fr 1fr;
88
92
  }
@@ -144,18 +148,6 @@ details > fieldset {
144
148
  background: var(--border);
145
149
  }
146
150
 
147
- .visually-hidden {
148
- position: absolute;
149
- width: 1px;
150
- height: 1px;
151
- padding: 0;
152
- margin: -1px;
153
- overflow: hidden;
154
- clip: rect(0 0 0 0);
155
- white-space: nowrap;
156
- border: 0;
157
- }
158
-
159
151
  .skip-link {
160
152
  position: absolute;
161
153
  left: 0.5rem;
@@ -217,31 +209,6 @@ details > fieldset {
217
209
  }
218
210
  }
219
211
 
220
- dialog {
221
- background: var(--surface);
222
- border: 1px solid var(--border);
223
- border-radius: var(--radius);
224
- padding: 1.5rem;
225
- width: min(420px, 90vw);
226
- max-height: 90vh;
227
- overflow-y: auto;
228
- color: var(--text);
229
- }
230
- dialog::backdrop {
231
- background: rgba(0, 0, 0, 0.45);
232
- -webkit-backdrop-filter: blur(2px);
233
- backdrop-filter: blur(2px);
234
- }
235
- .dialog-header {
236
- display: flex;
237
- justify-content: space-between;
238
- align-items: center;
239
- margin-bottom: 1.25rem;
240
- }
241
- .dialog-header h2 {
242
- margin: 0;
243
- font-size: 1.1rem;
244
- }
245
212
  .wallet-list {
246
213
  display: flex;
247
214
  flex-direction: column;
@@ -43,7 +43,6 @@ function onBlur(key: string): void {
43
43
 
44
44
  <template>
45
45
  <form novalidate @submit.prevent="emit('next')">
46
-
47
46
  <!-- Step 1: Identity -->
48
47
  <template v-if="formStep === 'identity'">
49
48
  <fieldset>
@@ -57,18 +56,32 @@ function onBlur(key: string): void {
57
56
  </UiFieldHint>
58
57
  </template>
59
58
  <template #default="{ attrs }">
60
- <input v-bind="attrs" v-model="form.packageName" placeholder="my_token" autocomplete="off" @blur="onBlur('packageName')" />
59
+ <input
60
+ type="text"
61
+ v-bind="attrs"
62
+ v-model="form.packageName"
63
+ placeholder="my_token"
64
+ autocomplete="off"
65
+ @blur="onBlur('packageName')"
66
+ />
61
67
  </template>
62
68
  </UiFormField>
63
69
  <UiFormField id="module" label="Module name" :error="visibleError('moduleName')">
64
70
  <template #label-suffix>
65
71
  <UiFieldHint field-id="module-name">
66
- The Move module inside the package (lowercase, e.g. <b>mytoken</b>). The coin type and
67
- its one-time-witness struct are derived from this name, so it is permanent too.
72
+ The Move module inside the package (lowercase, e.g. <b>mytoken</b>). The coin type
73
+ and its one-time-witness struct are derived from this name, so it is permanent too.
68
74
  </UiFieldHint>
69
75
  </template>
70
76
  <template #default="{ attrs }">
71
- <input v-bind="attrs" v-model="form.moduleName" placeholder="mytoken" autocomplete="off" @blur="onBlur('moduleName')" />
77
+ <input
78
+ type="text"
79
+ v-bind="attrs"
80
+ v-model="form.moduleName"
81
+ placeholder="mytoken"
82
+ autocomplete="off"
83
+ @blur="onBlur('moduleName')"
84
+ />
72
85
  </template>
73
86
  </UiFormField>
74
87
  </div>
@@ -78,8 +91,8 @@ function onBlur(key: string): void {
78
91
  </template>
79
92
  <template v-else>
80
93
  Coin type will be <span class="mono">{{ coinTypePreview }}</span
81
- >. The witness struct <span class="mono">{{ structPreview }}</span> is derived from
82
- the module name (a Sui requirement).
94
+ >. The witness struct <span class="mono">{{ structPreview }}</span> is derived from the
95
+ module name (a Sui requirement).
83
96
  </template>
84
97
  </p>
85
98
  </fieldset>
@@ -94,22 +107,35 @@ function onBlur(key: string): void {
94
107
  <UiFormField id="name" label="Token name" :error="visibleError('name')">
95
108
  <template #label-suffix>
96
109
  <UiFieldHint field-id="token-name">
97
- The human-readable display name shown in wallets and explorers (e.g. <b>My Token</b>).
98
- Editable later if you keep the Updatable metadata policy.
110
+ The human-readable display name shown in wallets and explorers (e.g.
111
+ <b>My Token</b>). Editable later if you keep the Updatable metadata policy.
99
112
  </UiFieldHint>
100
113
  </template>
101
114
  <template #default="{ attrs }">
102
- <input v-bind="attrs" v-model="form.name" autocomplete="off" @blur="onBlur('name')" />
115
+ <input
116
+ type="text"
117
+ v-bind="attrs"
118
+ v-model="form.name"
119
+ autocomplete="off"
120
+ @blur="onBlur('name')"
121
+ />
103
122
  </template>
104
123
  </UiFormField>
105
124
  <UiFormField id="symbol" label="Symbol" :error="visibleError('symbol')">
106
125
  <template #label-suffix>
107
126
  <UiFieldHint field-id="token-symbol">
108
- The short ticker shown in wallets (e.g. <b>MYT</b>), typically 3–5 uppercase letters.
127
+ The short ticker shown in wallets (e.g. <b>MYT</b>), typically 3–5 uppercase
128
+ letters.
109
129
  </UiFieldHint>
110
130
  </template>
111
131
  <template #default="{ attrs }">
112
- <input v-bind="attrs" v-model="form.symbol" autocomplete="off" @blur="onBlur('symbol')" />
132
+ <input
133
+ type="text"
134
+ v-bind="attrs"
135
+ v-model="form.symbol"
136
+ autocomplete="off"
137
+ @blur="onBlur('symbol')"
138
+ />
113
139
  </template>
114
140
  </UiFormField>
115
141
  </div>
@@ -118,8 +144,8 @@ function onBlur(key: string): void {
118
144
  <template #label-suffix>
119
145
  &nbsp;<span class="hint">(optional)</span>
120
146
  <UiFieldHint field-id="token-description">
121
- A short blurb stored in the coin metadata and shown in some wallets/explorers.
122
- Leave blank if you don't need one.
147
+ A short blurb stored in the coin metadata and shown in some wallets/explorers. Leave
148
+ blank if you don't need one.
123
149
  </UiFieldHint>
124
150
  </template>
125
151
  <template #default="{ attrs }">
@@ -136,7 +162,13 @@ function onBlur(key: string): void {
136
162
  </UiFieldHint>
137
163
  </template>
138
164
  <template #default="{ attrs }">
139
- <input v-bind="attrs" v-model="form.decimals" inputmode="numeric" @blur="onBlur('decimals')" />
165
+ <input
166
+ type="text"
167
+ v-bind="attrs"
168
+ v-model="form.decimals"
169
+ inputmode="numeric"
170
+ @blur="onBlur('decimals')"
171
+ />
140
172
  </template>
141
173
  </UiFormField>
142
174
 
@@ -158,14 +190,21 @@ function onBlur(key: string): void {
158
190
  <UiFormField id="supply" label="Initial supply" :error="visibleError('initialSupply')">
159
191
  <template #label-suffix>
160
192
  <UiFieldHint field-id="supply">
161
- The number of whole tokens to mint and send to the recipient at deployment.
162
- Uses your chosen decimal precision — with 9 decimals, 1 token = 1,000,000,000 base units on-chain.
163
- Enter <b>0</b> or leave blank for no initial supply.
164
- You can mint more later if you choose the Mintable supply policy.
193
+ The number of whole tokens to mint and send to the recipient at deployment. Uses your
194
+ chosen decimal precision — with 9 decimals, 1 token = 1,000,000,000 base units
195
+ on-chain. Enter <b>0</b> or leave blank for no initial supply. You can mint more later
196
+ if you choose the Mintable supply policy.
165
197
  </UiFieldHint>
166
198
  </template>
167
199
  <template #default="{ attrs }">
168
- <input v-bind="attrs" v-model="form.initialSupply" inputmode="numeric" placeholder="0" @blur="onBlur('initialSupply')" />
200
+ <input
201
+ type="text"
202
+ v-bind="attrs"
203
+ v-model="form.initialSupply"
204
+ inputmode="numeric"
205
+ placeholder="0"
206
+ @blur="onBlur('initialSupply')"
207
+ />
169
208
  </template>
170
209
  </UiFormField>
171
210
 
@@ -204,8 +243,8 @@ function onBlur(key: string): void {
204
243
  <UiFormField id="package-policy" label="Package code">
205
244
  <template #label-suffix>
206
245
  <UiFieldHint field-id="package-policy">
207
- <b>Immutable</b>: the UpgradeCap is burned — the on-chain code is sealed.
208
- Recommended for trust and auditability.<br />
246
+ <b>Immutable</b>: the UpgradeCap is burned — the on-chain code is sealed. Recommended
247
+ for trust and auditability.<br />
209
248
  <b>Upgradeable</b>: you keep the UpgradeCap and can push code changes to the package
210
249
  after deployment.
211
250
  </UiFieldHint>
@@ -221,14 +260,21 @@ function onBlur(key: string): void {
221
260
  <UiFormField id="recipient" label="Recipient" :error="visibleError('recipient')">
222
261
  <template #label-suffix>
223
262
  <UiFieldHint field-id="recipient">
224
- The Sui address that receives the TreasuryCap, MetadataCap, UpgradeCap (if kept),
225
- and any initial supply minted at deployment.
226
- Leave blank to send everything to the connected wallet.
227
- Enter a different address to separate the deployer wallet from the custody wallet.
263
+ The Sui address that receives the TreasuryCap, MetadataCap, UpgradeCap (if kept), and
264
+ any initial supply minted at deployment. Leave blank to send everything to the
265
+ connected wallet. Enter a different address to separate the deployer wallet from the
266
+ custody wallet.
228
267
  </UiFieldHint>
229
268
  </template>
230
269
  <template #default="{ attrs }">
231
- <input v-bind="attrs" v-model="form.recipient" placeholder="0x…" autocomplete="off" @blur="onBlur('recipient')" />
270
+ <input
271
+ type="text"
272
+ v-bind="attrs"
273
+ v-model="form.recipient"
274
+ placeholder="0x…"
275
+ autocomplete="off"
276
+ @blur="onBlur('recipient')"
277
+ />
232
278
  </template>
233
279
  </UiFormField>
234
280
 
@@ -236,7 +282,7 @@ function onBlur(key: string): void {
236
282
  </fieldset>
237
283
  </template>
238
284
 
239
- <div class="row" style="margin-top: 1rem">
285
+ <div class="row actions">
240
286
  <button v-if="formStep !== 'identity'" type="button" @click="emit('back')">Back</button>
241
287
  <button type="submit" class="primary" :disabled="!canProceed">
242
288
  {{ formStep === 'settings' ? 'Review &amp; deploy' : 'Next' }}
@@ -25,9 +25,7 @@ function getState(i: number): 'done' | 'active' | 'todo' {
25
25
  return 'todo'
26
26
  }
27
27
 
28
- const steps = computed(() =>
29
- ORDER.map((s, i) => ({ ...s, state: getState(i) }))
30
- )
28
+ const steps = computed(() => ORDER.map((s, i) => ({ ...s, state: getState(i) })))
31
29
  </script>
32
30
 
33
31
  <template>
@@ -38,7 +36,7 @@ const steps = computed(() =>
38
36
  <span v-if="s.state === 'active'" class="spinner" aria-hidden="true"></span>
39
37
  <span v-else-if="s.state === 'done'" aria-hidden="true">✓ </span>
40
38
  {{ s.label }}
41
- <span v-if="s.state === 'active'" class="visually-hidden">(in progress)</span>
39
+ <span v-if="s.state === 'active'" class="mw-visually-hidden">(in progress)</span>
42
40
  </li>
43
41
  </ol>
44
42
  <p class="hint">Keep this tab open and approve the wallet prompts.</p>
@@ -56,7 +56,7 @@ async function push(): Promise<void> {
56
56
  {{ open ? '▾' : '▸' }} Create a GitHub repo (optional)
57
57
  </button>
58
58
 
59
- <div v-if="open" class="card" style="margin-top: 0.5rem">
59
+ <div v-if="open" class="card github-push__form">
60
60
  <p v-once class="hint">
61
61
  Uses your <strong>own</strong> GitHub token — the repo is created in your account and this
62
62
  site's servers are never involved. The token is sent only to
@@ -67,24 +67,23 @@ async function push(): Promise<void> {
67
67
  </p>
68
68
 
69
69
  <label for="gh-repo">Repository name</label>
70
- <input id="gh-repo" v-model="repoName" autocomplete="off" />
70
+ <input id="gh-repo" v-model="repoName" type="text" autocomplete="off" />
71
71
 
72
72
  <label for="gh-token">GitHub token</label>
73
73
  <input
74
74
  id="gh-token"
75
75
  v-model="token"
76
76
  type="password"
77
- autocomplete="off"
77
+ autocomplete="new-password"
78
78
  spellcheck="false"
79
79
  placeholder="github_pat_… or ghp_…"
80
80
  />
81
81
 
82
- <label style="font-weight: 400; margin-top: 0.75rem">
83
- <input type="checkbox" v-model="isPrivate" style="width: auto" /> Make the repository
84
- private
82
+ <label class="github-push__private">
83
+ <input v-model="isPrivate" type="checkbox" /> Make the repository private
85
84
  </label>
86
85
 
87
- <div class="row" style="margin-top: 0.75rem">
86
+ <div class="row github-push__actions">
88
87
  <button type="button" class="primary" :disabled="busy || !token || !repoName" @click="push">
89
88
  <span v-if="busy" class="spinner" aria-hidden="true"></span> Create repo &amp; push
90
89
  </button>
@@ -98,3 +97,19 @@ async function push(): Promise<void> {
98
97
  </div>
99
98
  </div>
100
99
  </template>
100
+
101
+ <style scoped>
102
+ .github-push__form {
103
+ margin-top: 0.5rem;
104
+ }
105
+ .github-push__private {
106
+ margin-top: 0.75rem;
107
+ font-weight: 400;
108
+ }
109
+ .github-push__private input {
110
+ width: auto;
111
+ }
112
+ .github-push__actions {
113
+ margin-top: 0.75rem;
114
+ }
115
+ </style>