@meddleware/token-deployer-ui 0.0.9 → 0.0.11

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 (93) hide show
  1. package/LICENSE +11 -118
  2. package/README.md +11 -10
  3. package/package.json +22 -10
  4. package/src/App.vue +1 -1
  5. package/src/component-styles.css +39 -57
  6. package/src/components/GithubPush.vue +2 -2
  7. package/src/components/IconPicker.vue +44 -75
  8. package/src/components/LicensePicker.vue +4 -3
  9. package/src/components/ResultPanel.vue +2 -2
  10. package/src/components/ReviewPanel.vue +11 -3
  11. package/src/components/TokenDeployerView.vue +52 -48
  12. package/src/components/WalletBar.vue +12 -1
  13. package/src/components/WalrusBlobBrowser.vue +4 -4
  14. package/src/composables/useWalrusRelay.ts +1 -2
  15. package/src/config.ts +29 -30
  16. package/src/lib/buildPublishTx.ts +0 -10
  17. package/src/lib/deploy.ts +45 -3
  18. package/src/lib/deployExecutor.ts +19 -44
  19. package/src/lib/generatePackage.ts +12 -12
  20. package/src/lib/licenses.ts +4 -3
  21. package/src/lib/listMyTokens.ts +7 -11
  22. package/src/lib/readClient.ts +15 -7
  23. package/src/lib/template.ts +3 -0
  24. package/src/lib/types.ts +1 -1
  25. package/src/lib/walrus-constants.ts +2 -2
  26. package/src/main.ts +42 -61
  27. package/src/styles.css +22 -0
  28. package/src/template-src/files.json +7 -7
  29. package/.claude/move-language-reference/SKILL.md +0 -445
  30. package/.claude/skills/move-language-reference/SKILL.md +0 -445
  31. package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
  32. package/.claude/sui-move-bootcamp/SKILL.md +0 -128
  33. package/.editorconfig +0 -8
  34. package/.env.example +0 -75
  35. package/.gitattributes +0 -1
  36. package/.github/workflows/docker-publish.yml +0 -348
  37. package/.github/workflows/e2e-realchain.yml +0 -64
  38. package/.github/workflows/node-ci.yml +0 -43
  39. package/.github/workflows/npm-publish.yml +0 -83
  40. package/.htmlvalidate.mjs +0 -30
  41. package/.oxfmtrc.json +0 -5
  42. package/.oxlintrc.json +0 -10
  43. package/.vscode/extensions.json +0 -9
  44. package/AGENTS.md +0 -39
  45. package/CLAUDE.md +0 -159
  46. package/Dockerfile +0 -96
  47. package/eslint.config.ts +0 -63
  48. package/index.html +0 -13
  49. package/playwright.config.ts +0 -30
  50. package/postcss.config.js +0 -8
  51. package/public/CNAME +0 -1
  52. package/public/_headers +0 -12
  53. package/public/_redirects +0 -1
  54. package/public/favicon.ico +0 -0
  55. package/public/favicon.svg +0 -5
  56. package/public/og-image.png +0 -0
  57. package/public/robots.txt +0 -3
  58. package/public/sitemap.xml +0 -8
  59. package/scripts/e2e-browser.mjs +0 -15
  60. package/scripts/e2e-deploy.mjs +0 -222
  61. package/scripts/e2e-localnet.mjs +0 -65
  62. package/scripts/e2e-walrus-browser.mjs +0 -115
  63. package/scripts/gen-og-image.mjs +0 -31
  64. package/scripts/publish-localnet.mjs +0 -55
  65. package/scripts/regen-template.sh +0 -45
  66. package/scripts/sync-template-src.sh +0 -36
  67. package/scripts/verify-bytecode.mjs +0 -49
  68. package/src/__tests__/App.spec.js +0 -11
  69. package/src/composables/useAccessGate.ts +0 -127
  70. package/src/lib/accessGate.ts +0 -141
  71. package/src/lib/walrus.ts +0 -144
  72. package/stylelint.config.js +0 -33
  73. package/test-results/.last-run.json +0 -4
  74. package/tests/accessGate.test.ts +0 -108
  75. package/tests/buildPublishTx.test.ts +0 -243
  76. package/tests/deploy.test.ts +0 -81
  77. package/tests/errors.test.ts +0 -32
  78. package/tests/generatePackage.test.ts +0 -182
  79. package/tests/github.test.ts +0 -61
  80. package/tests/licenses.test.ts +0 -78
  81. package/tests/template.test.ts +0 -93
  82. package/tests/templateArtifact.test.ts +0 -61
  83. package/tests/templateParity.test.ts +0 -55
  84. package/tests/useAccessGate.test.ts +0 -101
  85. package/tests/useWallet.test.ts +0 -74
  86. package/tests/useWalrusRelayGating.test.ts +0 -30
  87. package/tests/validation.test.ts +0 -117
  88. package/tests/walrusUpload.test.ts +0 -93
  89. package/tests/walrus_url_patch.test.ts +0 -26
  90. package/tsconfig.json +0 -19
  91. package/typedoc.json +0 -34
  92. package/vite.config.ts +0 -65
  93. package/vitest.config.ts +0 -10
