@meddleware/token-deployer-ui 0.0.9 → 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.
package/src/config.ts CHANGED
@@ -7,16 +7,16 @@ import type { AccessGateConfig } from './lib/accessGate.js'
7
7
  const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
8
8
 
9
9
  /**
10
- * Public JSON-RPC fullnode URLs per network (overridable via `VITE_RPC_*`).
10
+ * Public gRPC fullnode URLs per network (overridable via `VITE_RPC_*`).
11
11
  *
12
- * NOTE: the official testnet fullnode (`fullnode.testnet.sui.io`) now serves gRPC
13
- * only and returns 404 for JSON-RPC, so a JSON-RPC-capable endpoint is used for
14
- * testnet. Operators should set `VITE_RPC_*` to their preferred (often paid) RPC
15
- * for production to avoid public rate limits.
12
+ * These are **gRPC(-web)** endpoints for `SuiGrpcClient` — public fullnodes have
13
+ * deprecated JSON-RPC, so a JSON-RPC-only URL will not work here. Operators should
14
+ * set `VITE_RPC_*` to their preferred (often paid) gRPC endpoint for production to
15
+ * avoid public rate limits.
16
16
  */
17
17
  export const RPC_URLS: Record<Network, string> = {
18
18
  mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
19
- testnet: env.VITE_RPC_TESTNET || 'https://sui-testnet-rpc.publicnode.com',
19
+ testnet: env.VITE_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
20
20
  localnet: env.VITE_RPC_LOCALNET || 'http://127.0.0.1:9000',
21
21
  }
22
22
 
