@meddleware/token-deployer-ui 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +8 -11
  2. package/.github/workflows/e2e-realchain.yml +1 -1
  3. package/.github/workflows/node-ci.yml +9 -0
  4. package/.github/workflows/npm-publish.yml +16 -2
  5. package/.htmlvalidate.mjs +30 -0
  6. package/CLAUDE.md +14 -7
  7. package/Dockerfile +1 -1
  8. package/LICENSE +11 -118
  9. package/README.md +3 -5
  10. package/eslint.config.ts +63 -0
  11. package/index.html +11 -2
  12. package/package.json +32 -10
  13. package/postcss.config.js +8 -0
  14. package/public/og-image.png +0 -0
  15. package/public/robots.txt +1 -1
  16. package/public/sitemap.xml +1 -1
  17. package/scripts/e2e-deploy.mjs +22 -17
  18. package/scripts/e2e-localnet.mjs +14 -12
  19. package/scripts/e2e-walrus-browser.mjs +1 -4
  20. package/scripts/gen-og-image.mjs +2 -2
  21. package/scripts/publish-localnet.mjs +5 -4
  22. package/src/App.vue +8 -6
  23. package/src/component-styles.css +5 -38
  24. package/src/components/ConfigForm.vue +74 -28
  25. package/src/components/DeployProgress.vue +2 -4
  26. package/src/components/GithubPush.vue +22 -7
  27. package/src/components/IconPicker.vue +103 -42
  28. package/src/components/LicensePicker.vue +66 -66
  29. package/src/components/MyTokens.vue +125 -0
  30. package/src/components/ResultPanel.vue +2 -4
  31. package/src/components/ReviewPanel.vue +1 -1
  32. package/src/components/TermsDialog.vue +28 -27
  33. package/src/components/TokenDeployerView.vue +109 -40
  34. package/src/components/WalletBar.vue +67 -78
  35. package/src/components/WalrusBlobBrowser.vue +33 -25
  36. package/src/composables/useAccessGate.ts +7 -2
  37. package/src/composables/useWalrusRelay.ts +1 -1
  38. package/src/config.ts +10 -13
  39. package/src/lib/accessGate.ts +28 -14
  40. package/src/lib/buildPublishTx.ts +1 -1
  41. package/src/lib/deploy.ts +36 -0
  42. package/src/lib/deployExecutor.ts +14 -12
  43. package/src/lib/generatePackage.ts +18 -13
  44. package/src/lib/licenses.ts +4 -3
  45. package/src/lib/listMyTokens.ts +41 -0
  46. package/src/lib/readClient.ts +15 -7
  47. package/src/lib/types.ts +1 -1
  48. package/src/lib/validation.ts +8 -4
  49. package/src/lib/walrus-constants.ts +1 -1
  50. package/src/lib/walrus.ts +22 -11
  51. package/src/main.ts +46 -58
  52. package/src/styles.css +13 -5
  53. package/src/template-src/files.json +7 -7
  54. package/stylelint.config.js +33 -0
  55. package/test-results/.last-run.json +1 -1
  56. package/tests/accessGate.test.ts +26 -21
  57. package/tests/deploy.test.ts +70 -2
  58. package/tests/generatePackage.test.ts +11 -0
  59. package/tests/licenses.test.ts +6 -3
  60. package/tests/listMyTokens.test.ts +30 -0
  61. package/tests/useAccessGate.test.ts +15 -19
  62. package/typedoc.json +2 -2
  63. package/vite.config.ts +9 -1
  64. package/src/components/WalrusIconUpload.vue +0 -205
package/src/lib/deploy.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Decoupled from the wallet via an `Executor` so it is unit-testable.
3
3
 
4
4
  import type { Transaction } from '@mysten/sui/transactions'
5
+ import type { SuiClientTypes } from '@mysten/sui/client'
5
6
  import { patchTemplateModule } from './template.js'