@@ -4,15 +4,12 @@ import { useWallet } from '@meddleware/wallet-adapter'
4
4
  import { buildDeployExecutor } from '../lib/deployExecutor.js'
5
5
  import { getReadClient } from '../lib/readClient.js'
6
6
  import { useWalrusRelay } from '../composables/useWalrusRelay.js'
7
- import { useAccessGate } from '../composables/useAccessGate.js'
7
+ import { useAccessGate } from '@meddleware/walrus-relay'
8
+ // The flow subpath carries no wasm; it loads the Walrus client lazily when an upload starts.
9
+ import { consumeStorageKey, createGatedAccess, runBlobUpload } from '@meddleware/walrus-client/flow'
8
10
  import { ICON_EPOCHS } from '../lib/walrus-constants.js'
9
- import { ICON_MAX_BYTES, validateIconFile } from '../config.js'
10
- import {
11
- fetchRelayChallenge,
12
- personalMessageForNonce,
13
- buildAccessProofToken,
14
- } from '../lib/accessGate.js'
15
- import type { WalrusNetwork } from '../lib/walrus.js'
11
+ import { ACCESS_GATE, ICON_MAX_BYTES, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS, validateIconFile } from '../config.js'
12
+ import type { WalrusNetwork } from '../config.js'
16
13
  import type { Network } from '../lib/types.js'
17
14
  import { UiNotice, UiFieldHint, UiSegmentedControl } from '@meddleware/ui'
18
15
  import WalrusBlobBrowser from './WalrusBlobBrowser.vue'
@@ -36,7 +33,7 @@ const isWalrusNetwork = computed(() => props.network !== 'localnet')
36
33
  const walrusNet: WalrusNetwork = props.network === 'mainnet' ? 'mainnet' : 'testnet'
37
34
 
38
35
  // NFT-gated relay access (inert when no gate is configured for this network).
39
- const accessGate = useAccessGate(walrusNet, { getClient: (n) => getReadClient(n) })
36
+ const accessGate = useAccessGate({ gate: ACCESS_GATE[walrusNet], getClient: () => getReadClient(walrusNet) })
40
37
 
