@meddleware/token-deployer-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.claude/move-language-reference/SKILL.md +445 -0
  2. package/.claude/skills/move-language-reference/SKILL.md +445 -0
  3. package/.claude/skills/sui-move-bootcamp/SKILL.md +128 -0
  4. package/.claude/sui-move-bootcamp/SKILL.md +128 -0
  5. package/.editorconfig +8 -0
  6. package/.env.example +75 -0
  7. package/.env.production +7 -0
  8. package/.gitattributes +1 -0
  9. package/.github/workflows/ci.yml +34 -0
  10. package/.github/workflows/token-deployer-e2e.yml +64 -0
  11. package/.oxfmtrc.json +5 -0
  12. package/.oxlintrc.json +10 -0
  13. package/.vscode/extensions.json +9 -0
  14. package/AGENTS.md +39 -0
  15. package/CLAUDE.md +159 -0
  16. package/LICENSE +121 -0
  17. package/README.md +234 -0
  18. package/env.d.ts +13 -0
  19. package/index.html +13 -0
  20. package/package.json +67 -0
  21. package/playwright.config.ts +30 -0
  22. package/public/CNAME +1 -0
  23. package/public/_headers +12 -0
  24. package/public/_redirects +1 -0
  25. package/public/favicon.ico +0 -0
  26. package/public/favicon.svg +5 -0
  27. package/public/og-image.png +0 -0
  28. package/public/robots.txt +3 -0
  29. package/public/sitemap.xml +8 -0
  30. package/scripts/e2e-browser.mjs +15 -0
  31. package/scripts/e2e-deploy.mjs +222 -0
  32. package/scripts/e2e-localnet.mjs +65 -0
  33. package/scripts/e2e-walrus-browser.mjs +115 -0
  34. package/scripts/gen-og-image.mjs +31 -0
  35. package/scripts/publish-localnet.mjs +55 -0
  36. package/scripts/regen-template.sh +45 -0
  37. package/scripts/sync-template-src.sh +36 -0
  38. package/scripts/verify-bytecode.mjs +49 -0
  39. package/src/App.vue +41 -0
  40. package/src/__tests__/App.spec.js +11 -0
  41. package/src/components/AppNotice.vue +15 -0
  42. package/src/components/ColorModeControl.vue +81 -0
  43. package/src/components/ConfigForm.vue +192 -0
  44. package/src/components/DeployProgress.vue +46 -0
  45. package/src/components/FieldHint.vue +79 -0
  46. package/src/components/FormField.vue +28 -0
  47. package/src/components/FormSelect.vue +43 -0
  48. package/src/components/GithubPush.vue +100 -0
  49. package/src/components/IconPicker.vue +410 -0
  50. package/src/components/LicensePicker.vue +182 -0
  51. package/src/components/ResultPanel.vue +88 -0
  52. package/src/components/ReviewPanel.vue +69 -0
  53. package/src/components/TermsDialog.vue +81 -0
  54. package/src/components/TokenDeployerView.vue +115 -0
  55. package/src/components/WalletBar.vue +108 -0
  56. package/src/components/WalrusBlobBrowser.vue +188 -0
  57. package/src/components/WalrusIconUpload.vue +205 -0
  58. package/src/composables/useAccessGate.ts +122 -0
  59. package/src/composables/useColorMode.ts +76 -0
  60. package/src/composables/useWalrusRelay.ts +21 -0
  61. package/src/config.ts +185 -0
  62. package/src/index.ts +1 -0
  63. package/src/lib/accessGate.ts +141 -0
  64. package/src/lib/buildPublishTx.ts +192 -0
  65. package/src/lib/deploy.ts +168 -0
  66. package/src/lib/deployExecutor.ts +61 -0
  67. package/src/lib/errors.ts +21 -0
  68. package/src/lib/form.ts +75 -0
  69. package/src/lib/generatePackage.ts +312 -0
  70. package/src/lib/github.ts +146 -0
  71. package/src/lib/licenses.ts +122 -0
  72. package/src/lib/readClient.ts +17 -0
  73. package/src/lib/template.ts +131 -0
  74. package/src/lib/types.ts +69 -0
  75. package/src/lib/validation.ts +172 -0
  76. package/src/lib/walrus-constants.ts +15 -0
  77. package/src/lib/walrus.ts +144 -0
  78. package/src/main.ts +154 -0
  79. package/src/move-template/sui_token_template.mv +0 -0
  80. package/src/move-template/template.ts +31 -0
  81. package/src/styles.css +384 -0
  82. package/src/template-src/files.json +10 -0
  83. package/test-results/.last-run.json +4 -0
  84. package/tests/accessGate.test.ts +108 -0
  85. package/tests/buildPublishTx.test.ts +243 -0
  86. package/tests/deploy.test.ts +81 -0
  87. package/tests/errors.test.ts +32 -0
  88. package/tests/generatePackage.test.ts +182 -0
  89. package/tests/github.test.ts +61 -0
  90. package/tests/licenses.test.ts +78 -0
  91. package/tests/template.test.ts +93 -0
  92. package/tests/templateArtifact.test.ts +61 -0
  93. package/tests/templateParity.test.ts +55 -0
  94. package/tests/useAccessGate.test.ts +101 -0
  95. package/tests/useWallet.test.ts +74 -0
  96. package/tests/useWalrusRelayGating.test.ts +30 -0
  97. package/tests/validation.test.ts +117 -0
  98. package/tests/walrusUpload.test.ts +93 -0
  99. package/tests/walrus_url_patch.test.ts +26 -0
  100. package/tsconfig.json +19 -0
  101. package/typedoc.json +34 -0
  102. package/vite.config.ts +65 -0
  103. package/vitest.config.ts +10 -0
