@meddleware/token-deployer-ui 0.0.15 → 0.0.17
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/README.md +31 -40
- package/package.json +4 -11
- package/src/components/ConfigForm.vue +1 -1
- package/src/components/DeployProgress.vue +1 -1
- package/src/components/GithubPush.vue +2 -2
- package/src/components/IconPicker.vue +2 -2
- package/src/components/MyTokens.vue +8 -10
- package/src/components/ResultPanel.vue +2 -2
- package/src/components/ReviewPanel.vue +1 -1
- package/src/components/TokenDeployerView.vue +3 -3
- package/src/components/WalletBar.vue +1 -1
- package/src/components/WalrusBlobBrowser.vue +4 -4
- package/src/config.ts +1 -1
- package/src/index.ts +2 -0
- package/src/lib/deployExecutor.ts +3 -4
- package/src/lib/form.ts +1 -1
- package/src/lib/validation.ts +36 -129
- package/src/main.ts +3 -10
- package/src/templateWasm.ts +8 -0
- package/src/wallet.ts +19 -0
- package/src/lib/buildPublishTx.ts +0 -182
- package/src/lib/deploy.ts +0 -210
- package/src/lib/generatePackage.ts +0 -317
- package/src/lib/listMyTokens.ts +0 -41
- package/src/lib/readClient.ts +0 -25
- package/src/lib/template.ts +0 -134
- package/src/lib/types.ts +0 -69
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +0 -31
- package/src/template-src/files.json +0 -10
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
// Client-side generator for the downloadable source package. Mirrors the CLI
|
|
2
|
-
// generator (blockchain/sui/sui-token-template/scripts/03_create_token.sh) so the
|
|
3
|
-
// zip a user downloads matches what the CLI would produce (and the on-chain
|
|
4
|
-
// bytecode). Template text is synced verbatim via scripts/sync-template-src.sh.
|
|
5
|
-
//
|
|
6
|
-
// Published.toml: since Sui v1.63, the CLI writes deployed addresses to Published.toml
|
|
7
|
-
// (not Move.lock). Because the deployer bypasses the CLI, we generate it when a
|
|
8
|
-
// PublishResult is available so the downloaded package is immediately usable as a
|
|
9
|
-
// dependency reference without requiring the user to reconstruct the file manually.
|
|
10
|
-
|
|
11
|
-
import { zipSync, strToU8 } from 'fflate'
|
|
12
|
-
import templateFiles from '../template-src/files.json'
|
|
13
|
-
import type { PublishResult, TokenConfig } from './types.js'
|
|
14
|
-
import {
|
|
15
|
-
SAFE_TEXT,
|
|
16
|
-
deriveStructName,
|
|
17
|
-
validateIdentifier,
|
|
18
|
-
hasAllowedIconScheme,
|
|
19
|
-
} from './validation.js'
|
|
20
|
-
|
|
21
|
-
const files = templateFiles as Record<string, string>
|
|
22
|
-
|
|
23
|
-
/** License identifiers may only contain SPDX-safe characters. */
|
|
24
|
-
const LICENSE_ID = /^[A-Za-z0-9.\-+]*$/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Defence in depth: re-assert the invariants the form validator enforces before
|
|
28
|
-
* substituting any user value into Move source or the publish shell script. A
|
|
29
|
-
* value that fails here (e.g. a newline or quote that could break out of a Move
|
|
30
|
-
* `b"..."` literal) throws rather than producing a malformed/injectable package,
|
|
31
|
-
* so the generator is safe even if called with an unvalidated config.
|
|
32
|
-
*/
|
|
33
|
-
function assertSafeConfig(cfg: TokenConfig): void {
|
|
34
|
-
const idFields: Array<[string, string]> = [
|
|
35
|
-
['package name', cfg.packageName],
|
|
36
|
-
['module name', cfg.moduleName],
|
|
37
|
-
]
|
|
38
|
-
for (const [label, value] of idFields) {
|
|
39
|
-
const err = validateIdentifier(value)
|
|
40
|
-
if (err) throw new Error(`Invalid ${label}: ${err}`)
|
|
41
|
-
}
|
|
42
|
-
if (cfg.structName !== deriveStructName(cfg.moduleName)) {
|
|
43
|
-
throw new Error('Invalid struct name: must be the module name uppercased.')
|
|
44
|
-
}
|
|
45
|
-
const textFields: Array<[string, string]> = [
|
|
46
|
-
['symbol', cfg.symbol],
|
|
47
|
-
['name', cfg.name],
|
|
48
|
-
['description', cfg.description],
|
|
49
|
-
['icon URL', cfg.iconUrl],
|
|
50
|
-
]
|
|
51
|
-
for (const [label, value] of textFields) {
|
|
52
|
-
if (!SAFE_TEXT.test(value)) {
|
|
53
|
-
throw new Error(`Invalid ${label}: contains quotes, backslashes or control characters.`)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Re-assert the icon-URL scheme allowlist here too (layered defence, independent of the
|
|
57
|
-
// form): a non-empty icon URL must use https:// or ipfs:// before it is baked into metadata.
|
|
58
|
-
if (cfg.iconUrl && !hasAllowedIconScheme(cfg.iconUrl)) {
|
|
59
|
-
throw new Error('Invalid icon URL: only https:// or ipfs:// URLs are allowed.')
|
|
60
|
-
}
|
|
61
|
-
if (!Number.isInteger(cfg.decimals) || cfg.decimals < 0 || cfg.decimals > 18) {
|
|
62
|
-
throw new Error('Invalid decimals: expected a whole number between 0 and 18.')
|
|
63
|
-
}
|
|
64
|
-
if (!LICENSE_ID.test(cfg.license)) {
|
|
65
|
-
throw new Error('Invalid license identifier.')
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Escape a string for safe use as a literal RegExp. */
|
|
70
|
-
function esc(s: string): string {
|
|
71
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Replace every occurrence of a plain-string needle. */
|
|
75
|
-
function replaceAll(input: string, needle: string, value: string): string {
|
|
76
|
-
return input.replace(new RegExp(esc(needle), 'g'), () => value)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Apply the shared X…X documentation placeholders. */
|
|
80
|
-
function applyDocPlaceholders(input: string, cfg: TokenConfig): string {
|
|
81
|
-
const map: Record<string, string> = {
|
|
82
|
-
XPACKAGENAMEX: cfg.packageName,
|
|
83
|
-
XMODULENAMEX: cfg.moduleName,
|
|
84
|
-
XSTRUCTNAMEX: cfg.structName,
|
|
85
|
-
XSYMBOLX: cfg.symbol,
|
|
86
|
-
XNAMEX: cfg.name,
|
|
87
|
-
XDESCRIPTIONX: cfg.description,
|
|
88
|
-
XDECIMALSX: String(cfg.decimals),
|
|
89
|
-
XPROJECTNAMEX: cfg.projectName,
|
|
90
|
-
XPACKAGEDESCRIPTIONX: cfg.packageDescription,
|
|
91
|
-
}
|
|
92
|
-
let out = input
|
|
93
|
-
for (const [k, v] of Object.entries(map)) out = replaceAll(out, k, v)
|
|
94
|
-
return out
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Chain IDs for the two production networks (hex short form).
|
|
98
|
-
const CHAIN_IDS: Record<string, string> = {
|
|
99
|
-
testnet: '4c78adac',
|
|
100
|
-
mainnet: '35834a8a',
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Sui CLI version that compiled the shipped template bytecode. Keep in sync
|
|
104
|
-
// with TEMPLATE_MODULE_B64 in src/move-template/template.ts; updated by regen-template.sh.
|
|
105
|
-
const TEMPLATE_TOOLCHAIN_VERSION = '1.76.1'
|
|
106
|
-
|
|
107
|
-
const isProprietary = (license: string) =>
|
|
108
|
-
['none', 'unlicensed', 'noassertion', ''].includes(license.trim().toLowerCase())
|
|
109
|
-
|
|
110
|
-
function spdxId(license: string): string {
|
|
111
|
-
return isProprietary(license) ? 'UNLICENSED' : license.trim()
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Move.toml: package name + license field. */
|
|
115
|
-
function renderMoveToml(cfg: TokenConfig): string {
|
|
116
|
-
let out = replaceAll(files['Move.toml'], 'sui_token_template', cfg.packageName)
|
|
117
|
-
out = out.replace(/^license = .*$/m, `license = "${spdxId(cfg.license)}"`)
|
|
118
|
-
return out
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** The Move source: identifiers, named constants, and the license header. */
|
|
122
|
-
function renderSource(cfg: TokenConfig): string {
|
|
123
|
-
let out = files['source.move']
|
|
124
|
-
|
|
125
|
-
// license header (lines 1-2) — always rewritten, whatever licence the template itself carries
|
|
126
|
-
const lines = out.split('\n')
|
|
127
|
-
if (isProprietary(cfg.license)) {
|
|
128
|
-
lines[0] = '// SPDX-License-Identifier: UNLICENSED'
|
|
129
|
-
lines[1] = '// All rights reserved. Proprietary and not licensed for redistribution.'
|
|
130
|
-
} else {
|
|
131
|
-
lines[0] = `// SPDX-License-Identifier: ${spdxId(cfg.license)}`
|
|
132
|
-
lines[1] = `// Licensed under the ${spdxId(cfg.license)} license; see the LICENSE file.`
|
|
133
|
-
}
|
|
134
|
-
out = lines.join('\n')
|
|
135
|
-
|
|
136
|
-
out = replaceAll(
|
|
137
|
-
out,
|
|
138
|
-
'module sui_token_template::sui_token_template;',
|
|
139
|
-
`module ${cfg.packageName}::${cfg.moduleName};`,
|
|
140
|
-
)
|
|
141
|
-
out = replaceAll(out, 'SUI_TOKEN_TEMPLATE', cfg.structName)
|
|
142
|
-
out = replaceAll(out, 'XPACKAGEDESCRIPTIONX', cfg.packageDescription)
|
|
143
|
-
out = replaceAll(out, 'XMODULENAMEX', cfg.moduleName)
|
|
144
|
-
|
|
145
|
-
out = replaceAll(out, 'const DECIMALS: u8 = 9;', `const DECIMALS: u8 = ${cfg.decimals};`)
|
|
146
|
-
out = replaceAll(out, 'b"TEMPLATE_SYMBOL"', `b"${cfg.symbol}"`)
|
|
147
|
-
out = replaceAll(out, 'b"TEMPLATE_NAME"', `b"${cfg.name}"`)
|
|
148
|
-
out = replaceAll(out, 'b"TEMPLATE_DESCRIPTION"', `b"${cfg.description}"`)
|
|
149
|
-
out = replaceAll(out, 'b"TEMPLATE_ICON_URL"', `b"${cfg.iconUrl}"`)
|
|
150
|
-
return out
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function renderPublishScript(cfg: TokenConfig): string {
|
|
154
|
-
let out = replaceAll(files['scripts/publish.sh'], 'SUI_TOKEN_TEMPLATE', cfg.structName)
|
|
155
|
-
out = replaceAll(out, 'XMODULENAMEX', cfg.moduleName)
|
|
156
|
-
out = replaceAll(out, 'XPACKAGENAMEX', cfg.packageName)
|
|
157
|
-
return out
|
|
158
|
-
}
|
|
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
|
-
|
|
163
|
-
function renderReadme(cfg: TokenConfig): string {
|
|
164
|
-
const out = applyDocPlaceholders(files['README.md'], cfg)
|
|
165
|
-
const licenseName = cfg.licenseName || spdxId(cfg.license)
|
|
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
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Replace a single table row in a markdown section in-place.
|
|
176
|
-
* Matches: `| <fieldName> | \`FILL_IN_AFTER_DEPLOY\` |`
|
|
177
|
-
*/
|
|
178
|
-
function fillRow(section: string, fieldName: string, value: string): string {
|
|
179
|
-
return section.replace(
|
|
180
|
-
`| ${fieldName} | \`FILL_IN_AFTER_DEPLOY\` |`,
|
|
181
|
-
`| ${fieldName} | \`${value}\` |`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Fill the deployment table rows for the specified network section only.
|
|
187
|
-
* Operates on the substring between the section header and the next `## ` heading.
|
|
188
|
-
*/
|
|
189
|
-
function fillNetworkSection(doc: string, network: string, result: PublishResult): string {
|
|
190
|
-
const header = `## ${network.charAt(0).toUpperCase() + network.slice(1)}\n`
|
|
191
|
-
const start = doc.indexOf(header)
|
|
192
|
-
if (start === -1) return doc
|
|
193
|
-
const afterHeader = start + header.length
|
|
194
|
-
const nextSection = doc.indexOf('\n## ', afterHeader)
|
|
195
|
-
const end = nextSection === -1 ? doc.length : nextSection
|
|
196
|
-
|
|
197
|
-
let section = doc.slice(start, end)
|
|
198
|
-
|
|
199
|
-
section = fillRow(section, 'Package ID', result.packageId)
|
|
200
|
-
if (result.treasuryCapId) section = fillRow(section, 'TreasuryCap ID', result.treasuryCapId)
|
|
201
|
-
if (result.metadataCapId) section = fillRow(section, 'MetadataCap ID', result.metadataCapId)
|
|
202
|
-
if (result.currencyId) section = fillRow(section, 'Currency object ID', result.currencyId)
|
|
203
|
-
|
|
204
|
-
if (result.upgradeCapId) {
|
|
205
|
-
section = fillRow(section, 'UpgradeCap ID (burned)', result.upgradeCapId)
|
|
206
|
-
section = section.replace(
|
|
207
|
-
'| Immutability confirmed | Yes (Y/N) |',
|
|
208
|
-
'| Immutability confirmed | No (pending) |',
|
|
209
|
-
)
|
|
210
|
-
} else {
|
|
211
|
-
section = fillRow(section, 'UpgradeCap ID (burned)', 'burned')
|
|
212
|
-
section = section.replace(
|
|
213
|
-
'| Immutability confirmed | Yes (Y/N) |',
|
|
214
|
-
'| Immutability confirmed | Yes |',
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const deployed = new Date().toISOString().replace('T', ' ').split('.')[0] + ' UTC'
|
|
219
|
-
section = fillRow(section, 'Deployed', deployed)
|
|
220
|
-
|
|
221
|
-
return doc.slice(0, start) + section + doc.slice(end)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Optionally fill deployments.md with the just-published IDs. */
|
|
225
|
-
function renderDeployments(cfg: TokenConfig, result?: PublishResult): string {
|
|
226
|
-
let out = applyDocPlaceholders(files['deployments.md'], cfg)
|
|
227
|
-
if (!result) return out
|
|
228
|
-
|
|
229
|
-
// Fill the network-specific table section with known artefact IDs
|
|
230
|
-
out = fillNetworkSection(out, result.network, result)
|
|
231
|
-
|
|
232
|
-
// Fill the <PACKAGE_ID> placeholder in the downstream Move.toml reference block
|
|
233
|
-
out = out.replace(/<PACKAGE_ID>/g, result.packageId)
|
|
234
|
-
|
|
235
|
-
// Append extra data not represented in the table (coin type, digest)
|
|
236
|
-
const note = [
|
|
237
|
-
'',
|
|
238
|
-
`<!-- Auto-filled by the Token Deployer on ${new Date().toISOString()} (${result.network}) -->`,
|
|
239
|
-
`<!-- Coin type: ${result.coinType} -->`,
|
|
240
|
-
`<!-- Publish digest: ${result.digest} -->`,
|
|
241
|
-
'',
|
|
242
|
-
].join('\n')
|
|
243
|
-
return `${out.trimEnd()}\n${note}`
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Generate a Published.toml for the deployed package.
|
|
248
|
-
* Since v1.63, the Sui CLI writes Published.toml (not Move.lock) to record deployed
|
|
249
|
-
* addresses per environment. The deployer bypasses the CLI, so we produce this file
|
|
250
|
-
* so the downloaded package is immediately usable as a Move dependency reference.
|
|
251
|
-
* Commit Published.toml to source control; add Pub.*.toml (ephemeral) to .gitignore.
|
|
252
|
-
*/
|
|
253
|
-
function renderPublishedToml(result: PublishResult): string {
|
|
254
|
-
const env = result.network === 'mainnet' ? 'mainnet' : 'testnet'
|
|
255
|
-
const chainId = CHAIN_IDS[env] ?? ''
|
|
256
|
-
const lines = [
|
|
257
|
-
`[published.${env}]`,
|
|
258
|
-
`chain-id = "${chainId}"`,
|
|
259
|
-
`original-id = "${result.packageId}"`,
|
|
260
|
-
`published-at = "${result.packageId}"`,
|
|
261
|
-
`version = 1`,
|
|
262
|
-
`toolchain-version = "${TEMPLATE_TOOLCHAIN_VERSION}"`,
|
|
263
|
-
`build-config = { flavor = "sui", edition = "2024" }`,
|
|
264
|
-
]
|
|
265
|
-
if (result.upgradeCapId) {
|
|
266
|
-
lines.push(`upgrade-capability = "${result.upgradeCapId}"`)
|
|
267
|
-
} else {
|
|
268
|
-
lines.push(
|
|
269
|
-
`# upgrade-capability burned via 0x2::package::make_immutable — package is immutable`,
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
return lines.join('\n') + '\n'
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export interface GeneratePackageOptions {
|
|
276
|
-
config: TokenConfig
|
|
277
|
-
/** Full license text; when omitted for a real license, no LICENSE file is written. */
|
|
278
|
-
licenseText?: string
|
|
279
|
-
/** Publish result, to pre-fill deployments.md. */
|
|
280
|
-
result?: PublishResult
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Build the in-memory file map for the generated package. */
|
|
284
|
-
export function buildPackageFiles(opts: GeneratePackageOptions): Record<string, string> {
|
|
285
|
-
const { config, licenseText } = opts
|
|
286
|
-
assertSafeConfig(config)
|
|
287
|
-
const out: Record<string, string> = {
|
|
288
|
-
'Move.toml': renderMoveToml(config),
|
|
289
|
-
[`sources/${config.moduleName}.move`]: renderSource(config),
|
|
290
|
-
'scripts/publish.sh': renderPublishScript(config),
|
|
291
|
-
'.gitignore': files['.gitignore'],
|
|
292
|
-
'README.md': renderReadme(config),
|
|
293
|
-
'deployments.md': renderDeployments(config, opts.result),
|
|
294
|
-
'CLAUDE.md': applyDocPlaceholders(files['CLAUDE.md'], config),
|
|
295
|
-
'AGENTS.md': applyDocPlaceholders(files['AGENTS.md'], config),
|
|
296
|
-
}
|
|
297
|
-
if (opts.result) {
|
|
298
|
-
// Published.toml records the deployed address per environment (Sui v1.63+ convention).
|
|
299
|
-
// Commit this file to source control; add Pub.*.toml (ephemeral) to .gitignore.
|
|
300
|
-
out['Published.toml'] = renderPublishedToml(opts.result)
|
|
301
|
-
}
|
|
302
|
-
if (!isProprietary(config.license) && licenseText) {
|
|
303
|
-
out['LICENSE'] = licenseText
|
|
304
|
-
}
|
|
305
|
-
return out
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Zip the generated package. Returns bytes suitable for a download Blob. */
|
|
309
|
-
export function generatePackageZip(opts: GeneratePackageOptions): Uint8Array {
|
|
310
|
-
const filesMap = buildPackageFiles(opts)
|
|
311
|
-
const zipInput: Record<string, Uint8Array> = {}
|
|
312
|
-
const root = opts.config.packageName
|
|
313
|
-
for (const [path, content] of Object.entries(filesMap)) {
|
|
314
|
-
zipInput[`${root}/${path}`] = strToU8(content)
|
|
315
|
-
}
|
|
316
|
-
return zipSync(zipInput, { level: 6 })
|
|
317
|
-
}
|
package/src/lib/listMyTokens.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Discover coins the connected wallet has deployed, by listing the TreasuryCap<T> objects it
|
|
2
|
-
// owns (the deploy sends the TreasuryCap to the sender). Read-only; mirrors access-gate's
|
|
3
|
-
// AdminCap-based "My Gates" discovery.
|
|
4
|
-
import { getReadClient } from './readClient.js'
|
|
5
|
-
import type { Network } from './types.js'
|
|
6
|
-
|
|
7
|
-
/** One deployed coin the wallet controls (holds the TreasuryCap for). */
|
|
8
|
-
export interface DeployedToken {
|
|
9
|
-
/** Full coin type, e.g. `0x<pkg>::mytoken::MYTOKEN`. */
|
|
10
|
-
coinType: string
|
|
11
|
-
/** The published package id (first segment of the coin type). */
|
|
12
|
-
packageId: string
|
|
13
|
-
/** Object id of the owned `TreasuryCap<T>`. */
|
|
14
|
-
treasuryCapId: string
|
|
15
|
-
/** The one-time-witness struct name (last segment) — a compact display label. */
|
|
16
|
-
label: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** List the coins `owner` has deployed on `network` (via owned `TreasuryCap<T>` objects). */
|
|
20
|
-
export async function listMyTokens(owner: string, network: Network): Promise<DeployedToken[]> {
|
|
21
|
-
const client = getReadClient(network)
|
|
22
|
-
// A type filter without type params matches every TreasuryCap<T> instantiation.
|
|
23
|
-
const { objects } = await client.listOwnedObjects({ owner, type: '0x2::coin::TreasuryCap' })
|
|
24
|
-
const seen = new Set<string>()
|
|
25
|
-
const out: DeployedToken[] = []
|
|
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
|
|
30
|
-
const coinType = m[1]
|
|
31
|
-
if (seen.has(coinType)) continue
|
|
32
|
-
seen.add(coinType)
|
|
33
|
-
out.push({
|
|
34
|
-
coinType,
|
|
35
|
-
packageId: coinType.split('::')[0] ?? '',
|
|
36
|
-
treasuryCapId: obj.objectId,
|
|
37
|
-
label: coinType.split('::').pop() ?? coinType,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
return out
|
|
41
|
-
}
|
package/src/lib/readClient.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
import type { Network } from './types.js'
|
|
5
|
-
import { RPC_URLS } from '../config.js'
|
|
6
|
-
|
|
7
|
-
const clients = new Map<Network, SuiGrpcClient>()
|
|
8
|
-
|
|
9
|
-
/** Return a memoised {@link SuiGrpcClient} for the given network. */
|
|
10
|
-
export function getReadClient(network: Network): SuiGrpcClient {
|
|
11
|
-
let c = clients.get(network)
|
|
12
|
-
if (!c) {
|
|
13
|
-
c = new SuiGrpcClient({ network, baseUrl: RPC_URLS[network] })
|
|
14
|
-
clients.set(network, c)
|
|
15
|
-
}
|
|
16
|
-
return c
|
|
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
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// Client-side Move bytecode patching.
|
|
2
|
-
//
|
|
3
|
-
// The current on-chain binary format (v7) cannot be round-tripped by
|
|
4
|
-
// @mysten/move-bytecode-template's `update_identifiers` / `update_constants`
|
|
5
|
-
// helpers (they throw "missing field `version`"). However, `deserialize` and
|
|
6
|
-
// `serialize` DO handle v7 losslessly, so we patch the decoded JSON directly:
|
|
7
|
-
// rename identifiers in place and overwrite constant-pool entries. This is
|
|
8
|
-
// verified end-to-end against a localnet publish (see scripts/publish-localnet.mjs).
|
|
9
|
-
|
|
10
|
-
import init, { deserialize, serialize } from '@mysten/move-bytecode-template'
|
|
11
|
-
import { bcs } from '@mysten/bcs'
|
|
12
|
-
import { fromBase64 } from '@mysten/sui/utils'
|
|
13
|
-
import { SAFE_TEXT } from './validation.js'
|
|
14
|
-
import {
|
|
15
|
-
TEMPLATE_MODULE_B64,
|
|
16
|
-
TEMPLATE_IDENTIFIERS,
|
|
17
|
-
TEMPLATE_DEFAULTS,
|
|
18
|
-
} from '../move-template/template.js'
|
|
19
|
-
|
|
20
|
-
let initPromise: Promise<unknown> | null = null
|
|
21
|
-
let wasmInput: unknown = undefined
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Register the wasm asset URL without loading it (browser). Call once at app
|
|
25
|
-
* startup, e.g. `configureWasm(wasmUrl)`. The actual fetch is deferred until the
|
|
26
|
-
* first patch, so the 343 kB wasm doesn't block initial page load. In Node/tests
|
|
27
|
-
* this is unnecessary — the nodejs build loads its own wasm.
|
|
28
|
-
*/
|
|
29
|
-
export function configureWasm(input: unknown): void {
|
|
30
|
-
wasmInput = input
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Initialise the wasm module (idempotent + cached). Lazily triggered by the
|
|
35
|
-
* patch pipeline; uses the URL registered via `configureWasm` in the browser.
|
|
36
|
-
*/
|
|
37
|
-
export function initTemplateWasm(): Promise<unknown> {
|
|
38
|
-
if (!initPromise) {
|
|
39
|
-
initPromise = Promise.resolve()
|
|
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.
|
|
44
|
-
.catch(() => undefined)
|
|
45
|
-
}
|
|
46
|
-
return initPromise
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** BCS bytes for a `vector<u8>` / `String` constant (uleb length + utf8). */
|
|
50
|
-
function vecU8(value: string): number[] {
|
|
51
|
-
return Array.from(bcs.string().serialize(value).toBytes())
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function sameBytes(a: number[], b: number[]): boolean {
|
|
55
|
-
return a.length === b.length && a.every((x, i) => x === b[i])
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface PatchParams {
|
|
59
|
-
moduleName: string
|
|
60
|
-
structName: string
|
|
61
|
-
symbol: string
|
|
62
|
-
name: string
|
|
63
|
-
description: string
|
|
64
|
-
iconUrl: string
|
|
65
|
-
decimals: number
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface DecodedConstant {
|
|
69
|
-
type_: 'U8' | { Vector: 'U8' } | unknown
|
|
70
|
-
data: number[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface DecodedModule {
|
|
74
|
-
identifiers: string[]
|
|
75
|
-
constant_pool: DecodedConstant[]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Patch the template module with the caller's token parameters and return
|
|
80
|
-
* publishable module bytes. Pure with respect to the shipped artifact — a fresh
|
|
81
|
-
* copy is decoded each call.
|
|
82
|
-
*/
|
|
83
|
-
export async function patchTemplateModule(params: PatchParams): Promise<Uint8Array> {
|
|
84
|
-
await initTemplateWasm()
|
|
85
|
-
|
|
86
|
-
const original = fromBase64(TEMPLATE_MODULE_B64)
|
|
87
|
-
const json = deserialize(original) as unknown as DecodedModule
|
|
88
|
-
|
|
89
|
-
// 1) Rename the module + struct identifiers in place (index-preserving, so all
|
|
90
|
-
// handle references stay valid; the verifier does not require sorted ids).
|
|
91
|
-
const rename: Record<string, string> = {
|
|
92
|
-
[TEMPLATE_IDENTIFIERS.module]: params.moduleName,
|
|
93
|
-
[TEMPLATE_IDENTIFIERS.struct]: params.structName,
|
|
94
|
-
}
|
|
95
|
-
json.identifiers = json.identifiers.map((id) => rename[id] ?? id)
|
|
96
|
-
|
|
97
|
-
// 2) Overwrite the five constant-pool defaults with the requested values.
|
|
98
|
-
patchU8Constant(json, TEMPLATE_DEFAULTS.decimals, params.decimals)
|
|
99
|
-
patchVecConstant(json, TEMPLATE_DEFAULTS.symbol, params.symbol)
|
|
100
|
-
patchVecConstant(json, TEMPLATE_DEFAULTS.name, params.name)
|
|
101
|
-
patchVecConstant(json, TEMPLATE_DEFAULTS.description, params.description)
|
|
102
|
-
patchVecConstant(json, TEMPLATE_DEFAULTS.iconUrl, params.iconUrl)
|
|
103
|
-
|
|
104
|
-
return new Uint8Array(serialize(json as unknown as Parameters<typeof serialize>[0]))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function patchU8Constant(json: DecodedModule, current: number, next: number): void {
|
|
108
|
-
const entry = json.constant_pool.find(
|
|
109
|
-
(c) => c.type_ === 'U8' && c.data.length === 1 && c.data[0] === current,
|
|
110
|
-
)
|
|
111
|
-
if (!entry) throw new Error(`template U8 constant ${current} not found (artifact drift?)`)
|
|
112
|
-
if (next < 0 || next > 255 || !Number.isInteger(next)) {
|
|
113
|
-
throw new Error(`decimals must be an integer 0..255, got ${next}`)
|
|
114
|
-
}
|
|
115
|
-
entry.data = [next]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Upper bound on a patched string constant, matching the form's icon-URL cap. */
|
|
119
|
-
const MAX_VEC_CONSTANT_LEN = 512
|
|
120
|
-
|
|
121
|
-
function patchVecConstant(json: DecodedModule, current: string, next: string): void {
|
|
122
|
-
// Defence in depth: the on-chain metadata must match the generated source, so
|
|
123
|
-
// reject the same characters the form/source guard rejects, and bound the size.
|
|
124
|
-
if (!SAFE_TEXT.test(next)) {
|
|
125
|
-
throw new Error('constant contains quotes, backslashes or control characters')
|
|
126
|
-
}
|
|
127
|
-
if (next.length > MAX_VEC_CONSTANT_LEN) {
|
|
128
|
-
throw new Error(`constant exceeds ${MAX_VEC_CONSTANT_LEN} characters`)
|
|
129
|
-
}
|
|
130
|
-
const currentBytes = vecU8(current)
|
|
131
|
-
const entry = json.constant_pool.find((c) => c.type_ !== 'U8' && sameBytes(c.data, currentBytes))
|
|
132
|
-
if (!entry) throw new Error(`template constant "${current}" not found (artifact drift?)`)
|
|
133
|
-
entry.data = vecU8(next)
|
|
134
|
-
}
|
package/src/lib/types.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Shared types for the token deployer.
|
|
2
|
-
|
|
3
|
-
/** A Sui network the deployer can target. */
|
|
4
|
-
export type Network = 'testnet' | 'mainnet' | 'localnet'
|
|
5
|
-
|
|
6
|
-
/** Whether the deployer keeps the ability to mint more supply after launch. */
|
|
7
|
-
export type SupplyPolicy = 'fixed' | 'mintable'
|
|
8
|
-
|
|
9
|
-
/** Whether description/icon stay updatable (MetadataCap retained) or are frozen. */
|
|
10
|
-
export type MetadataPolicy = 'updatable' | 'frozen'
|
|
11
|
-
|
|
12
|
-
/** Whether the package code can be upgraded, or is made immutable at publish. */
|
|
13
|
-
export type PackagePolicy = 'immutable' | 'upgradeable'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The full set of choices a user makes. `identifiers` are baked into the coin
|
|
17
|
-
* type and are permanent; `symbol`/`name`/`decimals` are permanent on-chain.
|
|
18
|
-
*/
|
|
19
|
-
export interface TokenConfig {
|
|
20
|
-
/** Move package name, e.g. "my_token" (snake_case identifier). */
|
|
21
|
-
packageName: string
|
|
22
|
-
/** Move module name, e.g. "mytoken" (snake_case identifier). */
|
|
23
|
-
moduleName: string
|
|
24
|
-
/** One-time-witness struct name, e.g. "MYTOKEN" (SCREAMING_SNAKE identifier). */
|
|
25
|
-
structName: string
|
|
26
|
-
|
|
27
|
-
symbol: string
|
|
28
|
-
name: string
|
|
29
|
-
description: string
|
|
30
|
-
/** Icon URL (may be a Walrus aggregator URL). Empty string allowed. */
|
|
31
|
-
iconUrl: string
|
|
32
|
-
/** 0..255; conventionally 9 on Sui. */
|
|
33
|
-
decimals: number
|
|
34
|
-
|
|
35
|
-
/** Whole-token initial supply minted to the recipient (before decimals scaling). Use 0 for none. */
|
|
36
|
-
initialSupply: bigint
|
|
37
|
-
supplyPolicy: SupplyPolicy
|
|
38
|
-
metadataPolicy: MetadataPolicy
|
|
39
|
-
packagePolicy: PackagePolicy
|
|
40
|
-
|
|
41
|
-
/** Address that receives the caps + initial supply. Defaults to the connected wallet. */
|
|
42
|
-
recipient: string
|
|
43
|
-
|
|
44
|
-
/** SPDX id (e.g. "0BSD", "MIT") or "NONE" for proprietary. Affects the downloadable package only. */
|
|
45
|
-
license: string
|
|
46
|
-
/** Human-readable license name for the README (defaults to the SPDX id). */
|
|
47
|
-
licenseName?: string
|
|
48
|
-
/** Free-text descriptions used only in generated docs. */
|
|
49
|
-
packageDescription: string
|
|
50
|
-
projectName: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Result of a successful on-chain publish. */
|
|
54
|
-
export interface PublishResult {
|
|
55
|
-
network: Network
|
|
56
|
-
packageId: string
|
|
57
|
-
coinType: string
|
|
58
|
-
treasuryCapId?: string
|
|
59
|
-
metadataCapId?: string
|
|
60
|
-
currencyId?: string
|
|
61
|
-
/** Version and digest of the pending Currency<T> object — needed for finalize_registration. */
|
|
62
|
-
currencyVersion?: string
|
|
63
|
-
currencyDigest?: string
|
|
64
|
-
upgradeCapId?: string
|
|
65
|
-
digest: string
|
|
66
|
-
/** Where the trivial fee was sent, and how much (MIST). */
|
|
67
|
-
feeRecipient: string
|
|
68
|
-
feeMist: string
|
|
69
|
-
}
|
|
Binary file
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|