@meddleware/token-deployer-ui 0.0.10 → 0.0.12

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 (87) hide show
  1. package/README.md +8 -5
  2. package/package.json +22 -8
  3. package/src/App.vue +1 -1
  4. package/src/component-styles.css +39 -57
  5. package/src/components/GithubPush.vue +2 -2
  6. package/src/components/IconPicker.vue +44 -75
  7. package/src/components/LicensePicker.vue +2 -2
  8. package/src/components/ResultPanel.vue +2 -2
  9. package/src/components/ReviewPanel.vue +11 -3
  10. package/src/components/TokenDeployerView.vue +52 -48
  11. package/src/components/WalletBar.vue +12 -1
  12. package/src/components/WalrusBlobBrowser.vue +4 -4
  13. package/src/composables/useWalrusRelay.ts +1 -2
  14. package/src/config.ts +19 -17
  15. package/src/lib/buildPublishTx.ts +0 -10
  16. package/src/lib/deploy.ts +9 -3
  17. package/src/lib/deployExecutor.ts +18 -45
  18. package/src/lib/template.ts +3 -0
  19. package/src/lib/walrus-constants.ts +2 -2
  20. package/src/main.ts +4 -6
  21. package/src/styles.css +22 -0
  22. package/.claude/move-language-reference/SKILL.md +0 -445
  23. package/.claude/skills/move-language-reference/SKILL.md +0 -445
  24. package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
  25. package/.claude/sui-move-bootcamp/SKILL.md +0 -128
  26. package/.editorconfig +0 -8
  27. package/.env.example +0 -72
  28. package/.gitattributes +0 -1
  29. package/.github/workflows/docker-publish.yml +0 -348
  30. package/.github/workflows/e2e-realchain.yml +0 -64
  31. package/.github/workflows/node-ci.yml +0 -43
  32. package/.github/workflows/npm-publish.yml +0 -83
  33. package/.htmlvalidate.mjs +0 -30
  34. package/.oxfmtrc.json +0 -5
  35. package/.oxlintrc.json +0 -10
  36. package/.vscode/extensions.json +0 -9
  37. package/AGENTS.md +0 -39
  38. package/CLAUDE.md +0 -166
  39. package/Dockerfile +0 -96
  40. package/eslint.config.ts +0 -63
  41. package/index.html +0 -22
  42. package/playwright.config.ts +0 -30
  43. package/postcss.config.js +0 -8
  44. package/public/CNAME +0 -1
  45. package/public/_headers +0 -12
  46. package/public/_redirects +0 -1
  47. package/public/favicon.ico +0 -0
  48. package/public/favicon.svg +0 -5
  49. package/public/og-image.png +0 -0
  50. package/public/robots.txt +0 -3
  51. package/public/sitemap.xml +0 -8
  52. package/scripts/e2e-browser.mjs +0 -15
  53. package/scripts/e2e-deploy.mjs +0 -227
  54. package/scripts/e2e-localnet.mjs +0 -67
  55. package/scripts/e2e-walrus-browser.mjs +0 -112
  56. package/scripts/gen-og-image.mjs +0 -31
  57. package/scripts/publish-localnet.mjs +0 -56
  58. package/scripts/regen-template.sh +0 -45
  59. package/scripts/sync-template-src.sh +0 -36
  60. package/scripts/verify-bytecode.mjs +0 -49
  61. package/src/__tests__/App.spec.js +0 -11
  62. package/src/composables/useAccessGate.ts +0 -127
  63. package/src/lib/accessGate.ts +0 -155
  64. package/src/lib/walrus.ts +0 -155
  65. package/stylelint.config.js +0 -33
  66. package/test-results/.last-run.json +0 -4
  67. package/tests/accessGate.test.ts +0 -113
  68. package/tests/buildPublishTx.test.ts +0 -243
  69. package/tests/deploy.test.ts +0 -149
  70. package/tests/errors.test.ts +0 -32
  71. package/tests/generatePackage.test.ts +0 -193
  72. package/tests/github.test.ts +0 -61
  73. package/tests/licenses.test.ts +0 -81
  74. package/tests/listMyTokens.test.ts +0 -30
  75. package/tests/template.test.ts +0 -93
  76. package/tests/templateArtifact.test.ts +0 -61
  77. package/tests/templateParity.test.ts +0 -55
  78. package/tests/useAccessGate.test.ts +0 -97
  79. package/tests/useWallet.test.ts +0 -74
  80. package/tests/useWalrusRelayGating.test.ts +0 -30
  81. package/tests/validation.test.ts +0 -117
  82. package/tests/walrusUpload.test.ts +0 -93
  83. package/tests/walrus_url_patch.test.ts +0 -26
  84. package/tsconfig.json +0 -19
  85. package/typedoc.json +0 -34
  86. package/vite.config.ts +0 -73
  87. package/vitest.config.ts +0 -10