@@ -0,0 +1,205 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, computed } from 'vue'
3
+ import { useWallet } from '@meddleware/wallet-adapter'
4
+ import { buildDeployExecutor } from '../lib/deployExecutor.js'
5
+ import { useWalrusRelay } from '../composables/useWalrusRelay.js'
6
+ import { ICON_EPOCHS } from '../lib/walrus-constants.js'
7
+ import { ICON_MAX_BYTES, validateIconFile } from '../config.js'
8
+ import type { WalrusNetwork } from '../lib/walrus.js'
9
+ import type { Network } from '../lib/types.js'
10
+ import AppNotice from './AppNotice.vue'
11
+ // Vite serves the Walrus wasm as an asset URL (lightweight import — just the URL).
12
+ import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
13
+
14
+ const props = defineProps<{ network: WalrusNetwork; connected: boolean }>()
15
+ const emit = defineEmits<{ (e: 'uploaded', url: string): void }>()
16
+
17
+ const { account } = useWallet()
18
+ const {
19
+ selectedRelayHost,
20
+ availableRelays,
21
+ estimatedCost,
22
+ fileSizeBytes,
23
+ checkOperatorRelayHealth,
24
+ } = useWalrusRelay(props.network)
25
+
26
+ const uploading = ref(false)
27
+ const status = ref('')
28
+ const error = ref<string | null>(null)
29
+ const fileName = ref('')
30
+ let bytes: Uint8Array | null = null
31
+
32
+ const maxSizeLabel = `${Math.round(ICON_MAX_BYTES / 1024)} KB`
33
+ const ICON_YEARS = Math.round(ICON_EPOCHS / 26)
34
+
35
+ onMounted(() => {
36
+ checkOperatorRelayHealth()
37
+ })
38
+
39
+ const showPublicRelayNotice = computed(() => availableRelays.value.length > 1)
40
+
41
+ const showFreeRelayTip = computed(() => {
42
+ const publicHost = availableRelays.value.find((r) => r.isPublic)?.host
43
+ return showPublicRelayNotice.value && selectedRelayHost.value !== publicHost
44
+ })
45
+
46
+ function onFile(e: Event): void {
47
+ error.value = null
48
+ bytes = null
49
+ fileName.value = ''
50
+ const f = (e.target as HTMLInputElement).files?.[0]
51
+ if (!f) return
52
+
53
+ // UX/cost guard only — the authoritative cap is the edge (relay) body limit.
54
+ const invalid = validateIconFile(f)
55
+ if (invalid) {
56
+ error.value = invalid
57
+ return
58
+ }
59
+
60
+ fileName.value = f.name
61
+ fileSizeBytes.value = f.size
62
+ const reader = new FileReader()
63
+ reader.onload = () => {
64
+ bytes = new Uint8Array(reader.result as ArrayBuffer)
65
+ }
66
+ reader.readAsArrayBuffer(f)
67
+ }
68
+
69
+ function onRelayChange(newHost: string): void {
70
+ selectedRelayHost.value = newHost
71
+ }
72
+
73
+ async function upload(): Promise<void> {
74
+ if (!bytes) {
75
+ error.value = 'Choose an image first.'
76
+ return
77
+ }
78
+ if (!account.value) {
79
+ error.value = 'Connect your wallet first.'
80
+ return
81
+ }
82
+ uploading.value = true
83
+ error.value = null
84
+ try {
85
+ // Lazily load the heavy @mysten/walrus code and Sui client only on demand.
86
+ const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } =
87
+ await import('../lib/walrus.js')
88
+ const { getReadClient } = await import('../lib/readClient.js')
89
+ const executor = await buildDeployExecutor(props.network as Network)
90
+ const suiClient = getReadClient(props.network as Network)
91
+ const client = createWalrusClient(props.network, {
92
+ wasmUrl: walrusWasmUrl,
93
+ uploadRelayHost: selectedRelayHost.value,
94
+ })
95
+ const flow = createBlobUploadFlow(client, bytes)
96
+
97
+ status.value = 'Encoding…'
98
+ await flow.encode()
99
+
100
+ // Disclose the relay fee before asking for a signature. `calculateUploadRelayTip`
101
+ // takes the UNENCODED size and throws if the tip exceeds the client max guard.
102
+ let feeNote = ''
103
+ try {
104
+ const tipMist = await client.walrus.calculateUploadRelayTip({ size: bytes.length })
105
+ const tipSui = Number(tipMist) / 1e9
106
+ feeNote = tipMist > 0n ? ` Relay fee ≈ ${tipSui.toFixed(4)} SUI + storage (WAL).` : ''
107
+ } catch {
108
+ // Non-fatal for disclosure; the register step surfaces any real tip error.
109
+ }
110
+
111
+ status.value = `Registering blob (approve in wallet)…${feeNote}`
112
+ const regTx = flow.register({
113
+ owner: account.value.address,
114
+ epochs: ICON_EPOCHS,
115
+ deletable: false,
116
+ })
117
+ // Set sender and build with the Sui client to resolve CoinWithBalance intents
118
+ regTx.setSenderIfNotSet(account.value.address)
119
+ await regTx.build({ client: suiClient })
120
+ const reg = await executor.signAndExecute(regTx)
121
+ await executor.waitForTransaction(reg.digest)
122
+
123
+ status.value = 'Uploading to the relay…'
124
+ await flow.upload({ digest: reg.digest })
125
+
126
+ status.value = 'Certifying (approve in wallet)…'
127
+ const certTx = flow.certify()
128
+ // Set sender and build the certify transaction with the Sui client
129
+ certTx.setSenderIfNotSet(account.value.address)
130
+ await certTx.build({ client: suiClient })
131
+ const cert = await executor.signAndExecute(certTx)
132
+ await executor.waitForTransaction(cert.digest)
133
+
134
+ const blob = await flow.getBlob()
135
+ emit('uploaded', walrusBlobUrl(props.network, blob.blobId))
136
+ status.value = 'Uploaded ✓ — URL filled in above.'
137
+ } catch (e) {
138
+ error.value = e instanceof Error ? e.message : String(e)
139
+ status.value = ''
140
+ } finally {
141
+ uploading.value = false
142
+ }
143
+ }
144
+ </script>
145
+
146
+ <template>
147
+ <div>
148
+ <label for="walrus-file" class="hint" style="display: block; font-weight: 600">
149
+ …or upload an image to Walrus
150
+ </label>
151
+ <div class="row">
152
+ <input
153
+ id="walrus-file"
154
+ type="file"
155
+ accept="image/*"
156
+ aria-label="Choose an image to upload to Walrus"
157
+ aria-describedby="walrus-help"
158
+ style="width: auto"
159
+ @change="onFile"
160
+ />
161
+ <button type="button" :disabled="uploading || !connected || !fileName" @click="upload">
162
+ <span v-if="uploading" class="spinner" aria-hidden="true"></span>
163
+ Upload to Walrus
164
+ </button>
165
+ </div>
166
+
167
+ <!-- Relay selection (shown only if multiple relays available) -->
168
+ <fieldset v-if="fileName && availableRelays.length > 1" style="margin: 0.75rem 0">
169
+ <legend>Upload relay</legend>
170
+ <div style="display: flex; flex-direction: column; gap: 0.5rem">
171
+ <label
172
+ v-for="option in availableRelays"
173
+ :key="option.host"
174
+ style="display: flex; align-items: center; gap: 0.5rem"
175
+ >
176
+ <input
177
+ type="radio"
178
+ :value="option.host"
179
+ :checked="selectedRelayHost === option.host"
180
+ @change="(e) => onRelayChange((e.target as HTMLInputElement).value)"
181
+ />
182
+ <span>{{ option.label }}</span>
183
+ </label>
184
+ </div>
185
+ </fieldset>
186
+
187
+ <!-- Cost estimation (shown after file selection) -->
188
+ <AppNotice v-if="fileName && estimatedCost" style="margin: 0.75rem 0">
189
+ <strong>Estimated cost:</strong> {{ estimatedCost.label }}
190
+ </AppNotice>
191
+
192
+ <!-- Public relay notice -->
193
+ <p v-if="showFreeRelayTip" class="hint" style="margin: 0.75rem 0">
194
+ Tip: select the public relay above to avoid the relay fee.
195
+ </p>
196
+
197
+ <p id="walrus-help" class="hint" aria-live="polite">
198
+ Max {{ maxSizeLabel }} · PNG/JPEG/WebP/SVG. Requires <strong>WAL</strong> (storage) and
199
+ <strong>SUI</strong> (gas + a small relay fee) in your wallet; two wallet approvals. Blob
200
+ stored for ~{{ ICON_YEARS }} years.
201
+ <span v-if="status">{{ status }}</span>
202
+ </p>
203
+ <AppNotice v-if="error" type="error">{{ error }}</AppNotice>
204
+ </div>
205
+ </template>
@@ -0,0 +1,122 @@
1
+ import { computed, ref } from 'vue'
2
+ import { ACCESS_GATE } from '../config.js'
3
+ import type { WalrusNetwork } from '../config.js'
4
+ import {
5
+ fetchAccessNfts,
6
+ buildPurchaseTx,
7
+ buildConsumeTx,
8
+ fetchRelayChallenge,
9
+ personalMessageForNonce,
10
+ buildAccessProofToken,
11
+ } from '../lib/accessGate.js'
12
+ import type { AccessGateConfig, OwnedObjectsClient } from '../lib/accessGate.js'
13
+ import type { Executor } from '../lib/deploy.js'
14
+ import type { Transaction } from '@mysten/sui/transactions'
15
+
16
+ /** A wallet personal-message signer, as exposed by `useWallet().signPersonalMessage`. */
17
+ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
18
+
19
+ /**
20
+ * Reactive NFT-gate state for the operator relay.
21
+ *
22
+ * When no gate is configured for the network (`ACCESS_GATE[network] == null`), the relay is
23
+ * treated as OPEN (`hasAccess === true`) and this composable is inert — behaviour is
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
26
+ * check), and `purchase`/`buildRelayAccessToken` drive the buy + prove flows.
27
+ */
28
+ export function useAccessGate(
29
+ network: WalrusNetwork,
30
+ deps: {
31
+ getClient: (network: WalrusNetwork) => OwnedObjectsClient
32
+ /** Override the configured gate (mainly for testing); defaults to `ACCESS_GATE[network]`. */
33
+ gate?: AccessGateConfig | null
34
+ },
35
+ ) {
36
+ const gate: AccessGateConfig | null = deps.gate !== undefined ? deps.gate : ACCESS_GATE[network]
37
+ const gateConfigured = gate !== null
38
+
39
+ // No gate → open. Gate → unknown until checked.
40
+ const hasAccess = ref<boolean | null>(gateConfigured ? null : true)
41
+ const usesRemaining = ref<number | null>(null)
42
+ /** Object id of the held access NFT (for the single-use consume step); null if none. */
43
+ const nftId = ref<string | null>(null)
44
+ const checking = ref(false)
45
+ const error = ref<string | null>(null)
46
+
47
+ /** Query whether `address` holds the gate NFT. Cheap; safe to call on connect. */
48
+ async function checkOwnership(address: string): Promise<void> {
49
+ if (!gate) {
50
+ hasAccess.value = true
51
+ return
52
+ }
53
+ checking.value = true
54
+ error.value = null
55
+ try {
56
+ const nfts = await fetchAccessNfts(deps.getClient(network), address, gate.nftType, gate.gateId)
57
+ hasAccess.value = nfts.length > 0
58
+ usesRemaining.value = nfts.length ? nfts[0].usesRemaining : null
59
+ nftId.value = nfts.length ? nfts[0].objectId : null
60
+ } catch (e) {
61
+ // A failed check must NOT hard-block the user: leave access unknown/false but keep the
62
+ // purchase path available (the gateway re-verifies server-side regardless).
63
+ hasAccess.value = false
64
+ nftId.value = null
65
+ error.value = e instanceof Error ? e.message : String(e)
66
+ } finally {
67
+ checking.value = false
68
+ }
69
+ }
70
+
71
+ /** Purchase access via the connected wallet, then re-check ownership. */
72
+ async function purchase(executor: Executor, address: string): Promise<void> {
73
+ if (!gate) throw new Error('No access gate configured for this network.')
74
+ const tx = buildPurchaseTx(gate)
75
+ const res = await executor.signAndExecute(tx)
76
+ if (res.digest) await executor.waitForTransaction(res.digest).catch(() => {})
77
+ await checkOwnership(address)
78
+ }
79
+
80
+ /** Build the consume PTB for a single-use NFT (woven into the upload flow before proving). */
81
+ function buildConsume(nftId: string, nonce: string): Transaction {
82
+ if (!gate) throw new Error('No access gate configured for this network.')
83
+ return buildConsumeTx(gate, nftId, nonce)
84
+ }
85
+
86
+ /**
87
+ * Fetch a challenge from the gateway (served at the operator relay host), sign it, and
88
+ * return the base64 access-proof token to pass to the Walrus client as the relay auth
89
+ * token. For single-use gates supply the on-chain consume `consumeDigest`.
90
+ */
91
+ async function buildRelayAccessToken(opts: {
92
+ relayHost: string
93
+ address: string
94
+ sign: PersonalMessageSigner
95
+ consumeDigest?: string
96
+ }): Promise<string> {
97
+ const challenge = await fetchRelayChallenge(opts.relayHost)
98
+ const { signature } = await opts.sign(personalMessageForNonce(challenge.nonce))
99
+ return buildAccessProofToken({
100
+ address: opts.address,
101
+ nonce: challenge.nonce,
102
+ signature,
103
+ consumeDigest: opts.consumeDigest,
104
+ })
105
+ }
106
+
107
+ return {
108
+ gate,
109
+ gateConfigured,
110
+ hasAccess,
111
+ usesRemaining,
112
+ nftId,
113
+ checking,
114
+ error,
115
+ /** Convenience: gate configured AND access confirmed. */
116
+ accessGranted: computed(() => !gateConfigured || hasAccess.value === true),
117
+ checkOwnership,
118
+ purchase,
119
+ buildConsume,
120
+ buildRelayAccessToken,
121
+ }
122
+ }
@@ -0,0 +1,76 @@
1
+ import { ref, watch, type Ref } from 'vue'
2
+
3
+ /**
4
+ * Colour-mode state manager (light | dark | system). Sets `data-theme` on
5
+ * <html>, persists the choice, and follows the OS in `system` mode.
6
+ *
7
+ * Vendored mirror of `@meddleware/ui`'s `useColorMode` for visual/UX parity with
8
+ * the dashboard — this repo is self-contained (no cross-repo links).
9
+ * TODO(unify): consume `@meddleware/ui` once it is published.
10
+ */
11
+
12
+ export type ColorMode = 'light' | 'dark' | 'system'
13
+
14
+ const STORAGE_KEY = 'sui-deployer:color-mode'
15
+
16
+ const mode: Ref<ColorMode> = ref<ColorMode>('system')
17
+ let initialized = false
18
+
19
+ function systemPrefersDark(): boolean {
20
+ return (
21
+ typeof window !== 'undefined' &&
22
+ typeof window.matchMedia === 'function' &&
23
+ window.matchMedia('(prefers-color-scheme: dark)').matches
24
+ )
25
+ }
26
+
27
+ function resolveDark(m: ColorMode): boolean {
28
+ return m === 'dark' || (m === 'system' && systemPrefersDark())
29
+ }
30
+
31
+ function apply(m: ColorMode): void {
32
+ if (typeof document === 'undefined') return
33
+ document.documentElement.setAttribute('data-theme', resolveDark(m) ? 'dark' : 'light')
34
+ }
35
+
36
+ export function useColorMode(defaultMode: ColorMode = 'system') {
37
+ if (!initialized) {
38
+ initialized = true
39
+ let stored: ColorMode | null = null
40
+ try {
41
+ stored =
42
+ typeof localStorage !== 'undefined'
43
+ ? (localStorage.getItem(STORAGE_KEY) as ColorMode | null)
44
+ : null
45
+ } catch {
46
+ /* no storage — ignore */
47
+ }
48
+ mode.value = stored ?? defaultMode
49
+ apply(mode.value)
50
+
51
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
52
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
53
+ if (mode.value === 'system') apply('system')
54
+ })
55
+ }
56
+
57
+ watch(mode, (m) => {
58
+ apply(m)
59
+ try {
60
+ if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, m)
61
+ } catch {
62
+ /* ignore */
63
+ }
64
+ })
65
+ }
66
+
67
+ function set(m: ColorMode): void {
68
+ mode.value = m
69
+ }
70
+
71
+ function cycle(): void {
72
+ set(mode.value === 'light' ? 'dark' : mode.value === 'dark' ? 'system' : 'light')
73
+ }
74
+
75
+ return { mode, set, cycle, isDark: () => resolveDark(mode.value) }
76
+ }
@@ -0,0 +1,21 @@
1
+ // Thin wrapper around the shared `@meddleware/walrus-relay-ui` composable: injects
2
+ // this app's per-network relay host pair so the relay-selection + tip-estimation logic
3
+ // lives in exactly one place (DRY). The return shape is unchanged, so consumers
4
+ // (`WalrusIconUpload.vue`, `IconPicker.vue`) and tests are unaffected.
5
+ import { useWalrusRelay as useSharedWalrusRelay } from '@meddleware/walrus-relay'
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'
9
+
10
+ export type { RelayAccessOptions, RelayOption }
11
+
12
+ /**
13
+ * Manage Walrus relay selection and cost estimation for `network`.
14
+ * See `@meddleware/walrus-relay-ui`'s `useWalrusRelay` for the full behaviour.
15
+ */
16
+ export function useWalrusRelay(network: WalrusNetwork, access: RelayAccessOptions = {}) {
17
+ return useSharedWalrusRelay(
18
+ { operator: WALRUS_RELAY_HOSTS[network], public: PUBLIC_WALRUS_RELAY_HOSTS[network] },
19
+ access,
20
+ )
21
+ }
package/src/config.ts ADDED
@@ -0,0 +1,185 @@
1
+ // Deployment-wide configuration. Operator values (fee + treasury) are read from
2
+ // Vite env vars so they can be set at build/deploy time without code changes.
3
+ // See .env.example.
4
+ import type { Network } from './lib/types.js'
5
+ import type { AccessGateConfig } from './lib/accessGate.js'
6
+
7
+ const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
8
+
9
+ /**
10
+ * Public JSON-RPC fullnode URLs per network (overridable via `VITE_RPC_*`).
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.
16
+ */
17
+ export const RPC_URLS: Record<Network, string> = {
18
+ mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
19
+ testnet: env.VITE_RPC_TESTNET || 'https://sui-testnet-rpc.publicnode.com',
20
+ localnet: env.VITE_RPC_LOCALNET || 'http://127.0.0.1:9000',
21
+ }
22
+
23
+ /** Sui explorer base (coin/object links in the result screen). */
24
+ export function explorerObjectUrl(network: Network, id: string): string {
25
+ return `https://suiscan.xyz/${network}/object/${id}`
26
+ }
27
+
28
+ /**
29
+ * The trivial fee (MIST), split from the user's gas coin into the operator
30
+ * treasury inside the publish PTB. Override with VITE_FEE_MIST.
31
+ */
32
+ export const FEE_MIST: bigint = BigInt(env.VITE_FEE_MIST || '1000000000') // 1 SUI default
33
+
34
+ const ZERO_ADDR = '0x0000000000000000000000000000000000000000000000000000000000000000'
35
+
36
+ /** Operator treasury per network (set VITE_FEE_TREASURY_* before launch). */
37
+ export const FEE_TREASURY: Record<Network, string> = {
38
+ mainnet: env.VITE_FEE_TREASURY_MAINNET || ZERO_ADDR,
39
+ testnet: env.VITE_FEE_TREASURY_TESTNET || ZERO_ADDR,
40
+ localnet: env.VITE_FEE_TREASURY_LOCALNET || ZERO_ADDR,
41
+ }
42
+
43
+ /** True when a real treasury has been configured for the network. */
44
+ export function isFeeConfigured(network: Network): boolean {
45
+ return FEE_TREASURY[network] !== ZERO_ADDR
46
+ }
47
+
48
+ /** Gas budget for the publish transaction (MIST). */
49
+ export const PUBLISH_GAS_BUDGET: bigint = BigInt(env.VITE_PUBLISH_GAS_BUDGET || '500000000')
50
+
51
+ /** Networks selectable in the UI. Ship testnet first. */
52
+ export const SELECTABLE_NETWORKS: Network[] = ['testnet', 'mainnet']
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Walrus icon-upload configuration
56
+ // ---------------------------------------------------------------------------
57
+ // Walrus is a separate protocol from Sui; only testnet/mainnet exist (no
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
60
+ // README.md / CLAUDE.md for the operator side.
61
+
62
+ /** A Walrus network — a strict subset of {@link Network} (no localnet). */
63
+ export type WalrusNetwork = 'testnet' | 'mainnet'
64
+
65
+ /** Public Mysten-operated upload relays — the fallback when no operator relay is set. */
66
+ export const PUBLIC_WALRUS_RELAY_HOSTS: Record<WalrusNetwork, string> = {
67
+ testnet: 'https://upload-relay.testnet.walrus.space',
68
+ mainnet: 'https://upload-relay.mainnet.walrus.space',
69
+ }
70
+
71
+ /**
72
+ * Upload relay host per Walrus network.
73
+ *
74
+ * Fallback order: `VITE_WALRUS_RELAY_*` (the operator's own relay — the only way
75
+ * to *collect* the native tip as revenue) → the public Mysten relay. Uploads
76
+ * through the public relay still work but tip Mysten, not the operator, so they
77
+ * earn nothing. A user may also override this at runtime by entering a relay URL.
78
+ */
79
+ export const WALRUS_RELAY_HOSTS: Record<WalrusNetwork, string> = {
80
+ testnet: env.VITE_WALRUS_RELAY_TESTNET || PUBLIC_WALRUS_RELAY_HOSTS.testnet,
81
+ mainnet: env.VITE_WALRUS_RELAY_MAINNET || PUBLIC_WALRUS_RELAY_HOSTS.mainnet,
82
+ }
83
+
84
+ /** True when an operator-owned relay (not the public fallback) is configured. */
85
+ export function isOperatorRelayConfigured(network: WalrusNetwork): boolean {
86
+ return WALRUS_RELAY_HOSTS[network] !== PUBLIC_WALRUS_RELAY_HOSTS[network]
87
+ }
88
+
89
+ /**
90
+ * gRPC fullnode URL for the Walrus icon-upload client (a `SuiGrpcClient`).
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.
97
+ */
98
+ 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',
101
+ }
102
+
103
+ /**
104
+ * Maximum relay tip the client will pay, in MIST.
105
+ *
106
+ * COUPLING CONSTRAINT: the Walrus SDK **hard-fails** an upload (throws) if the
107
+ * relay's configured tip exceeds this value. It must therefore be set with
108
+ * generous headroom *above* whatever tip the operator's relay charges — raising
109
+ * the relay tip past this ceiling breaks uploads until the frontend is
110
+ * redeployed with a higher value. Default 0.05 SUI: comfortably above a
111
+ * cents-scale icon tip, well below anything a user would object to.
112
+ */
113
+ export const WALRUS_MAX_TIP_MIST: bigint = BigInt(env.VITE_WALRUS_MAX_TIP_MIST || '50000000')
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // NFT-gated relay access (optional)
117
+ // ---------------------------------------------------------------------------
118
+ // 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`.
122
+
123
+ function parseAccessGate(network: WalrusNetwork): AccessGateConfig | null {
124
+ 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'),
135
+ }
136
+ }
137
+
138
+ /** The access gate per Walrus network, or `null` when gating is not configured. */
139
+ export const ACCESS_GATE: Record<WalrusNetwork, AccessGateConfig | null> = {
140
+ testnet: parseAccessGate('testnet'),
141
+ mainnet: parseAccessGate('mainnet'),
142
+ }
143
+
144
+ /** True when an NFT gate is configured for the network's operator relay. */
145
+ export function isAccessGateConfigured(network: WalrusNetwork): boolean {
146
+ return ACCESS_GATE[network] !== null
147
+ }
148
+
149
+ /**
150
+ * Maximum icon file size, in bytes (default 100 KiB).
151
+ *
152
+ * This is a UX / predictable-cost guard only — NOT a security control. The
153
+ * authoritative cap is a hard request-body limit at the edge (Cloudflare /
154
+ * reverse proxy in front of the relay); a malicious client bypasses this one.
155
+ */
156
+ export const ICON_MAX_BYTES: number = Number(env.VITE_ICON_MAX_BYTES || String(100 * 1024))
157
+
158
+ /** Accepted icon MIME types (override via `VITE_ICON_ALLOWED_TYPES`, comma-separated). */
159
+ export const ICON_ALLOWED_TYPES: string[] = (
160
+ env.VITE_ICON_ALLOWED_TYPES || 'image/png,image/jpeg,image/webp,image/svg+xml'
161
+ )
162
+ .split(',')
163
+ .map((t) => t.trim())
164
+ .filter(Boolean)
165
+
166
+ /**
167
+ * Validate a chosen icon file against the {@link ICON_ALLOWED_TYPES} /
168
+ * {@link ICON_MAX_BYTES} gate. Returns a user-facing error message, or `null`
169
+ * when the file is acceptable.
170
+ *
171
+ * UX / predictable-cost guard only — NOT a security control (a malicious client
172
+ * bypasses it; the authoritative cap is the edge request-body limit).
173
+ */
174
+ export function validateIconFile(file: { type: string; size: number }): string | null {
175
+ if (ICON_ALLOWED_TYPES.length && !ICON_ALLOWED_TYPES.includes(file.type)) {
176
+ const allowed = ICON_ALLOWED_TYPES.map((t) => t.replace('image/', '')).join(', ')
177
+ return `Unsupported image type${file.type ? ` (${file.type})` : ''}. Allowed: ${allowed}.`
178
+ }
179
+ if (file.size > ICON_MAX_BYTES) {
180
+ return `Image is too large (${Math.round(file.size / 1024)} KB). Maximum is ${Math.round(
181
+ ICON_MAX_BYTES / 1024,
182
+ )} KB.`
183
+ }
184
+ return null
185
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as TokenDeployerView } from './components/TokenDeployerView.vue'