@meddleware/token-deployer-ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/move-language-reference/SKILL.md +445 -0
- package/.claude/skills/sui-move-bootcamp/SKILL.md +128 -0
- package/.claude/sui-move-bootcamp/SKILL.md +128 -0
- package/.editorconfig +8 -0
- package/.env.example +75 -0
- package/.env.production +7 -0
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/token-deployer-e2e.yml +64 -0
- package/.oxfmtrc.json +5 -0
- package/.oxlintrc.json +10 -0
- package/.vscode/extensions.json +9 -0
- package/AGENTS.md +39 -0
- package/CLAUDE.md +159 -0
- package/LICENSE +121 -0
- package/README.md +234 -0
- package/env.d.ts +13 -0
- package/index.html +13 -0
- package/package.json +67 -0
- package/playwright.config.ts +30 -0
- package/public/CNAME +1 -0
- package/public/_headers +12 -0
- package/public/_redirects +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +5 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +3 -0
- package/public/sitemap.xml +8 -0
- package/scripts/e2e-browser.mjs +15 -0
- package/scripts/e2e-deploy.mjs +222 -0
- package/scripts/e2e-localnet.mjs +65 -0
- package/scripts/e2e-walrus-browser.mjs +115 -0
- package/scripts/gen-og-image.mjs +31 -0
- package/scripts/publish-localnet.mjs +55 -0
- package/scripts/regen-template.sh +45 -0
- package/scripts/sync-template-src.sh +36 -0
- package/scripts/verify-bytecode.mjs +49 -0
- package/src/App.vue +41 -0
- package/src/__tests__/App.spec.js +11 -0
- package/src/components/AppNotice.vue +15 -0
- package/src/components/ColorModeControl.vue +81 -0
- package/src/components/ConfigForm.vue +192 -0
- package/src/components/DeployProgress.vue +46 -0
- package/src/components/FieldHint.vue +79 -0
- package/src/components/FormField.vue +28 -0
- package/src/components/FormSelect.vue +43 -0
- package/src/components/GithubPush.vue +100 -0
- package/src/components/IconPicker.vue +410 -0
- package/src/components/LicensePicker.vue +182 -0
- package/src/components/ResultPanel.vue +88 -0
- package/src/components/ReviewPanel.vue +69 -0
- package/src/components/TermsDialog.vue +81 -0
- package/src/components/TokenDeployerView.vue +115 -0
- package/src/components/WalletBar.vue +108 -0
- package/src/components/WalrusBlobBrowser.vue +188 -0
- package/src/components/WalrusIconUpload.vue +205 -0
- package/src/composables/useAccessGate.ts +122 -0
- package/src/composables/useColorMode.ts +76 -0
- package/src/composables/useWalrusRelay.ts +21 -0
- package/src/config.ts +185 -0
- package/src/index.ts +1 -0
- package/src/lib/accessGate.ts +141 -0
- package/src/lib/buildPublishTx.ts +192 -0
- package/src/lib/deploy.ts +168 -0
- package/src/lib/deployExecutor.ts +61 -0
- package/src/lib/errors.ts +21 -0
- package/src/lib/form.ts +75 -0
- package/src/lib/generatePackage.ts +312 -0
- package/src/lib/github.ts +146 -0
- package/src/lib/licenses.ts +122 -0
- package/src/lib/readClient.ts +17 -0
- package/src/lib/template.ts +131 -0
- package/src/lib/types.ts +69 -0
- package/src/lib/validation.ts +172 -0
- package/src/lib/walrus-constants.ts +15 -0
- package/src/lib/walrus.ts +144 -0
- package/src/main.ts +154 -0
- package/src/move-template/sui_token_template.mv +0 -0
- package/src/move-template/template.ts +31 -0
- package/src/styles.css +384 -0
- package/src/template-src/files.json +10 -0
- package/test-results/.last-run.json +4 -0
- package/tests/accessGate.test.ts +108 -0
- package/tests/buildPublishTx.test.ts +243 -0
- package/tests/deploy.test.ts +81 -0
- package/tests/errors.test.ts +32 -0
- package/tests/generatePackage.test.ts +182 -0
- package/tests/github.test.ts +61 -0
- package/tests/licenses.test.ts +78 -0
- package/tests/template.test.ts +93 -0
- package/tests/templateArtifact.test.ts +61 -0
- package/tests/templateParity.test.ts +55 -0
- package/tests/useAccessGate.test.ts +101 -0
- package/tests/useWallet.test.ts +74 -0
- package/tests/useWalrusRelayGating.test.ts +30 -0
- package/tests/validation.test.ts +117 -0
- package/tests/walrusUpload.test.ts +93 -0
- package/tests/walrus_url_patch.test.ts +26 -0
- package/tsconfig.json +19 -0
- package/typedoc.json +34 -0
- package/vite.config.ts +65 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// End-to-end: patch -> publish (fee + immutable) -> finalize (mint + fixed supply
|
|
2
|
+
// + frozen metadata) against localnet, using the actual app lib.
|
|
3
|
+
import init from '@mysten/move-bytecode-template';
|
|
4
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
5
|
+
import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet';
|
|
6
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
7
|
+
import { patchTemplateModule } from '../src/lib/template.ts';
|
|
8
|
+
import { buildPublishTransaction, buildFinalizeTransaction, extractPublishResult, needsFinalize } from '../src/lib/buildPublishTx.ts';
|
|
9
|
+
|
|
10
|
+
await (init)();
|
|
11
|
+
const client = new SuiJsonRpcClient({ url: 'http://127.0.0.1:9000' });
|
|
12
|
+
const kp = new Ed25519Keypair(); const sender = kp.toSuiAddress();
|
|
13
|
+
const feeRecipient = new Ed25519Keypair().toSuiAddress();
|
|
14
|
+
await requestSuiFromFaucetV2({ host: 'http://127.0.0.1:9123', recipient: sender });
|
|
15
|
+
for (let i=0;i<25;i++){ const b=await client.getBalance({owner:sender}); if(BigInt(b.totalBalance)>0n)break; await new Promise(r=>setTimeout(r,1000)); }
|
|
16
|
+
|
|
17
|
+
const config = {
|
|
18
|
+
packageName:'fixed_token', moduleName:'fixedtok', structName:'FIXEDTOK',
|
|
19
|
+
symbol:'FIX', name:'Fixed Token', description:'Fixed supply token', iconUrl:'', decimals:6,
|
|
20
|
+
initialSupply: 1000n, supplyPolicy:'fixed', metadataPolicy:'frozen', packagePolicy:'immutable',
|
|
21
|
+
recipient: sender, license:'MIT', packageDescription:'', projectName:'',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const moduleBytes = await patchTemplateModule(config);
|
|
25
|
+
const feeMist = 250000000n;
|
|
26
|
+
const pubTx = buildPublishTransaction({ moduleBytes, sender, feeMist, feeRecipient, gasBudget: 500000000n, packagePolicy: config.packagePolicy });
|
|
27
|
+
const pub = await client.signAndExecuteTransaction({ signer: kp, transaction: pubTx, options: { showEffects:true, showObjectChanges:true, showBalanceChanges:true } });
|
|
28
|
+
console.log('publish status:', pub.effects?.status?.status);
|
|
29
|
+
if (pub.effects?.status?.status !== 'success') { console.log(JSON.stringify(pub.effects?.status)); process.exit(1); }
|
|
30
|
+
|
|
31
|
+
await client.waitForTransaction({ digest: pub.digest });
|
|
32
|
+
|
|
33
|
+
const result = extractPublishResult(pub.objectChanges ?? [], { network:'localnet', digest: pub.digest, feeRecipient, feeMist });
|
|
34
|
+
console.log('coinType:', result.coinType);
|
|
35
|
+
console.log('caps:', { t: !!result.treasuryCapId, m: !!result.metadataCapId, upgrade: result.upgradeCapId ?? '(burned=immutable)' });
|
|
36
|
+
|
|
37
|
+
// fee arrived?
|
|
38
|
+
const feeBal = await client.getBalance({ owner: feeRecipient });
|
|
39
|
+
console.log('fee recipient balance:', feeBal.totalBalance, feeBal.totalBalance === feeMist.toString() ? 'OK' : 'MISMATCH');
|
|
40
|
+
|
|
41
|
+
// finalize
|
|
42
|
+
console.log('needsFinalize:', needsFinalize(config, sender));
|
|
43
|
+
const finTx = buildFinalizeTransaction({ config, coinType: result.coinType, treasuryCapId: result.treasuryCapId, metadataCapId: result.metadataCapId, sender, gasBudget: 500000000n });
|
|
44
|
+
const fin = await client.signAndExecuteTransaction({ signer: kp, transaction: finTx, options: { showEffects:true, showObjectChanges:true } });
|
|
45
|
+
await client.waitForTransaction({ digest: fin.digest });
|
|
46
|
+
console.log('finalize status:', fin.effects?.status?.status);
|
|
47
|
+
if (fin.effects?.status?.status !== 'success') { console.log(JSON.stringify(fin.effects?.status)); process.exit(1); }
|
|
48
|
+
|
|
49
|
+
// minted coin present?
|
|
50
|
+
const coins = await client.getCoins({ owner: sender, coinType: result.coinType });
|
|
51
|
+
const total = coins.data.reduce((a,c)=>a+BigInt(c.balance), 0n);
|
|
52
|
+
const expected = 1000n * 10n**6n;
|
|
53
|
+
console.log('minted balance:', total.toString(), total===expected ? 'OK (1000 * 10^6)' : 'MISMATCH exp '+expected);
|
|
54
|
+
|
|
55
|
+
// treasury cap frozen (immutable owner)?
|
|
56
|
+
const tcap = await client.getObject({ id: result.treasuryCapId, options:{ showOwner:true } });
|
|
57
|
+
const owner = tcap.data?.owner ?? tcap.object?.owner ?? tcap.owner;
|
|
58
|
+
console.log('treasuryCap owner after fixed-supply freeze:', JSON.stringify(owner));
|
|
59
|
+
|
|
60
|
+
const ok = result.coinType.endsWith('::fixedtok::FIXEDTOK')
|
|
61
|
+
&& feeBal.totalBalance === feeMist.toString()
|
|
62
|
+
&& !result.upgradeCapId
|
|
63
|
+
&& total === expected;
|
|
64
|
+
console.log('\nE2E', ok ? 'PASS ✓' : 'FAIL ✗');
|
|
65
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Headless browser verification of the Walrus icon-upload widget on TESTNET.
|
|
2
|
+
// Injects a wallet-standard wallet backed by the FUNDED keypair (SUI_PRIV env),
|
|
3
|
+
// drives the widget's file upload, and asserts the icon field is filled with a
|
|
4
|
+
// working aggregator URL that serves the exact uploaded image.
|
|
5
|
+
//
|
|
6
|
+
// Prereq: build the app + serve on APP_URL, and export SUI_PRIV for the funded
|
|
7
|
+
// testnet address (with SUI + WAL).
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { chromium } from 'playwright'
|
|
11
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
|
|
12
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
|
|
13
|
+
import { toBase64 } from '@mysten/sui/utils'
|
|
14
|
+
|
|
15
|
+
const APP_URL = process.env.APP_URL || 'http://localhost:4173'
|
|
16
|
+
const RPC = process.env.RPC || 'https://sui-testnet-rpc.publicnode.com'
|
|
17
|
+
const ICON = '/tmp/test-icon.svg'
|
|
18
|
+
|
|
19
|
+
const _client = new SuiJsonRpcClient({ url: RPC, network: 'testnet' })
|
|
20
|
+
const keypair = Ed25519Keypair.fromSecretKey(process.env.SUI_PRIV)
|
|
21
|
+
const address = keypair.toSuiAddress()
|
|
22
|
+
|
|
23
|
+
function fail(m) {
|
|
24
|
+
console.error('WALRUS BROWSER E2E FAIL:', m)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
console.log('signer:', address)
|
|
30
|
+
const browser = await chromium.launch({
|
|
31
|
+
headless: true,
|
|
32
|
+
executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome',
|
|
33
|
+
// Bypass CORS so the in-browser tx.build() can hit the testnet RPC.
|
|
34
|
+
args: ['--disable-web-security', '--disable-features=IsolateOrigins,site-per-process'],
|
|
35
|
+
})
|
|
36
|
+
const page = await browser.newPage()
|
|
37
|
+
page.on('pageerror', (e) => console.log('[pageerror]', e.message))
|
|
38
|
+
|
|
39
|
+
// Node just signs raw bytes; the browser builds the tx (resolving CoinWithBalance).
|
|
40
|
+
await page.exposeFunction('__mockSignBytes', async (byteArr) => {
|
|
41
|
+
const bytes = new Uint8Array(byteArr)
|
|
42
|
+
const { signature } = await keypair.signTransaction(bytes)
|
|
43
|
+
return { bytes: toBase64(bytes), signature }
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await page.addInitScript(
|
|
47
|
+
({ address, pubkey }) => {
|
|
48
|
+
const account = { address, publicKey: new Uint8Array(pubkey), chains: ['sui:testnet'], features: ['sui:signTransaction'], label: 'Mock' }
|
|
49
|
+
const wallet = {
|
|
50
|
+
version: '1.0.0', name: 'Mock Test Wallet',
|
|
51
|
+
icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=',
|
|
52
|
+
chains: ['sui:testnet'], accounts: [account],
|
|
53
|
+
features: {
|
|
54
|
+
'standard:connect': { version: '1.0.0', connect: async () => ({ accounts: [account] }) },
|
|
55
|
+
'standard:events': { version: '1.0.0', on: () => () => {} },
|
|
56
|
+
'sui:signTransaction': {
|
|
57
|
+
version: '2.0.0',
|
|
58
|
+
signTransaction: async (input) => {
|
|
59
|
+
// A real wallet sets the sender from the connected account; do the same.
|
|
60
|
+
const client = window.__getSuiClient('testnet')
|
|
61
|
+
input.transaction.setSenderIfNotSet(input.account.address)
|
|
62
|
+
const bytes = await input.transaction.build({ client })
|
|
63
|
+
return window.__mockSignBytes(Array.from(bytes))
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
const cb = ({ register }) => register(wallet)
|
|
69
|
+
window.dispatchEvent(new CustomEvent('wallet-standard:register-wallet', { detail: cb }))
|
|
70
|
+
window.addEventListener('wallet-standard:app-ready', (e) => cb(e.detail))
|
|
71
|
+
},
|
|
72
|
+
{ address, pubkey: Array.from(keypair.getPublicKey().toRawBytes()) },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
await page.goto(APP_URL, { waitUntil: 'networkidle' })
|
|
76
|
+
await page.getByRole('button', { name: /Connect Mock Test Wallet/ }).click()
|
|
77
|
+
await page.getByText(/Mock Test Wallet ·/).waitFor({ timeout: 10000 })
|
|
78
|
+
console.log('wallet connected')
|
|
79
|
+
|
|
80
|
+
// Drive the widget: choose the file + click upload.
|
|
81
|
+
await page.locator('#walrus-file').setInputFiles(ICON)
|
|
82
|
+
await page.getByRole('button', { name: /Upload to Walrus/ }).click()
|
|
83
|
+
console.log('uploading to Walrus (2 wallet approvals, relay)…')
|
|
84
|
+
|
|
85
|
+
// Wait for the icon field to be populated with an aggregator URL.
|
|
86
|
+
// (arg must precede options in waitForFunction.)
|
|
87
|
+
try {
|
|
88
|
+
await page.waitForFunction(
|
|
89
|
+
() => (document.querySelector('#icon') ?? {}).value?.includes('/v1/blobs/'),
|
|
90
|
+
undefined,
|
|
91
|
+
{ timeout: 180000, polling: 1000 },
|
|
92
|
+
)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
const status = await page.locator('.hint[aria-live]').allInnerTexts().catch(() => [])
|
|
95
|
+
const err = await page.locator('.field-error').allInnerTexts().catch(() => [])
|
|
96
|
+
console.log('widget status:', JSON.stringify(status))
|
|
97
|
+
console.log('widget errors:', JSON.stringify(err))
|
|
98
|
+
throw e
|
|
99
|
+
}
|
|
100
|
+
const url = await page.locator('#icon').inputValue()
|
|
101
|
+
console.log('icon URL:', url)
|
|
102
|
+
await browser.close()
|
|
103
|
+
|
|
104
|
+
if (!url.includes('aggregator.walrus-testnet') || !url.includes('/v1/blobs/')) fail(`unexpected URL: ${url}`)
|
|
105
|
+
const res = await fetch(url)
|
|
106
|
+
const body = new Uint8Array(await res.arrayBuffer())
|
|
107
|
+
const expected = new Uint8Array(readFileSync(ICON))
|
|
108
|
+
const match = body.length === expected.length && body.every((b, i) => b === expected[i])
|
|
109
|
+
console.log('aggregator serves', body.length, 'bytes, matches uploaded image:', match)
|
|
110
|
+
if (!match) fail('aggregator content does not match uploaded image')
|
|
111
|
+
|
|
112
|
+
console.log('\nWALRUS BROWSER E2E PASS ✓ (widget upload -> renderable icon URL on testnet)')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch((e) => fail(e.message))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Regenerates public/og-image.png (1200x630 social card) via headless Chrome.
|
|
2
|
+
// Run from the app dir: `node scripts/gen-og-image.mjs`. Re-run if branding changes.
|
|
3
|
+
import { chromium } from 'playwright'
|
|
4
|
+
import { writeFileSync } from 'node:fs'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const OUT = fileURLToPath(new URL('../public/og-image.png', import.meta.url))
|
|
8
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
9
|
+
*{margin:0;box-sizing:border-box;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
|
|
10
|
+
body{width:1200px;height:630px;background:linear-gradient(135deg,#0b1120 0%,#111c33 60%,#0e2a3f 100%);color:#f4f7fb;display:flex;flex-direction:column;justify-content:center;padding:80px}
|
|
11
|
+
.badge{display:inline-block;font-size:26px;letter-spacing:.12em;text-transform:uppercase;color:#6ea8fe;border:2px solid #2b3a55;border-radius:999px;padding:10px 26px;width:max-content;margin-bottom:36px}
|
|
12
|
+
h1{font-size:82px;line-height:1.04;font-weight:800;letter-spacing:-.02em;margin-bottom:28px}
|
|
13
|
+
h1 .accent{color:#6ea8fe}
|
|
14
|
+
p{font-size:34px;line-height:1.35;color:#aebfd6;max-width:960px}
|
|
15
|
+
.row{display:flex;gap:18px;margin-top:52px}
|
|
16
|
+
.chip{font-size:24px;color:#cfe0f7;background:#16233d;border:1px solid #26365a;border-radius:10px;padding:12px 20px}
|
|
17
|
+
.url{position:absolute;bottom:56px;right:80px;font-size:26px;color:#6ea8fe;font-weight:600}
|
|
18
|
+
</style></head><body>
|
|
19
|
+
<div class="badge">Sui · client-side</div>
|
|
20
|
+
<h1>Create your own <span class="accent">Sui coin</span><br>in the browser</h1>
|
|
21
|
+
<p>Your wallet signs and pays gas — nothing is compiled or signed on a server. Keep the full, verifiable source package.</p>
|
|
22
|
+
<div class="row"><div class="chip">No server</div><div class="chip">No custody</div><div class="chip">Full source included</div></div>
|
|
23
|
+
<div class="url">tokens.meddleware.co.uk</div>
|
|
24
|
+
</body></html>`
|
|
25
|
+
|
|
26
|
+
const browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome' })
|
|
27
|
+
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 })
|
|
28
|
+
await page.setContent(html, { waitUntil: 'networkidle' })
|
|
29
|
+
writeFileSync(OUT, await page.screenshot({ type: 'png' }))
|
|
30
|
+
await browser.close()
|
|
31
|
+
console.log('wrote', OUT)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { bcs } from '@mysten/bcs';
|
|
3
|
+
import * as tpl from '@mysten/move-bytecode-template';
|
|
4
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
5
|
+
import { requestSuiFromFaucetV2 } from '@mysten/sui/faucet';
|
|
6
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
7
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
8
|
+
import { toBase64 } from '@mysten/sui/utils';
|
|
9
|
+
|
|
10
|
+
const init = tpl.default ?? tpl.init;
|
|
11
|
+
try { if (typeof init === 'function') await init(); } catch {}
|
|
12
|
+
|
|
13
|
+
const client = new SuiJsonRpcClient({ url: 'http://127.0.0.1:9000' });
|
|
14
|
+
|
|
15
|
+
const orig = new Uint8Array(readFileSync(new URL('../src/move-template/sui_token_template.mv', import.meta.url)));
|
|
16
|
+
const json = tpl.deserialize(orig);
|
|
17
|
+
const rename = { sui_token_template: 'mycoin', SUI_TOKEN_TEMPLATE: 'MYCOIN' };
|
|
18
|
+
json.identifiers = json.identifiers.map((id) => rename[id] ?? id);
|
|
19
|
+
const vec = (s) => Array.from(bcs.string().serialize(s).toBytes());
|
|
20
|
+
const patch = (cur, nw) => { const c = json.constant_pool.find((k)=>JSON.stringify(k.data)===JSON.stringify(vec(cur))); c.data = vec(nw); };
|
|
21
|
+
json.constant_pool.find((c)=>c.type_==='U8').data = [6];
|
|
22
|
+
patch('TEMPLATE_SYMBOL','MYC'); patch('TEMPLATE_NAME','My Coin');
|
|
23
|
+
patch('TEMPLATE_DESCRIPTION','A great coin'); patch('TEMPLATE_ICON_URL','https://x/y.svg');
|
|
24
|
+
const moduleBytes = new Uint8Array(tpl.serialize(json));
|
|
25
|
+
console.log('identifiers sorted?', JSON.stringify(json.identifiers) === JSON.stringify([...json.identifiers].sort()));
|
|
26
|
+
|
|
27
|
+
const kp = new Ed25519Keypair();
|
|
28
|
+
const addr = kp.toSuiAddress();
|
|
29
|
+
console.log('address:', addr);
|
|
30
|
+
await requestSuiFromFaucetV2({ host: 'http://127.0.0.1:9123', recipient: addr });
|
|
31
|
+
let bal = '0';
|
|
32
|
+
for (let i=0;i<25;i++){ const b = await client.getBalance({ owner: addr }); if (BigInt(b.totalBalance) > 0n){ bal=b.totalBalance; break;} await new Promise(r=>setTimeout(r,1000)); }
|
|
33
|
+
console.log('balance:', bal);
|
|
34
|
+
|
|
35
|
+
const tx = new Transaction();
|
|
36
|
+
tx.setSender(addr);
|
|
37
|
+
const [upgradeCap] = tx.publish({ modules: [toBase64(moduleBytes)], dependencies: ['0x1', '0x2'] });
|
|
38
|
+
tx.transferObjects([upgradeCap], addr);
|
|
39
|
+
tx.setGasBudget(500000000);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await client.signAndExecuteTransaction({ signer: kp, transaction: tx, options: { showEffects: true, showObjectChanges: true } });
|
|
43
|
+
const status = res.effects?.status?.status;
|
|
44
|
+
console.log('\nstatus:', status);
|
|
45
|
+
if (status !== 'success') { console.log('ERR:', JSON.stringify(res.effects?.status)); process.exit(1); }
|
|
46
|
+
for (const c of (res.objectChanges ?? [])) {
|
|
47
|
+
if (c.type==='published') console.log('packageId:', c.packageId);
|
|
48
|
+
if (c.type==='created') console.log(' created:', c.objectType);
|
|
49
|
+
}
|
|
50
|
+
console.log('\nPUBLISH PASS - verifier accepted the in-place patched module');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.log('\nPUBLISH FAIL:', e.message);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# Recompiles the canonical sui-token-template Move module and refreshes the
|
|
4
|
+
# bytecode artifact the deployer ships (src/move-template/*). Run this whenever
|
|
5
|
+
# blockchain/sui/sui-token-template/sources/sui_token_template.move changes, so
|
|
6
|
+
# the shipped bytecode stays in lock-step with the downloadable source generator.
|
|
7
|
+
#
|
|
8
|
+
# IMPORTANT: use `sui move build` (NOT `sui move test`) — the test profile emits
|
|
9
|
+
# instrumented bytecode (magic 0xDEADC0DE) that is not publishable.
|
|
10
|
+
|
|
11
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
APP_DIR="$HERE/.."
|
|
13
|
+
# The canonical template ships as the published @meddleware/sui-token-template
|
|
14
|
+
# package; resolve it from node_modules. A local checkout can be pointed at via
|
|
15
|
+
# SUI_TOKEN_TEMPLATE_DIR (e.g. during template co-development before publish).
|
|
16
|
+
TEMPLATE_DIR="${SUI_TOKEN_TEMPLATE_DIR:-$(cd "$APP_DIR" && node -e "process.stdout.write(require('path').dirname(require.resolve('@meddleware/sui-token-template/package.json')))" 2>/dev/null || true)}"
|
|
17
|
+
if [[ -z "${TEMPLATE_DIR:-}" || ! -f "$TEMPLATE_DIR/Move.toml" ]]; then
|
|
18
|
+
echo "ERROR: sui-token-template sources not found. Install @meddleware/sui-token-template or set SUI_TOKEN_TEMPLATE_DIR to a local checkout." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "Building $TEMPLATE_DIR (testnet build-env)…"
|
|
23
|
+
( cd "$TEMPLATE_DIR" && rm -rf build && sui move build --build-env testnet >/dev/null )
|
|
24
|
+
|
|
25
|
+
MV="$TEMPLATE_DIR/build/sui_token_template/bytecode_modules/sui_token_template.mv"
|
|
26
|
+
MAGIC="$(head -c 4 "$MV" | xxd -p)"
|
|
27
|
+
if [[ "$MAGIC" != "a11ceb0b" ]]; then
|
|
28
|
+
echo "ERROR: unexpected bytecode magic $MAGIC (expected a11ceb0b)" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
cp "$MV" "$APP_DIR/src/move-template/sui_token_template.mv"
|
|
33
|
+
B64="$(base64 -w0 "$MV")"
|
|
34
|
+
|
|
35
|
+
# Refresh only the base64 literal in template.ts, preserving the rest.
|
|
36
|
+
node - "$APP_DIR/src/move-template/template.ts" "$B64" <<'NODE'
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const [file, b64] = process.argv.slice(2);
|
|
39
|
+
let s = fs.readFileSync(file, 'utf8');
|
|
40
|
+
s = s.replace(/export const TEMPLATE_MODULE_B64 =\n\s*'[^']*';/, `export const TEMPLATE_MODULE_B64 =\n '${b64}';`);
|
|
41
|
+
fs.writeFileSync(file, s);
|
|
42
|
+
console.log('updated TEMPLATE_MODULE_B64 (%d base64 chars)', b64.length);
|
|
43
|
+
NODE
|
|
44
|
+
|
|
45
|
+
echo "Done. Verify the DEFAULT_* constants in template.ts still match the source."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# Copies the canonical sui-token-template text files into the app as a single
|
|
4
|
+
# JSON module (src/template-src/files.json), so the downloadable-package
|
|
5
|
+
# generator stays the single source of truth with the CLI generator. Re-run
|
|
6
|
+
# whenever the template's source/docs/scripts change.
|
|
7
|
+
|
|
8
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
APP_DIR="$HERE/.."
|
|
10
|
+
# Canonical template = the published @meddleware/sui-token-template package
|
|
11
|
+
# (resolved from node_modules); override with SUI_TOKEN_TEMPLATE_DIR for a local checkout.
|
|
12
|
+
T="${SUI_TOKEN_TEMPLATE_DIR:-$(cd "$APP_DIR" && node -e "process.stdout.write(require('path').dirname(require.resolve('@meddleware/sui-token-template/package.json')))" 2>/dev/null || true)}"
|
|
13
|
+
if [[ -z "${T:-}" || ! -f "$T/Move.toml" ]]; then
|
|
14
|
+
echo "ERROR: sui-token-template sources not found. Install @meddleware/sui-token-template or set SUI_TOKEN_TEMPLATE_DIR to a local checkout." >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
OUT="$APP_DIR/src/template-src/files.json"
|
|
18
|
+
mkdir -p "$APP_DIR/src/template-src"
|
|
19
|
+
|
|
20
|
+
node - "$T" "$OUT" <<'NODE'
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const [T, OUT] = process.argv.slice(2);
|
|
23
|
+
const read = (p) => fs.readFileSync(`${T}/${p}`, 'utf8');
|
|
24
|
+
const files = {
|
|
25
|
+
'Move.toml': read('Move.toml'),
|
|
26
|
+
'source.move': read('sources/sui_token_template.move'),
|
|
27
|
+
'scripts/publish.sh': read('templates/publish.sh'),
|
|
28
|
+
'.gitignore': read('templates/.gitignore'),
|
|
29
|
+
'README.md': read('templates/README.md'),
|
|
30
|
+
'deployments.md': read('templates/deployments.md'),
|
|
31
|
+
'CLAUDE.md': read('templates/CLAUDE.md'),
|
|
32
|
+
'AGENTS.md': read('templates/AGENTS.md'),
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(OUT, JSON.stringify(files, null, 2) + '\n');
|
|
35
|
+
console.log('wrote %s (%d files)', OUT, Object.keys(files).length);
|
|
36
|
+
NODE
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// v7-safe patch via direct JSON mutation (deserialize -> mutate -> serialize),
|
|
2
|
+
// bypassing update_identifiers/update_constants which cannot re-serialize v7.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { bcs } from '@mysten/bcs';
|
|
5
|
+
import * as tpl from '@mysten/move-bytecode-template';
|
|
6
|
+
|
|
7
|
+
const init = tpl.default ?? tpl.init;
|
|
8
|
+
try { if (typeof init === 'function') await init(); } catch {}
|
|
9
|
+
|
|
10
|
+
const orig = new Uint8Array(readFileSync(new URL('../src/move-template/sui_token_template.mv', import.meta.url)));
|
|
11
|
+
const json = tpl.deserialize(orig);
|
|
12
|
+
|
|
13
|
+
// 1) rename identifiers in place (indices preserved => all handle refs stay valid)
|
|
14
|
+
const rename = { sui_token_template: 'mycoin', SUI_TOKEN_TEMPLATE: 'MYCOIN' };
|
|
15
|
+
json.identifiers = json.identifiers.map((id) => rename[id] ?? id);
|
|
16
|
+
|
|
17
|
+
// 2) patch constant_pool by matching the known default values
|
|
18
|
+
const vec = (s) => Array.from(bcs.string().serialize(s).toBytes()); // uleb len + utf8 bytes (== vector<u8>)
|
|
19
|
+
const patchByCurrent = (currentStr, newStr) => {
|
|
20
|
+
const cur = vec(currentStr);
|
|
21
|
+
const c = json.constant_pool.find((k) => JSON.stringify(k.data) === JSON.stringify(cur));
|
|
22
|
+
if (!c) throw new Error('constant not found: ' + currentStr);
|
|
23
|
+
c.data = vec(newStr);
|
|
24
|
+
};
|
|
25
|
+
// decimals (U8)
|
|
26
|
+
json.constant_pool.find((c) => c.type_ === 'U8').data = [6];
|
|
27
|
+
patchByCurrent('TEMPLATE_SYMBOL', 'MYC');
|
|
28
|
+
patchByCurrent('TEMPLATE_NAME', 'My Coin');
|
|
29
|
+
patchByCurrent('TEMPLATE_DESCRIPTION', 'A great coin');
|
|
30
|
+
patchByCurrent('TEMPLATE_ICON_URL', 'https://x/y.svg');
|
|
31
|
+
|
|
32
|
+
const out = new Uint8Array(tpl.serialize(json));
|
|
33
|
+
const magic = Buffer.from(out.slice(0, 4)).toString('hex');
|
|
34
|
+
console.log('patched bytes=%d magic=%s (orig=%d)', out.length, magic, orig.length);
|
|
35
|
+
|
|
36
|
+
// re-deserialize the patched module to confirm structural validity
|
|
37
|
+
const back = tpl.deserialize(out);
|
|
38
|
+
const asStr = (d) => Buffer.from(d.slice(1)).toString('utf8');
|
|
39
|
+
console.log('identifiers now include:', back.identifiers.filter((i) => ['mycoin','MYCOIN','sui_token_template','SUI_TOKEN_TEMPLATE'].includes(i)));
|
|
40
|
+
console.log('constants now:', back.constant_pool.map((c) => c.type_==='U8'?`u8=${c.data[0]}`:asStr(c.data)));
|
|
41
|
+
|
|
42
|
+
const ok = magic === 'a11ceb0b'
|
|
43
|
+
&& back.identifiers.includes('mycoin') && back.identifiers.includes('MYCOIN')
|
|
44
|
+
&& !back.identifiers.includes('sui_token_template') && !back.identifiers.includes('SUI_TOKEN_TEMPLATE')
|
|
45
|
+
&& back.constant_pool.some((c) => c.type_==='U8' && c.data[0]===6)
|
|
46
|
+
&& ['MYC','My Coin','A great coin','https://x/y.svg'].every((v) => back.constant_pool.some((c)=>c.type_!=='U8'&&asStr(c.data)===v))
|
|
47
|
+
&& !back.constant_pool.some((c)=>c.type_!=='U8'&&asStr(c.data).startsWith('TEMPLATE_'));
|
|
48
|
+
console.log('\nJSON-MUTATION ROUND-TRIP', ok ? 'PASS ✓' : 'FAIL ✗');
|
|
49
|
+
process.exit(ok ? 0 : 1);
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import WalletBar from './components/WalletBar.vue'
|
|
3
|
+
import TokenDeployerView from './components/TokenDeployerView.vue'
|
|
4
|
+
import ColorModeControl from './components/ColorModeControl.vue'
|
|
5
|
+
import { useColorMode } from './composables/useColorMode.js'
|
|
6
|
+
|
|
7
|
+
const { mode: colorMode, set: setColorMode } = useColorMode('system')
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<a class="skip-link" href="#main-content">Skip to main content</a>
|
|
12
|
+
<main id="main-content">
|
|
13
|
+
<div class="theme-bar">
|
|
14
|
+
<ColorModeControl :model-value="colorMode" @update:model-value="setColorMode" />
|
|
15
|
+
</div>
|
|
16
|
+
<header v-once>
|
|
17
|
+
<h1>Create your own Sui coin</h1>
|
|
18
|
+
<p class="lede">
|
|
19
|
+
Deploy a coin on Sui directly from your browser. Your wallet signs and pays gas — nothing is
|
|
20
|
+
compiled or signed on a server — and you keep the full, verifiable source package.
|
|
21
|
+
</p>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<WalletBar />
|
|
25
|
+
|
|
26
|
+
<TokenDeployerView />
|
|
27
|
+
|
|
28
|
+
<footer v-once style="margin-top: 3rem; color: var(--muted); font-size: 0.9rem">
|
|
29
|
+
<p>
|
|
30
|
+
Client-side token deployment. This tool never takes custody of your token or keys. Symbol,
|
|
31
|
+
name, and decimals are permanent once published.
|
|
32
|
+
</p>
|
|
33
|
+
<p>
|
|
34
|
+
<strong>No endorsement.</strong> This is a neutral, self-serve tool. It does not review,
|
|
35
|
+
verify, or endorse any token created with it, nor its creator. A token existing here implies
|
|
36
|
+
nothing about its legitimacy or value — always do your own research before acquiring or
|
|
37
|
+
trading any coin.
|
|
38
|
+
</p>
|
|
39
|
+
</footer>
|
|
40
|
+
</main>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import App from '../App.vue'
|
|
5
|
+
|
|
6
|
+
describe('App', () => {
|
|
7
|
+
it('mounts renders properly', () => {
|
|
8
|
+
const wrapper = mount(App)
|
|
9
|
+
expect(wrapper.text()).toContain('You did it!')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ type?: 'info' | 'error' | 'ok' }>()
|
|
5
|
+
|
|
6
|
+
const role = computed(() => {
|
|
7
|
+
if (props.type === 'error') return 'alert'
|
|
8
|
+
if (props.type === 'ok') return 'status'
|
|
9
|
+
return undefined
|
|
10
|
+
})
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<p :class="['notice', type && type !== 'info' ? type : '']" :role="role"><slot /></p>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ColorMode } from '../composables/useColorMode'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Presentational light/dark/system control. Owns no state — bind with `v-model`
|
|
6
|
+
* to external state (`useColorMode`). Vendored mirror of `@meddleware/ui`'s
|
|
7
|
+
* `ColorModeControl` for parity with the dashboard.
|
|
8
|
+
* TODO(unify): consume `@meddleware/ui` once published.
|
|
9
|
+
*/
|
|
10
|
+
withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
modelValue: ColorMode
|
|
13
|
+
modes?: ColorMode[]
|
|
14
|
+
}>(),
|
|
15
|
+
{ modes: () => ['light', 'dark', 'system'] },
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
defineEmits<{ 'update:modelValue': [ColorMode] }>()
|
|
19
|
+
|
|
20
|
+
const LABEL: Record<ColorMode, string> = { light: 'Light', dark: 'Dark', system: 'System' }
|
|
21
|
+
const GLYPH: Record<ColorMode, string> = { light: '☀', dark: '☾', system: '◐' }
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="mode-control" role="group" aria-label="Colour mode">
|
|
26
|
+
<button
|
|
27
|
+
v-for="m in modes"
|
|
28
|
+
:key="m"
|
|
29
|
+
type="button"
|
|
30
|
+
class="mode-control__btn"
|
|
31
|
+
:class="{ 'is-active': modelValue === m }"
|
|
32
|
+
:aria-pressed="modelValue === m"
|
|
33
|
+
:title="LABEL[m]"
|
|
34
|
+
@click="$emit('update:modelValue', m)"
|
|
35
|
+
>
|
|
36
|
+
<span aria-hidden="true">{{ GLYPH[m] }}</span>
|
|
37
|
+
<span class="mode-control__label">{{ LABEL[m] }}</span>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<style scoped>
|
|
43
|
+
.mode-control {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
gap: 2px;
|
|
46
|
+
padding: 2px;
|
|
47
|
+
border: 1px solid var(--border);
|
|
48
|
+
border-radius: var(--radius);
|
|
49
|
+
background: var(--surface);
|
|
50
|
+
}
|
|
51
|
+
.mode-control__btn {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 0.35rem;
|
|
55
|
+
padding: 0.25rem 0.5rem;
|
|
56
|
+
border: 0;
|
|
57
|
+
border-radius: calc(var(--radius) - 3px);
|
|
58
|
+
background: transparent;
|
|
59
|
+
color: var(--text);
|
|
60
|
+
font: inherit;
|
|
61
|
+
font-size: 0.8rem;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
opacity: 0.7;
|
|
64
|
+
}
|
|
65
|
+
.mode-control__btn:hover {
|
|
66
|
+
opacity: 1;
|
|
67
|
+
}
|
|
68
|
+
.mode-control__btn.is-active {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
background: var(--lift);
|
|
71
|
+
}
|
|
72
|
+
.mode-control__btn:focus-visible {
|
|
73
|
+
outline: 2px solid var(--accent);
|
|
74
|
+
outline-offset: 1px;
|
|
75
|
+
}
|
|
76
|
+
@media (max-width: 640px) {
|
|
77
|
+
.mode-control__label {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|