@meddleware/token-deployer-ui 0.0.16 → 0.0.18
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 +26 -39
- package/package.json +3 -10
- package/src/components/ConfigForm.vue +1 -1
- package/src/components/DeployProgress.vue +1 -1
- package/src/components/GithubPush.vue +2 -2
- package/src/components/IconPicker.vue +2 -2
- package/src/components/MyTokens.vue +8 -10
- package/src/components/ResultPanel.vue +2 -2
- package/src/components/ReviewPanel.vue +1 -1
- package/src/components/TokenDeployerView.vue +3 -3
- package/src/components/WalletBar.vue +1 -1
- package/src/components/WalrusBlobBrowser.vue +4 -4
- package/src/config.ts +1 -1
- package/src/index.ts +2 -0
- package/src/lib/deployExecutor.ts +3 -4
- package/src/lib/form.ts +1 -1
- package/src/lib/validation.ts +36 -129
- package/src/main.ts +3 -10
- package/src/templateWasm.ts +8 -0
- package/src/wallet.ts +19 -0
- package/src/lib/buildPublishTx.ts +0 -182
- package/src/lib/deploy.ts +0 -210
- package/src/lib/generatePackage.ts +0 -317
- package/src/lib/listMyTokens.ts +0 -41
- package/src/lib/readClient.ts +0 -25
- package/src/lib/template.ts +0 -134
- package/src/lib/types.ts +0 -69
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +0 -31
- package/src/template-src/files.json +0 -10
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 `
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
3. A publish PTB
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
substituted into Move source
|
|
39
|
-
|
|
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
|
|
145
|
-
`@meddleware/sui-token-template`
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
- `
|
|
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 +
|
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.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "0BSD",
|
|
6
6
|
"repository": {
|
|
@@ -42,24 +42,20 @@
|
|
|
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
|
-
"@meddleware/walrus-client": "^0.0.
|
|
54
|
+
"@meddleware/walrus-client": "^0.0.22",
|
|
57
55
|
"@meddleware/walrus-relay": "^0.1.22",
|
|
58
|
-
"@mysten/bcs": "~2.1.2",
|
|
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": {
|
|
@@ -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 '
|
|
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,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import type { PublishResult, TokenConfig } from '
|
|
3
|
+
import type { PublishResult, TokenConfig } from '@meddleware/sui-token-client'
|
|
4
4
|
import { UiNotice, safeHref } from '@meddleware/ui'
|
|
5
|
-
import { buildPackageFiles } from '
|
|
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 '../
|
|
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 '
|
|
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 '
|
|
13
|
-
import
|
|
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.
|
|
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="
|
|
68
|
-
:href="
|
|
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="
|
|
78
|
-
:href="
|
|
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 '
|
|
3
|
+
import type { PublishResult, TokenConfig } from '@meddleware/sui-token-client'
|
|
4
4
|
import { explorerObjectUrl } from '../config.js'
|
|
5
|
-
import { generatePackageZip } from '
|
|
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 '
|
|
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 '
|
|
23
|
-
import type { DeployStep } from '
|
|
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 '
|
|
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 '
|
|
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 '../
|
|
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 '
|
|
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 '
|
|
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
|
@@ -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 '
|
|
9
|
-
import type {
|
|
10
|
-
import
|
|
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 '
|
|
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
|
|
package/src/lib/validation.ts
CHANGED
|
@@ -1,103 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
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
|
-
|
|
140
|
-
|
|
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 <
|
|
148
|
-
else if (decimals >
|
|
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 (
|
|
154
|
-
|
|
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
|
|
9
|
-
// Static:
|
|
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 './
|
|
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)
|
package/src/wallet.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The app's Sui read client per network: the wallet-adapter's shared, memoised gRPC client for the
|
|
2
|
+
// configured RPC URL (public fullnodes have dropped JSON-RPC). E2E builds replace it with a stub via
|
|
3
|
+
// `setReadClient`, because gRPC-web responses are binary and cannot be faked at the fetch layer.
|
|
4
|
+
import type { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
5
|
+
import { getSuiClient } from '@meddleware/wallet-adapter'
|
|
6
|
+
import type { TokenNetwork } from '@meddleware/sui-token-client'
|
|
7
|
+
import { RPC_URLS } from './config.js'
|
|
8
|
+
|
|
9
|
+
const overrides = new Map<TokenNetwork, SuiGrpcClient>()
|
|
10
|
+
|
|
11
|
+
/** The read client for `network` (an E2E stub when one is set). */
|
|
12
|
+
export function getReadClient(network: TokenNetwork): SuiGrpcClient {
|
|
13
|
+
return overrides.get(network) ?? getSuiClient(network, RPC_URLS[network])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Replace the client for `network` (E2E builds only). */
|
|
17
|
+
export function setReadClient(network: TokenNetwork, client: SuiGrpcClient): void {
|
|
18
|
+
overrides.set(network, client)
|
|
19
|
+
}
|