package/README.md CHANGED
@@ -22,7 +22,8 @@ and can optionally push it to a new GitHub repo in their own account.
22
22
  ([src/lib/generatePackage.ts](src/lib/generatePackage.ts)) that is **byte-identical**
23
23
  to the CLI generator's output, so the on-chain package is source-verifiable.
24
24
  5. Licenses are fetched live from SPDX ([src/lib/licenses.ts](src/lib/licenses.ts)); an
25
- optional icon upload goes to Walrus ([src/lib/walrus.ts](src/lib/walrus.ts)); and an
25
+ optional icon upload goes to Walrus (`@meddleware/walrus-client/flow`, from
26
+ [src/components/IconPicker.vue](src/components/IconPicker.vue)); and an
26
27
  optional GitHub push uses the user's own token ([src/lib/github.ts](src/lib/github.ts)).
27
28
 
28
29
  ## Security & trust model
@@ -163,14 +164,16 @@ Two tests enforce this invariant in CI so it can never silently drift:
163
164
  - `scripts/e2e-deploy.mjs` — the **network-parametrized real-chain deploy e2e** (headless
164
165
  browser + injected wallet): drives the full UI, does a REAL publish, waits for real
165
166
  confirmation, checks the result panel, downloads the source zip, and verifies coin type +
166
- operator fee on-chain. Covers exactly what the mocked Cypress suite cannot. `scripts/e2e-browser.mjs`
167
+ operator fee on-chain. Covers exactly what the mocked Playwright suite cannot. `scripts/e2e-browser.mjs`
167
168
  is a thin localnet wrapper over it.
168
- - `scripts/e2e-walrus-browser.mjs` — the Walrus icon uploader end-to-end on testnet
169
- with an injected wallet.
169
+ - `scripts/e2e-walrus-browser.mjs` (`npm run e2e:walrus`) — the Walrus icon uploader end-to-end on
170
+ testnet with an injected wallet (`SUI_PRIV` with SUI + WAL); `E2E_RELAY=operator` exercises the
171
+ NFT-gated relay (pass purchase, consume, signed proof). Manual only — it spends real funds.
170
172
 
171
173
  ## Real-chain e2e / launch runbook
172
174
 
173
- The Playwright mocked suite (`npm run test:e2e`) mocks all RPC via `VITE_E2E=1`, so it can prove
175
+ The Playwright mocked suite (`npm run test:e2e`) mocks all RPC in a dedicated e2e build
176
+ (`npm run build:e2e`, `--mode e2e`; production builds refuse `VITE_E2E`), so it can prove
174
177
  the form/wallet/publish UI but **not** on-chain confirmation, the result panel, or the source
175
178
  download. The real-chain harness fills that gap. It is **manual** — never wired into automatic CI — because testnet/mainnet runs
176
179
  publish a real token and spend real SUI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/token-deployer-ui",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "license": "0BSD",
6
6
  "exports": {
@@ -8,6 +8,13 @@
8
8
  "default": "./src/index.ts"
9
9
  }
10
10
  },