@@ -89,15 +89,12 @@ export function isOperatorRelayConfigured(network: WalrusNetwork): boolean {
89
89
  /**
90
90
  * gRPC fullnode URL for the Walrus icon-upload client (a `SuiGrpcClient`).
91
91
  *
92
- * IMPORTANT: this is a **gRPC** endpoint and is intentionally *separate* from
93
- * {@link RPC_URLS} (the JSON-RPC endpoints used by the deploy path). The two protocols
94
- * are not interchangeable — pointing the Walrus gRPC client at a JSON-RPC URL breaks it —
95
- * so this has its own `VITE_WALRUS_RPC_*` override rather than reusing `VITE_RPC_*`.
96
- * Defaults to the public gRPC fullnodes (rate-limited); set your own for production.
92
+ * Defaults to {@link RPC_URLS} (also gRPC); `VITE_WALRUS_RPC_*` overrides it only when
93
+ * Walrus traffic should use a different fullnode from the deploy path.
97
94
  */
98
95
  export const WALRUS_RPC_URLS: Record<WalrusNetwork, string> = {
99
- testnet: env.VITE_WALRUS_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
100
- mainnet: env.VITE_WALRUS_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
96
+ testnet: env.VITE_WALRUS_RPC_TESTNET || RPC_URLS.testnet,
97
+ mainnet: env.VITE_WALRUS_RPC_MAINNET || RPC_URLS.mainnet,
101
98
  }
102
99
 
103
100
  /**
@@ -30,27 +30,41 @@ export interface OwnedAccessNft {
30
30
  usesRemaining: number | null
31
31
  }
32
32
 
33
- /** Minimal structural subset of the JSON-RPC client used here. */
33
+ /** Minimal structural subset of the core (gRPC) client used here. */
34
34
  export interface OwnedObjectsClient {
35
- getOwnedObjects(params: {
36
- owner: string
37
- filter?: { StructType: string }
38
- options?: { showContent?: boolean; showType?: boolean }
39
- }): Promise<{ data: unknown[] }>
35
+ core: {
36
+ listOwnedObjects(params: {
37
+ owner: string
38
+ type?: string
39
+ include?: { json?: boolean }
40
+ }): Promise<{ objects: unknown[] }>
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
46
+ * fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both.
47
+ */
48
+ function structFields(v: unknown): Record<string, any> | undefined {
49
+ if (!v || typeof v !== 'object') return undefined
50
+ const o = v as Record<string, any>
51
+ const nested = o.fields
52
+ return nested && typeof nested === 'object' ? (nested as Record<string, any>) : o
40
53
  }
41
54
 
42
55
  function parseUsesRemaining(variant: any): number | null {
43
56
  if (variant == null) return null
57
+ const tag: string | undefined = variant.variant ?? variant.type ?? variant.$kind
58
+ if (tag === 'UnlimitedPass') return null
44
59
  const fields = variant.fields ?? variant
45
60
  const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
46
61
  return ur != null ? Number(ur) : null
47
62
  }
48
63
 
49
- /** Parse a `getOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
64
+ /** Parse a `listOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
50
65
  export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
51
- const obj = entry?.data ?? entry
52
- const objectId: string | undefined = obj?.objectId
53
- const inner = obj?.content?.fields?.data?.fields
66
+ const objectId: string | undefined = entry?.objectId
67
+ const inner = structFields(structFields(entry?.json)?.data)
54
68
  const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
55
69
  if (!objectId || !gateId) return null
56
70
  return { objectId, gateId, usesRemaining: parseUsesRemaining(inner?.variant) }
@@ -63,12 +77,12 @@ export async function fetchAccessNfts(
63
77
  nftType: string,
64
78
  gateId?: string,
65
79
  ): Promise<OwnedAccessNft[]> {
66
- const { data } = await client.getOwnedObjects({
80
+ const { objects } = await client.core.listOwnedObjects({
67
81
  owner,
68
- filter: { StructType: nftType },
69
- options: { showContent: true, showType: true },
82
+ type: nftType,
83
+ include: { json: true },
70
84
  })
71
- const parsed = (data ?? [])
85
+ const parsed = (objects ?? [])
72
86
  .map(parseOwnedAccessNft)
73
87
  .filter((n): n is OwnedAccessNft => n !== null)
74
88
  return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
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 })
@@ -122,12 +122,12 @@ function renderMoveToml(cfg: TokenConfig): string {
122
122
  function renderSource(cfg: TokenConfig): string {
123
123
  let out = files['source.move']
124
124
 
125
- // license header (lines 1-2)
125
+ // license header (lines 1-2) — always rewritten, whatever licence the template itself carries
126
126
  const lines = out.split('\n')
127
127
  if (isProprietary(cfg.license)) {
128
128
  lines[0] = '// SPDX-License-Identifier: UNLICENSED'
129
129
  lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
130
- } else if (spdxId(cfg.license) !== 'CC0-1.0') {
130
+ } else {
131
131
  lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
132
132
  lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
133
133
  }
@@ -157,18 +157,18 @@ function renderPublishScript(cfg: TokenConfig): string {
157
157
  return out
158
158
  }
159
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
+
160
163
  function renderReadme(cfg: TokenConfig): string {
161
- let out = applyDocPlaceholders(files['README.md'], cfg)
164
+ const out = applyDocPlaceholders(files['README.md'], cfg)
162
165
  const licenseName = cfg.licenseName || spdxId(cfg.license)
163
- if (isProprietary(cfg.license)) {
164
- out = out.replace(
165
- /^CC0 1\.0 Universal.*$/m,
166
- 'All rights reserved. This package is proprietary and not licensed for redistribution.',
167
- )
168
- } else if (spdxId(cfg.license) !== 'CC0-1.0') {
169
- out = out.replace(/^CC0 1\.0 Universal.*$/m, `${licenseName} — see the LICENSE file.`)
170
- }
171
- 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
+ )
172
172
  }
173
173
 
174
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
 
@@ -19,25 +19,21 @@ export interface DeployedToken {
19
19
  /** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
20
20
  export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
21
21
  const client = getReadClient(network)
22
- // The StructType filter without type params matches every TreasuryCap<T> instantiation.
23
- const res = await client.getOwnedObjects({
24
- owner,
25
- filter: { StructType: '0x2::coin::TreasuryCap' },
26
- options: { showType: true },
27
- })
22
+ // A type filter without type params matches every TreasuryCap<T> instantiation.
23
+ const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
28
24
  const seen = new Set<string>()
29
25
  const out: DeployedToken[] = []
30
- for (const item of res.data ?? []) {
31
- const data = item.data
32
- const m = String(data?.type ?? '').match(/^0x2::coin::TreasuryCap<(.+)>$/)
33
- if (!m || !data) continue
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
34
30
  const coinType = m[1]
35
31
  if (seen.has(coinType)) continue
36
32
  seen.add(coinType)
37
33
  out.push({
38
34
  coinType,
39
35
  packageId: coinType.split('::')[0] ?? '',
40
- treasuryCapId: data.objectId,
36
+ treasuryCapId: obj.objectId,
41
37
  label: coinType.split('::').pop() ?? coinType,
42
38
  })
43
39
  }
@@ -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
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
@@ -8,7 +8,8 @@ import './component-styles.css'
8
8
  import { configureWasm } from './lib/template.js'
9
9
  // Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
10
10
  // import it), so a dynamic import in the E2E block below could not split it out anyway.
11
- import { getReadClient } from './lib/readClient.js'
11
+ import { getReadClient, setReadClient } from './lib/readClient.js'
12
+ import type { SuiGrpcClient } from '@mysten/sui/grpc'
12
13
  import { useColorMode, useSeason } from '@meddleware/ui'
13
14
 
14
15
  // Apply the colour mode before mount so there is no theme flash. Defaults to
@@ -25,72 +26,54 @@ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_b
25
26
  // doesn't block initial page load.
26
27
  configureWasm(wasmUrl)
27
28
 
28
- // 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.
29
30
  // Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
30
31
  // Tree-shaking removes this entire block from production when VITE_E2E !== '1'.
31
32
  ;(async () => {
32
33
  if (import.meta.env.VITE_E2E === '1') {
33
34
  const { getWallets } = await import('@mysten/wallet-standard')
34
35
 
35
- // Mock fetch globally for RPC requests so Cypress intercepts are bypassed entirely
36
- const originalFetch = globalThis.fetch
37
- 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)
38
40
  const packageId = '0x' + 'bb'.repeat(32)
39
- const treasureCapId = '0x' + 'cc'.repeat(32)
41
+ const treasuryCapId = '0x' + 'cc'.repeat(32)
40
42
  const metadataCapId = '0x' + 'dd'.repeat(32)
41
-
42
- const mockRpcResponse = {
43
- digest: publishDigest,
44
- effects: {
45
- status: { status: 'success' },
46
- gasUsed: { computationCost: '1000', storageCost: '2000', storageRebate: '0' },
47
- },
48
- objectChanges: [
49
- { type: 'published', packageId },
50
- {
51
- type: 'created',
52
- objectType: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
53
- 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
+ ],
54
61
  },
55
- {
56
- type: 'created',
57
- objectType: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
58
- objectId: metadataCapId,
62
+ objectTypes: {
63
+ [treasuryCapId]: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
64
+ [metadataCapId]: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
59
65
  },
60
- ],
61
- events: [],
62
- transaction: null,
63
- balanceChanges: null,
64
- }
65
-
66
- globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
67
- const url = typeof input === 'string' ? input : input.toString()
68
- const method = init?.method || 'GET'
69
- const isRpcCall = (url.includes('fullnode') || url.includes('rpc')) && method === 'POST'
70
-
71
- if (isRpcCall) {
72
- const body = init?.body ? JSON.parse(String(init.body)) : {}
73
- const rpcMethod = body.method
74
-
75
- if (import.meta.env.VITE_E2E === '1') {
76
- console.log('[E2E Fetch Mock]', rpcMethod, 'URL:', url)
77
- }
78
-
79
- const result =
80
- rpcMethod === 'sui_executeTransactionBlock' || rpcMethod === 'sui_getTransactionBlock'
81
- ? mockRpcResponse
82
- : rpcMethod === 'suix_getCoins' || rpcMethod === 'suix_getBalance'
83
- ? { data: [], nextCursor: null, hasNextPage: false }
84
- : null
85
-
86
- return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id || 1, result }), {
87
- status: 200,
88
- headers: { 'content-type': 'application/json' },
89
- })
90
- }
91
-
92
- return originalFetch(input, init)
66
+ },
93
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)
94
77
 
95
78
  const E2E_ADDR = '0x' + 'a'.repeat(64)
96
79
  const E2E_PUB_KEY = new Uint8Array(32)