@meddleware/token-deployer-ui 0.0.15 → 0.0.17

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/README.md CHANGED
@@ -1,30 +1,30 @@
1
1
  # @meddleware/token-deployer-ui
2
2
 
3
3
  A browser front-end that lets anyone deploy their own Sui coin — a client-side
4
- equivalent of `blockchain/sui/sui-token-template`. **Everything runs in the
4
+ equivalent of [`sui-token-template`](https://github.com/meddleware-org/sui-token-template)'s CLI. **Everything runs in the
5
5
  browser**: the user's wallet signs and pays gas, and nothing is compiled or
6
6
  signed on a server. The user also gets the full, verifiable Move source package,
7
7
  and can optionally push it to a new GitHub repo in their own account.
8
8
 
9
9
  ## How it works
10
10
 
11
- 1. Ships one pre-compiled `sui_token_template` Move module whose metadata fields
12
- are DISTINCT named constants, so they can be patched
13
- ([src/move-template/](src/move-template/)).
11
+ The token logic is [`@meddleware/sui-token-client`](https://github.com/meddleware-org/sui-token-client);
12
+ this app is its UI.
13
+
14
+ 1. The client ships one pre-compiled `sui_token_template` module (generated from the pinned
15
+ `@meddleware/sui-token-template`) whose metadata fields are distinct named constants.
14
16
  2. In the browser, [`@mysten/move-bytecode-template`](https://www.npmjs.com/package/@mysten/move-bytecode-template)
15
- deserialises the module; the identifiers + constant pool are patched to the
16
- user's values ([src/lib/template.ts](src/lib/template.ts)) and it is re-serialised.
17
- 3. A publish PTB ([src/lib/buildPublishTx.ts](src/lib/buildPublishTx.ts)) publishes
18
- the module, applies the UpgradeCap policy, and splits a trivial fee to the
19
- operator treasury. A follow-up finalize PTB mints the initial supply and applies
20
- the supply/metadata policies when they differ from the defaults.
21
- 4. The same inputs generate a full, downloadable source package
22
- ([src/lib/generatePackage.ts](src/lib/generatePackage.ts)) that is **byte-identical**
23
- to the CLI generator's output, so the on-chain package is source-verifiable.
24
- 5. Licenses are fetched live from SPDX ([src/lib/licenses.ts](src/lib/licenses.ts)); an
25
- optional icon upload goes to Walrus (`@meddleware/walrus-client/flow`, from
26
- [src/components/IconPicker.vue](src/components/IconPicker.vue)); and an
27
- optional GitHub push uses the user's own token ([src/lib/github.ts](src/lib/github.ts)).
17
+ decodes it; the identifiers and constant pool are patched to the user's values and it is
18
+ re-encoded ([src/templateWasm.ts](src/templateWasm.ts) points the patcher at the wasm asset).
19
+ 3. A publish PTB publishes the module, applies the UpgradeCap policy and splits a trivial fee to the
20
+ operator treasury; a finalize PTB registers the currency, mints the initial supply and applies the
21
+ supply/metadata policies.
22
+ 4. The same inputs generate a full, downloadable source package that matches the CLI generator's
23
+ output, so the on-chain package is source-verifiable.
24
+ 5. Licences are fetched live from SPDX ([src/lib/licenses.ts](src/lib/licenses.ts)); an optional
25
+ icon upload goes to Walrus (`@meddleware/walrus-client/flow`, from
26
+ [src/components/IconPicker.vue](src/components/IconPicker.vue)); and an optional GitHub push uses
27
+ the user's own token ([src/lib/github.ts](src/lib/github.ts)).
28
28
 
29
29
  ## Security & trust model
30
30
 
@@ -34,11 +34,9 @@ and can optionally push it to a new GitHub repo in their own account.
34
34
  - **No endorsement.** The tool is neutral; it does not vet or endorse created
35
35
  tokens or their creators. This is stated in the review, result, and footer UI.
36
36
  - **Defence in depth.** User input is validated at the form
37
- ([src/lib/validation.ts](src/lib/validation.ts)) *and* re-asserted before it is
38
- substituted into Move source / shell scripts
39
- ([src/lib/generatePackage.ts](src/lib/generatePackage.ts)) and before it is
40
- patched into the bytecode ([src/lib/template.ts](src/lib/template.ts)), so no
41
- quote/backslash/control character or reserved Move keyword can ever produce an
37
+ ([src/lib/validation.ts](src/lib/validation.ts)) *and* re-asserted by the client
38
+ (`assertTokenConfig`) before it is patched into bytecode or substituted into Move source and
39
+ shell scripts, so no quote/backslash/control character or reserved Move keyword can produce an
42
40
  injectable package.
43
41
  - **Fee integrity.** The fee is split from the user's own gas coin to the operator
44
42
  treasury inside the publish PTB and cannot be redirected at runtime. A production
@@ -141,26 +139,15 @@ tip, so third-party use is *profitable* as long as the tip keeps margin.
141
139
 
142
140
  ## Keeping the template in sync (single source of truth)
143
141
 
144
- The shipped bytecode and the downloadable source both derive from
145
- `@meddleware/sui-token-template` (`optionalDependencies`). After changing that template, re-run:
146
-
147
- ```bash
148
- npm run regen:template # recompiles + refreshes src/move-template/*
149
- npm run sync:template # refreshes src/template-src/files.json
150
- npm run verify:template # bytecode round-trip + provenance/parity tests
151
- ```
152
-
153
- Two tests enforce this invariant in CI so it can never silently drift:
154
-
155
- - [tests/templateParity.test.ts](tests/templateParity.test.ts) — `files.json`
156
- matches the canonical template sources verbatim.
157
- - [tests/templateArtifact.test.ts](tests/templateArtifact.test.ts) — the shipped
158
- `.mv` still contains the identifiers/constants the patcher expects.
142
+ The bytecode and the downloadable source both come from `@meddleware/sui-token-client`, which
143
+ generates them from the exact `@meddleware/sui-token-template` it pins and checks them in CI. After a
144
+ template change: release the template, bump it in the client (`npm run gen:template`), release the
145
+ client, then bump the client here.
159
146
 
160
147
  ## Verification scripts
161
148
 
162
- - `scripts/publish-localnet.mjs` — patch → publish → finalize against a local Sui node
163
- (node, no browser).
149
+ - `npm run e2e:localnet` in `@meddleware/sui-token-client` — patch → publish → finalize against a
150
+ local Sui node (node, no browser).
164
151
  - `scripts/e2e-deploy.mjs` — the **network-parametrized real-chain deploy e2e** (headless
165
152
  browser + injected wallet): drives the full UI, does a REAL publish, waits for real
166
153
  confirmation, checks the result panel, downloads the source zip, and verifies coin type +
@@ -168,7 +155,11 @@ Two tests enforce this invariant in CI so it can never silently drift:
168
155
  is a thin localnet wrapper over it.
169
156
  - `scripts/e2e-walrus-browser.mjs` (`npm run e2e:walrus`) — the Walrus icon uploader end-to-end on
170
157
  testnet with an injected wallet (`SUI_PRIV` with SUI + WAL); `E2E_RELAY=operator` exercises the
171
- NFT-gated relay (pass purchase, consume, signed proof). Manual only — it spends real funds.
158
+ NFT-gated relay (pass purchase, consume, signed proof) and asserts the upload went through it.
159
+ The gateway only allows CORS from its own sites, so run the operator case against the dashboard:
160
+ `E2E_RELAY=operator APP_URL=https://dash.meddleware.co.uk/blockchain E2E_TAB="Token Deployer"
161
+ node scripts/e2e-walrus-browser.mjs` (a local build falls back to the public relay). Manual only —
162
+ it spends real funds (testnet WAL: `walrus get-wal`).
172
163
 
173
164
  ## Real-chain e2e / launch runbook
174
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/token-deployer-ui",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -42,31 +42,27 @@
42
42
  "lint": "npm run lint:css && npm run lint:js && npm run lint:html",
43
43
  "format": "oxfmt src/",
44
44
  "docs": "typedoc",
45
- "regen:template": "bash scripts/regen-template.sh",
46
- "sync:template": "bash scripts/sync-template-src.sh",
47
45
  "gen:og-image": "node scripts/gen-og-image.mjs",
48
- "verify:template": "node scripts/verify-bytecode.mjs && vitest run tests/templateArtifact.test.ts tests/templateParity.test.ts",
49
46
  "lint:css": "stylelint \"src/**/*.{css,vue}\"",
50
47
  "lint:js": "eslint .",
51
48
  "lint:html": "html-validate \"src/**/*.vue\""
52
49
  },
53
50
  "dependencies": {
54
51
  "@meddleware/design-tokens": "^0.1.7",
52
+ "@meddleware/sui-token-client": "^0.0.1",
55
53
  "@meddleware/ui": "^0.1.28",
56
54
  "@meddleware/walrus-client": "^0.0.21",
57
- "@meddleware/walrus-relay": "^0.1.21",
58
- "@mysten/bcs": "~2.1.2",
55
+ "@meddleware/walrus-relay": "^0.1.22",
59
56
  "@mysten/move-bytecode-template": "~0.4.1",
60
57
  "@mysten/sui": "^2.33.1",
61
58
  "@mysten/walrus": "~1.2.32",
62
- "fflate": "^0.8.3",
63
59
  "vue": "^3.5.0"
64
60
  },
65
61
  "peerDependencies": {
66
62
  "@meddleware/wallet-adapter": ">=0.0.12 <0.2.0"
67
63
  },
68
64
  "devDependencies": {
69
- "@meddleware/access-gate-client": "^0.0.1",
65
+ "@meddleware/access-gate-client": "^0.0.2",
70
66
  "@meddleware/wallet-adapter": "^0.0.12",
71
67
  "@playwright/test": "^1.56.0",
72
68
  "@types/node": "~24.12.2",
@@ -99,9 +95,6 @@
99
95
  "vue-eslint-parser": "^10.4.1",
100
96
  "vue-tsc": "~3.3.0"
101
97
  },
102
- "optionalDependencies": {
103
- "@meddleware/sui-token-template": "^1.0.5"
104
- },
105
98
  "engines": {
106
99
  "node": "^22.18.0 || >=24.12.0"
107
100
  },
@@ -3,7 +3,7 @@ import { computed, reactive } from 'vue'
3
3
  import type { FormModel } from '../lib/form.js'
4
4
  import type { FormErrors } from '../lib/validation.js'
5
5
  import { deriveStructName } from '../lib/validation.js'
6
- import type { Network } from '../lib/types.js'
6
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
7
7
  import LicensePicker from './LicensePicker.vue'
8
8
  import IconPicker from './IconPicker.vue'
9
9
  import { UiFormField, UiSelect, UiFieldHint } from '@meddleware/ui'
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { DeployStep } from '../lib/deploy.js'
2
+ import type { DeployStep } from '@meddleware/sui-token-client/deploy'
3
3
 
4
4
  const ORDER: { key: DeployStep; label: string }[] = [
5
5
  { key: 'patching', label: 'Preparing the module' },
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from 'vue'
3
- import type { PublishResult, TokenConfig } from '../lib/types.js'
3
+ import type { PublishResult, TokenConfig } from '@meddleware/sui-token-client'
4
4
  import { UiNotice, safeHref } from '@meddleware/ui'
5
- import { buildPackageFiles } from '../lib/generatePackage.js'
5
+ import { buildPackageFiles } from '@meddleware/sui-token-client/package'
6
6
  import { fetchLicenseText } from '../lib/licenses.js'
7
7
  import { createRepoAndPush } from '../lib/github.js'
8
8
 
@@ -2,7 +2,7 @@
2
2
  import { ref, computed, onMounted, watch } from 'vue'
3
3
  import { useWallet } from '@meddleware/wallet-adapter'
4
4
  import { buildDeployExecutor } from '../lib/deployExecutor.js'
5
- import { getReadClient } from '../lib/readClient.js'
5
+ 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.
@@ -10,7 +10,7 @@ import { consumeStorageKey, createGatedAccess, runBlobUpload } from '@meddleware
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'
13
- import type { Network } from '../lib/types.js'
13
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
14
14
  import { UiNotice, UiFieldHint, UiSegmentedControl } from '@meddleware/ui'
15
15
  import WalrusBlobBrowser from './WalrusBlobBrowser.vue'
16
16
  import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
@@ -9,8 +9,9 @@ import {
9
9
  suiExplorerUrl,
10
10
  type SuiNetwork,
11
11
  } from '@meddleware/ui'
12
- import { listMyTokens, type DeployedToken } from '../lib/listMyTokens.js'
13
- import type { Network } from '../lib/types.js'
12
+ import { listMyTokens, type DeployedToken } from '@meddleware/sui-token-client'
13
+ import { getReadClient } from '../wallet.js'
14
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
14
15
 
15
16
  const props = defineProps<{ owner: string | null; network: Network }>()
16
17
 
@@ -18,9 +19,6 @@ const props = defineProps<{ owner: string | null; network: Network }>()
18
19
  const explorerNetwork = computed<SuiNetwork | null>(() =>
19
20
  props.network === 'localnet' ? null : props.network,
20
21
  )
21
- function objectHref(id: string): string | null {
22
- return explorerNetwork.value ? suiExplorerUrl('object', id, explorerNetwork.value) : null
23
- }
24
22
 
25
23
  const tokens = ref<DeployedToken[]>([])
26
24
  const loading = ref(false)
@@ -34,7 +32,7 @@ async function load(): Promise<void> {
34
32
  loading.value = true
35
33
  error.value = null
36
34
  try {
37
- tokens.value = await listMyTokens(props.owner, props.network)
35
+ tokens.value = await listMyTokens(getReadClient(props.network), props.owner)
38
36
  } catch (e) {
39
37
  error.value = e instanceof Error ? e.message : String(e)
40
38
  tokens.value = []
@@ -64,8 +62,8 @@ defineExpose({ reload: load })
64
62
  <dd>
65
63
  <CopyableAddress :address="t.packageId" label="Copy package id">
66
64
  <ExplorerLink
67
- v-if="objectHref(t.packageId)"
68
- :href="objectHref(t.packageId)!"
65
+ v-if="explorerNetwork"
66
+ :href="suiExplorerUrl('object', t.packageId, explorerNetwork)"
69
67
  :value="t.packageId"
70
68
  />
71
69
  </CopyableAddress>
@@ -74,8 +72,8 @@ defineExpose({ reload: load })
74
72
  <dd>
75
73
  <CopyableAddress :address="t.treasuryCapId" label="Copy treasury cap id">
76
74
  <ExplorerLink
77
- v-if="objectHref(t.treasuryCapId)"
78
- :href="objectHref(t.treasuryCapId)!"
75
+ v-if="explorerNetwork"
76
+ :href="suiExplorerUrl('object', t.treasuryCapId, explorerNetwork)"
79
77
  :value="t.treasuryCapId"
80
78
  />
81
79
  </CopyableAddress>
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue'
3
- import type { PublishResult, TokenConfig } from '../lib/types.js'
3
+ import type { PublishResult, TokenConfig } from '@meddleware/sui-token-client'
4
4
  import { explorerObjectUrl } from '../config.js'
5
- import { generatePackageZip } from '../lib/generatePackage.js'
5
+ import { generatePackageZip } from '@meddleware/sui-token-client/package'
6
6
  import { fetchLicenseText } from '../lib/licenses.js'
7
7
  import GithubPush from './GithubPush.vue'
8
8
  import { UiNotice, safeHref } from '@meddleware/ui'
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { TokenConfig, Network } from '../lib/types.js'
2
+ import type { TokenConfig, TokenNetwork as Network } from '@meddleware/sui-token-client'
3
3
  import { FEE_MIST, FEE_TREASURY, isFeeConfigured } from '../config.js'
4
4
  import { UiNotice } from '@meddleware/ui'
5
5
 
@@ -19,12 +19,12 @@ import {
19
19
  import { emptyForm, toTokenConfig } from '../lib/form.js'
20
20
  import { validateForm } from '../lib/validation.js'
21
21
  import type { FormErrors } from '../lib/validation.js'
22
- import { deployToken } from '../lib/deploy.js'
23
- import type { DeployStep } from '../lib/deploy.js'
22
+ import { deployToken } from '@meddleware/sui-token-client/deploy'
23
+ import type { DeployStep } from '@meddleware/sui-token-client/deploy'
24
24
  import { buildDeployExecutor } from '../lib/deployExecutor.js'
25
25
  import { FEE_MIST, FEE_TREASURY, PUBLISH_GAS_BUDGET, isFeeConfigured } from '../config.js'
26
26
  import { extractErrorMessage } from '../lib/errors.js'
27
- import type { Network, PublishResult } from '../lib/types.js'
27
+ import type { TokenNetwork as Network, PublishResult } from '@meddleware/sui-token-client'
28
28
 
29
29
  type Step = 'identity' | 'token' | 'settings' | 'review' | 'deploying' | 'done'
30
30
 
@@ -2,7 +2,7 @@
2
2
  import { computed, ref, watch } from 'vue'
3
3
  import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
4
4
  import { SELECTABLE_NETWORKS } from '../config.js'
5
- import type { Network } from '../lib/types.js'
5
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
6
6
 
7
7
  /** Wallet-supplied icons are untrusted: render only inline images or https URLs. */
8
8
  function safeIcon(icon: string | undefined): string | undefined {
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch } from 'vue'
3
3
  import { useWallet } from '@meddleware/wallet-adapter'
4
- import { getReadClient } from '../lib/readClient.js'
4
+ import { getReadClient } from '../wallet.js'
5
5
  import type { OwnedBlob } from '@meddleware/walrus-client'
6
6
  import { WALRUS_RPC_URLS, type WalrusNetwork } from '../config.js'
7
- import type { Network } from '../lib/types.js'
8
- import { UiNotice } from '@meddleware/ui'
7
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
8
+ import { UiNotice, safeHref } from '@meddleware/ui'
9
9
 
10
10
  const props = defineProps<{ network: WalrusNetwork; connected: boolean }>()
11
11
  const emit = defineEmits<{ (e: 'select', url: string): void }>()
@@ -104,7 +104,7 @@ function selectBlob(item: BlobItem): void {
104
104
  @click="selectBlob(b)"
105
105
  >
106
106
  <img
107
- :src="b.url"
107
+ :src="safeHref(b.url)"
108
108
  :alt="`Blob ${b.blobId.slice(0, 8)}…`"
109
109
  loading="lazy"
110
110
  @error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Deployment-wide configuration. Operator values (fee + treasury) are read from
2
2
  // Vite env vars so they can be set at build/deploy time without code changes.
3
3
  // See .env.example.
4
- import type { Network } from './lib/types.js'
4
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
5
5
  import { relayGateConfig, type RelayGateConfig } from '@meddleware/walrus-relay'
6
6
 
7
7
  const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import './component-styles.css'
2
+ // The embedded component deploys too: point the template patcher at the wasm asset.
3
+ import './templateWasm.js'
2
4
  export { default as TokenDeployerView } from './components/TokenDeployerView.vue'
@@ -5,10 +5,9 @@
5
5
 
6
6
  import { buildExecutor } from '@meddleware/wallet-adapter'
7
7
  import type { Transaction } from '@mysten/sui/transactions'
8
- import { toSuiTxResult } from './deploy.js'
9
- import type { SuiTxResult, Executor } from './deploy.js'
10
- import type { Network } from './types.js'
11
- import { getReadClient } from './readClient.js'
8
+ import { toSuiTxResult, type Executor, type SuiTxResult } from '@meddleware/sui-token-client/deploy'
9
+ import type { TokenNetwork as Network } from '@meddleware/sui-token-client'
10
+ import { getReadClient } from '../wallet.js'
12
11
  import { RPC_URLS } from '../config.js'
13
12
 
14
13
  /**
package/src/lib/form.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // The reactive form model (all text inputs are strings) and its conversion into
2
2
  // the strongly-typed TokenConfig used by the deploy + generate pipelines.
3
3
 
4
- import type { MetadataPolicy, PackagePolicy, SupplyPolicy, TokenConfig } from './types.js'
4
+ import type { MetadataPolicy, PackagePolicy, SupplyPolicy, TokenConfig } from '@meddleware/sui-token-client'
5
5
  import { deriveStructName, parseSupply } from './validation.js'
6
6
  import { DEFAULT_LICENSE_ID } from './licenses.js'
7
7
 
@@ -1,103 +1,22 @@
1
- // Pure form validation. Kept separate from Vue so it is unit-testable and can be
2
- // reused by the review step. Rules protect both the on-chain publish and the
3
- // validity of the generated Move source.
1
+ // The form layer: turns the token rules from @meddleware/sui-token-client into per-field messages for
2
+ // the UI. The rules themselves (identifiers, safe text, icon schemes, decimals, supply bounds) live
3
+ // in the client, which also enforces them with assertTokenConfig before any deploy or generation.
4
+
5
+ import {
6
+ MAX_U64,
7
+ SAFE_TEXT,
8
+ TOKEN_LIMITS,
9
+ hasAllowedIconScheme,
10
+ isValidDecimals,
11
+ parseSupply,
12
+ validateIdentifier,
13
+ type TokenConfig,
14
+ } from '@meddleware/sui-token-client'
15
+
16
+ export { deriveStructName, parseSupply } from '@meddleware/sui-token-client'
4
17
 
5
- import type { TokenConfig } from './types.js'
6
-
7
- /** Max supply Sui `coin::mint` accepts (u64). */
8
- export const MAX_U64 = 18_446_744_073_709_551_615n
9
-
10
- /** Sui OTW rule: the witness struct is the module name, uppercased. */
11
- export function deriveStructName(moduleName: string): string {
12
- return moduleName.toUpperCase()
13
- }
14
-
15
- /** Move identifier shape: lowercase start, then lowercase/digits/underscores. */
16
- export const MOVE_IDENT = /^[a-z][a-z0-9_]*$/
17
- /**
18
- * Printable ASCII excluding the double-quote and backslash, so values embed
19
- * safely inside a Move `b"..."` byte-string literal in the downloadable source.
20
- */
21
- export const SAFE_TEXT = /^[\x20-\x21\x23-\x5b\x5d-\x7e]*$/
22
18
  const SUI_ADDRESS = /^0x[0-9a-fA-F]{64}$/
23
19
 
24
- /** Upper bound on package/module identifiers — bounds bytecode/const-pool size. */
25
- export const MAX_IDENT_LEN = 64
26
-
27
- /**
28
- * Schemes permitted for the on-chain `icon_url` metadata. `https:` covers a Walrus
29
- * aggregator URL and any hosted image; `ipfs:` covers decentralised pins. This is a
30
- * defence-in-depth **content** guard layered *on top of* downstream wallet/explorer URL
31
- * sanitisation (not a replacement for it): it stops `javascript:`, `data:`, and insecure
32
- * `http:` from ever being baked into the generated coin metadata, while renderers remain
33
- * responsible for their own sanitisation. Empty (no icon) is allowed by the caller.
34
- */
35
- export const ICON_URL_ALLOWED_SCHEMES = ['https://', 'ipfs://'] as const
36
-
37
- /** True if `url` begins with an allowed icon scheme (case-insensitive). */
38
- export function hasAllowedIconScheme(url: string): boolean {
39
- const u = url.trim().toLowerCase()
40
- return ICON_URL_ALLOWED_SCHEMES.some((s) => u.startsWith(s))
41
- }
42
-
43
- /**
44
- * Move reserved words. A package or module named after one of these compiles
45
- * incorrectly or fails, so they are rejected up front. Lowercase-compared, which
46
- * also covers the derived (uppercased) OTW struct name.
47
- */
48
- export const MOVE_RESERVED_WORDS: ReadonlySet<string> = new Set([
49
- 'abort',
50
- 'acquires',
51
- 'as',
52
- 'break',
53
- 'const',
54
- 'continue',
55
- 'copy',
56
- 'else',
57
- 'entry',
58
- 'enum',
59
- 'false',
60
- 'for',
61
- 'friend',
62
- 'fun',
63
- 'has',
64
- 'if',
65
- 'in',
66
- 'invariant',
67
- 'let',
68
- 'loop',
69
- 'macro',
70
- 'match',
71
- 'module',
72
- 'move',
73
- 'mut',
74
- 'native',
75
- 'package',
76
- 'phantom',
77
- 'public',
78
- 'return',
79
- 'script',
80
- 'spec',
81
- 'struct',
82
- 'true',
83
- 'type',
84
- 'use',
85
- 'while',
86
- 'key',
87
- 'store',
88
- 'drop',
89
- ])
90
-
91
- /** Validate one Move identifier field; returns an error message or null. */
92
- export function validateIdentifier(value: string): string | null {
93
- if (!value) return 'Required'
94
- if (value.length > MAX_IDENT_LEN) return `${MAX_IDENT_LEN} characters maximum`
95
- if (!/^[a-z]/.test(value)) return 'Must start with a lowercase letter'
96
- if (!MOVE_IDENT.test(value)) return 'Lowercase letters, digits and underscores only'
97
- if (MOVE_RESERVED_WORDS.has(value)) return `"${value}" is a reserved keyword`
98
- return null
99
- }
100
-
101
20
  export type FormErrors = Partial<Record<keyof TokenConfig, string>>
102
21
 
103
22
  export interface ValidatableForm {
@@ -112,7 +31,14 @@ export interface ValidatableForm {
112
31
  recipient: string
113
32
  }
114
33
 
115
- /** Validate the user-entered fields. Returns a map of field -> message. */
34
+ function textError(value: string, max: number, required: boolean): string | undefined {
35
+ if (required && !value.trim()) return 'Required'
36
+ if (value.length > max) return `Maximum ${max} characters`
37
+ if (!SAFE_TEXT.test(value)) return 'Double quotes and backslashes are not supported'
38
+ return undefined
39
+ }
40
+
41
+ /** Validate the user-entered fields. Returns a map of field → message. */
116
42
  export function validateForm(form: ValidatableForm): FormErrors {
117
43
  const errors: FormErrors = {}
118
44
 
@@ -121,39 +47,28 @@ export function validateForm(form: ValidatableForm): FormErrors {
121
47
  const moduleNameError = validateIdentifier(form.moduleName)
122
48
  if (moduleNameError) errors.moduleName = moduleNameError
123
49
 
124
- if (!form.symbol.trim()) errors.symbol = 'Required'
125
- else if (form.symbol.length > 32) errors.symbol = 'Maximum 32 characters'
126
- else if (!SAFE_TEXT.test(form.symbol))
127
- errors.symbol = 'Double quotes and backslashes are not supported'
128
-
129
- if (!form.name.trim()) errors.name = 'Required'
130
- else if (form.name.length > 64) errors.name = 'Maximum 64 characters'
131
- else if (!SAFE_TEXT.test(form.name))
132
- errors.name = 'Double quotes and backslashes are not supported'
133
-
134
- if (form.description.length > 256) errors.description = 'Maximum 256 characters'
135
- else if (!SAFE_TEXT.test(form.description))
136
- errors.description = 'Double quotes and backslashes are not supported'
137
-
50
+ const symbol = textError(form.symbol, TOKEN_LIMITS.symbol, true)
51
+ if (symbol) errors.symbol = symbol
52
+ const name = textError(form.name, TOKEN_LIMITS.name, true)
53
+ if (name) errors.name = name
54
+ const description = textError(form.description, TOKEN_LIMITS.description, false)
55
+ if (description) errors.description = description
138
56
  if (form.iconUrl) {
139
- if (form.iconUrl.length > 512) errors.iconUrl = 'Maximum 512 characters'
140
- else if (!SAFE_TEXT.test(form.iconUrl))
141
- errors.iconUrl = 'Double quotes and backslashes are not supported'
57
+ const icon = textError(form.iconUrl, TOKEN_LIMITS.iconUrl, false)
58
+ if (icon) errors.iconUrl = icon
142
59
  else if (!hasAllowedIconScheme(form.iconUrl)) errors.iconUrl = 'Use an https:// or ipfs:// URL'
143
60
  }
144
61
 
145
62
  const decimals = Number(form.decimals)
146
63
  if (!Number.isInteger(decimals)) errors.decimals = 'Enter a whole number'
147
- else if (decimals < 0) errors.decimals = 'Must be 0 or greater'
148
- else if (decimals > 18) errors.decimals = 'Maximum is 18 (9 is typical on Sui)'
64
+ else if (decimals < TOKEN_LIMITS.decimals.min) errors.decimals = `Must be ${TOKEN_LIMITS.decimals.min} or greater`
65
+ else if (decimals > TOKEN_LIMITS.decimals.max) errors.decimals = `Maximum is ${TOKEN_LIMITS.decimals.max} (9 is typical on Sui)`
149
66
 
150
67
  const supply = parseSupply(form.initialSupply)
151
68
  if (supply === null) {
152
69
  errors.initialSupply = 'Enter a whole number, or leave blank for none'
153
- } else if (Number.isInteger(decimals) && decimals >= 0 && decimals <= 18) {
154
- if (supply * 10n ** BigInt(decimals) > MAX_U64) {
155
- errors.initialSupply = 'Supply exceeds the maximum for this decimal precision'
156
- }
70
+ } else if (isValidDecimals(decimals) && supply * 10n ** BigInt(decimals) > MAX_U64) {
71
+ errors.initialSupply = 'Supply exceeds the maximum for this decimal precision'
157
72
  }
158
73
 
159
74
  if (form.recipient && !SUI_ADDRESS.test(form.recipient)) {
@@ -163,14 +78,6 @@ export function validateForm(form: ValidatableForm): FormErrors {
163
78
  return errors
164
79
  }
165
80
 
166
- /** Parse the supply text into a non-negative bigint, or null if invalid. */
167
- export function parseSupply(input: string): bigint | null {
168
- const s = input.trim()
169
- if (s === '') return 0n
170
- if (!/^\d+$/.test(s)) return null
171
- return BigInt(s)
172
- }
173
-
174
81
  export function isValid(errors: FormErrors): boolean {
175
82
  return Object.keys(errors).length === 0
176
83
  }
package/src/main.ts CHANGED
@@ -5,10 +5,10 @@ import { createApp } from 'vue'
5
5
  import App from './App.vue'
6
6
  import './styles.css'
7
7
  import './component-styles.css'
8
- import { configureWasm } from './lib/template.js'
9
- // Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
8
+ import './templateWasm.js'
9
+ // Static: the read client is already in the main chunk (IconPicker, WalrusBlobBrowser and MyTokens
10
10
  // import it), so a dynamic import in the E2E block below could not split it out anyway.
11
- import { setReadClient } from './lib/readClient.js'
11
+ import { setReadClient } from './wallet.js'
12
12
  import type { SuiGrpcClient } from '@mysten/sui/grpc'
13
13
  import { useColorMode, useSeason } from '@meddleware/ui'
14
14
 
@@ -19,13 +19,6 @@ useColorMode('system')
19
19
  // Enable seasonal theming (sets data-season on <html>; seasons.css is imported above).
20
20
  useSeason()
21
21
 
22
- // Vite serves the wasm as an asset URL; wasm-bindgen's init loads it on demand.
23
- import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_bg.wasm?url'
24
-
25
- // Register the URL only; the 343 kB wasm is fetched lazily at first deploy so it
26
- // doesn't block initial page load.
27
- configureWasm(wasmUrl)
28
-
29
22
  // E2E-only: stub the Sui client, register a mock wallet and expose getSuiClient for headless tests.
30
23
  // Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
31
24
  // Only the `e2e` build mode may set VITE_E2E (vite.config.ts refuses otherwise); both are static,
@@ -0,0 +1,8 @@
1
+ // Point @meddleware/sui-token-client's template patcher at the bytecode wasm as a Vite asset URL.
2
+ // Imported by both entries (the standalone app's main.ts and the library entry the dashboard
3
+ // embeds), so a deploy works wherever the component runs. The ~343 kB wasm is fetched only at the
4
+ // first deploy.
5
+ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_bg.wasm?url'
6
+ import { configureTemplateWasm } from '@meddleware/sui-token-client/template'
7
+
8
+ configureTemplateWasm(wasmUrl)