@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
@@ -6,7 +6,16 @@ import ReviewPanel from './ReviewPanel.vue'
6
6
  import DeployProgress from './DeployProgress.vue'
7
7
  import ResultPanel from './ResultPanel.vue'
8
8
  import TermsDialog from './TermsDialog.vue'
9
- import { UiNotice, UiStepper, type StepperStep } from '@meddleware/ui'
9
+ import MyTokens from './MyTokens.vue'
10
+ import {
11
+ AppTabNav,
12
+ UiNotice,
13
+ UiStepper,
14
+ UiTabPanel,
15
+ UiToolIntro,
16
+ type AppTab,
17
+ type StepperStep,
18
+ } from '@meddleware/ui'
10
19
  import { emptyForm, toTokenConfig } from '../lib/form.js'
11
20
  import { validateForm } from '../lib/validation.js'
12
21
  import type { FormErrors } from '../lib/validation.js'
@@ -32,6 +41,14 @@ const CONFIG_STEPS: ConfigFormStep[] = ['identity', 'token', 'settings']
32
41
  const { account } = useWallet()
33
42
  const { network } = useNetwork()
34
43
 
44
+ // Top-level tabs — Deploy (the wizard) and My Tokens (coins this wallet has deployed), so the
45
+ // layout matches the other Sui tools (intro → tabs → content).
46
+ const TABS: AppTab[] = [
47
+ { id: 'deploy', label: 'Deploy' },
48
+ { id: 'my-tokens', label: 'My Tokens' },
49
+ ]
50
+ const activeTab = ref('deploy')
51
+
35
52
  const form = reactive(emptyForm())
36
53
  const step = ref<Step>('identity')
37
54
  const deployStep = ref<DeployStep | null>(null)
