@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,31 @@
1
+ // AUTO-GENERATED ARTIFACT — do not edit by hand.
2
+ //
3
+ // Base64 of the compiled `sui_token_template` Move module (binary format v7),
4
+ // produced by `sui move build --build-env testnet` from
5
+ // blockchain/sui/sui-token-template. Regenerate with scripts/regen-template.sh
6
+ // whenever the canonical template source changes (see move-template/README.md).
7
+ //
8
+ // The DEFAULT_* values below are the constant-pool defaults compiled into the
9
+ // module. The deployer patches the module by matching these exact defaults and
10
+ // replacing them (see lib/template.ts). They are DISTINCT on purpose: identical
11
+ // Move constants deduplicate into a single unpatchable pool entry.
12
+
13
+ export const TEMPLATE_MODULE_B64 =
14
+ 'oRzrCwcAAAUKAQAMAgweAyohBEsIBVNaB60B3QEIigNgBuoDVAq+BAUMwwQ6AA8BDgIGAgcCEAIRAAICAAEDBwACBAwBAAEDAAABAAEDAQwBAAEFBQIAAAoAAQABEgMEAAMJCAkBAAMLBgcBAgQMDQEBDAUNCgsAAwUCBQQMBA4CCAAHCAUAAgsEAQgACwIBCAABCgIBCAEBCAAHCQACCAEIAQgBCAEHCAUCCwMBCQALAgEJAAILAwEJAAcIBQELBAEJAAEGCAUBBQELAgEIAAIJAAUBCwQBCAATQ3VycmVuY3lJbml0aWFsaXplcgtNZXRhZGF0YUNhcBJTVUlfVE9LRU5fVEVNUExBVEUGU3RyaW5nC1RyZWFzdXJ5Q2FwCVR4Q29udGV4dARjb2luDWNvaW5fcmVnaXN0cnkLZHVtbXlfZmllbGQIZmluYWxpemUEaW5pdBVuZXdfY3VycmVuY3lfd2l0aF9vdHcPcHVibGljX3RyYW5zZmVyBnNlbmRlcgZzdHJpbmcSc3VpX3Rva2VuX3RlbXBsYXRlCHRyYW5zZmVyCnR4X2NvbnRleHQEdXRmOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIBCQoCEA9URU1QTEFURV9TWU1CT0wKAg4NVEVNUExBVEVfTkFNRQoCFRRURU1QTEFURV9ERVNDUklQVElPTgoCEhFURU1QTEFURV9JQ09OX1VSTAACAQgBAAAAAAIbCwAHAAcBEQEHAhEBBwMRAQcEEQEKATgADAMKATgBDAILAwoBLhEFOAILAgsBLhEFOAMCAAA='
15
+
16
+ /** Identifiers compiled into the template module (renamed at patch time). */
17
+ export const TEMPLATE_IDENTIFIERS = {
18
+ /** module identifier (lowercase) */
19
+ module: 'sui_token_template',
20
+ /** one-time-witness struct identifier (uppercase) */
21
+ struct: 'SUI_TOKEN_TEMPLATE',
22
+ } as const
23
+
24
+ /** Distinct constant-pool defaults the deployer patches away from. */
25
+ export const TEMPLATE_DEFAULTS = {
26
+ decimals: 9,
27
+ symbol: 'TEMPLATE_SYMBOL',
28
+ name: 'TEMPLATE_NAME',
29
+ description: 'TEMPLATE_DESCRIPTION',
30
+ iconUrl: 'TEMPLATE_ICON_URL',
31
+ } as const
package/src/styles.css ADDED
@@ -0,0 +1,384 @@
1
+ /* Accessible, lightweight styling. System fonts, dark/light aware, high-contrast
2
+ focus rings, and reduced-motion respect. No heavy CSS framework. */
3
+
4
+ /* Oxblood / Indigo (masonic) palette — vendored mirror of the shared design
5
+ system for visual parity with the dashboard. The repo is self-contained
6
+ (no cross-repo links), so the token VALUES are copied here.
7
+ TODO(unify): consume `@meddleware/design-tokens` once it is published.
8
+ Dark mode is driven by `data-theme` (see composables/useColorMode.ts, which
9
+ also enables the light/dark/system control); the `:not([data-theme])` media
10
+ block preserves OS-follow before that runs (anti-flash). */
11
+ :root {
12
+ --bg: #f7f4f1;
13
+ --surface: #ffffff;
14
+ --border: #ded6cf;
15
+ --text: #201b19;
16
+ --muted: #6e635c;
17
+ --accent: #5e1622;
18
+ --accent-contrast: #ffffff;
19
+ --danger: #b3261e;
20
+ --ok: #1c5e4a;
21
+ --radius: 10px;
22
+ --lift: #efeae6;
23
+ color-scheme: light;
24
+ }
25
+
26
+ :root[data-theme="dark"] {
27
+ --bg: #120e10;
28
+ --surface: #1c1618;
29
+ --border: #372b2e;
30
+ --text: #f2eae6;
31
+ --muted: #b7a9a3;
32
+ --accent: #b0465a;
33
+ --accent-contrast: #ffffff;
34
+ --danger: #f08a7e;
35
+ --ok: #5bb392;
36
+ --lift: #241c1f;
37
+ color-scheme: dark;
38
+ }
39
+
40
+ /* Pre-JS OS-follow (only until useColorMode sets an explicit data-theme). */
41
+ @media (prefers-color-scheme: dark) {
42
+ :root:not([data-theme]) {
43
+ --bg: #120e10;
44
+ --surface: #1c1618;
45
+ --border: #372b2e;
46
+ --text: #f2eae6;
47
+ --muted: #b7a9a3;
48
+ --accent: #b0465a;
49
+ --accent-contrast: #ffffff;
50
+ --danger: #f08a7e;
51
+ --ok: #5bb392;
52
+ --lift: #241c1f;
53
+ color-scheme: dark;
54
+ }
55
+ }
56
+
57
+ * {
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ /* Top-right colour-mode control row (parity with the dashboard). */
62
+ .theme-bar {
63
+ display: flex;
64
+ justify-content: flex-end;
65
+ margin-bottom: 0.75rem;
66
+ }
67
+
68
+ body {
69
+ margin: 0;
70
+ background: var(--bg);
71
+ color: var(--text);
72
+ font:
73
+ 16px/1.55 system-ui,
74
+ -apple-system,
75
+ 'Segoe UI',
76
+ Roboto,
77
+ sans-serif;
78
+ }
79
+
80
+ main {
81
+ max-width: 760px;
82
+ margin: 0 auto;
83
+ padding: 1.5rem 1.25rem 4rem;
84
+ }
85
+
86
+ header {
87
+ padding-bottom: 1.5rem;
88
+ border-bottom: 1px solid var(--border);
89
+ margin-bottom: 1.5rem;
90
+ }
91
+
92
+ h1 {
93
+ font-size: clamp(1.5rem, 4vw, 2rem);
94
+ line-height: 1.2;
95
+ }
96
+ h2 {
97
+ font-size: 1.15rem;
98
+ margin-top: 2rem;
99
+ }
100
+ p.lede {
101
+ color: var(--muted);
102
+ font-size: 1.05rem;
103
+ }
104
+ a {
105
+ color: var(--accent);
106
+ }
107
+
108
+ .card {
109
+ background: var(--surface);
110
+ border: 1px solid var(--border);
111
+ border-radius: var(--radius);
112
+ padding: 1.25rem;
113
+ margin: 1.5rem 0 1rem;
114
+ border-top: 2px solid var(--border);
115
+ }
116
+
117
+ label {
118
+ display: block;
119
+ font-weight: 600;
120
+ margin: 0.9rem 0 0.3rem;
121
+ }
122
+ .hint {
123
+ color: var(--muted);
124
+ font-weight: 400;
125
+ font-size: 0.9rem;
126
+ }
127
+
128
+ input,
129
+ select,
130
+ textarea {
131
+ width: 100%;
132
+ padding: 0.6rem 0.7rem;
133
+ font: inherit;
134
+ color: var(--text);
135
+ background: var(--bg);
136
+ border: 1px solid var(--border);
137
+ border-radius: 8px;
138
+ }
139
+ textarea {
140
+ min-height: 4rem;
141
+ resize: vertical;
142
+ }
143
+
144
+ input[aria-invalid='true'],
145
+ select[aria-invalid='true'],
146
+ textarea[aria-invalid='true'] {
147
+ border-color: var(--danger);
148
+ }
149
+
150
+ .field-error {
151
+ color: var(--danger);
152
+ font-size: 0.88rem;
153
+ margin-top: 0.3rem;
154
+ }
155
+
156
+ :focus-visible {
157
+ outline: 3px solid var(--accent);
158
+ outline-offset: 2px;
159
+ }
160
+
161
+ button {
162
+ font: inherit;
163
+ font-weight: 600;
164
+ padding: 0.6rem 1rem;
165
+ border-radius: 8px;
166
+ border: 1px solid var(--border);
167
+ background: var(--surface);
168
+ color: var(--text);
169
+ cursor: pointer;
170
+ }
171
+ button.primary {
172
+ background: var(--accent);
173
+ color: var(--accent-contrast);
174
+ border-color: transparent;
175
+ }
176
+ button:disabled {
177
+ opacity: 0.55;
178
+ cursor: not-allowed;
179
+ }
180
+
181
+ .row {
182
+ display: flex;
183
+ gap: 0.75rem;
184
+ flex-wrap: wrap;
185
+ align-items: center;
186
+ }
187
+ .grid2 {
188
+ display: grid;
189
+ grid-template-columns: 1fr;
190
+ gap: 0 1rem;
191
+ }
192
+ @media (min-width: 520px) {
193
+ .grid2 {
194
+ grid-template-columns: 1fr 1fr;
195
+ }
196
+ }
197
+
198
+ fieldset {
199
+ background: var(--lift);
200
+ border: 1px solid var(--border);
201
+ border-radius: 8px;
202
+ padding: 0.5rem 1rem 1rem;
203
+ margin: 1rem 0;
204
+ }
205
+ legend {
206
+ font-weight: 700;
207
+ padding: 0 0.4rem;
208
+ }
209
+
210
+ details {
211
+ margin: 1rem 0;
212
+ }
213
+
214
+ details > summary {
215
+ color: var(--accent);
216
+ font-weight: 700;
217
+ margin: 0.5rem 0;
218
+ padding: 0.75rem;
219
+ background: var(--lift);
220
+ border-radius: 8px;
221
+ border: 1px solid transparent;
222
+ transition: border-color 0.2s ease;
223
+ }
224
+
225
+ details > summary:hover {
226
+ border-color: var(--border);
227
+ }
228
+
229
+ details[open] > summary {
230
+ border-color: var(--border);
231
+ margin-bottom: 1rem;
232
+ }
233
+
234
+ details > fieldset {
235
+ margin-top: 0;
236
+ }
237
+
238
+ .permanent {
239
+ color: var(--danger);
240
+ font-weight: 600;
241
+ }
242
+ .mono {
243
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
244
+ word-break: break-all;
245
+ }
246
+ .badge {
247
+ display: inline-block;
248
+ font-size: 0.8rem;
249
+ padding: 0.15rem 0.5rem;
250
+ border-radius: 999px;
251
+ background: var(--border);
252
+ }
253
+
254
+ .visually-hidden {
255
+ position: absolute;
256
+ width: 1px;
257
+ height: 1px;
258
+ padding: 0;
259
+ margin: -1px;
260
+ overflow: hidden;
261
+ clip: rect(0 0 0 0);
262
+ white-space: nowrap;
263
+ border: 0;
264
+ }
265
+
266
+ /* Keyboard skip link: hidden until focused, then visible at the top-left. */
267
+ .skip-link {
268
+ position: absolute;
269
+ left: 0.5rem;
270
+ top: -3rem;
271
+ z-index: 100;
272
+ background: var(--accent);
273
+ color: #fff;
274
+ padding: 0.5rem 0.9rem;
275
+ border-radius: 6px;
276
+ text-decoration: none;
277
+ transition: top 0.15s ease;
278
+ }
279
+ .skip-link:focus {
280
+ top: 0.5rem;
281
+ }
282
+ @media (prefers-reduced-motion: reduce) {
283
+ .skip-link {
284
+ transition: none;
285
+ }
286
+ }
287
+
288
+ .steps {
289
+ list-style: none;
290
+ padding: 0;
291
+ }
292
+ .steps li {
293
+ padding: 0.35rem 0;
294
+ color: var(--muted);
295
+ }
296
+ .steps li[data-state='active'] {
297
+ color: var(--text);
298
+ font-weight: 600;
299
+ }
300
+ .steps li[data-state='done'] {
301
+ color: var(--ok);
302
+ }
303
+
304
+ .spinner {
305
+ width: 1em;
306
+ height: 1em;
307
+ border: 2px solid currentColor;
308
+ border-right-color: transparent;
309
+ border-radius: 50%;
310
+ display: inline-block;
311
+ animation: spin 0.7s linear infinite;
312
+ vertical-align: -0.15em;
313
+ }
314
+ @keyframes spin {
315
+ to {
316
+ transform: rotate(360deg);
317
+ }
318
+ }
319
+ @media (prefers-reduced-motion: reduce) {
320
+ .spinner {
321
+ animation: none;
322
+ }
323
+ * {
324
+ scroll-behavior: auto;
325
+ }
326
+ }
327
+
328
+ footer {
329
+ border-top: 1px solid var(--border);
330
+ padding-top: 1.5rem;
331
+ margin-top: 2rem;
332
+ }
333
+
334
+ .notice {
335
+ border-left: 4px solid var(--accent);
336
+ padding: 0.5rem 0.75rem;
337
+ background: var(--surface);
338
+ border-radius: 6px;
339
+ }
340
+ .notice.error {
341
+ border-color: var(--danger);
342
+ }
343
+ .notice.ok {
344
+ border-color: var(--ok);
345
+ }
346
+
347
+ dialog {
348
+ background: var(--surface);
349
+ border: 1px solid var(--border);
350
+ border-radius: var(--radius);
351
+ padding: 1.5rem;
352
+ width: min(420px, 90vw);
353
+ max-height: 90vh;
354
+ overflow-y: auto;
355
+ color: var(--text);
356
+ }
357
+ dialog::backdrop {
358
+ background: rgba(0, 0, 0, 0.45);
359
+ -webkit-backdrop-filter: blur(2px);
360
+ backdrop-filter: blur(2px);
361
+ }
362
+ .dialog-header {
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ margin-bottom: 1.25rem;
367
+ }
368
+ .dialog-header h2 {
369
+ margin: 0;
370
+ font-size: 1.1rem;
371
+ }
372
+ .wallet-list {
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 0.5rem;
376
+ margin-top: 1rem;
377
+ }
378
+ .wallet-list button {
379
+ display: flex;
380
+ align-items: center;
381
+ gap: 0.6rem;
382
+ width: 100%;
383
+ justify-content: flex-start;
384
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "Move.toml": "[package]\nedition = \"2024\"\nname = \"sui_token_template\"\nversion = \"1.0.0\"\nlicense = \"CC0-1.0\"\n\n\n[addresses]\nsui_token_template = \"0x0\"\n",
3
+ "source.move": "// SPDX-License-Identifier: CC0-1.0\n// This work is dedicated to the public domain under CC0.\n\n/// XPACKAGEDESCRIPTIONX\n///\n/// ## Overview\n///\n/// Defines the `SUI_TOKEN_TEMPLATE` type witness and initializes the token via the modern\n/// `coin_registry::new_currency_with_otw` pattern (the framework's documented\n/// replacement for deprecated `coin::create_currency`). Name, symbol, and decimals\n/// are permanently immutable. Description and icon URL remain updatable via\n/// governance entry points, using a `MetadataCap<SUI_TOKEN_TEMPLATE>` rather than the `TreasuryCap`.\n///\n/// ## Key Types\n///\n/// - `SUI_TOKEN_TEMPLATE` — the type witness (zero-field struct with `drop` ability) used for one-time-witness initialization\n///\n/// ## Deployment\n///\n/// `init()` is a two-step OTW flow:\n/// 1. `coin_registry::new_currency_with_otw` builds the `Currency<SUI_TOKEN_TEMPLATE>` with an\n/// empty icon URL (network-specific values are set post-publish, not hardcoded\n/// here — see below) and mints `TreasuryCap<SUI_TOKEN_TEMPLATE>`.\n/// 2. `.finalize(ctx)` claims a live `MetadataCap<SUI_TOKEN_TEMPLATE>` (NOT\n/// `finalize_and_delete_metadata_cap`, which would permanently lock metadata)\n/// and transfers the pending `Currency<SUI_TOKEN_TEMPLATE>` to the registry address `0xc`.\n///\n/// Both `TreasuryCap<SUI_TOKEN_TEMPLATE>` and `MetadataCap<SUI_TOKEN_TEMPLATE>` are transferred to the\n/// transaction sender (the deployer). From there, `scripts/publish.sh`:\n/// - calls `coin_registry::finalize_registration` (required for OTW currencies —\n/// promotes the pending `Currency<SUI_TOKEN_TEMPLATE>` into a real shared, RPC-discoverable\n/// object),\n/// - calls `coin_registry::set_icon_url` using the freshly-held `MetadataCap` and a\n/// network-specific `SUI_TOKEN_TEMPLATE_ICON_URL` env var — this is how the icon differs\n/// between testnet and mainnet without ever hardcoding a URL in this source file.\n///\n/// Both caps should be transferred to their intended custodians (e.g. treasury,\n/// governance multisig) according to your deployment runbook. The package itself is\n/// published with immutability constraints (UpgradeCap burned) to prevent future\n/// code modifications; this is independent of metadata mutability, which remains\n/// governance-controlled via the `MetadataCap`.\nmodule sui_token_template::sui_token_template;\n\nuse sui::coin::TreasuryCap;\nuse sui::coin_registry::{Self, Currency, MetadataCap};\n\n/// Token parameters, declared as named constants rather than inline literals.\n///\n/// Each `const` becomes a distinct entry in the compiled module's constant pool,\n/// which is what lets the browser-based deployer patch them client-side via\n/// `@mysten/move-bytecode-template::update_constants` — no Move recompilation.\n/// The generator (`scripts/03_create_token.sh`) substitutes these same values into\n/// the downloadable source package, so the shipped bytecode and the generated\n/// source stay in lock-step (the on-chain package is source-verifiable).\n///\n/// The default values are deliberately DISTINCT: identical constants are\n/// deduplicated into a single pool entry by the Move compiler, which would make\n/// them impossible to patch independently.\nconst DECIMALS: u8 = 9;\nconst SYMBOL: vector<u8> = b\"TEMPLATE_SYMBOL\";\nconst NAME: vector<u8> = b\"TEMPLATE_NAME\";\nconst DESCRIPTION: vector<u8> = b\"TEMPLATE_DESCRIPTION\";\nconst ICON_URL: vector<u8> = b\"TEMPLATE_ICON_URL\";\n\n/// SUI_TOKEN_TEMPLATE is the type witness — a zero-sized struct that encodes type identity.\n/// The struct has no fields and only the `drop` ability (consumed once in `init()`).\n/// Paired with `coin_registry::new_currency_with_otw()`, it creates a\n/// `TreasuryCap<SUI_TOKEN_TEMPLATE>` and a `Currency<SUI_TOKEN_TEMPLATE>` registry entry. The token type\n/// becomes `<PACKAGE_ID>::XMODULENAMEX::SUI_TOKEN_TEMPLATE` globally.\npublic struct SUI_TOKEN_TEMPLATE has drop {}\n\n/// `init()` is called exactly once when the package is published. It initializes the token:\n/// 1. Builds the `Currency<SUI_TOKEN_TEMPLATE>` and `TreasuryCap<SUI_TOKEN_TEMPLATE>` via `coin_registry::new_currency_with_otw`.\n/// 2. Finalizes (without deleting the `MetadataCap`), transferring the pending\n/// `Currency<SUI_TOKEN_TEMPLATE>` to the registry address.\n/// 3. Transfers `TreasuryCap<SUI_TOKEN_TEMPLATE>` and `MetadataCap<SUI_TOKEN_TEMPLATE>` to the transaction sender.\nfun init(witness: SUI_TOKEN_TEMPLATE, ctx: &mut TxContext) {\n let (builder, treasury_cap) = coin_registry::new_currency_with_otw(\n witness,\n DECIMALS,\n SYMBOL.to_string(),\n NAME.to_string(),\n DESCRIPTION.to_string(),\n ICON_URL.to_string(), // may be updated per-network by scripts/publish.sh after finalize_registration\n ctx,\n );\n\n let metadata_cap = builder.finalize(ctx);\n\n transfer::public_transfer(treasury_cap, ctx.sender());\n transfer::public_transfer(metadata_cap, ctx.sender());\n}\n\n#[test_only]\n/// Test-only wrapper around the real `init()`, constructing the `SUI_TOKEN_TEMPLATE` witness\n/// on the caller's behalf (struct literals are restricted to their defining\n/// module, so external test modules cannot do\n/// this themselves). Mirrors production behavior exactly: `TreasuryCap<SUI_TOKEN_TEMPLATE>`\n/// and `MetadataCap<SUI_TOKEN_TEMPLATE>` go to the sender; `Currency<SUI_TOKEN_TEMPLATE>` is transferred to\n/// the registry address `@0xc` (retrievable via `take_from_address`/\n/// `return_to_address` in a later transaction — `Currency<T>` has only `key`,\n/// not `store`, so it cannot be shared or transferred from outside `coin_registry`\n/// by any other means).\npublic fun test_init(ctx: &mut TxContext) {\n init(SUI_TOKEN_TEMPLATE {}, ctx);\n}\n\n#[test]\nfun test_treasury_cap_transferred_to_sender() {\n use sui::test_scenario;\n\n let admin = @0xAD;\n let mut s = test_scenario::begin(admin);\n test_init(s.ctx());\n s.next_tx(admin);\n let cap = s.take_from_sender<TreasuryCap<SUI_TOKEN_TEMPLATE>>();\n s.return_to_sender(cap);\n s.end();\n}\n\n#[test]\nfun test_treasury_cap_present_in_sender() {\n use sui::test_scenario;\n\n let admin = @0xAD;\n let mut s = test_scenario::begin(admin);\n test_init(s.ctx());\n s.next_tx(admin);\n assert!(s.has_most_recent_for_sender<TreasuryCap<SUI_TOKEN_TEMPLATE>>(), 0);\n s.end();\n}\n\n#[test]\nfun test_metadata_cap_present_in_sender() {\n use sui::test_scenario;\n\n let admin = @0xAD;\n let mut s = test_scenario::begin(admin);\n test_init(s.ctx());\n s.next_tx(admin);\n assert!(s.has_most_recent_for_sender<MetadataCap<SUI_TOKEN_TEMPLATE>>(), 0);\n s.end();\n}\n\n#[test]\nfun test_currency_transferred_to_registry_after_init() {\n use sui::test_scenario;\n\n // Sui's CoinRegistry shared object lives at the well-known address 0xc; OTW\n // currencies are transferred there pending `coin_registry::finalize_registration`.\n let registry_address = @0xc;\n let admin = @0xAD;\n let mut s = test_scenario::begin(admin);\n test_init(s.ctx());\n s.next_tx(admin);\n let currency = s.take_from_address<Currency<SUI_TOKEN_TEMPLATE>>(registry_address);\n test_scenario::return_to_address(registry_address, currency);\n s.end();\n}\n",
4
+ "scripts/publish.sh": "#!/usr/bin/env bash\nset -euo pipefail\n\nPKG_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n\n# ─ Utility functions (inlined; no external common.sh in standalone packages) ─\n\nlog() {\n\techo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*\"\n}\n\nrequire_env() {\n\tlocal var_name=$1\n\tif [[ -z \"${!var_name:-}\" ]]; then\n\t\tlog \"ERROR: $var_name is not set\"\n\t\texit 1\n\tfi\n}\n\nload_env() {\n\tlocal network=$1\n\tlocal env_file=\"$PKG_DIR/.env.${network}\"\n\tif [[ ! -f \"$env_file\" ]]; then\n\t\tmkdir -p \"$(dirname \"$env_file\")\"\n\t\ttouch \"$env_file\"\n\t\tlog \"Created empty .env.${network}\"\n\tfi\n\tif [[ -f \"$env_file\" ]]; then\n\t\tset +u\n\t\tsource \"$env_file\"\n\t\tset -u\n\tfi\n}\n\nsave_env() {\n\tlocal network=$1\n\tlocal key=$2\n\tlocal value=$3\n\tlocal env_file=\"$PKG_DIR/.env.${network}\"\n\tmkdir -p \"$(dirname \"$env_file\")\"\n\tif grep -q \"^${key}=\" \"$env_file\" 2>/dev/null; then\n\t\tsed -i.bak \"s|^${key}=.*|${key}=${value}|\" \"$env_file\"\n\t\trm -f \"${env_file}.bak\"\n\telse\n\t\techo \"${key}=${value}\" >> \"$env_file\"\n\tfi\n\tlog \"Saved ${key}=${value} to .env.${network}\"\n}\n\nget_published_at() {\n\tlocal json=$1\n\techo \"$json\" | jq -r '.objectChanges[] | select(.type == \"published\") | .packageId'\n}\n\nget_created_object() {\n\tlocal json=$1\n\tlocal struct_substr=$2\n\techo \"$json\" | jq -r --arg type \"$struct_substr\" \\\n\t\t'.objectChanges[] | select(.type == \"created\" and (.objectType | contains($type))) | .objectId' | head -1\n}\n\nis_object_exists() {\n\tlocal obj_id=$1\n\tsui client object \"$obj_id\" --json 2>/dev/null | jq -e '.data != null' > /dev/null 2>&1\n}\n\n# ─────────────────────────────────────────────────────────────────────────────\n\nNETWORK=${1:-testnet}\nOUTPUT_ENV=false\nCONFIRM_IMMUTABLE=false\n\nwhile [[ $# -gt 1 ]]; do\n\tcase \"$1\" in\n\t\t--output-env) OUTPUT_ENV=true; shift ;;\n\t\t--confirm-immutable) CONFIRM_IMMUTABLE=true; shift ;;\n\t\t*) shift ;;\n\tesac\ndone\n\nload_env \"$NETWORK\"\n\n# Idempotency check\nif [[ -n \"${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID:-}\" ]]; then\n\tlog \"XPACKAGENAMEX package already published: $SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\"\n\n\t# Non-fatal immutability warning if cap still exists\n\tif [[ -n \"${SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID:-}\" ]]; then\n\t\tif is_object_exists \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\"; then\n\t\t\tlog \"WARNING: UpgradeCap $SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID still exists — package is NOT yet immutable\"\n\t\t\tlog \"To make it immutable, run: bash $0 $NETWORK --confirm-immutable\"\n\t\telse\n\t\t\tlog \"UpgradeCap confirmed consumed — package is immutable\"\n\t\tfi\n\tfi\n\n\tif [[ \"$OUTPUT_ENV\" == \"true\" ]]; then\n\t\techo \"SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID=$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\"\n\t\techo \"SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID=$SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\"\n\t\techo \"SUI_TOKEN_TEMPLATE_METADATA_CAP_ID=$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\"\n\t\techo \"SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID=$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\"\n\tfi\n\texit 0\nfi\n\nrequire_env \"SUI_TOKEN_TEMPLATE_ICON_URL\"\n\nlog \"Publishing XPACKAGENAMEX package from $PKG_DIR...\"\n\nPUBLISH_OUTPUT=$(sui client publish \"$PKG_DIR\" --json 2>&1)\n\nSUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID=$(get_published_at \"$PUBLISH_OUTPUT\")\nSUI_TOKEN_TEMPLATE_TREASURY_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"TreasuryCap\")\nSUI_TOKEN_TEMPLATE_METADATA_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"MetadataCap\")\nPENDING_CURRENCY_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"Currency\")\nSUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"UpgradeCap\")\n\nif [[ -z \"$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\" ]] || [[ -z \"$SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\" ]] || [[ -z \"$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\" ]] || [[ -z \"$PENDING_CURRENCY_ID\" ]] || [[ -z \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\" ]]; then\n\tlog \"ERROR: Failed to extract object IDs from publish output\"\n\tlog \"Output: $PUBLISH_OUTPUT\"\n\texit 1\nfi\n\nlog \"Published XPACKAGENAMEX package successfully\"\nlog \" SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID=$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\"\nlog \" SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID=$SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\"\nlog \" SUI_TOKEN_TEMPLATE_METADATA_CAP_ID=$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\"\nlog \" PENDING_CURRENCY_ID=$PENDING_CURRENCY_ID (owned by registry address 0xc, pending finalize_registration)\"\nlog \" SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID=$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID (pending immutability confirmation)\"\n\n# ─ OTW currencies require a mandatory second step to become a real, shared, ─\n# ─ RPC-discoverable Currency<SUI_TOKEN_TEMPLATE> object. \"Can be performed by anyone\" ─\n# ─ per the framework docs, but doing it here keeps the deploy pipeline self-contained. ─\n\nlog \"Promoting Currency<SUI_TOKEN_TEMPLATE> via coin_registry::finalize_registration...\"\n\nFINALIZE_REG_OUTPUT=$(sui client call \\\n\t--package 0x2 \\\n\t--module coin_registry \\\n\t--function finalize_registration \\\n\t--type-args \"${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}::XMODULENAMEX::SUI_TOKEN_TEMPLATE\" \\\n\t--args 0xc \"$PENDING_CURRENCY_ID\" \\\n\t--gas-budget 100000000 \\\n\t--json 2>&1)\n\nSUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID=$(echo \"$FINALIZE_REG_OUTPUT\" | jq -r '.objectChanges[] | select(.type == \"created\" and (.objectType | contains(\"Currency\"))) | .objectId' | head -1)\n\nif [[ -z \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" ]]; then\n\tlog \"ERROR: Failed to extract the shared Currency<SUI_TOKEN_TEMPLATE> object ID from finalize_registration output\"\n\tlog \"Output: $FINALIZE_REG_OUTPUT\"\n\texit 1\nfi\n\nlog \" SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID=$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID (shared Currency<SUI_TOKEN_TEMPLATE>)\"\n\n# ─ Set the network-specific icon URL while we still hold MetadataCap as sender ─\n# ─ (this is what makes the icon configurable per network instead of hardcoded) ─\n\nlog \"Setting icon URL via coin_registry::set_icon_url...\"\n\nsui client call \\\n\t--package 0x2 \\\n\t--module coin_registry \\\n\t--function set_icon_url \\\n\t--type-args \"${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}::XMODULENAMEX::SUI_TOKEN_TEMPLATE\" \\\n\t--args \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" \"$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\" \"$SUI_TOKEN_TEMPLATE_ICON_URL\" \\\n\t--gas-budget 100000000 \\\n\t--json > /dev/null\n\nlog \" Icon URL set to: $SUI_TOKEN_TEMPLATE_ICON_URL\"\n\n# ─ Irreversible step: burn UpgradeCap to make the package immutable ─\n\nif [[ \"$CONFIRM_IMMUTABLE\" == \"true\" ]]; then\n\tlog \"Making XPACKAGENAMEX package immutable (this is IRREVERSIBLE)...\"\n\tsui client call \\\n\t\t--package 0x2 \\\n\t\t--module package \\\n\t\t--function make_immutable \\\n\t\t--args \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\" \\\n\t\t--gas-budget 100000000 \\\n\t\t--json > /dev/null\n\n\t# Verify immutability\n\tOWNER=$(sui client object \"$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\" --json 2>/dev/null | jq -r '.data.owner')\n\tif [[ \"$OWNER\" != \"Immutable\" ]]; then\n\t\tlog \"ERROR: Expected package owner to be Immutable, got: $OWNER\"\n\t\texit 1\n\tfi\n\n\tlog \"✓ Package $SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID is now permanently immutable\"\nelse\n\tlog \"\"\n\tlog \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\tlog \"UpgradeCap NOT burned — package remains upgradeable.\"\n\tlog \"\"\n\tlog \"To burn the UpgradeCap and make the package permanently immutable,\"\n\tlog \"either:\"\n\tlog \" 1. Re-run with --confirm-immutable flag:\"\n\tlog \" bash $0 $NETWORK --confirm-immutable\"\n\tlog \" 2. Or confirm interactively now:\"\n\tlog \"\"\n\tread -r -p \"Type CONFIRM to burn the UpgradeCap and make the package immutable: \" ANSWER\n\tif [[ \"$ANSWER\" == \"CONFIRM\" ]]; then\n\t\tlog \"Burning UpgradeCap...\"\n\t\tsui client call \\\n\t\t\t--package 0x2 \\\n\t\t\t--module package \\\n\t\t\t--function make_immutable \\\n\t\t\t--args \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\" \\\n\t\t\t--gas-budget 100000000 \\\n\t\t\t--json > /dev/null\n\n\t\t# Verify immutability\n\t\tOWNER=$(sui client object \"$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\" --json 2>/dev/null | jq -r '.data.owner')\n\t\tif [[ \"$OWNER\" != \"Immutable\" ]]; then\n\t\t\tlog \"ERROR: Expected package owner to be Immutable, got: $OWNER\"\n\t\t\texit 1\n\t\tfi\n\n\t\tlog \"✓ Package $SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID is now permanently immutable\"\n\telse\n\t\tlog \"Skipped immutability step. UpgradeCap $SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID is retained.\"\n\tfi\n\tlog \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\tlog \"\"\nfi\n\nif [[ \"$OUTPUT_ENV\" == \"true\" ]]; then\n\techo \"SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID=$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\"\n\techo \"SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID=$SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\"\n\techo \"SUI_TOKEN_TEMPLATE_METADATA_CAP_ID=$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\"\n\techo \"SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID=$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\"\n\techo \"SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID=$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\"\nelse\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\" \"$SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID\"\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\" \"$SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID\"\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\" \"$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\"\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\"\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\" \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\"\nfi\n",
5
+ ".gitignore": "# Sui build artefacts\nbuild/\n\n# Sui client keystore and config — never commit private keys\nsui.keystore\nsui.aliases\nclient.yaml\n.sui/\n\n# macOS\n.DS_Store\n\n# Editor\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# Ephemeral Sui publication files (localnet/devnet) — do not commit\nPub.*.toml",
6
+ "README.md": "# XNAMEX (XSYMBOLX)\n\nXDESCRIPTIONX\n\n## Token Details\n\n| Field | Value |\n| --- | --- |\n| Package Name | `XPACKAGENAMEX` |\n| Module | `XMODULENAMEX` |\n| Struct | `XSTRUCTNAMEX` |\n| Symbol | `XSYMBOLX` |\n| Decimals | `XDECIMALSX` |\n\n## Deploy\n\nTo publish this token to testnet or mainnet:\n\n```bash\nXSTRUCTNAMEX_ICON_URL=https://... bash scripts/publish.sh\n```\n\n`XSTRUCTNAMEX_ICON_URL` is required and network-specific (testnet/mainnet typically point\nat different uploaded images) — the icon is never hardcoded in `sources/XMODULENAMEX.move`.\n`publish.sh` also calls `coin_registry::finalize_registration`, a mandatory second\nstep for OTW-created currencies that promotes the pending `Currency<XSTRUCTNAMEX>` into a\nreal shared, RPC-discoverable object.\n\n## Token Type (After Deployment)\n\nOnce published, the token type will be:\n\n```text\n<PACKAGE_ID>::XMODULENAMEX::XSTRUCTNAMEX\n```\n\n## Design Philosophy\n\nThis token package was generated from [sui-token-template](https://github.com/MeddleWare-Org/sui-token-template) and is intentionally minimal:\n\n- **No minting/burning logic** — Controlled by the downstream packages that consume the coin type\n- **Partially mutable metadata** — Name, symbol, and decimals are permanently fixed. Description and icon URL are updatable via `MetadataCap<XSTRUCTNAMEX>` (governance-gated), using `coin_registry::new_currency_with_otw` + `finalize()` (deliberately *not* `finalize_and_delete_metadata_cap()`, which would lock metadata forever)\n- **Package can be made frozen** — `scripts/publish.sh` extracts the UpgradeCap after publish and offers to call `0x2::package::make_immutable` on it (interactive confirmation, or pass `--confirm-immutable` for non-interactive use). This action is irreversible. Until confirmed, the package remains upgradeable. Metadata mutability is independent of this — it's controlled by the `MetadataCap`, not package upgradeability.\n\n## Tests\n\nRun the inline unit tests:\n\n```bash\nsui move test\n```\n\nFour tests verify:\n\n1. `test_treasury_cap_transferred_to_sender` — TreasuryCap is transferred to deployer\n2. `test_treasury_cap_present_in_sender` — TreasuryCap is present after init\n3. `test_metadata_cap_present_in_sender` — `MetadataCap<XSTRUCTNAMEX>` is present after init\n4. `test_currency_transferred_to_registry_after_init` — `Currency<XSTRUCTNAMEX>` is transferred to the registry address, pending `finalize_registration`\n\n## License\n\nCC0 1.0 Universal — public domain.\n",
7
+ "deployments.md": "# XSYMBOLX Token Deployments\n\n> **After each deployment, replace the placeholder values in the relevant section with the actual artefact IDs printed by `scripts/publish.sh`.**\n\n## Testnet\n\n| Field | Value |\n| ------------------ | ------- |\n| Package ID | `FILL_IN_AFTER_DEPLOY` |\n| TreasuryCap ID | `FILL_IN_AFTER_DEPLOY` |\n| MetadataCap ID | `FILL_IN_AFTER_DEPLOY` |\n| Currency object ID | `FILL_IN_AFTER_DEPLOY` |\n| UpgradeCap ID (burned) | `FILL_IN_AFTER_DEPLOY` |\n| Immutability confirmed | Yes (Y/N) |\n| Blob ID | `FILL_IN_AFTER_DEPLOY` |\n| Blob Object ID | `FILL_IN_AFTER_DEPLOY` |\n| Blob URL | `FILL_IN_AFTER_DEPLOY` |\n| Deployer Address | `FILL_IN_AFTER_DEPLOY` |\n| Treasury Address | `FILL_IN_AFTER_DEPLOY` |\n| Deployed | `FILL_IN_AFTER_DEPLOY` |\n| Epochs purchased | 50 |\n\n## Mainnet\n\n| Field | Value |\n| ------------------ | ------- |\n| Package ID | `FILL_IN_AFTER_DEPLOY` |\n| TreasuryCap ID | `FILL_IN_AFTER_DEPLOY` |\n| MetadataCap ID | `FILL_IN_AFTER_DEPLOY` |\n| Currency object ID | `FILL_IN_AFTER_DEPLOY` |\n| UpgradeCap ID (burned) | `FILL_IN_AFTER_DEPLOY` |\n| Immutability confirmed | Yes (Y/N) |\n| Blob ID | `FILL_IN_AFTER_DEPLOY` |\n| Blob Object ID | `FILL_IN_AFTER_DEPLOY` |\n| Blob URL | `FILL_IN_AFTER_DEPLOY` |\n| Deployer Address | `FILL_IN_AFTER_DEPLOY` |\n| Treasury Address | `FILL_IN_AFTER_DEPLOY` |\n| Deployed | `FILL_IN_AFTER_DEPLOY` |\n| Epochs purchased | 200 |\n\n## Notes\n\n- `TreasuryCap` and `MetadataCap` are both transferred to the **deployer** address at publish time (`init()`'s `ctx.sender()`), not directly to governance. Transfer them to their intended custodians according to your deployment runbook.\n- `UpgradeCap` is burned via `0x2::package::make_immutable` when `scripts/publish.sh` is run with `--confirm-immutable` (interactive CONFIRM prompt otherwise). This is irreversible — confirm the deployment is final before accepting. Verify immutability with `sui client object <PACKAGE_ID> --json | jq -r .data.owner` → `\"Immutable\"`.\n- Blob Object ID (not Blob ID) is required for lifetime extension.\n\n## Blob lifetime monitoring\n\nTestnet: extend before fewer than 10 epochs remain.\nMainnet: extend before fewer than 50 epochs remain.\n\n```bash\nwalrus extend --blob-obj-id <BLOB_OBJECT_ID> --epochs-extended <N> --context <testnet|mainnet>\n```\n\n## Downstream Move.toml reference\n\n```toml\n[addresses]\nXMODULENAMEX = \"<PACKAGE_ID>\" # use network-appropriate value\n```\n",
8
+ "CLAUDE.md": "# CLAUDE.md — XPACKAGENAMEX Package\n\n> **coin_registry OTW pattern — read before verifying deployment**\n>\n> This package uses `coin_registry::new_currency_with_otw` + `builder.finalize(ctx)`.\n> `TreasuryCap` and `MetadataCap` are both transferred to the deployer at publish time.\n> Run `coin_registry::finalize_registration` immediately after publish to promote the\n> pending `Currency` to a real shared object (required for `suix_getCoinMetadata` to work).\n\n## What This Package Is\n\nThis is the concrete XSYMBOLX coin type for XPROJECTNAMEX. It was generated from the\n`sui-token-template` and deployed to Sui testnet/mainnet.\n\nCoin type: `<PACKAGE_ID>::XMODULENAMEX::XSTRUCTNAMEX`\n\n## coin_registry Pattern\n\nThe coin was initialized using:\n\n```move\nlet (builder, treasury_cap) = coin_registry::new_currency_with_otw(witness, ...);\nlet metadata_cap = builder.finalize(ctx);\ntransfer::public_transfer(treasury_cap, ctx.sender());\ntransfer::public_transfer(metadata_cap, ctx.sender());\n```\n\n`init()` builds with an empty icon URL — the real, network-specific value is set\nimmediately post-publish by `scripts/publish.sh` (see below), never hardcoded in source.\n\n| Aspect | Behavior |\n| --- | --- |\n| `Currency<XSTRUCTNAMEX>` registry object | Yes — created pending, promoted to shared via `coin_registry::finalize_registration` |\n| `suix_getCoinMetadata` | Should return non-null data once `finalize_registration` has run — verify post-redeploy |\n| **Immutable fields** | Name, symbol, decimals |\n| **Mutable fields** | Description, icon URL |\n\n## Verification\n\n```bash\n# Package on-chain; check immutability via owner field\nsui client object FILL_IN_AFTER_DEPLOY --json | jq -r '.data.owner'\n# → \"Immutable\" once make_immutable has been confirmed during publish\n\n# TreasuryCap at governance address\nsui client objects FILL_IN_AFTER_DEPLOY | grep -i treasurycap\n\n# Currency<XSTRUCTNAMEX> is discoverable (post finalize_registration)\nsuix_getCoinMetadata \"<PACKAGE_ID>::XMODULENAMEX::XSTRUCTNAMEX\"\n```\n\n## Deployment Artefacts\n\nSee [deployments.md](deployments.md) for all IDs (package, TreasuryCap, MetadataCap,\nCurrency object, Walrus blob).\n\n## Working Rules\n\n- Do not manually edit `sources/XMODULENAMEX.move`. If regeneration is needed, use `create_token.sh`\n from the `sui-token-template` scripts. The icon URL is never hardcoded here — it is\n supplied per-network via the `XSTRUCTNAMEX_ICON_URL` env var consumed by `scripts/publish.sh`.\n- Description and icon URL may be updated post-deploy via `MetadataCap` (governance-gated).\n Name, symbol, and decimals are permanently fixed.\n- Refer to `sui-token-template/CLAUDE.md` for template-level guidance.\n",
9
+ "AGENTS.md": "# AGENTS.md — XPACKAGENAMEX Package\n\n## What This Package Is\n\nThe deployed XSYMBOLX coin type for XPROJECTNAMEX (Sui testnet/mainnet).\n\nCoin type: `<PACKAGE_ID>::XMODULENAMEX::XSTRUCTNAMEX`\n\nGenerated from `sui-token-template`. Becomes immutable once `scripts/publish.sh` is run with `--confirm-immutable` (or the interactive CONFIRM prompt is accepted) — this burns the UpgradeCap via `0x2::package::make_immutable`. Verify with `sui client object <PACKAGE_ID> --json | jq -r .data.owner` → expect `\"Immutable\"`.\n\n## Coin Registry Pattern\n\nThis coin uses `coin_registry::new_currency_with_otw` + `.finalize(ctx)` (the\nframework's documented replacement for deprecated `coin::create_currency`) to\nproduce a `Currency<XSTRUCTNAMEX>` registry entry. Because OTW currencies require a\nmandatory second transaction (`coin_registry::finalize_registration`) to become\nshared and RPC-discoverable, `suix_getCoinMetadata` only returns non-null data\n*after* that step has run — `scripts/publish.sh` performs it automatically.\nMetadata fields:\n\n- **Immutable**: Name, symbol, decimals\n- **Mutable**: Description and icon URL — gated by `MetadataCap<XSTRUCTNAMEX>`. The\n *initial* per-network icon is set once directly by `scripts/publish.sh` via\n `coin_registry::set_icon_url`. Subsequent updates go through governance.\n\n## Verification Commands\n\n```bash\n# Confirm package exists and is immutable\nsui client object FILL_IN_AFTER_DEPLOY\n\n# Confirm TreasuryCap at governance\nsui client objects FILL_IN_AFTER_DEPLOY | grep -i treasurycap\n```\n\n## Key Files\n\n| File | Purpose |\n| --- | --- |\n| `sources/XMODULENAMEX.move` | Generated coin source (do not edit manually) |\n| `deployments.md` | Artefact IDs (package, TreasuryCap, MetadataCap, Currency object, blob) |\n| `scripts/publish.sh` | Build + publish + finalize_registration + set initial icon URL |\n\nFor template-level guidance, see `sui-token-template/AGENTS.md`.\n"
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import {
3
+ parseOwnedAccessNft,
4
+ fetchAccessNfts,
5
+ buildPurchaseTx,
6
+ buildConsumeTx,
7
+ personalMessageForNonce,
8
+ buildAccessProofToken,
9
+ fetchRelayChallenge,
10
+ } from '../src/lib/accessGate.js'
11
+ import type { AccessGateConfig, OwnedObjectsClient } from '../src/lib/accessGate.js'
12
+
13
+ afterEach(() => vi.restoreAllMocks())
14
+
15
+ const PKG = '0x00000000000000000000000000000000000000000000000000000000000000aa'
16
+ const GATE = '0x00000000000000000000000000000000000000000000000000000000000000a1'
17
+ const NFT_TYPE = `${PKG}::access_gate::AccessNFT`
18
+
19
+ const cfg: AccessGateConfig = {
20
+ packageId: PKG,
21
+ gateId: GATE,
22
+ nftType: NFT_TYPE,
23
+ soulbound: false,
24
+ priceMist: 1000n,
25
+ }
26
+
27
+ function entry(objectId: string, gateId: string, uses?: number) {
28
+ return {
29
+ data: {
30
+ objectId,
31
+ content: {
32
+ fields: {
33
+ data: {
34
+ fields: {
35
+ gate_id: gateId,
36
+ variant:
37
+ uses === undefined
38
+ ? { variant: 'UnlimitedPass', fields: {} }
39
+ : { variant: 'SingleUse', fields: { uses_remaining: String(uses) } },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ }
46
+ }
47
+
48
+ function commandsJson(tx: { getData: () => unknown }): string {
49
+ return JSON.stringify(tx.getData())
50
+ }
51
+
52
+ describe('accessGate lib', () => {
53
+ it('parses unlimited pass and single-use NFTs', () => {
54
+ expect(parseOwnedAccessNft(entry('0x1', GATE))).toEqual({
55
+ objectId: '0x1',
56
+ gateId: GATE,
57
+ usesRemaining: null,
58
+ })
59
+ expect(parseOwnedAccessNft(entry('0x2', GATE, 5))?.usesRemaining).toBe(5)
60
+ expect(parseOwnedAccessNft({ data: { objectId: '0x9', content: { fields: {} } } })).toBeNull()
61
+ })
62
+
63
+ it('fetchAccessNfts filters by gate and passes the StructType filter', async () => {
64
+ const client: OwnedObjectsClient = {
65
+ getOwnedObjects: vi.fn(async () => ({
66
+ data: [entry('0x1', GATE), entry('0x2', '0xother', 1)],
67
+ })),
68
+ }
69
+ const nfts = await fetchAccessNfts(client, '0xowner', NFT_TYPE, GATE)
70
+ expect(nfts).toHaveLength(1)
71
+ expect(nfts[0].objectId).toBe('0x1')
72
+ expect(client.getOwnedObjects).toHaveBeenCalledWith(
73
+ expect.objectContaining({ owner: '0xowner', filter: { StructType: NFT_TYPE } }),
74
+ )
75
+ })
76
+
77
+ it('buildPurchaseTx splits gas and calls purchase', () => {
78
+ const json = commandsJson(buildPurchaseTx(cfg))
79
+ expect(json).toContain('SplitCoins')
80
+ expect(json).toContain('"function":"purchase"')
81
+ })
82
+
83
+ it('buildConsumeTx selects consume vs consume_soulbound', () => {
84
+ expect(commandsJson(buildConsumeTx(cfg, '0x1', 'n'))).toContain('"function":"consume"')
85
+ expect(commandsJson(buildConsumeTx({ ...cfg, soulbound: true }, '0x1', 'n'))).toContain(
86
+ '"function":"consume_soulbound"',
87
+ )
88
+ })
89
+
90
+ it('encodes the proof token and message', () => {
91
+ expect(new TextDecoder().decode(personalMessageForNonce('x'))).toBe('nft-gate:access:x')
92
+ const token = buildAccessProofToken({ address: '0x1', nonce: 'n', signature: 's', consumeDigest: 'd' })
93
+ expect(JSON.parse(Buffer.from(token, 'base64').toString('utf-8'))).toEqual({
94
+ address: '0x1',
95
+ nonce: 'n',
96
+ signature: 's',
97
+ consumeDigest: 'd',
98
+ })
99
+ })
100
+
101
+ it('fetchRelayChallenge parses the gateway response', async () => {
102
+ vi.stubGlobal(
103
+ 'fetch',
104
+ vi.fn(async () => new Response(JSON.stringify({ nonce: 'abc', expiresAt: 7 }), { status: 200 })),
105
+ )
106
+ expect(await fetchRelayChallenge('https://relay.example')).toEqual({ nonce: 'abc', expiresAt: 7 })
107
+ })
108
+ })