11
+ "files": [
12
+ "src",
13
+ "!src/__tests__",
14
+ "env.d.ts",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
11
18
  "publishConfig": {
12
19
  "access": "public"
13
20
  },
@@ -19,13 +26,15 @@
19
26
  "test:unit": "vitest",
20
27
  "test": "vitest run",
21
28
  "test:watch": "vitest",
22
- "build:e2e": "VITE_E2E=1 vite build",
29
+ "build:e2e": "VITE_E2E=1 vite build --mode e2e",
30
+ "check:bundle": "node scripts/check-prod-bundle.mjs",
23
31
  "test:e2e": "playwright test",
24
32
  "test:e2e:ui": "playwright test --ui",
25
33
  "e2e:install": "playwright install --with-deps chromium firefox",
26
34
  "e2e:localnet": "VITE_RPC_TESTNET=${VITE_RPC_TESTNET:-http://127.0.0.1:9000} VITE_FEE_TREASURY_TESTNET=${VITE_FEE_TREASURY_TESTNET:-0xfee0000000000000000000000000000000000000000000000000000000000000} vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=localnet node scripts/e2e-deploy.mjs'",
27
35
  "e2e:testnet": "vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=testnet node scripts/e2e-deploy.mjs'",
28
36
  "e2e:mainnet": "vite build && start-server-and-test preview http://localhost:4173 'E2E_NETWORK=mainnet node scripts/e2e-deploy.mjs'",
37
+ "e2e:walrus": "vite build && start-server-and-test preview http://localhost:4173 'node scripts/e2e-walrus-browser.mjs'",
29
38
  "lint": "npm run lint:css && npm run lint:js && npm run lint:html",
30
39
  "format": "oxfmt src/",
31
40
  "docs": "typedoc",
@@ -40,16 +49,21 @@
40
49
  "dependencies": {
41
50
  "@meddleware/design-tokens": "^0.1.7",
42
51
  "@meddleware/ui": "^0.1.28",
43
- "@meddleware/wallet-adapter": "^0.0.10",
44
- "@meddleware/walrus-relay": "^0.1.19",
45
- "@mysten/bcs": "~2.1.0",
46
- "@mysten/move-bytecode-template": "~0.4.0",
52
+ "@meddleware/walrus-client": "^0.0.21",
53
+ "@meddleware/walrus-relay": "^0.1.20",
54
+ "@mysten/bcs": "~2.1.2",
55
+ "@mysten/move-bytecode-template": "~0.4.1",
47
56
  "@mysten/sui": "^2.33.1",
48
- "@mysten/walrus": "~1.2.0",
57
+ "@mysten/walrus": "~1.2.32",
49
58
  "fflate": "^0.8.3",
50
59
  "vue": "^3.5.0"
51
60
  },
61
+ "peerDependencies": {
62
+ "@meddleware/wallet-adapter": ">=0.0.12 <0.2.0"
63
+ },
52
64
  "devDependencies": {
65
+ "@meddleware/access-gate-client": "^0.0.1",
66
+ "@meddleware/wallet-adapter": "^0.0.12",
53
67
  "@playwright/test": "^1.56.0",
54
68
  "@types/node": "~24.12.2",
55
69
  "@vitejs/plugin-vue": "^6.0.8",
@@ -77,7 +91,7 @@
77
91
  "typescript": "~6.0.0",
78
92
  "vite": "^8.1.5",
79
93
  "vite-plugin-vue-devtools": "^8.1.5",
80
- "vitest": "^4.1.10",
94
+ "vitest": "~5.0.2",
81
95
  "vue-eslint-parser": "^10.4.1",
82
96
  "vue-tsc": "~3.3.0"
83
97
  },
package/src/App.vue CHANGED
@@ -8,7 +8,7 @@ const { mode: colorMode, set: setColorMode } = useColorMode('system')
8
8
 
9
9
  <template>
10
10
  <a class="skip-link" href="#main-content">Skip to main content</a>
11
- <main id="main-content">
11
+ <main id="main-content" class="mw-token-deployer">
12
12
  <header class="page-header">
13
13
  <ColorModeControl
14
14
  class="theme-toggle"
@@ -1,9 +1,11 @@
1
1
  /* Component utility styles for TokenDeployerView and its sub-components.
2
2
  Imported by index.ts so these are available in both standalone and library (dashboard) mode.
3
+ Every rule is scoped under `:where(.mw-token-deployer)` so the library injects nothing global
4
+ into an embedding host, while `:where()` keeps the original (element-level) specificity.
3
5
  Relies on design-token CSS variables (--bg, --surface, --border, etc.) provided by the
4
6
  consumer — either @meddleware/design-tokens (standalone) or the dashboard's own import. */
5
7
 
6
- .card {
8
+ :where(.mw-token-deployer) .card {
7
9
  background: var(--surface);
8
10
  border: 1px solid var(--border);
9
11
  border-radius: var(--radius);
@@ -12,20 +14,20 @@
12
14
  border-top: 2px solid var(--border);
13
15
  }
14
16
 
15
- label {
17
+ :where(.mw-token-deployer) label {
16
18
  display: block;
17
19
  font-weight: 600;
18
20
  margin: 0.9rem 0 0.3rem;
19
21
  }
20
- .hint {
22
+ :where(.mw-token-deployer) .hint {
21
23
  color: var(--muted);
22
24
  font-weight: 400;
23
25
  font-size: 0.9rem;
24
26
  }
25
27
 
26
- input,
27
- select,
28
- textarea {
28
+ :where(.mw-token-deployer) input,
29
+ :where(.mw-token-deployer) select,
30
+ :where(.mw-token-deployer) textarea {
29
31
  width: 100%;
30
32
  padding: 0.6rem 0.7rem;
31
33
  font: inherit;
@@ -34,24 +36,24 @@ textarea {
34
36
  border: 1px solid var(--border);
35
37
  border-radius: 8px;
36
38
  }
37
- textarea {
39
+ :where(.mw-token-deployer) textarea {
38
40
  min-height: 4rem;
39
41
  resize: vertical;
40
42
  }
41
43
 
42
- input[aria-invalid='true'],
43
- select[aria-invalid='true'],
44
- textarea[aria-invalid='true'] {
44
+ :where(.mw-token-deployer) input[aria-invalid='true'],
45
+ :where(.mw-token-deployer) select[aria-invalid='true'],
46
+ :where(.mw-token-deployer) textarea[aria-invalid='true'] {
45
47
  border-color: var(--danger);
46
48
  }
47
49
 
48
- .field-error {
50
+ :where(.mw-token-deployer) .field-error {
49
51
  color: var(--danger);
50
52
  font-size: 0.88rem;
51
53
  margin-top: 0.3rem;
52
54
  }
53
55
 
54
- button {
56
+ :where(.mw-token-deployer) button {
55
57
  font: inherit;
56
58
  font-weight: 600;
57
59
  padding: 0.6rem 1rem;
@@ -61,54 +63,54 @@ button {
61
63
  color: var(--text);
62
64
  cursor: pointer;
63
65
  }
64
- button.primary {
66
+ :where(.mw-token-deployer) button.primary {
65
67
  background: var(--accent);
66
68
  color: var(--accent-contrast);
67
69
  border-color: transparent;
68
70
  }
69
- button:disabled {
71
+ :where(.mw-token-deployer) button:disabled {
70
72
  opacity: 0.55;
71
73
  cursor: not-allowed;
72
74
  }
73
75
 
74
- .row {
76
+ :where(.mw-token-deployer) .row {
75
77
  display: flex;
76
78
  gap: 0.75rem;
77
79
  flex-wrap: wrap;
78
80
  align-items: center;
79
81
  }
80
82
  /* A row of form/navigation actions below a block of content. */
81
- .actions {
83
+ :where(.mw-token-deployer) .actions {
82
84
  margin-top: 1rem;
83
85
  }
84
- .grid2 {
86
+ :where(.mw-token-deployer) .grid2 {
85
87
  display: grid;
86
88
  grid-template-columns: 1fr;
87
89
  gap: 0 1rem;
88
90
  }
89
91
  @media (width >= 520px) {
90
- .grid2 {
92
+ :where(.mw-token-deployer) .grid2 {
91
93
  grid-template-columns: 1fr 1fr;
92
94
  }
93
95
  }
94
96
 
95
- fieldset {
97
+ :where(.mw-token-deployer) fieldset {
96
98
  background: var(--lift);
97
99
  border: 1px solid var(--border);
98
100
  border-radius: 8px;
99
101
  padding: 0.5rem 1rem 1rem;
100
102
  margin: 1rem 0;
101
103
  }
102
- legend {
104
+ :where(.mw-token-deployer) legend {
103
105
  font-weight: 700;
104
106
  padding: 0 0.4rem;
105
107
  }
106
108
 
107
- details {
109
+ :where(.mw-token-deployer) details {
108
110
  margin: 1rem 0;
109
111
  }
110
112
 
111
- details > summary {
113
+ :where(.mw-token-deployer) details > summary {
112
114
  color: var(--accent);
113
115
  font-weight: 700;
114
116
  margin: 0.5rem 0;
@@ -119,28 +121,28 @@ details > summary {
119
121
  transition: border-color 0.2s ease;
120
122
  }
121
123
 
122
- details > summary:hover {
124
+ :where(.mw-token-deployer) details > summary:hover {
123
125
  border-color: var(--border);
124
126
  }
125
127
 
126
- details[open] > summary {
128
+ :where(.mw-token-deployer) details[open] > summary {
127
129
  border-color: var(--border);
128
130
  margin-bottom: 1rem;
129
131
  }
130
132
 
131
- details > fieldset {
133
+ :where(.mw-token-deployer) details > fieldset {
132
134
  margin-top: 0;
133
135
  }
134
136
 
135
- .permanent {
137
+ :where(.mw-token-deployer) .permanent {
136
138
  color: var(--danger);
137
139
  font-weight: 600;
138
140
  }
139
- .mono {
141
+ :where(.mw-token-deployer) .mono {
140
142
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
141
143
  word-break: break-all;
142
144
  }
143
- .badge {
145
+ :where(.mw-token-deployer) .badge {
144
146
  display: inline-block;
145
147
  font-size: 0.8rem;
146
148
  padding: 0.15rem 0.5rem;
@@ -148,44 +150,24 @@ details > fieldset {
148
150
  background: var(--border);
149
151
  }
150
152
 
151
- .skip-link {
152
- position: absolute;
153
- left: 0.5rem;
154
- top: -3rem;
155
- z-index: 100;
156
- background: var(--accent);
157
- color: #fff;
158
- padding: 0.5rem 0.9rem;
159
- border-radius: 6px;
160
- text-decoration: none;
161
- transition: top 0.15s ease;
162
- }
163
- .skip-link:focus {
164
- top: 0.5rem;
165
- }
166
- @media (prefers-reduced-motion: reduce) {
167
- .skip-link {
168
- transition: none;
169
- }
170
- }
171
153
 
172
- .steps {
154
+ :where(.mw-token-deployer) .steps {
173
155
  list-style: none;
174
156
  padding: 0;
175
157
  }
176
- .steps li {
158
+ :where(.mw-token-deployer) .steps li {
177
159
  padding: 0.35rem 0;
178
160
  color: var(--muted);
179
161
  }
180
- .steps li[data-state='active'] {
162
+ :where(.mw-token-deployer) .steps li[data-state='active'] {
181
163
  color: var(--text);
182
164
  font-weight: 600;
183
165
  }
184
- .steps li[data-state='done'] {
166
+ :where(.mw-token-deployer) .steps li[data-state='done'] {
185
167
  color: var(--ok);
186
168
  }
187
169
 
188
- .spinner {
170
+ :where(.mw-token-deployer) .spinner {
189
171
  width: 1em;
190
172
  height: 1em;
191
173
  border: 2px solid currentColor;
@@ -201,21 +183,21 @@ details > fieldset {
201
183
  }
202
184
  }
203
185
  @media (prefers-reduced-motion: reduce) {
204
- .spinner {
186
+ :where(.mw-token-deployer) .spinner {
205
187
  animation: none;
206
188
  }
207
- * {
189
+ :where(.mw-token-deployer) * {
208
190
  scroll-behavior: auto;
209
191
  }
210
192
  }
211
193
 
212
- .wallet-list {
194
+ :where(.mw-token-deployer) .wallet-list {
213
195
  display: flex;
214
196
  flex-direction: column;
215
197
  gap: 0.5rem;
216
198
  margin-top: 1rem;
217
199
  }
218
- .wallet-list button {
200
+ :where(.mw-token-deployer) .wallet-list button {
219
201
  display: flex;
220
202
  align-items: center;
221
203
  gap: 0.6rem;
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from 'vue'
3
3
  import type { PublishResult, TokenConfig } from '../lib/types.js'
4
- import { UiNotice } from '@meddleware/ui'
4
+ import { UiNotice, safeHref } from '@meddleware/ui'
5
5
  import { buildPackageFiles } from '../lib/generatePackage.js'
6
6
  import { fetchLicenseText } from '../lib/licenses.js'
7
7
  import { createRepoAndPush } from '../lib/github.js'
@@ -92,7 +92,7 @@ async function push(): Promise<void> {
92
92
 
93
93
  <UiNotice v-if="error" type="error">{{ error }}</UiNotice>
94
94
  <UiNotice v-if="doneUrl" type="ok">
95
- Pushed: <a :href="doneUrl" target="_blank" rel="noopener noreferrer">{{ doneUrl }}</a>
95
+ Pushed: <a :href="safeHref(doneUrl)" target="_blank" rel="noopener noreferrer">{{ doneUrl }}</a>
96
96
  </UiNotice>
97
97
  </div>
98
98
  </div>
@@ -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<{
@@ -131,7 +131,7 @@ async function openLicenseModal(): Promise<void> {
131
131
  <template v-else>
132
132
  <p class="license-spdx">
133
133
  <a
134
- :href="`https://spdx.org/licenses/${modelValue}.html`"
134
+ :href="safeHref(`https://spdx.org/licenses/${encodeURIComponent(modelValue)}.html`)"
135
135
  target="_blank"
136
136
  rel="noopener noreferrer"
137
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">