@meddleware/token-deployer-ui 0.0.1
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/.claude/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/sui-move-bootcamp/SKILL.md +128 -0
- package/.claude/sui-move-bootcamp/SKILL.md +128 -0
- package/.editorconfig +8 -0
- package/.env.example +75 -0
- package/.env.production +7 -0
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/token-deployer-e2e.yml +64 -0
- package/.oxfmtrc.json +5 -0
- package/.oxlintrc.json +10 -0
- package/.vscode/extensions.json +9 -0
- package/AGENTS.md +39 -0
- package/CLAUDE.md +159 -0
- package/LICENSE +121 -0
- package/README.md +234 -0
- package/env.d.ts +13 -0
- package/index.html +13 -0
- package/package.json +67 -0
- package/playwright.config.ts +30 -0
- package/public/CNAME +1 -0
- package/public/_headers +12 -0
- package/public/_redirects +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +5 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +3 -0
- package/public/sitemap.xml +8 -0
- package/scripts/e2e-browser.mjs +15 -0
- package/scripts/e2e-deploy.mjs +222 -0
- package/scripts/e2e-localnet.mjs +65 -0
- package/scripts/e2e-walrus-browser.mjs +115 -0
- package/scripts/gen-og-image.mjs +31 -0
- package/scripts/publish-localnet.mjs +55 -0
- package/scripts/regen-template.sh +45 -0
- package/scripts/sync-template-src.sh +36 -0
- package/scripts/verify-bytecode.mjs +49 -0
- package/src/App.vue +41 -0
- package/src/__tests__/App.spec.js +11 -0
- package/src/components/AppNotice.vue +15 -0
- package/src/components/ColorModeControl.vue +81 -0
- package/src/components/ConfigForm.vue +192 -0
- package/src/components/DeployProgress.vue +46 -0
- package/src/components/FieldHint.vue +79 -0
- package/src/components/FormField.vue +28 -0
- package/src/components/FormSelect.vue +43 -0
- package/src/components/GithubPush.vue +100 -0
- package/src/components/IconPicker.vue +410 -0
- package/src/components/LicensePicker.vue +182 -0
- package/src/components/ResultPanel.vue +88 -0
- package/src/components/ReviewPanel.vue +69 -0
- package/src/components/TermsDialog.vue +81 -0
- package/src/components/TokenDeployerView.vue +115 -0
- package/src/components/WalletBar.vue +108 -0
- package/src/components/WalrusBlobBrowser.vue +188 -0
- package/src/components/WalrusIconUpload.vue +205 -0
- package/src/composables/useAccessGate.ts +122 -0
- package/src/composables/useColorMode.ts +76 -0
- package/src/composables/useWalrusRelay.ts +21 -0
- package/src/config.ts +185 -0
- package/src/index.ts +1 -0
- package/src/lib/accessGate.ts +141 -0
- package/src/lib/buildPublishTx.ts +192 -0
- package/src/lib/deploy.ts +168 -0
- package/src/lib/deployExecutor.ts +61 -0
- package/src/lib/errors.ts +21 -0
- package/src/lib/form.ts +75 -0
- package/src/lib/generatePackage.ts +312 -0
- package/src/lib/github.ts +146 -0
- package/src/lib/licenses.ts +122 -0
- package/src/lib/readClient.ts +17 -0
- package/src/lib/template.ts +131 -0
- package/src/lib/types.ts +69 -0
- package/src/lib/validation.ts +172 -0
- package/src/lib/walrus-constants.ts +15 -0
- package/src/lib/walrus.ts +144 -0
- package/src/main.ts +154 -0
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +31 -0
- package/src/styles.css +384 -0
- package/src/template-src/files.json +10 -0
- package/test-results/.last-run.json +4 -0
- package/tests/accessGate.test.ts +108 -0
- package/tests/buildPublishTx.test.ts +243 -0
- package/tests/deploy.test.ts +81 -0
- package/tests/errors.test.ts +32 -0
- package/tests/generatePackage.test.ts +182 -0
- package/tests/github.test.ts +61 -0
- package/tests/licenses.test.ts +78 -0
- package/tests/template.test.ts +93 -0
- package/tests/templateArtifact.test.ts +61 -0
- package/tests/templateParity.test.ts +55 -0
- package/tests/useAccessGate.test.ts +101 -0
- package/tests/useWallet.test.ts +74 -0
- package/tests/useWalrusRelayGating.test.ts +30 -0
- package/tests/validation.test.ts +117 -0
- package/tests/walrusUpload.test.ts +93 -0
- package/tests/walrus_url_patch.test.ts +26 -0
- package/tsconfig.json +19 -0
- package/typedoc.json +34 -0
- package/vite.config.ts +65 -0
- package/vitest.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @meddleware/token-deployer-ui
|
|
2
|
+
|
|
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
|
|
5
|
+
browser**: the user's wallet signs and pays gas, and nothing is compiled or
|
|
6
|
+
signed on a server. The user also gets the full, verifiable Move source package,
|
|
7
|
+
and can optionally push it to a new GitHub repo in their own account.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
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/)).
|
|
14
|
+
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 ([src/lib/walrus.ts](src/lib/walrus.ts)); and an
|
|
26
|
+
optional GitHub push uses the user's own token ([src/lib/github.ts](src/lib/github.ts)).
|
|
27
|
+
|
|
28
|
+
## Security & trust model
|
|
29
|
+
|
|
30
|
+
- **No backend, no key custody.** The only network calls are to the Sui RPC, the
|
|
31
|
+
user's wallet, SPDX (`raw.githubusercontent.com`), the Walrus relay/aggregator
|
|
32
|
+
(only if the icon uploader is used), and GitHub (only if the push is used).
|
|
33
|
+
- **No endorsement.** The tool is neutral; it does not vet or endorse created
|
|
34
|
+
tokens or their creators. This is stated in the review, result, and footer UI.
|
|
35
|
+
- **Defence in depth.** User input is validated at the form
|
|
36
|
+
([src/lib/validation.ts](src/lib/validation.ts)) *and* re-asserted before it is
|
|
37
|
+
substituted into Move source / shell scripts
|
|
38
|
+
([src/lib/generatePackage.ts](src/lib/generatePackage.ts)) and before it is
|
|
39
|
+
patched into the bytecode ([src/lib/template.ts](src/lib/template.ts)), so no
|
|
40
|
+
quote/backslash/control character or reserved Move keyword can ever produce an
|
|
41
|
+
injectable package.
|
|
42
|
+
- **Fee integrity.** The fee is split from the user's own gas coin to the operator
|
|
43
|
+
treasury inside the publish PTB and cannot be redirected at runtime. A production
|
|
44
|
+
build **fails** if the treasury is unset (see below), so revenue can't silently
|
|
45
|
+
burn to `0x0`.
|
|
46
|
+
- **GitHub token hygiene.** The optional PAT is `type=password`, `autocomplete=off`,
|
|
47
|
+
sent only to `api.github.com` over HTTPS, never stored or logged, and cleared as
|
|
48
|
+
soon as the push finishes.
|
|
49
|
+
|
|
50
|
+
## Develop
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install
|
|
54
|
+
npm run dev # Vite dev server
|
|
55
|
+
npm run test # Vitest unit tests (61 tests)
|
|
56
|
+
npm run type-check # vue-tsc
|
|
57
|
+
npm run build # production bundle -> dist/ (runs the treasury guard)
|
|
58
|
+
npm run docs # TypeDoc API reference -> docs/api/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Operator configuration
|
|
62
|
+
|
|
63
|
+
Committed defaults live in [.env.production](.env.production) (build) and
|
|
64
|
+
[.env.development](.env.development) (dev server). Override per environment in the
|
|
65
|
+
host dashboard. See [.env.example](.env.example) for the full list.
|
|
66
|
+
|
|
67
|
+
| Variable | Purpose | Default |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `VITE_FEE_TREASURY_TESTNET` / `_MAINNET` | Operator treasury that receives the fee. **A production build fails if either is the zero address** (bypass with `VITE_ALLOW_UNSET_TREASURY=1`). | committed operator address |
|
|
70
|
+
| `VITE_FEE_MIST` | Flat fee split from the user's gas coin. | `1000000000` (1 SUI) |
|
|
71
|
+
| `VITE_RPC_TESTNET` / `_MAINNET` | JSON-RPC endpoint (set a paid RPC for production). | public endpoints |
|
|
72
|
+
| `VITE_PUBLISH_GAS_BUDGET` | Publish gas budget (MIST). | `500000000` |
|
|
73
|
+
| `VITE_PUBLIC_URL` | Site origin for canonical/OG/JSON-LD URLs. | `https://tokens.meddleware.co.uk` |
|
|
74
|
+
| `VITE_WALRUS_RELAY_TESTNET` / `_MAINNET` | Your own upload relay (to collect the tip). Unset → public Mysten relay (earns nothing). | public relay |
|
|
75
|
+
| `VITE_WALRUS_MAX_TIP_MIST` | Client-side max relay tip. **Must exceed your relay's tip** or uploads fail. | `50000000` (0.05 SUI) |
|
|
76
|
+
| `VITE_ICON_MAX_BYTES` / `VITE_ICON_ALLOWED_TYPES` | Client icon size/type gate (UX only). | `102400` / PNG,JPEG,WebP,SVG |
|
|
77
|
+
|
|
78
|
+
## Walrus icon upload relay (optional — for per-upload revenue)
|
|
79
|
+
|
|
80
|
+
Icon uploads go through a Walrus **upload relay**. Using the public Mysten relay
|
|
81
|
+
works out of the box but earns you nothing. To charge users per upload, run your
|
|
82
|
+
own relay and configure `VITE_WALRUS_RELAY_*` — the tip is paid **on-chain by the
|
|
83
|
+
user's wallet to your address**, no custom backend.
|
|
84
|
+
|
|
85
|
+
**How the tip is safe (walrus SDK 1.1.7 + `crates/walrus-upload-relay`):** the tip
|
|
86
|
+
is baked into the same tx the user signs and bound to the blob by
|
|
87
|
+
`blobDigest ‖ SHA256(nonce) ‖ size`. The relay verifies the on-chain payment —
|
|
88
|
+
amount checked against the *actual received body length* — **before** the expensive
|
|
89
|
+
fan-out. A user cannot strip the tip or underpay for a large blob; either way the
|
|
90
|
+
relay rejects and you incur no fan-out cost. The relay performs **no on-chain tx**,
|
|
91
|
+
so you never pay gas; storage (WAL) is paid by the user at registration.
|
|
92
|
+
|
|
93
|
+
**Host it (not a Cloudflare Worker).** The relay RS2-encodes blobs and opens
|
|
94
|
+
hundreds of concurrent connections to storage nodes — beyond the Workers model. Run
|
|
95
|
+
the stock `mysten/walrus-upload-relay` Docker image on a container host (Fly.io with
|
|
96
|
+
scale-to-zero bounds idle cost; or a small VPS + Caddy). Your frontend stays static
|
|
97
|
+
(GitHub Pages / Cloudflare Pages); the relay is a separate backend.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker run -p 3000:3000 \
|
|
101
|
+
-v $HOME/.config/walrus/walrus_upload_relay_config.yaml:/opt/walrus/walrus_upload_relay_config.yaml \
|
|
102
|
+
-v $HOME/.config/walrus/client_config.yaml:/opt/walrus/client_config.yaml \
|
|
103
|
+
mysten/walrus-upload-relay --context testnet \
|
|
104
|
+
--walrus-config /opt/walrus/client_config.yaml \
|
|
105
|
+
--server-address 0.0.0.0:3000 \
|
|
106
|
+
--relay-config /opt/walrus/walrus_upload_relay_config.yaml
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Set a `linear` tip with margin** in `walrus_upload_relay_config.yaml`: `base` ≥
|
|
110
|
+
per-request overhead (RPC + connection) and `encoded_size_mul_per_kib` above your
|
|
111
|
+
egress cost/KiB, plus a fat margin (it also absorbs SUI/fiat drift and storage-node
|
|
112
|
+
retry amplification). Tip `address` = your wallet. Keep `VITE_WALRUS_MAX_TIP_MIST`
|
|
113
|
+
comfortably above this tip (the SDK hard-fails uploads if the tip exceeds it). The
|
|
114
|
+
relay reads its tip only at **startup**, so changing it means rewrite-YAML + restart.
|
|
115
|
+
|
|
116
|
+
**Protect it at the edge (the authoritative controls).** Put the relay behind
|
|
117
|
+
Cloudflare (proxied DNS) with:
|
|
118
|
+
|
|
119
|
+
- a **hard request-body cap ~256 KB** — icons are <100 KB, but the relay otherwise
|
|
120
|
+
buffers up to a hardcoded **1 GiB** body into RAM *before* the tip check (the one
|
|
121
|
+
real abuse vector: ingress/RAM/RPC on rejected uploads; no egress/gas);
|
|
122
|
+
- a **per-IP** WAF rate-limit on `/v1/blob-upload-relay` (per-*wallet* limiting isn't
|
|
123
|
+
worth it — see [CLAUDE.md](CLAUDE.md) "Walrus relay economics & deferred decisions");
|
|
124
|
+
- **origin locked to Cloudflare** (Authenticated Origin Pulls or a Cloudflare Tunnel)
|
|
125
|
+
so the origin IP can't be hit directly.
|
|
126
|
+
|
|
127
|
+
Open CORS means anyone can use your relay, but every served upload paid a size-scaled
|
|
128
|
+
tip, so third-party use is *profitable* as long as the tip keeps margin.
|
|
129
|
+
|
|
130
|
+
## Deploy (Cloudflare Pages)
|
|
131
|
+
|
|
132
|
+
- Build command: `npm run build` · Output directory: `dist`
|
|
133
|
+
- [public/_headers](public/_headers) sets security headers + asset caching;
|
|
134
|
+
[public/_redirects](public/_redirects) provides the SPA fallback.
|
|
135
|
+
- SEO: [index.html](index.html) has env-aware canonical/OG/Twitter/JSON-LD, a real
|
|
136
|
+
`public/og-image.png` (regenerate with `npm run gen:og-image`), and a crawlable
|
|
137
|
+
static fallback; `public/robots.txt` + `public/sitemap.xml` are included.
|
|
138
|
+
- Before going live, confirm the treasury vars are set (the build enforces this)
|
|
139
|
+
and point the subdomain at the deployment.
|
|
140
|
+
|
|
141
|
+
## Keeping the template in sync (single source of truth)
|
|
142
|
+
|
|
143
|
+
The shipped bytecode and the downloadable source both derive from
|
|
144
|
+
`@meddleware/sui-token-template` (`optionalDependencies`). After changing that template, re-run:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm run regen:template # recompiles + refreshes src/move-template/*
|
|
148
|
+
npm run sync:template # refreshes src/template-src/files.json
|
|
149
|
+
npm run verify:template # bytecode round-trip + provenance/parity tests
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Two tests enforce this invariant in CI so it can never silently drift:
|
|
153
|
+
|
|
154
|
+
- [tests/templateParity.test.ts](tests/templateParity.test.ts) — `files.json`
|
|
155
|
+
matches the canonical template sources verbatim.
|
|
156
|
+
- [tests/templateArtifact.test.ts](tests/templateArtifact.test.ts) — the shipped
|
|
157
|
+
`.mv` still contains the identifiers/constants the patcher expects.
|
|
158
|
+
|
|
159
|
+
## Verification scripts
|
|
160
|
+
|
|
161
|
+
- `scripts/publish-localnet.mjs` — patch → publish → finalize against a local Sui node
|
|
162
|
+
(node, no browser).
|
|
163
|
+
- `scripts/e2e-deploy.mjs` — the **network-parametrized real-chain deploy e2e** (headless
|
|
164
|
+
browser + injected wallet): drives the full UI, does a REAL publish, waits for real
|
|
165
|
+
confirmation, checks the result panel, downloads the source zip, and verifies coin type +
|
|
166
|
+
operator fee on-chain. Covers exactly what the mocked Cypress suite cannot. `scripts/e2e-browser.mjs`
|
|
167
|
+
is a thin localnet wrapper over it.
|
|
168
|
+
- `scripts/e2e-walrus-browser.mjs` — the Walrus icon uploader end-to-end on testnet
|
|
169
|
+
with an injected wallet.
|
|
170
|
+
|
|
171
|
+
## Real-chain e2e / launch runbook
|
|
172
|
+
|
|
173
|
+
The Playwright mocked suite (`npm run test:e2e`) mocks all RPC via `VITE_E2E=1`, so it can prove
|
|
174
|
+
the form/wallet/publish UI but **not** on-chain confirmation, the result panel, or the source
|
|
175
|
+
download. The real-chain harness fills that gap. It is **manual** — never wired into automatic CI — because testnet/mainnet runs
|
|
176
|
+
publish a real token and spend real SUI.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm run e2e:localnet # fresh keypair, localnet faucet — no real funds; run this freely
|
|
180
|
+
npm run e2e:testnet # real testnet publish — needs a funded key (see below)
|
|
181
|
+
npm run e2e:mainnet # real mainnet publish — additionally needs E2E_MAINNET_CONFIRM=1
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each script builds the app (production build, so the treasury guard runs) and serves `dist` before
|
|
185
|
+
driving it. `E2E_NETWORK` selects the chain; the runner derives the app network (localnet borrows
|
|
186
|
+
testnet mode with the RPC pointed at the local node).
|
|
187
|
+
|
|
188
|
+
**Prerequisites for a real (testnet/mainnet) run:**
|
|
189
|
+
|
|
190
|
+
| Env | Purpose |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `SUI_PRIV` | bech32 `suiprivkey1…` for a **funded** deployer key. Export the CLI key: `sui keytool export --key-identity <addr>`. Never faucet-funded on a real network. |
|
|
193
|
+
| `VITE_FEE_TREASURY_TESTNET` / `_MAINNET` | Operator treasury baked into the build (already set in `.env.production`). The build fails if it is the zero address. |
|
|
194
|
+
| `E2E_FEE_ADDR` | The treasury to watch for the fee-delta assertion — set it equal to the built treasury. If unset, the fee check is skipped (the app still charges the fee). |
|
|
195
|
+
| `VITE_RPC_TESTNET` / `_MAINNET` and `E2E_RPC` | JSON-RPC endpoint for the build (browser) and the node-side verifier. Use a JSON-RPC-capable endpoint — the official testnet fullnode is gRPC-only. |
|
|
196
|
+
| `E2E_MAINNET_CONFIRM=1` | **Mainnet only.** Without it the mainnet runner aborts cleanly (fail-safe default) so it can never publish by accident. |
|
|
197
|
+
|
|
198
|
+
A full fee-charging run needs the key to hold **≥ gas + fee** (~0.5 SUI gas + 1 SUI fee = ~1.5 SUI).
|
|
199
|
+
For a first dry run you can lower `VITE_FEE_MIST` or leave the treasury unset (`VITE_ALLOW_UNSET_TREASURY=1`)
|
|
200
|
+
so only ~0.5 SUI of gas is needed.
|
|
201
|
+
|
|
202
|
+
Example (testnet):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
export SUI_PRIV="$(sui keytool export --key-identity 0xYOURADDR --json | jq -r .exportedPrivateKey)"
|
|
206
|
+
VITE_RPC_TESTNET=https://sui-testnet-rpc.publicnode.com \
|
|
207
|
+
E2E_RPC=https://sui-testnet-rpc.publicnode.com \
|
|
208
|
+
VITE_FEE_TREASURY_TESTNET=0xYOURTREASURY \
|
|
209
|
+
E2E_FEE_ADDR=0xYOURTREASURY \
|
|
210
|
+
npm run e2e:testnet
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
> **Do not run `e2e:testnet` until all other launch work is complete** — a premature published token
|
|
214
|
+
> can attract traders before you are ready.
|
|
215
|
+
|
|
216
|
+
### MVP manual verification checklist
|
|
217
|
+
|
|
218
|
+
The human backstop for anything the harness does not assert (run once against testnet before launch):
|
|
219
|
+
|
|
220
|
+
- [ ] Connect a real wallet (testnet), confirm the network selector shows testnet.
|
|
221
|
+
- [ ] Fill the form; the coin-type preview updates as the module name changes.
|
|
222
|
+
- [ ] Review → Confirm & deploy; wallet prompts to sign and pay gas.
|
|
223
|
+
- [ ] "Confirming on-chain" reaches **done** (no timeout).
|
|
224
|
+
- [ ] Result panel shows the correct coin type, package id, and cap ids.
|
|
225
|
+
- [ ] The SuiScan package link opens the deployed package.
|
|
226
|
+
- [ ] "Download source package (.zip)" produces a buildable Move package.
|
|
227
|
+
- [ ] "Deploy another" resets the form.
|
|
228
|
+
- [ ] (If used) the icon upload returns a Walrus aggregator URL that renders the image.
|
|
229
|
+
- [ ] The operator treasury received the fee.
|
|
230
|
+
|
|
231
|
+
## API reference
|
|
232
|
+
|
|
233
|
+
`npm run docs` generates a TypeDoc site under `docs/api/` covering the deployment
|
|
234
|
+
library (`src/lib`), composables, and config. See [typedoc.json](typedoc.json).
|
package/env.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module '*.vue' {
|
|
4
|
+
import type { DefineComponent } from 'vue'
|
|
5
|
+
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
|
6
|
+
export default component
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Raw ?url imports (Vite) for the move-bytecode-template wasm asset.
|
|
10
|
+
declare module '*?url' {
|
|
11
|
+
const src: string
|
|
12
|
+
export default src
|
|
13
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<link rel="icon" href="/favicon.ico">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Vite App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meddleware/token-deployer-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": { "default": "./src/index.ts" }
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
12
|
+
"preview": "vite preview",
|
|
13
|
+
"type-check": "vue-tsc --noEmit",
|
|
14
|
+
"test:unit": "vitest",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"build:e2e": "VITE_E2E=1 vite build",
|
|
18
|
+
"test:e2e": "playwright test",
|
|
19
|
+
"test:e2e:ui": "playwright test --ui",
|
|
20
|
+
"e2e:install": "playwright install --with-deps chromium firefox",
|
|
21
|
+
"e2e:localnet": "VITE_RPC_TESTNET=${VITE_RPC_TESTNET:-http://127.0.0.1:9000} VITE_FEE_TREASURY_TESTNET=${VITE_FEE_TREASURY_TESTNET:-0xfee0000000000000000000000000000000000000000000000000000000000000} vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=localnet node scripts/e2e-deploy.mjs'",
|
|
22
|
+
"e2e:testnet": "vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=testnet node scripts/e2e-deploy.mjs'",
|
|
23
|
+
"e2e:mainnet": "vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=mainnet node scripts/e2e-deploy.mjs'",
|
|
24
|
+
"lint": "oxlint . --fix",
|
|
25
|
+
"format": "oxfmt src/",
|
|
26
|
+
"docs": "typedoc",
|
|
27
|
+
"regen:template": "bash scripts/regen-template.sh",
|
|
28
|
+
"sync:template": "bash scripts/sync-template-src.sh",
|
|
29
|
+
"gen:og-image": "node scripts/gen-og-image.mjs",
|
|
30
|
+
"verify:template": "node scripts/verify-bytecode.mjs && vitest run tests/templateArtifact.test.ts tests/templateParity.test.ts"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@meddleware/wallet-adapter": "^0.0.7",
|
|
34
|
+
"@meddleware/walrus-relay": "^0.1.0",
|
|
35
|
+
"@mysten/bcs": "~2.1.0",
|
|
36
|
+
"@mysten/move-bytecode-template": "~0.4.0",
|
|
37
|
+
"@mysten/sui": "^2.31.0",
|
|
38
|
+
"@mysten/walrus": "~1.2.0",
|
|
39
|
+
"fflate": "^0.8.3",
|
|
40
|
+
"vue": "^3.5.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "~24.12.2",
|
|
44
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
45
|
+
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
46
|
+
"@playwright/test": "^1.56.0",
|
|
47
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
48
|
+
"@vue/test-utils": "^2.4.0",
|
|
49
|
+
"jsdom": "^29.1.1",
|
|
50
|
+
"oxfmt": "^0.59.0",
|
|
51
|
+
"oxlint": "~1.76.0",
|
|
52
|
+
"playwright": "^1.62.0",
|
|
53
|
+
"start-server-and-test": "^3.0.11",
|
|
54
|
+
"typedoc": "^0.28.20",
|
|
55
|
+
"typescript": "~6.0.0",
|
|
56
|
+
"vite": "^8.1.5",
|
|
57
|
+
"vite-plugin-vue-devtools": "^8.1.5",
|
|
58
|
+
"vitest": "^4.1.10",
|
|
59
|
+
"vue-tsc": "~3.3.0"
|
|
60
|
+
},
|
|
61
|
+
"optionalDependencies": {
|
|
62
|
+
"@meddleware/sui-token-template": "^1.0.0"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": "^22.18.0 || >=24.12.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// Mocked-RPC UI e2e. Standardised on Playwright (robust; replaces the former
|
|
4
|
+
// Cypress mocked suite, which exits SIGILL in the sandbox). The app is built with
|
|
5
|
+
// VITE_E2E=1 so main.ts registers the mock wallet + fetch mock (tree-shaken from
|
|
6
|
+
// prod). Real-chain e2e stays in scripts/e2e-deploy.mjs (npm run e2e:localnet…).
|
|
7
|
+
const PORT = 4173
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
testDir: './e2e',
|
|
11
|
+
fullyParallel: true,
|
|
12
|
+
forbidOnly: !!process.env.CI,
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
reporter: 'list',
|
|
15
|
+
use: {
|
|
16
|
+
baseURL: `http://localhost:${PORT}`,
|
|
17
|
+
viewport: { width: 1280, height: 800 },
|
|
18
|
+
trace: 'on-first-retry',
|
|
19
|
+
},
|
|
20
|
+
webServer: {
|
|
21
|
+
command: `npm run build:e2e && npm run preview -- --port ${PORT} --strictPort`,
|
|
22
|
+
url: `http://localhost:${PORT}/`,
|
|
23
|
+
reuseExistingServer: !process.env.CI,
|
|
24
|
+
timeout: 180_000,
|
|
25
|
+
},
|
|
26
|
+
projects: [
|
|
27
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
28
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
29
|
+
],
|
|
30
|
+
})
|
package/public/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sui-token-deployer.meddleware.co.uk
|
package/public/_headers
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Cloudflare Pages headers. Security baseline; connect-src stays broad because
|
|
2
|
+
# wallet extensions, Walrus relays, and per-network Sui RPCs vary.
|
|
3
|
+
/*
|
|
4
|
+
X-Content-Type-Options: nosniff
|
|
5
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
6
|
+
X-Frame-Options: DENY
|
|
7
|
+
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()
|
|
8
|
+
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
|
|
9
|
+
|
|
10
|
+
# Long-cache hashed build assets.
|
|
11
|
+
/assets/*
|
|
12
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* /index.html 200
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Token Deployer">
|
|
2
|
+
<circle cx="16" cy="16" r="15" fill="#2f6feb"/>
|
|
3
|
+
<circle cx="16" cy="16" r="15" fill="none" stroke="#1b4fc0" stroke-width="2"/>
|
|
4
|
+
<text x="16" y="21" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#fff" text-anchor="middle">S</text>
|
|
5
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Thin wrapper: localnet deploy e2e. The real implementation is the network-parametrized
|
|
2
|
+
// `e2e-deploy.mjs`; this just pins E2E_NETWORK=localnet so the historical entry point and its
|
|
3
|
+
// localnet defaults (fresh keypair + faucet, 0xfee… treasury, 1 SUI fee assertion) keep working.
|
|
4
|
+
//
|
|
5
|
+
// Prereq (as before): build the app with the testnet RPC pointed at the local node and a
|
|
6
|
+
// treasury set, then serve dist on APP_URL (default http://localhost:4173):
|
|
7
|
+
// VITE_RPC_TESTNET=http://127.0.0.1:9000 VITE_FEE_TREASURY_TESTNET=<addr> vite build
|
|
8
|
+
//
|
|
9
|
+
// Equivalent to `E2E_NETWORK=localnet node scripts/e2e-deploy.mjs` — or just `npm run e2e:localnet`.
|
|
10
|
+
|
|
11
|
+
process.env.E2E_NETWORK = 'localnet'
|
|
12
|
+
// Preserve the legacy FEE_ADDR override name for anyone still setting it.
|
|
13
|
+
if (process.env.FEE_ADDR && !process.env.E2E_FEE_ADDR) process.env.E2E_FEE_ADDR = process.env.FEE_ADDR
|
|
14
|
+
|
|
15
|
+
await import('./e2e-deploy.mjs')
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Network-parametrized headless-browser deploy e2e. Loads the built app, injects a
|
|
2
|
+
// wallet-standard wallet backed by a node-held keypair, drives the full UI, and performs a
|
|
3
|
+
// REAL publish against the target chain — then verifies coin type + operator fee on-chain.
|
|
4
|
+
//
|
|
5
|
+
// This is the single source of truth for the real-chain deploy path; `e2e-browser.mjs` is a
|
|
6
|
+
// thin localnet wrapper over it. It covers exactly what the mocked Cypress suite cannot: real
|
|
7
|
+
// confirmation, the result panel, the source-zip download, and on-chain verification.
|
|
8
|
+
//
|
|
9
|
+
// Two axes matter and are DISTINCT:
|
|
10
|
+
// - E2E_NETWORK = the physical chain the RPC talks to: localnet | testnet | mainnet.
|
|
11
|
+
// - APP_NETWORK = the network the app runs in (its only UI options are testnet/mainnet;
|
|
12
|
+
// it drives the `sui:<net>` chain label and which fee treasury applies).
|
|
13
|
+
// localnet is run as APP_NETWORK=testnet with the testnet RPC overridden to the local node,
|
|
14
|
+
// so the app believes it is on testnet while publishing to 127.0.0.1:9000. mainnet is the
|
|
15
|
+
// only case that changes the in-UI network selector.
|
|
16
|
+
//
|
|
17
|
+
// The npm scripts (e2e:localnet / e2e:testnet / e2e:mainnet) build the app with the matching
|
|
18
|
+
// VITE_RPC_* / VITE_FEE_TREASURY_* envs and serve dist on APP_URL before invoking this runner.
|
|
19
|
+
|
|
20
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
21
|
+
import { chromium } from 'playwright'
|
|
22
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
|
|
23
|
+
import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet'
|
|
24
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
|
|
25
|
+
import { Transaction } from '@mysten/sui/transactions'
|
|
26
|
+
import { toBase64 } from '@mysten/sui/utils'
|
|
27
|
+
|
|
28
|
+
const ZERO_ADDR = '0x' + '0'.repeat(64)
|
|
29
|
+
|
|
30
|
+
const NETWORK = process.env.E2E_NETWORK || 'localnet'
|
|
31
|
+
if (!['localnet', 'testnet', 'mainnet'].includes(NETWORK)) {
|
|
32
|
+
fail(`E2E_NETWORK must be localnet|testnet|mainnet (got "${NETWORK}")`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The app only ever runs in testnet or mainnet mode; localnet borrows testnet mode.
|
|
36
|
+
const APP_NETWORK = NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
|
|
37
|
+
|
|
38
|
+
const DEFAULT_RPC = {
|
|
39
|
+
localnet: 'http://127.0.0.1:9000',
|
|
40
|
+
testnet: 'https://sui-testnet-rpc.publicnode.com',
|
|
41
|
+
mainnet: 'https://fullnode.mainnet.sui.io:443',
|
|
42
|
+
}
|
|
43
|
+
const RPC = process.env.E2E_RPC || DEFAULT_RPC[NETWORK]
|
|
44
|
+
|
|
45
|
+
const APP_URL = process.env.APP_URL || 'http://localhost:4173'
|
|
46
|
+
const FAUCET = process.env.E2E_FAUCET || 'http://127.0.0.1:9123'
|
|
47
|
+
const FEE_ADDR = process.env.E2E_FEE_ADDR || (NETWORK === 'localnet' ? '0xfee' + '0'.repeat(61) : '')
|
|
48
|
+
const EXPECT_FEE_MIST = BigInt(process.env.E2E_FEE_MIST || '1000000000') // 1 SUI, matches VITE_FEE_MIST default
|
|
49
|
+
const RESULT_TIMEOUT = Number(process.env.E2E_RESULT_TIMEOUT || '60000')
|
|
50
|
+
|
|
51
|
+
function fail(msg) {
|
|
52
|
+
console.error('E2E FAIL:', msg)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Mainnet is fail-safe by default: it must never publish a real token — and spend real
|
|
58
|
+
// SUI — by accident or in CI. Require an explicit opt-in and abort cleanly otherwise.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
if (NETWORK === 'mainnet' && process.env.E2E_MAINNET_CONFIRM !== '1') {
|
|
61
|
+
console.log(
|
|
62
|
+
'e2e-deploy: mainnet run is gated. Set E2E_MAINNET_CONFIRM=1 to publish a REAL token on mainnet.\n' +
|
|
63
|
+
'Skipping (this is the expected, safe default).',
|
|
64
|
+
)
|
|
65
|
+
process.exit(0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Keypair + funding. localnet mints a throwaway key from the faucet; testnet/mainnet use an
|
|
70
|
+
// externally funded key supplied via SUI_PRIV (never a faucet on a real network).
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
let keypair
|
|
73
|
+
if (NETWORK === 'localnet') {
|
|
74
|
+
keypair = new Ed25519Keypair()
|
|
75
|
+
} else {
|
|
76
|
+
if (!process.env.SUI_PRIV) {
|
|
77
|
+
fail(
|
|
78
|
+
`SUI_PRIV is required for a ${NETWORK} run (bech32 suiprivkey1… — export the funded key with ` +
|
|
79
|
+
`\`sui keytool export --key-identity <addr>\`). Never funds from a faucet on ${NETWORK}.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
keypair = Ed25519Keypair.fromSecretKey(process.env.SUI_PRIV)
|
|
83
|
+
}
|
|
84
|
+
const address = keypair.toSuiAddress()
|
|
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 })
|
|
89
|
+
|
|
90
|
+
const EXPECT_FEE = Boolean(FEE_ADDR) && FEE_ADDR !== ZERO_ADDR
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
console.log(`e2e-deploy: E2E_NETWORK=${NETWORK} APP_NETWORK=${APP_NETWORK} RPC=${RPC}`)
|
|
94
|
+
|
|
95
|
+
if (NETWORK === 'localnet') {
|
|
96
|
+
await requestSuiFromFaucetV2({ host: FAUCET, recipient: address })
|
|
97
|
+
for (let i = 0; i < 25; i++) {
|
|
98
|
+
const b = await client.getBalance({ owner: address })
|
|
99
|
+
if (BigInt(b.totalBalance) > 0n) break
|
|
100
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const balance = BigInt((await client.getBalance({ owner: address })).totalBalance)
|
|
105
|
+
console.log('sender:', address, 'balance:', balance.toString(), 'MIST')
|
|
106
|
+
if (balance === 0n) fail(`sender ${address} has no SUI on ${NETWORK}`)
|
|
107
|
+
|
|
108
|
+
const feeBefore = EXPECT_FEE
|
|
109
|
+
? BigInt((await client.getBalance({ owner: FEE_ADDR })).totalBalance)
|
|
110
|
+
: 0n
|
|
111
|
+
|
|
112
|
+
// Browser resolution: explicit CHROME_PATH → a locally-installed Chrome → Playwright's
|
|
113
|
+
// bundled Chromium (CI installs it via `npx playwright install chromium`).
|
|
114
|
+
const chromePath =
|
|
115
|
+
process.env.CHROME_PATH ||
|
|
116
|
+
(existsSync('/usr/bin/google-chrome') ? '/usr/bin/google-chrome' : undefined)
|
|
117
|
+
const browser = await chromium.launch({ headless: true, executablePath: chromePath })
|
|
118
|
+
const page = await browser.newPage()
|
|
119
|
+
page.on('console', (m) => m.type() === 'error' && console.log('[browser error]', m.text()))
|
|
120
|
+
page.on('pageerror', (e) => console.log('[pageerror]', e.message))
|
|
121
|
+
|
|
122
|
+
// Node-side signer: build + sign the transaction the wallet forwards.
|
|
123
|
+
await page.exposeFunction('__mockSign', async (txJson) => {
|
|
124
|
+
const tx = Transaction.from(txJson)
|
|
125
|
+
const bytes = await tx.build({ client })
|
|
126
|
+
const { signature } = await keypair.signTransaction(bytes)
|
|
127
|
+
return { bytes: toBase64(bytes), signature }
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Inject a wallet-standard wallet before app scripts run. The account advertises the app
|
|
131
|
+
// network's chain so wallet-standard's required-feature/chain checks are satisfied.
|
|
132
|
+
await page.addInitScript(
|
|
133
|
+
({ address, pubkey, chain }) => {
|
|
134
|
+
const account = {
|
|
135
|
+
address,
|
|
136
|
+
publicKey: new Uint8Array(pubkey),
|
|
137
|
+
chains: [chain],
|
|
138
|
+
features: ['sui:signTransaction'],
|
|
139
|
+
label: 'Mock',
|
|
140
|
+
}
|
|
141
|
+
const wallet = {
|
|
142
|
+
version: '1.0.0',
|
|
143
|
+
name: 'Mock Test Wallet',
|
|
144
|
+
icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=',
|
|
145
|
+
chains: [chain],
|
|
146
|
+
accounts: [account],
|
|
147
|
+
features: {
|
|
148
|
+
'standard:connect': { version: '1.0.0', connect: async () => ({ accounts: [account] }) },
|
|
149
|
+
'standard:events': { version: '1.0.0', on: () => () => {} },
|
|
150
|
+
'sui:signTransaction': {
|
|
151
|
+
version: '2.0.0',
|
|
152
|
+
signTransaction: async (input) => {
|
|
153
|
+
const txJson = await input.transaction.toJSON()
|
|
154
|
+
return await window.__mockSign(txJson)
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
const callback = ({ register }) => register(wallet)
|
|
160
|
+
window.dispatchEvent(new CustomEvent('wallet-standard:register-wallet', { detail: callback }))
|
|
161
|
+
window.addEventListener('wallet-standard:app-ready', (e) => callback(e.detail))
|
|
162
|
+
},
|
|
163
|
+
{ address, pubkey: Array.from(keypair.getPublicKey().toRawBytes()), chain: `sui:${APP_NETWORK}` },
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
await page.goto(APP_URL, { waitUntil: 'networkidle' })
|
|
167
|
+
|
|
168
|
+
// Connect: open the dialog, select the app network if it differs from the testnet default,
|
|
169
|
+
// then pick the injected wallet.
|
|
170
|
+
await page.getByRole('button', { name: 'Connect Wallet' }).click()
|
|
171
|
+
if (APP_NETWORK !== 'testnet') {
|
|
172
|
+
await page.locator('#dialog-network-select').selectOption(APP_NETWORK)
|
|
173
|
+
}
|
|
174
|
+
await page.getByRole('button', { name: /Mock Test Wallet/ }).click()
|
|
175
|
+
await page.getByText(/Mock Test Wallet ·/).waitFor({ timeout: 10000 })
|
|
176
|
+
console.log('wallet connected in UI')
|
|
177
|
+
|
|
178
|
+
// Fill the form.
|
|
179
|
+
await page.locator('#name').fill('Browser Coin')
|
|
180
|
+
await page.locator('#symbol').fill('BRWC')
|
|
181
|
+
await page.locator('#description').fill('Deployed via headless browser e2e')
|
|
182
|
+
await page.locator('#decimals').fill('6')
|
|
183
|
+
await page.locator('#package').fill('browser_coin')
|
|
184
|
+
await page.locator('#module').fill('browsercoin')
|
|
185
|
+
|
|
186
|
+
await page.getByRole('button', { name: /Review & deploy/ }).click()
|
|
187
|
+
await page.getByRole('button', { name: /Confirm & deploy/ }).click()
|
|
188
|
+
console.log('confirmed; deploying (real publish + confirmation)…')
|
|
189
|
+
|
|
190
|
+
// Wait for the result screen — this is the confirmation the mocked suite cannot reach.
|
|
191
|
+
await page.getByRole('heading', { name: /is live on/ }).waitFor({ timeout: RESULT_TIMEOUT })
|
|
192
|
+
const coinType = await page.locator('dd.mono').first().innerText()
|
|
193
|
+
console.log('UI result coinType:', coinType)
|
|
194
|
+
|
|
195
|
+
// Verify the in-browser source-package download (generatePackage + zip + SPDX license fetch).
|
|
196
|
+
const [download] = await Promise.all([
|
|
197
|
+
page.waitForEvent('download', { timeout: 30000 }),
|
|
198
|
+
page.getByRole('button', { name: /Download source package/ }).click(),
|
|
199
|
+
])
|
|
200
|
+
const zipBuf = readFileSync(await download.path())
|
|
201
|
+
const isZip = zipBuf[0] === 0x50 && zipBuf[1] === 0x4b
|
|
202
|
+
console.log('download:', download.suggestedFilename(), zipBuf.length, 'bytes, zip:', isZip)
|
|
203
|
+
if (!isZip) fail('downloaded file is not a zip')
|
|
204
|
+
|
|
205
|
+
await browser.close()
|
|
206
|
+
|
|
207
|
+
// Independent on-chain verification.
|
|
208
|
+
if (!coinType.includes('::browsercoin::BROWSERCOIN')) fail(`unexpected coin type: ${coinType}`)
|
|
209
|
+
|
|
210
|
+
if (EXPECT_FEE) {
|
|
211
|
+
const feeAfter = BigInt((await client.getBalance({ owner: FEE_ADDR })).totalBalance)
|
|
212
|
+
const feeDelta = feeAfter - feeBefore
|
|
213
|
+
console.log('fee delivered this run:', feeDelta.toString(), 'MIST (to', FEE_ADDR + ')')
|
|
214
|
+
if (feeDelta !== EXPECT_FEE_MIST) fail(`expected ${EXPECT_FEE_MIST} MIST fee this run, got ${feeDelta}`)
|
|
215
|
+
} else {
|
|
216
|
+
console.log('no treasury configured — fee assertion skipped (app charges no fee)')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(`\nDEPLOY E2E PASS ✓ (${NETWORK}: injected wallet, real publish, result panel, zip${EXPECT_FEE ? ', fee delivered' : ''})`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((e) => fail(e.message))
|