@meddleware/token-deployer-ui 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -118
- package/README.md +11 -10
- package/package.json +22 -10
- package/src/App.vue +1 -1
- package/src/component-styles.css +39 -57
- package/src/components/GithubPush.vue +2 -2
- package/src/components/IconPicker.vue +44 -75
- package/src/components/LicensePicker.vue +4 -3
- package/src/components/ResultPanel.vue +2 -2
- package/src/components/ReviewPanel.vue +11 -3
- package/src/components/TokenDeployerView.vue +52 -48
- package/src/components/WalletBar.vue +12 -1
- package/src/components/WalrusBlobBrowser.vue +4 -4
- package/src/composables/useWalrusRelay.ts +1 -2
- package/src/config.ts +29 -30
- package/src/lib/buildPublishTx.ts +0 -10
- package/src/lib/deploy.ts +45 -3
- package/src/lib/deployExecutor.ts +19 -44
- package/src/lib/generatePackage.ts +12 -12
- package/src/lib/licenses.ts +4 -3
- package/src/lib/listMyTokens.ts +7 -11
- package/src/lib/readClient.ts +15 -7
- package/src/lib/template.ts +3 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/walrus-constants.ts +2 -2
- package/src/main.ts +42 -61
- package/src/styles.css +22 -0
- package/src/template-src/files.json +7 -7
- package/.claude/move-language-reference/SKILL.md +0 -445
- package/.claude/skills/move-language-reference/SKILL.md +0 -445
- package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
- package/.claude/sui-move-bootcamp/SKILL.md +0 -128
- package/.editorconfig +0 -8
- package/.env.example +0 -75
- package/.gitattributes +0 -1
- package/.github/workflows/docker-publish.yml +0 -348
- package/.github/workflows/e2e-realchain.yml +0 -64
- package/.github/workflows/node-ci.yml +0 -43
- package/.github/workflows/npm-publish.yml +0 -83
- package/.htmlvalidate.mjs +0 -30
- package/.oxfmtrc.json +0 -5
- package/.oxlintrc.json +0 -10
- package/.vscode/extensions.json +0 -9
- package/AGENTS.md +0 -39
- package/CLAUDE.md +0 -159
- package/Dockerfile +0 -96
- package/eslint.config.ts +0 -63
- package/index.html +0 -13
- package/playwright.config.ts +0 -30
- package/postcss.config.js +0 -8
- package/public/CNAME +0 -1
- package/public/_headers +0 -12
- package/public/_redirects +0 -1
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +0 -5
- package/public/og-image.png +0 -0
- package/public/robots.txt +0 -3
- package/public/sitemap.xml +0 -8
- package/scripts/e2e-browser.mjs +0 -15
- package/scripts/e2e-deploy.mjs +0 -222
- package/scripts/e2e-localnet.mjs +0 -65
- package/scripts/e2e-walrus-browser.mjs +0 -115
- package/scripts/gen-og-image.mjs +0 -31
- package/scripts/publish-localnet.mjs +0 -55
- package/scripts/regen-template.sh +0 -45
- package/scripts/sync-template-src.sh +0 -36
- package/scripts/verify-bytecode.mjs +0 -49
- package/src/__tests__/App.spec.js +0 -11
- package/src/composables/useAccessGate.ts +0 -127
- package/src/lib/accessGate.ts +0 -141
- package/src/lib/walrus.ts +0 -144
- package/stylelint.config.js +0 -33
- package/test-results/.last-run.json +0 -4
- package/tests/accessGate.test.ts +0 -108
- package/tests/buildPublishTx.test.ts +0 -243
- package/tests/deploy.test.ts +0 -81
- package/tests/errors.test.ts +0 -32
- package/tests/generatePackage.test.ts +0 -182
- package/tests/github.test.ts +0 -61
- package/tests/licenses.test.ts +0 -78
- package/tests/template.test.ts +0 -93
- package/tests/templateArtifact.test.ts +0 -61
- package/tests/templateParity.test.ts +0 -55
- package/tests/useAccessGate.test.ts +0 -101
- package/tests/useWallet.test.ts +0 -74
- package/tests/useWalrusRelayGating.test.ts +0 -30
- package/tests/validation.test.ts +0 -117
- package/tests/walrusUpload.test.ts +0 -93
- package/tests/walrus_url_patch.test.ts +0 -26
- package/tsconfig.json +0 -19
- package/typedoc.json +0 -34
- package/vite.config.ts +0 -65
- package/vitest.config.ts +0 -10
|
@@ -1,61 +1,36 @@
|
|
|
1
|
-
// Builds
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// for the token deploy flow; we sign via the shared wallet connection but execute
|
|
6
|
-
// via SuiClient so we can request showEffects + showObjectChanges.
|
|
1
|
+
// Builds the deploy Executor from the wallet-adapter's shared executor, executing through this
|
|
2
|
+
// app's gRPC client (`getReadClient`, which E2E builds stub) and asking for the effects and
|
|
3
|
+
// object types `extractPublishResult` needs. Signing, the chain check and the account binding
|
|
4
|
+
// are the wallet-adapter's.
|
|
7
5
|
|
|
8
|
-
import {
|
|
9
|
-
import { useWallet } from '@meddleware/wallet-adapter'
|
|
6
|
+
import { buildExecutor } from '@meddleware/wallet-adapter'
|
|
10
7
|
import type { Transaction } from '@mysten/sui/transactions'
|
|
8
|
+
import { toSuiTxResult } from './deploy.js'
|
|
11
9
|
import type { SuiTxResult, Executor } from './deploy.js'
|
|
12
10
|
import type { Network } from './types.js'
|
|
11
|
+
import { getReadClient } from './readClient.js'
|
|
13
12
|
import { RPC_URLS } from '../config.js'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
|
-
* Build a deploy Executor bound to the currently connected wallet
|
|
17
|
-
* wallet-adapter singleton) and the given network's RPC URL.
|
|
15
|
+
* Build a deploy Executor bound to the currently connected wallet and `network`.
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* `signAndExecute` requests `effects + objectTypes` and maps the result with
|
|
18
|
+
* {@link toSuiTxResult}, so the caller can extract the published package id, coin type,
|
|
19
|
+
* TreasuryCap, MetadataCap and the Currency ref. A failed transaction is returned (not thrown);
|
|
20
|
+
* `deploy.ts` checks its status.
|
|
21
|
+
*
|
|
22
|
+
* @throws {Error} if no wallet is connected, it cannot sign transactions, or its account does not
|
|
23
|
+
* list `sui:<network>`.
|
|
22
24
|
*/
|
|
23
25
|
export async function buildDeployExecutor(network: Network): Promise<Executor> {
|
|
24
|
-
const
|
|
25
|
-
const wallet = currentWallet.value
|
|
26
|
-
const acct = account.value
|
|
27
|
-
if (!wallet || !acct) throw new Error('Connect a wallet first.')
|
|
28
|
-
|
|
29
|
-
const suiClient = new SuiJsonRpcClient({ url: RPC_URLS[network], network })
|
|
30
|
-
const chain = `sui:${network}` as const
|
|
31
|
-
|
|
32
|
-
const signFeature = wallet.features['sui:signTransaction'] as
|
|
33
|
-
| {
|
|
34
|
-
signTransaction: (input: {
|
|
35
|
-
transaction: Transaction
|
|
36
|
-
account: typeof acct
|
|
37
|
-
chain: `sui:${string}`
|
|
38
|
-
}) => Promise<{ bytes: string; signature: string }>
|
|
39
|
-
}
|
|
40
|
-
| undefined
|
|
41
|
-
if (!signFeature) throw new Error('This wallet cannot sign transactions.')
|
|
42
|
-
|
|
26
|
+
const executor = await buildExecutor(network, RPC_URLS[network], { client: getReadClient(network) })
|
|
43
27
|
return {
|
|
44
28
|
async signAndExecute(tx: Transaction): Promise<SuiTxResult> {
|
|
45
|
-
const {
|
|
46
|
-
|
|
47
|
-
account: acct,
|
|
48
|
-
chain,
|
|
49
|
-
})
|
|
50
|
-
const res = await suiClient.executeTransactionBlock({
|
|
51
|
-
transactionBlock: bytes,
|
|
52
|
-
signature,
|
|
53
|
-
options: { showEffects: true, showObjectChanges: true },
|
|
54
|
-
})
|
|
55
|
-
return res as unknown as SuiTxResult
|
|
29
|
+
const { result } = await executor.signAndExecute(tx, { include: { effects: true, objectTypes: true } })
|
|
30
|
+
return toSuiTxResult(result)
|
|
56
31
|
},
|
|
57
32
|
async waitForTransaction(digest: string): Promise<void> {
|
|
58
|
-
await
|
|
33
|
+
await executor.waitForTransaction(digest)
|
|
59
34
|
},
|
|
60
35
|
}
|
|
61
36
|
}
|
|
@@ -122,12 +122,12 @@ function renderMoveToml(cfg: TokenConfig): string {
|
|
|
122
122
|
function renderSource(cfg: TokenConfig): string {
|
|
123
123
|
let out = files['source.move']
|
|
124
124
|
|
|
125
|
-
// license header (lines 1-2)
|
|
125
|
+
// license header (lines 1-2) — always rewritten, whatever licence the template itself carries
|
|
126
126
|
const lines = out.split('\n')
|
|
127
127
|
if (isProprietary(cfg.license)) {
|
|
128
128
|
lines[0] = '// SPDX-License-Identifier: UNLICENSED'
|
|
129
129
|
lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
|
|
130
|
-
} else
|
|
130
|
+
} else {
|
|
131
131
|
lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
|
|
132
132
|
lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
|
|
133
133
|
}
|
|
@@ -157,18 +157,18 @@ function renderPublishScript(cfg: TokenConfig): string {
|
|
|
157
157
|
return out
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** The template README's licence line, whatever licence the template itself ships with. */
|
|
161
|
+
const TEMPLATE_LICENSE_LINE = /^(?:CC0 1\.0 Universal|BSD Zero Clause License).*$/m
|
|
162
|
+
|
|
160
163
|
function renderReadme(cfg: TokenConfig): string {
|
|
161
|
-
|
|
164
|
+
const out = applyDocPlaceholders(files['README.md'], cfg)
|
|
162
165
|
const licenseName = cfg.licenseName || spdxId(cfg.license)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
'All rights reserved. This package is proprietary and not licensed for redistribution.'
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
out = out.replace(/^CC0 1\.0 Universal.*$/m, `${licenseName} — see the LICENSE file.`)
|
|
170
|
-
}
|
|
171
|
-
return out
|
|
166
|
+
return out.replace(
|
|
167
|
+
TEMPLATE_LICENSE_LINE,
|
|
168
|
+
isProprietary(cfg.license)
|
|
169
|
+
? 'All rights reserved. This package is proprietary and not licensed for redistribution.'
|
|
170
|
+
: `${licenseName} — see the LICENSE file.`,
|
|
171
|
+
)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
package/src/lib/licenses.ts
CHANGED
|
@@ -23,9 +23,9 @@ export const NO_LICENSE: SpdxLicense = {
|
|
|
23
23
|
deprecated: false,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Ids surfaced first in the picker.
|
|
26
|
+
/** Ids surfaced first in the picker. 0BSD is the project default; CC0 stays available. */
|
|
27
27
|
export const POPULAR_LICENSE_IDS = [
|
|
28
|
-
'
|
|
28
|
+
'0BSD',
|
|
29
29
|
'MIT',
|
|
30
30
|
'Apache-2.0',
|
|
31
31
|
'BSD-3-Clause',
|
|
@@ -37,9 +37,10 @@ export const POPULAR_LICENSE_IDS = [
|
|
|
37
37
|
'MPL-2.0',
|
|
38
38
|
'ISC',
|
|
39
39
|
'Unlicense',
|
|
40
|
+
'CC0-1.0',
|
|
40
41
|
] as const
|
|
41
42
|
|
|
42
|
-
export const DEFAULT_LICENSE_ID = '
|
|
43
|
+
export const DEFAULT_LICENSE_ID = '0BSD'
|
|
43
44
|
|
|
44
45
|
type Fetcher = typeof fetch
|
|
45
46
|
|
package/src/lib/listMyTokens.ts
CHANGED
|
@@ -19,25 +19,21 @@ export interface DeployedToken {
|
|
|
19
19
|
/** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
|
|
20
20
|
export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
|
|
21
21
|
const client = getReadClient(network)
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
owner,
|
|
25
|
-
filter: { StructType: '0x2::coin::TreasuryCap' },
|
|
26
|
-
options: { showType: true },
|
|
27
|
-
})
|
|
22
|
+
// A type filter without type params matches every TreasuryCap<T> instantiation.
|
|
23
|
+
const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
|
|
28
24
|
const seen = new Set<string>()
|
|
29
25
|
const out: DeployedToken[] = []
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
const m =
|
|
33
|
-
if (!m
|
|
26
|
+
for (const obj of objects) {
|
|
27
|
+
// gRPC may render the framework address in long form (0x000…02).
|
|
28
|
+
const m = obj.type.match(/^0x0*2::coin::TreasuryCap<(.+)>$/)
|
|
29
|
+
if (!m) continue
|
|
34
30
|
const coinType = m[1]
|
|
35
31
|
if (seen.has(coinType)) continue
|
|
36
32
|
seen.add(coinType)
|
|
37
33
|
out.push({
|
|
38
34
|
coinType,
|
|
39
35
|
packageId: coinType.split('::')[0] ?? '',
|
|
40
|
-
treasuryCapId:
|
|
36
|
+
treasuryCapId: obj.objectId,
|
|
41
37
|
label: coinType.split('::').pop() ?? coinType,
|
|
42
38
|
})
|
|
43
39
|
}
|
package/src/lib/readClient.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
// Cached
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
1
|
+
// Cached gRPC client per network, shared by read-only queries (listOwnedObjects, etc.) and
|
|
2
|
+
// the deploy executor. Public fullnodes have deprecated JSON-RPC, so this is a SuiGrpcClient.
|
|
3
|
+
import { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
4
4
|
import type { Network } from './types.js'
|
|
5
5
|
import { RPC_URLS } from '../config.js'
|
|
6
6
|
|
|
7
|
-
const clients = new Map<Network,
|
|
7
|
+
const clients = new Map<Network, SuiGrpcClient>()
|
|
8
8
|
|
|
9
|
-
/** Return a memoised {@link
|
|
10
|
-
export function getReadClient(network: Network):
|
|
9
|
+
/** Return a memoised {@link SuiGrpcClient} for the given network. */
|
|
10
|
+
export function getReadClient(network: Network): SuiGrpcClient {
|
|
11
11
|
let c = clients.get(network)
|
|
12
12
|
if (!c) {
|
|
13
|
-
c = new
|
|
13
|
+
c = new SuiGrpcClient({ network, baseUrl: RPC_URLS[network] })
|
|
14
14
|
clients.set(network, c)
|
|
15
15
|
}
|
|
16
16
|
return c
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Replace the client for `network` (E2E builds only — lets `main.ts` inject a stub client in
|
|
21
|
+
* place of the network, since gRPC-web responses are binary and cannot be faked via `fetch`).
|
|
22
|
+
*/
|
|
23
|
+
export function setReadClient(network: Network, client: SuiGrpcClient): void {
|
|
24
|
+
clients.set(network, client)
|
|
25
|
+
}
|
package/src/lib/template.ts
CHANGED
|
@@ -38,6 +38,9 @@ export function initTemplateWasm(): Promise<unknown> {
|
|
|
38
38
|
if (!initPromise) {
|
|
39
39
|
initPromise = Promise.resolve()
|
|
40
40
|
.then(() => (init as unknown as (i?: unknown) => unknown)(wasmInput))
|
|
41
|
+
// Deliberate: under Node the package's nodejs build loads its own wasm and this init call
|
|
42
|
+
// is unnecessary (and may throw). A genuine browser init failure is not hidden — the first
|
|
43
|
+
// bytecode call in patchTemplateModule then fails with the wasm error.
|
|
41
44
|
.catch(() => undefined)
|
|
42
45
|
}
|
|
43
46
|
return initPromise
|
package/src/lib/types.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface TokenConfig {
|
|
|
41
41
|
/** Address that receives the caps + initial supply. Defaults to the connected wallet. */
|
|
42
42
|
recipient: string
|
|
43
43
|
|
|
44
|
-
/** SPDX id (e.g. "
|
|
44
|
+
/** SPDX id (e.g. "0BSD", "MIT") or "NONE" for proprietary. Affects the downloadable package only. */
|
|
45
45
|
license: string
|
|
46
46
|
/** Human-readable license name for the README (defaults to the SPDX id). */
|
|
47
47
|
licenseName?: string
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Lightweight Walrus constants with NO heavy imports, so
|
|
2
|
-
//
|
|
1
|
+
// Lightweight Walrus constants with NO heavy imports, so the eagerly rendered `IconPicker.vue`
|
|
2
|
+
// component and the lazily loaded upload
|
|
3
3
|
// can share a single source of truth without pulling `@mysten/walrus` into the
|
|
4
4
|
// component's chunk.
|
|
5
5
|
|
package/src/main.ts
CHANGED
|
@@ -8,7 +8,8 @@ import './component-styles.css'
|
|
|
8
8
|
import { configureWasm } from './lib/template.js'
|
|
9
9
|
// Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
|
|
10
10
|
// import it), so a dynamic import in the E2E block below could not split it out anyway.
|
|
11
|
-
import {
|
|
11
|
+
import { setReadClient } from './lib/readClient.js'
|
|
12
|
+
import type { SuiGrpcClient } from '@mysten/sui/grpc'
|
|
12
13
|
import { useColorMode, useSeason } from '@meddleware/ui'
|
|
13
14
|
|
|
14
15
|
// Apply the colour mode before mount so there is no theme flash. Defaults to
|
|
@@ -25,72 +26,55 @@ import wasmUrl from '@mysten/move-bytecode-template/web/move_bytecode_template_b
|
|
|
25
26
|
// doesn't block initial page load.
|
|
26
27
|
configureWasm(wasmUrl)
|
|
27
28
|
|
|
28
|
-
// E2E-only: register mock wallet and expose getSuiClient for headless tests.
|
|
29
|
+
// E2E-only: stub the Sui client, register a mock wallet and expose getSuiClient for headless tests.
|
|
29
30
|
// Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
|
|
30
|
-
//
|
|
31
|
+
// Only the `e2e` build mode may set VITE_E2E (vite.config.ts refuses otherwise); both are static,
|
|
32
|
+
// so every other build tree-shakes this entire block (verified by `npm run check:bundle`).
|
|
31
33
|
;(async () => {
|
|
32
|
-
if (import.meta.env.VITE_E2E === '1') {
|
|
34
|
+
if (import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1') {
|
|
33
35
|
const { getWallets } = await import('@mysten/wallet-standard')
|
|
34
36
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// Stub gRPC client in place of the network (gRPC-web is binary, so it cannot be faked
|
|
38
|
+
// at the fetch layer). Returns a successful publish whose effects carry the created
|
|
39
|
+
// TreasuryCap/MetadataCap, and empty owned-object lists.
|
|
40
|
+
const publishDigest = 'E2E' + 'a'.repeat(41)
|
|
38
41
|
const packageId = '0x' + 'bb'.repeat(32)
|
|
39
|
-
const
|
|
42
|
+
const treasuryCapId = '0x' + 'cc'.repeat(32)
|
|
40
43
|
const metadataCapId = '0x' + 'dd'.repeat(32)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const created = (objectId: string) => ({
|
|
45
|
+
objectId,
|
|
46
|
+
outputState: 'ObjectWrite',
|
|
47
|
+
idOperation: 'Created',
|
|
48
|
+
outputVersion: '1',
|
|
49
|
+
outputDigest: publishDigest,
|
|
50
|
+
})
|
|
51
|
+
const executed = {
|
|
52
|
+
$kind: 'Transaction',
|
|
53
|
+
Transaction: {
|
|
54
|
+
digest: publishDigest,
|
|
55
|
+
status: { success: true, error: null },
|
|
56
|
+
effects: {
|
|
57
|
+
changedObjects: [
|
|
58
|
+
{ objectId: packageId, outputState: 'PackageWrite', idOperation: 'Created' },
|
|
59
|
+
created(treasuryCapId),
|
|
60
|
+
created(metadataCapId),
|
|
61
|
+
],
|
|
54
62
|
},
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
objectId: metadataCapId,
|
|
63
|
+
objectTypes: {
|
|
64
|
+
[treasuryCapId]: `0x2::coin::TreasuryCap<${packageId}::mytoken::MYTOKEN>`,
|
|
65
|
+
[metadataCapId]: `0x2::coin_registry::MetadataCap<${packageId}::mytoken::MYTOKEN>`,
|
|
59
66
|
},
|
|
60
|
-
|
|
61
|
-
events: [],
|
|
62
|
-
transaction: null,
|
|
63
|
-
balanceChanges: null,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
67
|
-
const url = typeof input === 'string' ? input : input.toString()
|
|
68
|
-
const method = init?.method || 'GET'
|
|
69
|
-
const isRpcCall = (url.includes('fullnode') || url.includes('rpc')) && method === 'POST'
|
|
70
|
-
|
|
71
|
-
if (isRpcCall) {
|
|
72
|
-
const body = init?.body ? JSON.parse(String(init.body)) : {}
|
|
73
|
-
const rpcMethod = body.method
|
|
74
|
-
|
|
75
|
-
if (import.meta.env.VITE_E2E === '1') {
|
|
76
|
-
console.log('[E2E Fetch Mock]', rpcMethod, 'URL:', url)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result =
|
|
80
|
-
rpcMethod === 'sui_executeTransactionBlock' || rpcMethod === 'sui_getTransactionBlock'
|
|
81
|
-
? mockRpcResponse
|
|
82
|
-
: rpcMethod === 'suix_getCoins' || rpcMethod === 'suix_getBalance'
|
|
83
|
-
? { data: [], nextCursor: null, hasNextPage: false }
|
|
84
|
-
: null
|
|
85
|
-
|
|
86
|
-
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id || 1, result }), {
|
|
87
|
-
status: 200,
|
|
88
|
-
headers: { 'content-type': 'application/json' },
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return originalFetch(input, init)
|
|
67
|
+
},
|
|
93
68
|
}
|
|
69
|
+
const noObjects = async () => ({ objects: [], hasNextPage: false, cursor: null })
|
|
70
|
+
const stubClient = {
|
|
71
|
+
executeTransaction: async () => executed,
|
|
72
|
+
waitForTransaction: async () => executed,
|
|
73
|
+
listOwnedObjects: noObjects,
|
|
74
|
+
core: { listOwnedObjects: noObjects },
|
|
75
|
+
} as unknown as SuiGrpcClient
|
|
76
|
+
setReadClient('testnet', stubClient)
|
|
77
|
+
setReadClient('mainnet', stubClient)
|
|
94
78
|
|
|
95
79
|
const E2E_ADDR = '0x' + 'a'.repeat(64)
|
|
96
80
|
const E2E_PUB_KEY = new Uint8Array(32)
|
|
@@ -154,9 +138,6 @@ configureWasm(wasmUrl)
|
|
|
154
138
|
unregister?.()
|
|
155
139
|
unregister = undefined
|
|
156
140
|
}
|
|
157
|
-
|
|
158
|
-
// Expose getSuiClient for tests to build transactions
|
|
159
|
-
w.__getSuiClient = getReadClient
|
|
160
141
|
}
|
|
161
142
|
|
|
162
143
|
createApp(App).mount('#app')
|
package/src/styles.css
CHANGED
|
@@ -58,3 +58,25 @@ footer {
|
|
|
58
58
|
padding-top: 1.5rem;
|
|
59
59
|
margin-top: 2rem;
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/* Skip link (standalone app shell only — not part of the embeddable library styles). */
|
|
63
|
+
.skip-link {
|
|
64
|
+
position: absolute;
|
|
65
|
+
left: 0.5rem;
|
|
66
|
+
top: -3rem;
|
|
67
|
+
z-index: 100;
|
|
68
|
+
background: var(--accent);
|
|
69
|
+
color: #fff;
|
|
70
|
+
padding: 0.5rem 0.9rem;
|
|
71
|
+
border-radius: 6px;
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
transition: top 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
.skip-link:focus {
|
|
76
|
+
top: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
@media (prefers-reduced-motion: reduce) {
|
|
79
|
+
.skip-link {
|
|
80
|
+
transition: none;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"Move.toml": "[package]\nedition = \"2024\"\nname = \"sui_token_template\"\nversion = \"1.0.0\"\nlicense = \"
|
|
3
|
-
"source.move": "// SPDX-License-Identifier:
|
|
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",
|
|
2
|
+
"Move.toml": "[package]\nedition = \"2024\"\nname = \"sui_token_template\"\nversion = \"1.0.0\"\nlicense = \"0BSD\"\n\n\n[addresses]\nsui_token_template = \"0x0\"\n",
|
|
3
|
+
"source.move": "// SPDX-License-Identifier: 0BSD\n// Licensed under the 0BSD license; see the LICENSE file.\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>` from the\n/// constants below (the icon URL is the `ICON_URL` constant, which a deployer patches and\n/// `scripts/publish.sh` may replace per network — see below) and mints\n/// `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. Package immutability is a\n/// separate, explicit step: the UpgradeCap is burned by `deploy_token.sh`, or by\n/// `scripts/publish.sh` only with `--confirm-immutable` / an interactive CONFIRM. It is\n/// independent of metadata mutability, which stays with whoever holds 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\n# Publish the XPACKAGENAMEX coin package and finish its registration.\n#\n# Usage:\n# bash scripts/publish.sh [<network>] [--confirm-immutable] [--output-env]\n#\n# <network> Sui CLI environment alias (default: testnet). The ACTIVE env must match —\n# the script never switches environments for you.\n# --confirm-immutable Burn the UpgradeCap without prompting (IRREVERSIBLE).\n# --output-env Also print the resulting IDs as KEY=VALUE lines on stdout.\n#\n# Steps (each is resumable — IDs are saved to .env.<network> as soon as they exist, so a re-run\n# continues where a failed run stopped instead of publishing a second package):\n# 1. publish → package, TreasuryCap, MetadataCap, pending Currency, UpgradeCap\n# 2. coin_registry::finalize_registration → shared, wallet-discoverable Currency<SUI_TOKEN_TEMPLATE>\n# 3. coin_registry::set_icon_url → network-specific icon (needs SUI_TOKEN_TEMPLATE_ICON_URL)\n# 4. package::make_immutable → only with --confirm-immutable or an interactive CONFIRM;\n# verified by the UpgradeCap no longer existing.\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)] $*\" >&2\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 env_file=\"$PKG_DIR/.env.$1\"\n\t[[ -f \"$env_file\" ]] || { touch \"$env_file\"; log \"Created empty .env.$1\"; }\n\tset +u\n\t# shellcheck source=/dev/null\n\tsource \"$env_file\"\n\tset -u\n}\n\nsave_env() {\n\tlocal network=$1 key=$2 value=$3\n\tlocal env_file=\"$PKG_DIR/.env.${network}\"\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\techo \"$1\" | jq -r '.objectChanges[] | select(.type == \"published\") | .packageId'\n}\n\n# get_created_object <json> <type suffix> — the single created object whose type ends with the suffix.\nget_created_object() {\n\techo \"$1\" | jq -r --arg t \"$2\" \\\n\t\t'[.objectChanges[] | select(.type == \"created\" and (.objectType | endswith($t))) | .objectId]\n\t\t | if length == 1 then .[0] else \"\" end'\n}\n\n# run_json <cmd…> — run a sui CLI command, return its JSON body; on failure log the output and exit.\nrun_json() {\n\tlocal out\n\tif ! out=$(\"$@\" 2>&1); then\n\t\tlog \"ERROR: command failed: $*\"\n\t\tlog \"$out\"\n\t\texit 1\n\tfi\n\techo \"$out\" | awk '/^{/,0'\n}\n\n# True if the object currently exists on-chain (a deleted/consumed object makes the CLI exit non-zero).\nis_object_exists() {\n\tsui client object \"$1\" --json 2>/dev/null | jq -e '.objectId != null' > /dev/null 2>&1\n}\n\n# ─ Arguments ─────────────────────────────────────────────────────────────────\n\nNETWORK=testnet\nOUTPUT_ENV=false\nCONFIRM_IMMUTABLE=false\nif [[ $# -gt 0 && \"$1\" != --* ]]; then\n\tNETWORK=$1\n\tshift\nfi\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t\t--output-env) OUTPUT_ENV=true ;;\n\t\t--confirm-immutable) CONFIRM_IMMUTABLE=true ;;\n\t\t*) log \"ERROR: unknown argument '$1' (expected [<network>] [--confirm-immutable] [--output-env])\"; exit 1 ;;\n\tesac\n\tshift\ndone\n\nACTIVE_ENV=\"$(sui client active-env 2>/dev/null || true)\"\nif [[ \"$ACTIVE_ENV\" != \"$NETWORK\" ]]; then\n\tlog \"ERROR: active Sui env is '${ACTIVE_ENV}', but this run targets '${NETWORK}'.\"\n\tlog \" Switch first: sui client switch --env ${NETWORK}\"\n\texit 1\nfi\n\nload_env \"$NETWORK\"\nCOIN_TYPE_SUFFIX=\"::XMODULENAMEX::SUI_TOKEN_TEMPLATE\"\n\n# ─ 1. Publish ────────────────────────────────────────────────────────────────\n\nif [[ -n \"${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID:-}\" ]]; then\n\tlog \"XPACKAGENAMEX already published: $SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID (resuming)\"\nelse\n\trequire_env \"SUI_TOKEN_TEMPLATE_ICON_URL\"\n\tlog \"Publishing XPACKAGENAMEX package from $PKG_DIR...\"\n\tif [[ \"$NETWORK\" == \"localnet\" ]]; then\n\t\t# Sui >= 1.80 only publishes to environments declared in Move.toml; localnet uses an\n\t\t# ephemeral test-publish (its pubfile is discarded — localnet state is throwaway anyway).\n\t\tPUBLISH_OUTPUT=$(run_json sui client test-publish \"$PKG_DIR\" --json --build-env testnet \\\n\t\t\t--pubfile-path \"${TMPDIR:-/tmp}/pub-localnet-$$-${RANDOM}.toml\")\n\telse\n\t\tPUBLISH_OUTPUT=$(run_json sui client publish \"$PKG_DIR\" --json)\n\tfi\n\n\tSUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID=$(get_published_at \"$PUBLISH_OUTPUT\")\n\tSUI_TOKEN_TEMPLATE_TREASURY_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"::coin::TreasuryCap<${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}${COIN_TYPE_SUFFIX}>\")\n\tSUI_TOKEN_TEMPLATE_METADATA_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"::coin_registry::MetadataCap<${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}${COIN_TYPE_SUFFIX}>\")\n\tSUI_TOKEN_TEMPLATE_PENDING_CURRENCY_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"::coin_registry::Currency<${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}${COIN_TYPE_SUFFIX}>\")\n\tSUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID=$(get_created_object \"$PUBLISH_OUTPUT\" \"::package::UpgradeCap\")\n\n\tfor v in SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID SUI_TOKEN_TEMPLATE_METADATA_CAP_ID \\\n\t\tSUI_TOKEN_TEMPLATE_PENDING_CURRENCY_ID SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID; do\n\t\tif [[ -z \"${!v}\" || \"${!v}\" == \"null\" ]]; then\n\t\t\tlog \"ERROR: could not extract $v from publish output\"\n\t\t\tlog \"Output: $PUBLISH_OUTPUT\"\n\t\t\texit 1\n\t\tfi\n\tdone\n\tfor v in SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID SUI_TOKEN_TEMPLATE_TREASURY_CAP_ID SUI_TOKEN_TEMPLATE_METADATA_CAP_ID \\\n\t\tSUI_TOKEN_TEMPLATE_PENDING_CURRENCY_ID SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID; do\n\t\tsave_env \"$NETWORK\" \"$v\" \"${!v}\"\n\tdone\n\tlog \"Published XPACKAGENAMEX package successfully\"\nfi\nCOIN_TYPE=\"${SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID}${COIN_TYPE_SUFFIX}\"\n\n# ─ 2. finalize_registration ──────────────────────────────────────────────────\n# OTW currencies need this second step to become a real, shared, RPC-discoverable\n# Currency<SUI_TOKEN_TEMPLATE>. Anyone may perform it; doing it here keeps deploys self-contained.\n\nif [[ -n \"${SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID:-}\" ]]; then\n\tlog \"Currency already registered: $SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\"\nelse\n\trequire_env \"SUI_TOKEN_TEMPLATE_PENDING_CURRENCY_ID\"\n\tlog \"Promoting Currency<SUI_TOKEN_TEMPLATE> via coin_registry::finalize_registration...\"\n\tFINALIZE_REG_OUTPUT=$(run_json sui client call \\\n\t\t--package 0x2 \\\n\t\t--module coin_registry \\\n\t\t--function finalize_registration \\\n\t\t--type-args \"$COIN_TYPE\" \\\n\t\t--args 0xc \"$SUI_TOKEN_TEMPLATE_PENDING_CURRENCY_ID\" \\\n\t\t--gas-budget 100000000 \\\n\t\t--json)\n\tSUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID=$(get_created_object \"$FINALIZE_REG_OUTPUT\" \"::coin_registry::Currency<${COIN_TYPE}>\")\n\tif [[ -z \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" ]]; then\n\t\tlog \"ERROR: could not extract the shared Currency<SUI_TOKEN_TEMPLATE> ID from finalize_registration output\"\n\t\tlog \"Output: $FINALIZE_REG_OUTPUT\"\n\t\texit 1\n\tfi\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\"\nfi\n\n# ─ 3. Network-specific icon URL (while the sender still holds MetadataCap) ──\n\nif [[ \"${SUI_TOKEN_TEMPLATE_ICON_URL_SET:-}\" == \"${SUI_TOKEN_TEMPLATE_ICON_URL:-}\" && -n \"${SUI_TOKEN_TEMPLATE_ICON_URL:-}\" ]]; then\n\tlog \"Icon URL already set: $SUI_TOKEN_TEMPLATE_ICON_URL\"\nelse\n\trequire_env \"SUI_TOKEN_TEMPLATE_ICON_URL\"\n\tlog \"Setting icon URL via coin_registry::set_icon_url...\"\n\trun_json sui client call \\\n\t\t--package 0x2 \\\n\t\t--module coin_registry \\\n\t\t--function set_icon_url \\\n\t\t--type-args \"$COIN_TYPE\" \\\n\t\t--args \"$SUI_TOKEN_TEMPLATE_METADATA_OBJECT_ID\" \"$SUI_TOKEN_TEMPLATE_METADATA_CAP_ID\" \"$SUI_TOKEN_TEMPLATE_ICON_URL\" \\\n\t\t--gas-budget 100000000 \\\n\t\t--json > /dev/null\n\tsave_env \"$NETWORK\" \"SUI_TOKEN_TEMPLATE_ICON_URL_SET\" \"$SUI_TOKEN_TEMPLATE_ICON_URL\"\nfi\n\n# ─ 4. Irreversible: burn the UpgradeCap to make the package immutable ───────\n\nburn_upgrade_cap() {\n\tlog \"Making XPACKAGENAMEX package immutable (this is IRREVERSIBLE)...\"\n\trun_json sui 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\t# A package object is always \"Immutable\"-owned, so the only proof of immutability is that the\n\t# UpgradeCap was consumed.\n\tif is_object_exists \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\"; then\n\t\tlog \"ERROR: UpgradeCap $SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID still exists after make_immutable\"\n\t\texit 1\n\tfi\n\tlog \"✓ Package $SUI_TOKEN_TEMPLATE_TOKEN_PACKAGE_ID is now permanently immutable (UpgradeCap consumed)\"\n}\n\nif [[ -z \"${SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID:-}\" ]] || ! is_object_exists \"$SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID\"; then\n\tlog \"UpgradeCap already consumed — package is immutable\"\nelif [[ \"$CONFIRM_IMMUTABLE\" == \"true\" ]]; then\n\tburn_upgrade_cap\nelse\n\tlog \"\"\n\tlog \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\tlog \"UpgradeCap $SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID NOT burned — the package remains upgradeable.\"\n\tlog \"Burn it now, or later with: bash $0 $NETWORK --confirm-immutable\"\n\tread -r -p \"Type CONFIRM to burn the UpgradeCap and make the package immutable: \" ANSWER\n\tif [[ \"$ANSWER\" == \"CONFIRM\" ]]; then\n\t\tburn_upgrade_cap\n\telse\n\t\tlog \"Skipped immutability step. UpgradeCap $SUI_TOKEN_TEMPLATE_UPGRADE_CAP_ID is retained.\"\n\tfi\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\"\nfi\n",
|
|
5
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\
|
|
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
|
|
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 `
|
|
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
|
|
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\nBSD Zero Clause License (0BSD) — see the LICENSE file.\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 by confirming the UpgradeCap no longer exists (`sui client object <UPGRADE_CAP_ID>` fails); the package's own owner field is always `Immutable` and proves nothing.\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 wallets and the gRPC/GraphQL\n> coin-metadata APIs to resolve it). `scripts/publish.sh` does this automatically.\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()` registers the icon URL from the `ICON_URL` constant chosen at generation time;\n`scripts/publish.sh` then sets the network-specific value from `XSTRUCTNAMEX_ICON_URL` via\n`coin_registry::set_icon_url` (while the deployer still holds the `MetadataCap`).\n\n| Aspect | Behavior |\n| --- | --- |\n| `Currency<XSTRUCTNAMEX>` registry object | Yes — created pending, promoted to shared via `coin_registry::finalize_registration` |\n| Coin metadata (gRPC `getCoinMetadata`) | Returns the registry `Currency` once `finalize_registration` has run |\n| **Immutable fields** | Name, symbol, decimals |\n| **Mutable fields** | Description, icon URL |\n\n## Verification\n\n```bash\n# Package immutability: the UpgradeCap must no longer exist (a package object is always\n# \"Immutable\"-owned, so its owner field proves nothing).\nsui client object <UPGRADE_CAP_ID> --json # → error \"object deleted / not found\" once burned\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): shared object\nsui client object <CURRENCY_OBJECT_ID> --json | jq '.owner' # → { \"Shared\": … }\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 by confirming the UpgradeCap is gone: `sui client object <UPGRADE_CAP_ID>` must fail (object deleted) — a package object is always `Immutable`-owned, so its owner field proves nothing.\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 discoverable, wallets and the gRPC/GraphQL coin-metadata APIs only resolve\nit *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
10
|
}
|