@@ -55,10 +72,14 @@ function stepHasErrors(keys: (keyof FormErrors)[]): boolean {
55
72
  const canProceed = computed((): boolean => {
56
73
  if (!connected.value) return false
57
74
  switch (step.value) {
58
- case 'identity': return !stepHasErrors(IDENTITY_KEYS)
59
- case 'token': return !stepHasErrors(TOKEN_KEYS)
60
- case 'settings': return !stepHasErrors(SETTINGS_KEYS)
61
- default: return true
75
+ case 'identity':
76
+ return !stepHasErrors(IDENTITY_KEYS)
77
+ case 'token':
78
+ return !stepHasErrors(TOKEN_KEYS)
79
+ case 'settings':
80
+ return !stepHasErrors(SETTINGS_KEYS)
81
+ default:
82
+ return true
62
83
  }
63
84
  })
64
85
 
@@ -90,18 +111,36 @@ function onStepperBack(i: number): void {
90
111
 
91
112
  function goNext(): void {
92
113
  if (!canProceed.value) return
93
- if (step.value === 'identity') { step.value = 'token'; return }
94
- if (step.value === 'token') { step.value = 'settings'; return }
114
+ if (step.value === 'identity') {
115
+ step.value = 'token'
116
+ return
117
+ }
118
+ if (step.value === 'token') {
119
+ step.value = 'settings'
120
+ return
121
+ }
95
122
  if (step.value === 'settings') {
96
- if (!termsAccepted.value) { showTerms.value = true; return }
123
+ if (!termsAccepted.value) {
124
+ showTerms.value = true
125
+ return
126
+ }
97
127
  step.value = 'review'
98
128
  }
99
129
  }
100
130
 
101
131
  function goBack(): void {
102
- if (step.value === 'token') { step.value = 'identity'; return }
103
- if (step.value === 'settings') { step.value = 'token'; return }
104
- if (step.value === 'review') { step.value = 'settings'; return }
132
+ if (step.value === 'token') {
133
+ step.value = 'identity'
134
+ return
135
+ }
136
+ if (step.value === 'settings') {
137
+ step.value = 'token'
138
+ return
139
+ }
140
+ if (step.value === 'review') {
141
+ step.value = 'settings'
142
+ return
143
+ }
105
144
  }
106
145
 
107
146
  function onTermsAccept(): void {
@@ -117,7 +156,10 @@ function onTermsCancel(): void {
117
156
 
118
157
  async function confirmDeploy(): Promise<void> {
119
158
  const sender = account.value?.address
120
- if (!sender) { deployError.value = 'Connect a wallet first.'; return }
159
+ if (!sender) {
160
+ deployError.value = 'Connect a wallet first.'
161
+ return
162
+ }
121
163
  deployError.value = null
122
164
  step.value = 'deploying'
123
165
  deployStep.value = null
@@ -149,34 +191,61 @@ function restart(): void {
149
191
  </script>
150
192
 
151
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
+
152
208
  <WalletGuard message="Connect a Sui wallet to deploy your token.">
153
- <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
154
- <ConfigForm
155
- v-if="step === 'identity' || step === 'token' || step === 'settings'"
156
- :form="form"
157
- :errors="errors"
158
- :can-proceed="canProceed"
159
- :network="network"
160
- :connected="connected"
161
- :form-step="formStep"
162
- @next="goNext"
163
- @back="goBack"
164
- />
165
- <ReviewPanel
166
- v-if="step === 'review'"
167
- :config="config"
168
- :network="network"
169
- @back="goBack"
170
- @confirm="confirmDeploy"
171
- />
172
- <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
173
- <ResultPanel
174
- v-if="step === 'done' && result"
175
- :result="result"
176
- :config="config"
177
- @restart="restart"
178
- />
179
- <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
180
- <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
209
+ <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" />
241
+ </UiTabPanel>
181
242
  </WalletGuard>
243
+
244
+ <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
182
245
  </template>
246
+
247
+ <style scoped>
248
+ .deployer-tabs {
249
+ margin: 0 0 1rem;
250
+ }
251
+ </style>
@@ -3,12 +3,12 @@ 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
- import { UiSelect, UiNotice } from '@meddleware/ui'
6
+ import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
7
7
 
8
8
  const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
9
9
  const { network, setNetwork } = useNetwork()
10
10
 
11
- const dialogEl = ref<HTMLDialogElement>()
11
+ const dialogOpen = ref(false)
12
12
 
13
13
  const shortAddress = computed(() => {
14
14
  const a = account.value?.address
@@ -20,88 +20,77 @@ const networkModel = computed({
20
20
  set: (v: string) => setNetwork(v as Network),
21
21
  })
22
22
 
23
- function openDialog(): void {
24
- dialogEl.value?.showModal()
25
- }
26
-
27
- function closeDialog(): void {
28
- dialogEl.value?.close()
29
- }
30
-
31
- watch(() => account.value, (acc) => {
32
- if (acc) {
33
- closeDialog()
34
- }
35
- })
23
+ // Close the connect dialog once a wallet connects.
24
+ watch(
25
+ () => account.value,
26
+ (acc) => {
27
+ if (acc) dialogOpen.value = false
28
+ },
29
+ )
36
30
  </script>
37
31
 
38
32
  <template>
39
- <div class="card" role="region" aria-label="Wallet and network">
33
+ <section class="card wallet-bar" aria-label="Wallet and network">
40
34
  <template v-if="account">
41
- <div class="row" style="justify-content: space-between">
42
- <div class="row">
43
- <label for="network-select" style="margin: 0">Network</label>
44
- <UiSelect id="network-select" v-model="networkModel">
45
- <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
46
- </UiSelect>
47
- </div>
48
-
49
- <div class="row">
50
- <span class="badge" aria-label="Connected wallet">
51
- {{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
52
- </span>
53
- <button type="button" @click="disconnect">Disconnect</button>
54
- </div>
55
- </div>
56
- </template>
57
- <template v-else>
58
- <div class="row">
59
- <div v-if="!wallets.length" style="width: 100%">
60
- <p class="hint" style="margin: 0">
61
- No Sui wallet detected. Install a Sui wallet extension to continue.
62
- </p>
63
- </div>
64
- <button v-else type="button" class="primary" @click="openDialog">
65
- Connect Wallet
66
- </button>
67
- </div>
35
+ <label class="row wallet-bar__network">
36
+ Network
37
+ <UiSelect v-model="networkModel">
38
+ <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
39
+ </UiSelect>
40
+ </label>
41
+ <p class="row wallet-bar__account">
42
+ <span class="badge">
43
+ <span class="mw-visually-hidden">Connected wallet:</span>
44
+ {{ currentWallet?.name }} · <span class="mono">{{ shortAddress }}</span>
45
+ </span>
46
+ <button type="button" @click="disconnect">Disconnect</button>
47
+ </p>
68
48
  </template>
49
+ <p v-else-if="!wallets.length" class="hint wallet-bar__none">
50
+ No Sui wallet detected. Install a Sui wallet extension to continue.
51
+ </p>
52
+ <button v-else type="button" class="primary" @click="dialogOpen = true">Connect Wallet</button>
69
53
 
70
- <UiNotice v-if="error" type="error" style="margin-top: 0.75rem">{{ error }}</UiNotice>
71
- </div>
54
+ <UiNotice v-if="error" type="error" class="wallet-bar__error">{{ error }}</UiNotice>
55
+ </section>
72
56
 
73
- <dialog ref="dialogEl" aria-labelledby="wallet-dialog-title" @click.self="closeDialog">
74
- <div class="dialog-header">
75
- <h2 id="wallet-dialog-title">Connect Wallet</h2>
76
- <button
77
- type="button"
78
- aria-label="Close"
79
- style="background: none; border: none; font-size: 1.5rem; padding: 0; cursor: pointer; color: var(--text)"
80
- @click="closeDialog"
81
- >
82
- ×
83
- </button>
84
- </div>
57
+ <UiDialog v-model:open="dialogOpen" title="Connect Wallet">
58
+ <UiFormField id="dialog-network-select" label="Network">
59
+ <template #default="{ attrs }">
60
+ <UiSelect v-bind="attrs" v-model="networkModel">
61
+ <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
62
+ </UiSelect>
63
+ </template>
64
+ </UiFormField>
85
65
 
86
- <div>
87
- <label for="dialog-network-select">Network</label>
88
- <UiSelect id="dialog-network-select" v-model="networkModel">
89
- <option v-for="n in SELECTABLE_NETWORKS" :key="n" :value="n">{{ n }}</option>
90
- </UiSelect>
91
- </div>
92
-
93
- <div class="wallet-list">
94
- <button
95
- v-for="w in wallets"
96
- :key="w.name"
97
- type="button"
98
- class="primary"
99
- :disabled="connecting"
100
- @click="connect(w)"
101
- >
102
- <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
103
- {{ w.name }}
104
- </button>
105
- </div>
106
- </dialog>
66
+ <menu class="wallet-list">
67
+ <li v-for="w in wallets" :key="w.name">
68
+ <button type="button" class="primary" :disabled="connecting" @click="connect(w)">
69
+ <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
70
+ {{ w.name }}
71
+ </button>
72
+ </li>
73
+ </menu>
74
+ </UiDialog>
107
75
  </template>
76
+
77
+ <style scoped>
78
+ .wallet-bar {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ gap: 0.75rem;
84
+ }
85
+ .wallet-bar__network,
86
+ .wallet-bar__account,
87
+ .wallet-bar__none {
88
+ margin: 0;
89
+ }
90
+ .wallet-bar__none {
91
+ width: 100%;
92
+ }
93
+ .wallet-bar__error {
94
+ flex-basis: 100%;
95
+ }
96
+ </style>
@@ -46,9 +46,8 @@ async function load(): Promise<void> {
46
46
  blobs.value = []
47
47
  hasLoaded.value = false
48
48
  try {
49
- const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import(
50
- '../lib/walrus.js'
51
- )
49
+ const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
50
+ await import('../lib/walrus.js')
52
51
  const suiClient = getReadClient(props.network as Network)
53
52
  const client = createWalrusClient(props.network)
54
53
  const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
@@ -75,9 +74,13 @@ function selectBlob(item: BlobItem): void {
75
74
  type="text"
76
75
  placeholder="0x… or leave blank for your wallet"
77
76
  aria-label="Wallet address to browse"
78
- style="flex: 1"
77
+ class="blob-browser__address"
79
78
  />
80
- <button type="button" :disabled="loading || (!connected && !addressInput.trim())" @click="load">
79
+ <button
80
+ type="button"
81
+ :disabled="loading || (!connected && !addressInput.trim())"
82
+ @click="load"
83
+ >
81
84
  <span v-if="loading" class="spinner" aria-hidden="true"></span>
82
85
  Load
83
86
  </button>
@@ -90,26 +93,26 @@ function selectBlob(item: BlobItem): void {
90
93
  No Walrus blobs found at this address.
91
94
  </p>
92
95
 
93
- <div v-if="blobs.length > 0" class="blob-grid" role="listbox" aria-label="Walrus blobs">
94
- <button
95
- v-for="b in blobs"
96
- :key="b.objectId"
97
- type="button"
98
- class="blob-thumb"
99
- :class="{ selected: selected === b.blobId }"
100
- role="option"
101
- :aria-selected="selected === b.blobId"
102
- @click="selectBlob(b)"
103
- >
104
- <img
105
- :src="b.url"
106
- :alt="`Blob ${b.blobId.slice(0, 8)}…`"
107
- loading="lazy"
108
- @error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
109
- />
110
- <span class="blob-meta">{{ formatSize(b.size) }}</span>
111
- </button>
112
- </div>
96
+ <!-- A list of toggle buttons (native keyboard behaviour); aria-pressed marks the chosen blob. -->
97
+ <ul v-if="blobs.length > 0" role="list" class="blob-grid" aria-label="Walrus blobs">
98
+ <li v-for="b in blobs" :key="b.objectId">
99
+ <button
100
+ type="button"
101
+ class="blob-thumb"
102
+ :class="{ selected: selected === b.blobId }"
103
+ :aria-pressed="selected === b.blobId"
104
+ @click="selectBlob(b)"
105
+ >
106
+ <img
107
+ :src="b.url"
108
+ :alt="`Blob ${b.blobId.slice(0, 8)}…`"
109
+ loading="lazy"
110
+ @error="(e) => (e.target as HTMLElement).parentElement?.classList.add('no-img')"
111
+ />
112
+ <span class="blob-meta">{{ formatSize(b.size) }}</span>
113
+ </button>
114
+ </li>
115
+ </ul>
113
116
  </div>
114
117
  </template>
115
118
 
@@ -127,7 +130,12 @@ function selectBlob(item: BlobItem): void {
127
130
  margin-top: 0.25rem;
128
131
  }
129
132
 
133
+ .blob-browser__address {
134
+ flex: 1;
135
+ }
136
+
130
137
  .blob-thumb {
138
+ width: 100%;
131
139
  display: flex;
132
140
  flex-direction: column;
133
141
  align-items: center;
@@ -22,7 +22,7 @@ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature
22
22
  * When no gate is configured for the network (`ACCESS_GATE[network] == null`), the relay is
23
23
  * treated as OPEN (`hasAccess === true`) and this composable is inert — behaviour is
24
24
  * unchanged from before gating existed. When a gate IS configured, `checkOwnership` decides
25
- * access with a single `getOwnedObjects` call (the same cheap pattern as the relay health
25
+ * access with a single `listOwnedObjects` call (the same cheap pattern as the relay health
26
26
  * check), and `purchase`/`buildRelayAccessToken` drive the buy + prove flows.
27
27
  */
28
28
  export function useAccessGate(
@@ -53,7 +53,12 @@ export function useAccessGate(
53
53
  checking.value = true
54
54
  error.value = null
55
55
  try {
56
- const nfts = await fetchAccessNfts(deps.getClient(network), address, gate.nftType, gate.gateId)
56
+ const nfts = await fetchAccessNfts(
57
+ deps.getClient(network),
58
+ address,
59
+ gate.nftType,
60
+ gate.gateId,
61
+ )
57
62
  hasAccess.value = nfts.length > 0
58
63
  usesRemaining.value = nfts.length ? nfts[0].usesRemaining : null
59
64
  nftId.value = nfts.length ? nfts[0].objectId : null
@@ -1,7 +1,7 @@
1
1
  // Thin wrapper around the shared `@meddleware/walrus-relay-ui` composable: injects
2
2
  // this app's per-network relay host pair so the relay-selection + tip-estimation logic
3
3
  // lives in exactly one place (DRY). The return shape is unchanged, so consumers
4
- // (`WalrusIconUpload.vue`, `IconPicker.vue`) and tests are unaffected.
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
7
  import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
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
@@ -103,7 +103,7 @@ export function buildFinalizeTransaction(args: BuildFinalizeArgs): Transaction {
103
103
  target: `${SUI_FRAMEWORK}::coin_registry::finalize_registration`,
104
104
  typeArguments: [coinType],
105
105
  arguments: [
106
- tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
106
+ tx.object('0xc'), // &mut CoinRegistry (shared object at well-known address)
107
107
  tx.receivingRef(args.currencyRef), // Receiving<Currency<T>> — pending object at @0xc
108
108
  ],
109
109
  })