41
38
  const {
42
39
  selectedRelayHost,
@@ -142,82 +139,53 @@ async function upload(): Promise<void> {
142
139
  uploadError.value = 'Connect your wallet first.'
143
140
  return
144
141
  }
142
+ const address = account.value.address
145
143
  uploading.value = true
146
144
  uploadError.value = null
147
145
  try {
148
- const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } =
149
- await import('../lib/walrus.js')
150
146
  const executor = await buildDeployExecutor(props.network as Network)
151
- const suiClient = getReadClient(props.network as Network)
152
147
 
153
- // If the gated operator relay is selected, prove NFT access: (single-use only) consume
154
- // one use on-chain bound to the challenge nonce, then sign the challenge. The token rides
155
- // the relay Authorization header; the nft-gate gateway verifies it before proxying.
156
- let uploadRelayAuthToken: string | undefined
148
+ // The gated operator relay needs proof of an access pass. For a single-use pass the on-chain
149
+ // consume is persisted before the upload and reused on a retry (the gateway redeems a consume
150
+ // digest once, on a successful upload), so an interrupted upload never burns a use; a fresh use
151
+ // is spent only after the gateway reports the stored one redeemed.
157
152
  const publicHost = availableRelays.value.find((r) => r.isPublic)?.host
158
- const usingOperatorRelay = selectedRelayHost.value !== publicHost
159
- if (accessGate.gateConfigured && usingOperatorRelay && account.value) {
160
- const challenge = await fetchRelayChallenge(selectedRelayHost.value)
161
- let consumeDigest: string | undefined
162
- if (accessGate.usesRemaining.value != null && accessGate.nftId.value) {
163
- status.value = 'Consuming one access use (approve in wallet)…'
164
- const consumeTx = accessGate.buildConsume(accessGate.nftId.value, challenge.nonce)
165
- const cres = await executor.signAndExecute(consumeTx)
166
- await executor.waitForTransaction(cres.digest)
167
- consumeDigest = cres.digest
168
- }
169
- status.value = 'Signing relay access proof…'
170
- const { signature } = await signPersonalMessage(personalMessageForNonce(challenge.nonce))
171
- uploadRelayAuthToken = buildAccessProofToken({
172
- address: account.value.address,
173
- nonce: challenge.nonce,
174
- signature,
175
- consumeDigest,
176
- })
177
- }
178
-
179
- const client = createWalrusClient(walrusNet, {
153
+ const gate = accessGate.gate
154
+ const nftId = accessGate.nftId.value
155
+ const gated = !!gate && accessGate.hasAccess.value === true && !!nftId && selectedRelayHost.value !== publicHost
156
+ const access =
157
+ gated && gate && nftId
158
+ ? createGatedAccess({
159
+ storage: window.localStorage,
160
+ key: consumeStorageKey(walrusNet, gate.gateId, address),
161
+ relayHost: selectedRelayHost.value,
162
+ address,
163
+ nftId,
164
+ singleUse: accessGate.usesRemaining.value !== null,
165
+ buildConsume: (id, nonce) => accessGate.buildConsume(id, nonce),
166
+ signAndExecute: (tx) => executor.signAndExecute(tx),
167
+ waitForTransaction: (digest) => executor.waitForTransaction(digest),
168
+ sign: signPersonalMessage,
169
+ onStatus: (p) => (status.value = p.detail ?? ''),
170
+ })
171
+ : undefined
172
+
173
+ const result = await runBlobUpload({
174
+ bytes,
175
+ network: walrusNet,
176
+ relayHost: selectedRelayHost.value,
177
+ address,
178
+ rpcUrl: WALRUS_RPC_URLS[walrusNet],
180
179
  wasmUrl: walrusWasmUrl,
181
- uploadRelayHost: selectedRelayHost.value,
182
- uploadRelayAuthToken,
183
- })
184
- const flow = createBlobUploadFlow(client, bytes)
185
-
186
- status.value = 'Encoding…'
187
- await flow.encode()
188
-
189
- let feeNote = ''
190
- try {
191
- const tipMist = await client.walrus.calculateUploadRelayTip({ size: bytes.length })
192
- const tipSui = Number(tipMist) / 1e9
193
- feeNote = tipMist > 0n ? ` Relay fee ≈ ${tipSui.toFixed(4)} SUI + storage (WAL).` : ''
194
- } catch {
195
- // Non-fatal — the register step surfaces any real tip error.
196
- }
197
-
198
- status.value = `Registering blob (approve in wallet)…${feeNote}`
199
- const regTx = flow.register({
200
- owner: account.value.address,
180
+ maxTipMist: Number(WALRUS_MAX_TIP_MIST),
201
181
  epochs: ICON_EPOCHS,
202
182
  deletable: !permanent.value,
183
+ executor,
184
+ suiClient: getReadClient(props.network as Network),
185
+ access,
186
+ onStatus: (p) => (status.value = p.detail ?? ''),
203
187
  })
204
- regTx.setSenderIfNotSet(account.value.address)
205
- await regTx.build({ client: suiClient })
206
- const reg = await executor.signAndExecute(regTx)
207
- await executor.waitForTransaction(reg.digest)
208
-
209
- status.value = 'Uploading to the relay…'
210
- await flow.upload({ digest: reg.digest })
211
-
212
- status.value = 'Certifying (approve in wallet)…'
213
- const certTx = flow.certify()
214
- certTx.setSenderIfNotSet(account.value.address)
215
- await certTx.build({ client: suiClient })
216
- const cert = await executor.signAndExecute(certTx)
217
- await executor.waitForTransaction(cert.digest)
218
-
219
- const blob = await flow.getBlob()
220
- emit('update:modelValue', walrusBlobUrl(walrusNet, blob.blobId))
188
+ emit('update:modelValue', result.url)
221
189
  tab.value = 'url'
222
190
  status.value = 'Uploaded ✓ — URL filled in above.'
223
191
  } catch (e) {
@@ -225,6 +193,7 @@ async function upload(): Promise<void> {
225
193
  status.value = ''
226
194
  } finally {
227
195
  uploading.value = false
196
+ if (account.value) void refreshAccess() // uses remaining may have changed
228
197
  }
229
198
  }
230
199
 
@@ -2,7 +2,7 @@
2
2
  import { computed, onMounted, ref } from 'vue'
3
3
  import { fetchLicenseList, fetchLicenseText, NO_LICENSE } from '../lib/licenses.js'
4
4
  import type { SpdxLicense } from '../lib/licenses.js'
5
- import { UiDialog, UiFieldHint, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
5
+ import { UiDialog, UiFieldHint, UiFormField, UiNotice, UiSelect, safeHref } from '@meddleware/ui'
6
6
 
7
7
  const props = defineProps<{ modelValue: string }>()
8
8
  const emit = defineEmits<{
@@ -73,7 +73,8 @@ async function openLicenseModal(): Promise<void> {
73
73
  <UiFieldHint field-id="license-select">
74
74
  Applies only to the downloadable Move source package — embedded in the LICENSE file and
75
75
  source headers. Has <b>no effect on the on-chain token</b> or its transferability. Choose
76
- <b>CC0-1.0</b> (default) for public domain. Choose <b>None</b> to retain all rights.
76
+ <b>0BSD</b> (default) for a minimal permissive licence, or <b>CC0-1.0</b> for public domain.
77
+ Choose <b>None</b> to retain all rights.
77
78
  </UiFieldHint>
78
79
  </template>
79
80
  <template #default="{ attrs }">
@@ -130,7 +131,7 @@ async function openLicenseModal(): Promise<void> {
130
131
  <template v-else>
131
132
  <p class="license-spdx">
132
133
  <a
133
- :href="`https://spdx.org/licenses/${modelValue}.html`"
134
+ :href="safeHref(`https://spdx.org/licenses/${encodeURIComponent(modelValue)}.html`)"
134
135
  target="_blank"
135
136
  rel="noopener noreferrer"
136
137
  >View on SPDX ↗</a
@@ -5,7 +5,7 @@ import { explorerObjectUrl } from '../config.js'
5
5
  import { generatePackageZip } from '../lib/generatePackage.js'
6
6
  import { fetchLicenseText } from '../lib/licenses.js'
7
7
  import GithubPush from './GithubPush.vue'
8
- import { UiNotice } from '@meddleware/ui'
8
+ import { UiNotice, safeHref } from '@meddleware/ui'
9
9
 
10
10
  const props = defineProps<{ result: PublishResult; config: TokenConfig }>()
11
11
  defineEmits<{ (e: 'restart'): void }>()
@@ -51,7 +51,7 @@ async function downloadPackage(): Promise<void> {
51
51
  <dd class="mono">{{ result.coinType }}</dd>
52
52
  <dt>Package</dt>
53
53
  <dd class="mono">
54
- <a :href="packageUrl" target="_blank" rel="noopener noreferrer">
54
+ <a :href="safeHref(packageUrl)" target="_blank" rel="noopener noreferrer">
55
55
  {{ result.packageId }}
56
56
  </a>
57
57
  </dd>
@@ -1,12 +1,15 @@
1
1
  <script setup lang="ts">
2
2
  import type { TokenConfig, Network } from '../lib/types.js'
3
- import { FEE_MIST } from '../config.js'
3
+ import { FEE_MIST, FEE_TREASURY, isFeeConfigured } from '../config.js'
4
4
  import { UiNotice } from '@meddleware/ui'
5
5
 
6
6
  const props = defineProps<{ config: TokenConfig; network: Network }>()
7
7
  defineEmits<{ (e: 'back'): void; (e: 'confirm'): void }>()
8
8
 
9
+ // Mirrors TokenDeployerView: the fee is only charged when a real treasury is configured.
10
+ const feeCharged = isFeeConfigured(props.network)
9
11
  const feeSui = Number(FEE_MIST) / 1e9
12
+ const treasury = FEE_TREASURY[props.network]
10
13
  </script>
11
14
 
12
15
  <template>
@@ -51,8 +54,13 @@ const feeSui = Number(FEE_MIST) / 1e9
51
54
  </dl>
52
55
 
53
56
  <p v-once class="hint">
54
- Your wallet signs and pays gas. A one-off fee of <strong>{{ feeSui }} SUI</strong> is included
55
- in the publish transaction. Nothing is compiled or signed on a server.
57
+ Your wallet signs and pays gas.
58
+ <template v-if="feeCharged">
59
+ A one-off fee of <strong>{{ feeSui }} SUI</strong> is included in the publish transaction,
60
+ paid to the operator treasury <span class="mono">{{ treasury }}</span>.
61
+ </template>
62
+ <template v-else>No operator fee is charged on this network.</template>
63
+ Nothing is compiled or signed on a server.
56
64
  </p>
57
65
 
58
66
  <p v-once class="hint">
@@ -191,57 +191,61 @@ function restart(): void {
191
191
  </script>
192
192
 
193
193
  <template>
194
- <UiToolIntro
195
- >Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
196
- network
197
- }}).</UiToolIntro
198
- >
199
-
200
- <AppTabNav
201
- v-model="activeTab"
202
- :tabs="TABS"
203
- id-prefix="token-deployer"
204
- aria-label="Sections"
205
- class="deployer-tabs"
206
- />
207
-
208
- <WalletGuard message="Connect a Sui wallet to deploy your token.">
194
+ <div class="mw-token-deployer">
195
+ <UiToolIntro
196
+ >Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
197
+ network
198
+ }}).</UiToolIntro
199
+ >
200
+
201
+ <AppTabNav
202
+ v-model="activeTab"
203
+ :tabs="TABS"
204
+ id-prefix="token-deployer"
205
+ aria-label="Sections"
206
+ class="deployer-tabs"
207
+ />
208
+
209
+ <!-- The tab list and its panel always render (the selected tab controls a live panel); the wallet
210
+ prompt replaces only the panel's content until a wallet is connected. -->
209
211
  <UiTabPanel id-prefix="token-deployer" :tab="activeTab">
210
- <template v-if="activeTab === 'deploy'">
211
- <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
212
- <ConfigForm
213
- v-if="step === 'identity' || step === 'token' || step === 'settings'"
214
- :form="form"
215
- :errors="errors"
216
- :can-proceed="canProceed"
217
- :network="network"
218
- :connected="connected"
219
- :form-step="formStep"
220
- @next="goNext"
221
- @back="goBack"
222
- />
223
- <ReviewPanel
224
- v-if="step === 'review'"
225
- :config="config"
226
- :network="network"
227
- @back="goBack"
228
- @confirm="confirmDeploy"
229
- />
230
- <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
231
- <ResultPanel
232
- v-if="step === 'done' && result"
233
- :result="result"
234
- :config="config"
235
- @restart="restart"
236
- />
237
- <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
238
- </template>
239
-
240
- <MyTokens v-else :owner="account?.address ?? null" :network="network" />
212
+ <WalletGuard message="Connect a Sui wallet to deploy your token.">
213
+ <template v-if="activeTab === 'deploy'">
214
+ <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
215
+ <ConfigForm
216
+ v-if="step === 'identity' || step === 'token' || step === 'settings'"
217
+ :form="form"
218
+ :errors="errors"
219
+ :can-proceed="canProceed"
220
+ :network="network"
221
+ :connected="connected"
222
+ :form-step="formStep"
223
+ @next="goNext"
224
+ @back="goBack"
225
+ />
226
+ <ReviewPanel
227
+ v-if="step === 'review'"
228
+ :config="config"
229
+ :network="network"
230
+ @back="goBack"
231
+ @confirm="confirmDeploy"
232
+ />
233
+ <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
234
+ <ResultPanel
235
+ v-if="step === 'done' && result"
236
+ :result="result"
237
+ :config="config"
238
+ @restart="restart"
239
+ />
240
+ <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
241
+ </template>
242
+
243
+ <MyTokens v-else :owner="account?.address ?? null" :network="network" />
244
+ </WalletGuard>
241
245
  </UiTabPanel>
242
- </WalletGuard>
243
246
 
244
- <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
247
+ <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
248
+ </div>
245
249
  </template>
246
250
 
247
251
  <style scoped>
@@ -3,6 +3,17 @@ import { computed, ref, watch } from 'vue'
3
3
  import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
4
4
  import { SELECTABLE_NETWORKS } from '../config.js'
5
5
  import type { Network } from '../lib/types.js'
6
+
7
+ /** Wallet-supplied icons are untrusted: render only inline images or https URLs. */
8
+ function safeIcon(icon: string | undefined): string | undefined {
9
+ if (!icon) return undefined
10
+ if (/^data:image\/(png|svg\+xml|jpeg|webp|gif);/i.test(icon)) return icon
11
+ try {
12
+ return new URL(icon).protocol === 'https:' ? icon : undefined
13
+ } catch {
14
+ return undefined
15
+ }
16
+ }
6
17
  import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
7
18
 
8
19
  const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
@@ -66,7 +77,7 @@ watch(
66
77
  <menu class="wallet-list">
67
78
  <li v-for="w in wallets" :key="w.name">
68
79
  <button type="button" class="primary" :disabled="connecting" @click="connect(w)">
69
- <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
80
+ <img v-if="safeIcon(w.icon)" :src="safeIcon(w.icon)" alt="" width="20" height="20" />
70
81
  {{ w.name }}
71
82
  </button>
72
83
  </li>
@@ -2,7 +2,8 @@
2
2
  import { ref, watch } from 'vue'
3
3
  import { useWallet } from '@meddleware/wallet-adapter'
4
4
  import { getReadClient } from '../lib/readClient.js'
5
- import type { OwnedBlob, WalrusNetwork } from '../lib/walrus.js'
5
+ import type { OwnedBlob } from '@meddleware/walrus-client'
6
+ import { WALRUS_RPC_URLS, type WalrusNetwork } from '../config.js'
6
7
  import type { Network } from '../lib/types.js'
7
8
  import { UiNotice } from '@meddleware/ui'
8
9
 
@@ -46,10 +47,9 @@ async function load(): Promise<void> {
46
47
  blobs.value = []
47
48
  hasLoaded.value = false
48
49
  try {
49
- const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
50
- await import('../lib/walrus.js')
50
+ const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
51
51
  const suiClient = getReadClient(props.network as Network)
52
- const client = createWalrusClient(props.network)
52
+ const client = createWalrusClient({ network: props.network, rpcUrl: WALRUS_RPC_URLS[props.network] })
53
53
  const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
54
54
  blobs.value = raw.map((b) => ({ ...b, url: walrusBlobUrl(props.network, b.blobId) }))
55
55
  hasLoaded.value = true
@@ -4,8 +4,7 @@
4
4
  // (`IconPicker.vue`) and tests are unaffected.
5
5
  import { useWalrusRelay as useSharedWalrusRelay } from '@meddleware/walrus-relay'
6
6
  import type { RelayAccessOptions, RelayOption } from '@meddleware/walrus-relay'
7
- import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
8
- import type { WalrusNetwork } from '../lib/walrus.js'
7
+ import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS, type WalrusNetwork } from '../config.js'
9
8
 
10
9
  export type { RelayAccessOptions, RelayOption }
11
10
 
package/src/config.ts CHANGED
@@ -2,21 +2,21 @@
2
2
  // Vite env vars so they can be set at build/deploy time without code changes.
3
3
  // See .env.example.
4
4
  import type { Network } from './lib/types.js'
5
- import type { AccessGateConfig } from './lib/accessGate.js'
5
+ import { relayGateConfig, type RelayGateConfig } from '@meddleware/walrus-relay'
6
6
 
7
7
  const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
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
 
@@ -56,7 +56,7 @@ export const SELECTABLE_NETWORKS: Network[] = ['testnet', 'mainnet']
56
56
  // ---------------------------------------------------------------------------
57
57
  // Walrus is a separate protocol from Sui; only testnet/mainnet exist (no
58
58
  // localnet). Icon uploads go through an upload relay (direct-to-storage-node
59
- // writes fail from browsers). See `src/lib/walrus.ts` and the relay runbook in
59
+ // writes fail from browsers). The upload is `@meddleware/walrus-client/flow`; see the relay runbook in
60
60
  // README.md / CLAUDE.md for the operator side.
61
61
 
62
62
  /** A Walrus network — a strict subset of {@link Network} (no localnet). */
@@ -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
  /**
@@ -116,27 +113,29 @@ export const WALRUS_MAX_TIP_MIST: bigint = BigInt(env.VITE_WALRUS_MAX_TIP_MIST |
116
113
  // NFT-gated relay access (optional)
117
114
  // ---------------------------------------------------------------------------
118
115
  // When the operator relay runs behind an `nft-gate` gateway, using it requires
119
- // holding the gate's access NFT. Configure the gate per network via env; when a
120
- // gate is NOT configured the relay behaves exactly as before (no gating). See
121
- // `src/lib/accessGate.ts` and `useAccessGate`.
116
+ // holding the gate's access NFT. The operator sets the gate id, soulbound flag (default true) and
117
+ // price per network via env; `relayGateConfig` (walrus-relay) fixes the package and PlatformConfig
118
+ // to the published access_gate deployment. When no gate id is set the relay is ungated.
122
119
 
123
- function parseAccessGate(network: WalrusNetwork): AccessGateConfig | null {
120
+ /** The gate config for a network, or `null` (ungated) without a gate id or a recorded deployment. */
121
+ export function parseAccessGate(network: WalrusNetwork, envSource: Record<string, string | undefined> = env): RelayGateConfig | null {
124
122
  const NET = network.toUpperCase()
125
- const packageId = env[`VITE_ACCESS_GATE_PACKAGE_${NET}`]
126
- const gateId = env[`VITE_ACCESS_GATE_ID_${NET}`]
127
- const nftType = env[`VITE_ACCESS_GATE_NFT_TYPE_${NET}`]
128
- if (!packageId || !gateId || !nftType) return null
129
- return {
130
- packageId,
131
- gateId,
132
- nftType,
133
- soulbound: (env[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] ?? 'true') !== 'false',
134
- priceMist: BigInt(env[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
123
+ const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
124
+ if (!gateId) return null
125
+ try {
126
+ return relayGateConfig(network, {
127
+ gateId,
128
+ soulbound: (envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] ?? 'true') !== 'false',
129
+ priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
130
+ })
131
+ } catch (e) {
132
+ console.warn(`[token-deployer-ui] access gate for ${network} disabled: ${e instanceof Error ? e.message : String(e)}`)
133
+ return null
135
134
  }
136
135
  }
137
136
 
138
137
  /** The access gate per Walrus network, or `null` when gating is not configured. */
139
- export const ACCESS_GATE: Record<WalrusNetwork, AccessGateConfig | null> = {
138
+ export const ACCESS_GATE: Record<WalrusNetwork, RelayGateConfig | null> = {
140
139
  testnet: parseAccessGate('testnet'),
141
140
  mainnet: parseAccessGate('mainnet'),
142
141
  }
@@ -73,16 +73,6 @@ export interface BuildFinalizeArgs {
73
73
  gasBudget: bigint
74
74
  }
75
75
 
76
- /** Whether a finalize transaction is needed at all for this config. */
77
- export function needsFinalize(config: TokenConfig, sender: string): boolean {
78
- return (
79
- config.initialSupply > 0n ||
80
- config.supplyPolicy === 'fixed' ||
81
- config.metadataPolicy === 'frozen' ||
82
- config.recipient.toLowerCase() !== sender.toLowerCase()
83
- )
84
- }
85
-
86
76
  /**
87
77
  * Finalize PTB: register the coin in the registry, mint initial supply, apply
88
78
  * supply/metadata policy, and route caps to the recipient.
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>
@@ -51,10 +87,16 @@ export interface DeployArgs {
51
87
  onStep?: (step: DeployStep) => void
52
88
  }
53
89
 
90
+ /**
91
+ * Only an explicit `success` effects status counts: a missing status (an executor that did not
92
+ * return effects) is treated as failure, never as success.
93
+ */
54
94
  function assertSuccess(res: SuiTxResult, what: string): void {
55
95
  const status = res.effects?.status?.status
56
- if (status && status !== 'success') {
57
- throw new Error(`${what} failed: ${res.effects?.status?.error ?? status}`)
96
+ if (status !== 'success') {
97
+ throw new Error(
98
+ `${what} failed: ${res.effects?.status?.error ?? status ?? 'no effects status returned'}`,
99
+ )
58
100
  }
59
101
  }
60
102
 
@@ -67,7 +109,7 @@ export async function deployToken(args: DeployArgs): Promise<PublishResult> {
67
109
  const structName = deriveStructName(config.moduleName)
68
110
  const normalizedConfig: TokenConfig = { ...config, structName }
69
111
  const recipient = normalizedConfig.recipient || args.sender
70
- const isE2e = import.meta.env.VITE_E2E === '1'
112
+ const isE2e = import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1'
71
113
 
72
114
  onStep?.('patching')
73
115
  const moduleBytes = await patchTemplateModule({