6
7
  import {
7
8
  buildPublishTransaction,
@@ -25,6 +26,41 @@ export interface SuiTxResult {
25
26
  effects?: { status?: { status?: string; error?: string } }
26
27
  }
27
28
 
29
+ /** The core-API execution result {@link toSuiTxResult} consumes (effects + object types). */
30
+ export type CoreExecutionResult = SuiClientTypes.TransactionResult<{
31
+ effects: true
32
+ objectTypes: true
33
+ }>
34
+
35
+ /**
36
+ * Map a core-API (gRPC) execution result onto {@link SuiTxResult}: created/mutated objects
37
+ * come from `effects.changedObjects` (types from the `objectTypes` map), and a newly written
38
+ * package becomes the `published` entry. Failed transactions are returned with a failure
39
+ * status (not thrown) so {@link deployToken}'s `assertSuccess` reports them uniformly.
40
+ */
41
+ export function toSuiTxResult(res: CoreExecutionResult): SuiTxResult {
42
+ const tx = res.Transaction ?? res.FailedTransaction
43
+ const types = tx.objectTypes ?? {}
44
+ const objectChanges: NonNullable<SuiTxResult['objectChanges']> = []
45
+ for (const c of tx.effects?.changedObjects ?? []) {
46
+ if (c.outputState === 'PackageWrite') {
47
+ if (c.idOperation === 'Created') objectChanges.push({ type: 'published', packageId: c.objectId })
48
+ } else if (c.outputState === 'ObjectWrite') {
49
+ objectChanges.push({
50
+ type: c.idOperation === 'Created' ? 'created' : 'mutated',
51
+ objectId: c.objectId,
52
+ objectType: types[c.objectId],
53
+ version: c.outputVersion ?? undefined,
54
+ digest: c.outputDigest ?? undefined,
55
+ })
56
+ }
57
+ }
58
+ const status = tx.status.success
59
+ ? { status: 'success' }
60
+ : { status: 'failure', error: tx.status.error.message }
61
+ return { digest: tx.digest, objectChanges, effects: { status } }
62
+ }
63
+
28
64
  export interface Executor {
29
65
  /** Sign with the wallet and execute; must request object changes + effects. */
30
66
  signAndExecute(tx: Transaction): Promise<SuiTxResult>
@@ -1,23 +1,25 @@
1
1
  // Builds a deploy Executor using the wallet-adapter's shared signing capability
2
- // + SuiClient for execution (returns objectChanges required by extractPublishResult).
2
+ // + the app's gRPC client for execution (returns the object changes required by
3
+ // extractPublishResult).
3
4
  //
4
5
  // The wallet-adapter's own Executor only returns { digest } which is not sufficient
5
6
  // for the token deploy flow; we sign via the shared wallet connection but execute
6
- // via SuiClient so we can request showEffects + showObjectChanges.
7
+ // via our own client so we can request effects + object types.
7
8
 
8
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
9
+ import { fromBase64 } from '@mysten/sui/utils'
9
10
  import { useWallet } from '@meddleware/wallet-adapter'
10
11
  import type { Transaction } from '@mysten/sui/transactions'
12
+ import { toSuiTxResult } from './deploy.js'
11
13
  import type { SuiTxResult, Executor } from './deploy.js'
12
14
  import type { Network } from './types.js'
13
- import { RPC_URLS } from '../config.js'
15
+ import { getReadClient } from './readClient.js'
14
16
 
15
17
  /**
16
18
  * Build a deploy Executor bound to the currently connected wallet (via the
17
- * wallet-adapter singleton) and the given network's RPC URL.
19
+ * wallet-adapter singleton) and the given network's gRPC client.
18
20
  *
19
21
  * The returned executor signs via `sui:signTransaction` and executes via
20
- * `SuiClient.executeTransactionBlock` with `showEffects + showObjectChanges`
22
+ * `executeTransaction` with `effects + objectTypes`, mapped by {@link toSuiTxResult}
21
23
  * so the caller can extract the published package ID, coin type, TreasuryCap, etc.
22
24
  */
23
25
  export async function buildDeployExecutor(network: Network): Promise<Executor> {
@@ -26,7 +28,7 @@ export async function buildDeployExecutor(network: Network): Promise<Executor> {
26
28
  const acct = account.value
27
29
  if (!wallet || !acct) throw new Error('Connect a wallet first.')
28
30
 
29
- const suiClient = new SuiJsonRpcClient({ url: RPC_URLS[network], network })
31
+ const suiClient = getReadClient(network)
30
32
  const chain = `sui:${network}` as const
31
33
 
32
34
  const signFeature = wallet.features['sui:signTransaction'] as
@@ -47,12 +49,12 @@ export async function buildDeployExecutor(network: Network): Promise<Executor> {
47
49
  account: acct,
48
50
  chain,
49
51
  })
50
- const res = await suiClient.executeTransactionBlock({
51
- transactionBlock: bytes,
52
- signature,
53
- options: { showEffects: true, showObjectChanges: true },
52
+ const res = await suiClient.executeTransaction({
53
+ transaction: fromBase64(bytes),
54
+ signatures: [signature],
55
+ include: { effects: true, objectTypes: true },
54
56
  })
55
- return res as unknown as SuiTxResult
57
+ return toSuiTxResult(res)
56
58
  },
57
59
  async waitForTransaction(digest: string): Promise<void> {
58
60
  await suiClient.waitForTransaction({ digest })
@@ -11,7 +11,12 @@
11
11
  import { zipSync, strToU8 } from 'fflate'
12
12
  import templateFiles from '../template-src/files.json'
13
13
  import type { PublishResult, TokenConfig } from './types.js'
14
- import { SAFE_TEXT, deriveStructName, validateIdentifier, hasAllowedIconScheme } from './validation.js'
14
+ import {
15
+ SAFE_TEXT,
16
+ deriveStructName,
17
+ validateIdentifier,
18
+ hasAllowedIconScheme,
19
+ } from './validation.js'
15
20
 
16
21
  const files = templateFiles as Record<string, string>
17
22
 
@@ -117,12 +122,12 @@ function renderMoveToml(cfg: TokenConfig): string {
117
122
  function renderSource(cfg: TokenConfig): string {
118
123
  let out = files['source.move']
119
124
 
120
- // license header (lines 1-2)
125
+ // license header (lines 1-2) — always rewritten, whatever licence the template itself carries
121
126
  const lines = out.split('\n')
122
127
  if (isProprietary(cfg.license)) {
123
128
  lines[0] = '// SPDX-License-Identifier: UNLICENSED'
124
129
  lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
125
- } else if (spdxId(cfg.license) !== 'CC0-1.0') {
130
+ } else {
126
131
  lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
127
132
  lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
128
133
  }
@@ -152,18 +157,18 @@ function renderPublishScript(cfg: TokenConfig): string {
152
157
  return out
153
158
  }
154
159
 
160
+ /** The template README's licence line, whatever licence the template itself ships with. */
161
+ const TEMPLATE_LICENSE_LINE = /^(?:CC0 1\.0 Universal|BSD Zero Clause License).*$/m
162
+
155
163
  function renderReadme(cfg: TokenConfig): string {
156
- let out = applyDocPlaceholders(files['README.md'], cfg)
164
+ const out = applyDocPlaceholders(files['README.md'], cfg)
157
165
  const licenseName = cfg.licenseName || spdxId(cfg.license)
158
- if (isProprietary(cfg.license)) {
159
- out = out.replace(
160
- /^CC0 1\.0 Universal.*$/m,
161
- 'All rights reserved. This package is proprietary and not licensed for redistribution.',
162
- )
163
- } else if (spdxId(cfg.license) !== 'CC0-1.0') {
164
- out = out.replace(/^CC0 1\.0 Universal.*$/m, `${licenseName} — see the LICENSE file.`)
165
- }
166
- return out
166
+ return out.replace(
167
+ TEMPLATE_LICENSE_LINE,
168
+ isProprietary(cfg.license)
169
+ ? 'All rights reserved. This package is proprietary and not licensed for redistribution.'
170
+ : `${licenseName} — see the LICENSE file.`,
171
+ )
167
172
  }
168
173
 
169
174
  /**
@@ -23,9 +23,9 @@ export const NO_LICENSE: SpdxLicense = {
23
23
  deprecated: false,
24
24
  }
25
25
 
26
- /** Ids surfaced first in the picker. CC0 is the project default. */
26
+ /** Ids surfaced first in the picker. 0BSD is the project default; CC0 stays available. */
27
27
  export const POPULAR_LICENSE_IDS = [
28
- 'CC0-1.0',
28
+ '0BSD',
29
29
  'MIT',
30
30
  'Apache-2.0',
31
31
  'BSD-3-Clause',
@@ -37,9 +37,10 @@ export const POPULAR_LICENSE_IDS = [
37
37
  'MPL-2.0',
38
38
  'ISC',
39
39
  'Unlicense',
40
+ 'CC0-1.0',
40
41
  ] as const
41
42
 
42
- export const DEFAULT_LICENSE_ID = 'CC0-1.0'
43
+ export const DEFAULT_LICENSE_ID = '0BSD'
43
44
 
44
45
  type Fetcher = typeof fetch
45
46
 
@@ -0,0 +1,41 @@
1
+ // Discover coins the connected wallet has deployed, by listing the TreasuryCap<T> objects it
2
+ // owns (the deploy sends the TreasuryCap to the sender). Read-only; mirrors access-gate's
3
+ // AdminCap-based "My Gates" discovery.
4
+ import { getReadClient } from './readClient.js'
5
+ import type { Network } from './types.js'
6
+
7
+ /** One deployed coin the wallet controls (holds the TreasuryCap for). */
8
+ export interface DeployedToken {
9
+ /** Full coin type, e.g. `0x<pkg>::mytoken::MYTOKEN`. */
10
+ coinType: string
11
+ /** The published package id (first segment of the coin type). */
12
+ packageId: string
13
+ /** Object id of the owned `TreasuryCap<T>`. */
14
+ treasuryCapId: string
15
+ /** The one-time-witness struct name (last segment) — a compact display label. */
16
+ label: string
17
+ }
18
+
19
+ /** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
20
+ export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
21
+ const client = getReadClient(network)
22
+ // A type filter without type params matches every TreasuryCap<T> instantiation.
23
+ const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
24
+ const seen = new Set<string>()
25
+ const out: DeployedToken[] = []
26
+ for (const obj of objects) {
27
+ // gRPC may render the framework address in long form (0x000…02).
28
+ const m = obj.type.match(/^0x0*2::coin::TreasuryCap<(.+)>$/)
29
+ if (!m) continue
30
+ const coinType = m[1]
31
+ if (seen.has(coinType)) continue
32
+ seen.add(coinType)
33
+ out.push({
34
+ coinType,
35
+ packageId: coinType.split('::')[0] ?? '',
36
+ treasuryCapId: obj.objectId,
37
+ label: coinType.split('::').pop() ?? coinType,
38
+ })
39
+ }
40
+ return out
41
+ }
@@ -1,17 +1,25 @@
1
- // Cached SuiClient per network for read-only queries (getOwnedObjects, etc.).
2
- // Replaces getSuiClient from the deleted useSuiClient.ts composable.
3
- import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
1
+ // Cached gRPC client per network, shared by read-only queries (listOwnedObjects, etc.) and
2
+ // the deploy executor. Public fullnodes have deprecated JSON-RPC, so this is a SuiGrpcClient.
3
+ import { SuiGrpcClient } from '@mysten/sui/grpc'
4
4
  import type { Network } from './types.js'
5
5
  import { RPC_URLS } from '../config.js'
6
6
 
7
- const clients = new Map<Network, SuiJsonRpcClient>()
7
+ const clients = new Map<Network, SuiGrpcClient>()
8
8
 
9
- /** Return a memoised {@link SuiJsonRpcClient} for the given network. */
10
- export function getReadClient(network: Network): SuiJsonRpcClient {
9
+ /** Return a memoised {@link SuiGrpcClient} for the given network. */
10
+ export function getReadClient(network: Network): SuiGrpcClient {
11
11
  let c = clients.get(network)
12
12
  if (!c) {
13
- c = new SuiJsonRpcClient({ url: RPC_URLS[network], network })
13
+ c = new SuiGrpcClient({ network, baseUrl: RPC_URLS[network] })
14
14
  clients.set(network, c)
15
15
  }
16
16
  return c
17
17
  }
18
+
19
+ /**
20
+ * Replace the client for `network` (E2E builds only — lets `main.ts` inject a stub client in
21
+ * place of the network, since gRPC-web responses are binary and cannot be faked via `fetch`).
22
+ */
23
+ export function setReadClient(network: Network, client: SuiGrpcClient): void {
24
+ clients.set(network, client)
25
+ }
package/src/lib/types.ts CHANGED
@@ -41,7 +41,7 @@ export interface TokenConfig {
41
41
  /** Address that receives the caps + initial supply. Defaults to the connected wallet. */
42
42
  recipient: string
43
43
 
44
- /** SPDX id (e.g. "MIT", "CC0-1.0") or "NONE" for proprietary. Affects the downloadable package only. */
44
+ /** SPDX id (e.g. "0BSD", "MIT") or "NONE" for proprietary. Affects the downloadable package only. */
45
45
  license: string
46
46
  /** Human-readable license name for the README (defaults to the SPDX id). */
47
47
  licenseName?: string
@@ -123,18 +123,22 @@ export function validateForm(form: ValidatableForm): FormErrors {
123
123
 
124
124
  if (!form.symbol.trim()) errors.symbol = 'Required'
125
125
  else if (form.symbol.length > 32) errors.symbol = 'Maximum 32 characters'
126
- else if (!SAFE_TEXT.test(form.symbol)) errors.symbol = 'Double quotes and backslashes are not supported'
126
+ else if (!SAFE_TEXT.test(form.symbol))
127
+ errors.symbol = 'Double quotes and backslashes are not supported'
127
128
 
128
129
  if (!form.name.trim()) errors.name = 'Required'
129
130
  else if (form.name.length > 64) errors.name = 'Maximum 64 characters'
130
- else if (!SAFE_TEXT.test(form.name)) errors.name = 'Double quotes and backslashes are not supported'
131
+ else if (!SAFE_TEXT.test(form.name))
132
+ errors.name = 'Double quotes and backslashes are not supported'
131
133
 
132
134
  if (form.description.length > 256) errors.description = 'Maximum 256 characters'
133
- else if (!SAFE_TEXT.test(form.description)) errors.description = 'Double quotes and backslashes are not supported'
135
+ else if (!SAFE_TEXT.test(form.description))
136
+ errors.description = 'Double quotes and backslashes are not supported'
134
137
 
135
138
  if (form.iconUrl) {
136
139
  if (form.iconUrl.length > 512) errors.iconUrl = 'Maximum 512 characters'
137
- else if (!SAFE_TEXT.test(form.iconUrl)) errors.iconUrl = 'Double quotes and backslashes are not supported'
140
+ else if (!SAFE_TEXT.test(form.iconUrl))
141
+ errors.iconUrl = 'Double quotes and backslashes are not supported'
138
142
  else if (!hasAllowedIconScheme(form.iconUrl)) errors.iconUrl = 'Use an https:// or ipfs:// URL'
139
143
  }
140
144
 
@@ -1,5 +1,5 @@
1
1
  // Lightweight Walrus constants with NO heavy imports, so both the (lazily loaded)
2
- // `lib/walrus.ts` client and the eagerly rendered `WalrusIconUpload.vue` component
2
+ // `lib/walrus.ts` client and the eagerly rendered `IconPicker.vue` component
3
3
  // can share a single source of truth without pulling `@mysten/walrus` into the
4
4
  // component's chunk.
5
5
 
package/src/lib/walrus.ts CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { SuiGrpcClient } from '@mysten/sui/grpc'
14
14
  import { walrus, blobIdFromInt } from '@mysten/walrus'
15
- import type { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
15
+ import type { ClientWithCoreApi } from '@mysten/sui/client'
16
16
  import { WALRUS_RELAY_HOSTS, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS } from '../config.js'
17
17
 
18
18
  export { ICON_EPOCHS } from './walrus-constants.js'
@@ -107,33 +107,44 @@ export interface OwnedBlob {
107
107
  certified: boolean
108
108
  }
109
109
 
110
+ /**
111
+ * Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
112
+ * fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both, since the
113
+ * SDK documents that the `json` shape may vary between API implementations.
114
+ */
115
+ function structFields(v: unknown): Record<string, unknown> | undefined {
116
+ if (!v || typeof v !== 'object') return undefined
117
+ const o = v as Record<string, unknown>
118
+ const nested = o.fields
119
+ return nested && typeof nested === 'object' ? (nested as Record<string, unknown>) : o
120
+ }
121
+
110
122
  /**
111
123
  * Return all Walrus blobs owned by `owner`. Uses `getBlobType()` for dynamic
112
124
  * package resolution so no hardcoded addresses are needed.
113
125
  */
114
126
  export async function fetchOwnedWalrusBlobs(
115
- suiClient: SuiJsonRpcClient,
127
+ suiClient: ClientWithCoreApi,
116
128
  walrusClient: ReturnType<typeof createWalrusClient>,
117
129
  owner: string,
118
130
  ): Promise<OwnedBlob[]> {
119
131
  const blobType = await walrusClient.walrus.getBlobType()
120
- const { data } = await suiClient.getOwnedObjects({
132
+ const { objects } = await suiClient.core.listOwnedObjects({
121
133
  owner,
122
- filter: { StructType: blobType },
123
- options: { showContent: true },
134
+ type: blobType,
135
+ include: { json: true },
124
136
  })
125
137
  const blobs: OwnedBlob[] = []
126
- for (const item of data) {
127
- if (!item.data) continue
128
- const fields = (item.data.content as { fields?: Record<string, unknown> } | undefined)?.fields
138
+ for (const obj of objects) {
139
+ const fields = structFields(obj.json)
129
140
  if (!fields) continue
130
- const storage = fields.storage as { fields?: { end_epoch?: unknown } } | undefined
141
+ const storage = structFields(fields.storage)
131
142
  try {
132
143
  blobs.push({
133
- objectId: item.data.objectId,
144
+ objectId: obj.objectId,
134
145
  blobId: blobIdFromInt(BigInt(fields.blob_id as string)),
135
146
  size: Number(fields.size),
136
- endEpoch: Number(storage?.fields?.end_epoch ?? 0),
147
+ endEpoch: Number(storage?.end_epoch ?? 0),
137
148
  certified: fields.certified_epoch !== null && fields.certified_epoch !== undefined,
138
149
  })
139
150
  } catch {
package/src/main.ts CHANGED
@@ -6,12 +6,19 @@ import App from './App.vue'
6
6
  import './styles.css'
7
7
  import './component-styles.css'
8
8
  import { configureWasm } from './lib/template.js'
9
- import { useColorMode } from '@meddleware/ui'
9
+ // Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
10
+ // import it), so a dynamic import in the E2E block below could not split it out anyway.
11
+ import { getReadClient, setReadClient } from './lib/readClient.js'
12
+ import type { SuiGrpcClient } from '@mysten/sui/grpc'
13
+ import { useColorMode, useSeason } from '@meddleware/ui'
10
14
 
11
15
  // Apply the colour mode before mount so there is no theme flash. Defaults to
12
16
  // `system` (follows the OS) while enabling the in-app light/dark/system control.
13
17
  useColorMode('system')
14
18
 
19
+ // Enable seasonal theming (sets data-season on <html>; seasons.css is imported above).
20
+ useSeason()
21
+
15
22
  // Vite serves the wasm as an asset URL; wasm-bindgen's init loads it on demand.
16
23
  import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_bg.wasm?url'
17
24
 
@@ -19,73 +26,54 @@ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_b
19
26
  // doesn't block initial page load.
20
27
  configureWasm(wasmUrl)
21
28
 
22
- // E2E-only: register mock wallet and expose getSuiClient for headless tests.
29
+ // E2E-only: stub the Sui client, register a mock wallet and expose getSuiClient for headless tests.
23
30
  // Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
24
31
  // Tree-shaking removes this entire block from production when VITE_E2E !== '1'.
25
32
  ;(async () => {
26
33
  if (import.meta.env.VITE_E2E === '1') {
27
34
  const { getWallets } = await import('@mysten/wallet-standard')
28
- const { getReadClient: getSuiClient } = await import('./lib/readClient.js')
29
35
 
30
- // Mock fetch globally for RPC requests so Cypress intercepts are bypassed entirely
31
- const originalFetch = globalThis.fetch
32
- const publishDigest = '0x' + 'aa'.repeat(32)
36
+ // Stub gRPC client in place of the network (gRPC-web is binary, so it cannot be faked
37
+ // at the fetch layer). Returns a successful publish whose effects carry the created
38
+ // TreasuryCap/MetadataCap, and empty owned-object lists.
39
+ const publishDigest = 'E2E' + 'a'.repeat(41)
33
40
  const packageId = '0x' + 'bb'.repeat(32)
34
- const treasureCapId = '0x' + 'cc'.repeat(32)
41
+ const treasuryCapId = '0x' + 'cc'.repeat(32)
35
42
  const metadataCapId = '0x' + 'dd'.repeat(32)
36
-
37
- const mockRpcResponse = {
38
- digest: publishDigest,
39
- effects: {
40
- status: { status: 'success' },
41
- gasUsed: { computationCost: '1000', storageCost: '2000', storageRebate: '0' },
42
- },
43
- objectChanges: [
44
- { type: 'published', packageId },
45
- {
46
- type: 'created',
47
- objectType: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
48
- objectId: treasureCapId,
43
+ const created = (objectId: string) => ({
44
+ objectId,
45
+ outputState: 'ObjectWrite',
46
+ idOperation: 'Created',
47
+ outputVersion: '1',
48
+ outputDigest: publishDigest,
49
+ })
50
+ const executed = {
51
+ $kind: 'Transaction',
52
+ Transaction: {
53
+ digest: publishDigest,
54
+ status: { success: true, error: null },
55
+ effects: {
56
+ changedObjects: [
57
+ { objectId: packageId, outputState: 'PackageWrite', idOperation: 'Created' },
58
+ created(treasuryCapId),
59
+ created(metadataCapId),
60
+ ],
49
61
  },
50
- {
51
- type: 'created',
52
- objectType: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
53
- objectId: metadataCapId,
62
+ objectTypes: {
63
+ [treasuryCapId]: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
64
+ [metadataCapId]: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
54
65
  },
55
- ],
56
- events: [],
57
- transaction: null,
58
- balanceChanges: null,
59
- }
60
-
61
- globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
62
- const url = typeof input === 'string' ? input : input.toString()
63
- const method = init?.method || 'GET'
64
- const isRpcCall = (url.includes('fullnode') || url.includes('rpc')) && method === 'POST'
65
-
66
- if (isRpcCall) {
67
- const body = init?.body ? JSON.parse(String(init.body)) : {}
68
- const rpcMethod = body.method
69
-
70
- if (import.meta.env.VITE_E2E === '1') {
71
- console.log('[E2E Fetch Mock]', rpcMethod, 'URL:', url)
72
- }
73
-
74
- const result =
75
- rpcMethod === 'sui_executeTransactionBlock' || rpcMethod === 'sui_getTransactionBlock'
76
- ? mockRpcResponse
77
- : rpcMethod === 'suix_getCoins' || rpcMethod === 'suix_getBalance'
78
- ? { data: [], nextCursor: null, hasNextPage: false }
79
- : null
80
-
81
- return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id || 1, result }), {
82
- status: 200,
83
- headers: { 'content-type': 'application/json' },
84
- })
85
- }
86
-
87
- return originalFetch(input, init)
66
+ },
88
67
  }
68
+ const noObjects = async () => ({ objects: [], hasNextPage: false, cursor: null })
69
+ const stubClient = {
70
+ executeTransaction: async () => executed,
71
+ waitForTransaction: async () => executed,
72
+ listOwnedObjects: noObjects,
73
+ core: { listOwnedObjects: noObjects },
74
+ } as unknown as SuiGrpcClient
75
+ setReadClient('testnet', stubClient)
76
+ setReadClient('mainnet', stubClient)
89
77
 
90
78
  const E2E_ADDR = '0x' + 'a'.repeat(64)
91
79
  const E2E_PUB_KEY = new Uint8Array(32)
@@ -151,7 +139,7 @@ configureWasm(wasmUrl)
151
139
  }
152
140
 
153
141
  // Expose getSuiClient for tests to build transactions
154
- w.__getSuiClient = getSuiClient
142
+ w.__getSuiClient = getReadClient
155
143
  }
156
144
 
157
145
  createApp(App).mount('#app')
package/src/styles.css CHANGED
@@ -2,12 +2,18 @@
2
2
  Token variables come from @meddleware/design-tokens, imported before this.
3
3
  Component utility classes live in component-styles.css. */
4
4
 
5
- .theme-bar {
6
- display: flex;
7
- justify-content: flex-end;
5
+ /* The colour-mode control is the page header's first grid item, aligned to the inline end. */
6
+ .theme-toggle {
7
+ justify-self: end;
8
8
  margin-bottom: 0.75rem;
9
9
  }
10
10
 
11
+ .page-footer {
12
+ margin-top: 3rem;
13
+ color: var(--muted);
14
+ font-size: 0.9rem;
15
+ }
16
+
11
17
  body {
12
18
  margin: 0;
13
19
  background: var(--bg);
@@ -21,12 +27,14 @@ body {
21
27
  }
22
28
 
23
29
  main {
24
- max-width: 760px;
30
+ max-width: var(--mw-tool-content-max);
25
31
  margin: 0 auto;
26
32
  padding: 1.5rem 1.25rem 4rem;
27
33
  }
28
34
 
29
- header {
35
+ /* A single-column grid so the colour-mode control can sit at the inline end without a wrapper. */
36
+ .page-header {
37
+ display: grid;
30
38
  padding-bottom: 1.5rem;
31
39
  border-bottom: 1px solid var(--border);
32
40
  margin-bottom: 1.5rem;