@meddleware/token-deployer-ui 0.0.10 → 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/README.md +8 -5
- package/package.json +19 -8
- 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 +2 -2
- 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 +19 -17
- package/src/lib/buildPublishTx.ts +0 -10
- package/src/lib/deploy.ts +9 -3
- package/src/lib/deployExecutor.ts +18 -45
- package/src/lib/template.ts +3 -0
- package/src/lib/walrus-constants.ts +2 -2
- package/src/main.ts +4 -6
- package/src/styles.css +22 -0
- 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 -72
- 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 -166
- package/Dockerfile +0 -96
- package/eslint.config.ts +0 -63
- package/index.html +0 -22
- 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 -227
- package/scripts/e2e-localnet.mjs +0 -67
- package/scripts/e2e-walrus-browser.mjs +0 -112
- package/scripts/gen-og-image.mjs +0 -31
- package/scripts/publish-localnet.mjs +0 -56
- 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 -155
- package/src/lib/walrus.ts +0 -155
- package/stylelint.config.js +0 -33
- package/test-results/.last-run.json +0 -4
- package/tests/accessGate.test.ts +0 -113
- package/tests/buildPublishTx.test.ts +0 -243
- package/tests/deploy.test.ts +0 -149
- package/tests/errors.test.ts +0 -32
- package/tests/generatePackage.test.ts +0 -193
- package/tests/github.test.ts +0 -61
- package/tests/licenses.test.ts +0 -81
- package/tests/listMyTokens.test.ts +0 -30
- 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 -97
- 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 -73
- package/vitest.config.ts +0 -10
package/tests/github.test.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { createRepoAndPush, toBase64Utf8 } from '../src/lib/github.js'
|
|
3
|
-
|
|
4
|
-
function jsonRes(body: unknown, ok = true, status = 200): Response {
|
|
5
|
-
return { ok, status, json: async () => body } as Response
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
describe('toBase64Utf8', () => {
|
|
9
|
-
it('encodes UTF-8 correctly', () => {
|
|
10
|
-
expect(toBase64Utf8('hello')).toBe('aGVsbG8=')
|
|
11
|
-
// round-trips multibyte
|
|
12
|
-
expect(Buffer.from(toBase64Utf8('héllo—'), 'base64').toString('utf8')).toBe('héllo—')
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('createRepoAndPush', () => {
|
|
17
|
-
it('creates the repo then PUTs each file, reporting progress', async () => {
|
|
18
|
-
const fetcher = vi.fn<(url: string, _init?: RequestInit) => Promise<Response>>(async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
19
|
-
if (url.endsWith('/user/repos')) {
|
|
20
|
-
return jsonRes({ full_name: 'alice/my_token', html_url: 'https://github.com/alice/my_token', owner: { login: 'alice' }, name: 'my_token' })
|
|
21
|
-
}
|
|
22
|
-
return jsonRes({ content: {} }, true, 201)
|
|
23
|
-
}) as unknown as typeof fetch
|
|
24
|
-
|
|
25
|
-
const progress: number[] = []
|
|
26
|
-
const res = await createRepoAndPush({
|
|
27
|
-
token: 'ghp_x',
|
|
28
|
-
repoName: 'my_token',
|
|
29
|
-
files: { 'Move.toml': 'a', 'sources/mytoken.move': 'b' },
|
|
30
|
-
fetcher,
|
|
31
|
-
onProgress: (d) => progress.push(d),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
expect(res.htmlUrl).toBe('https://github.com/alice/my_token')
|
|
35
|
-
expect(res.owner).toBe('alice')
|
|
36
|
-
// 1 create + 2 file PUTs
|
|
37
|
-
const calls = (fetcher as unknown as { mock: { calls: [string, RequestInit][] } }).mock.calls
|
|
38
|
-
expect(calls.length).toBe(3)
|
|
39
|
-
expect(calls[0][1]?.method).toBe('POST')
|
|
40
|
-
expect(calls[1][1]?.method).toBe('PUT')
|
|
41
|
-
expect(progress).toEqual([1, 2])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('surfaces the GitHub error message on repo-create failure', async () => {
|
|
45
|
-
const fetcher = vi.fn<() => Promise<Response>>(async (): Promise<Response> => jsonRes({ message: 'name already exists on this account' }, false, 422)) as unknown as typeof fetch
|
|
46
|
-
await expect(
|
|
47
|
-
createRepoAndPush({ token: 't', repoName: 'dupe', files: { a: 'x' }, fetcher }),
|
|
48
|
-
).rejects.toThrow(/already exists/)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('sends the token as a Bearer header', async () => {
|
|
52
|
-
const fetcher = vi.fn<(url: string) => Promise<Response>>(async (url: string): Promise<Response> =>
|
|
53
|
-
url.endsWith('/user/repos')
|
|
54
|
-
? jsonRes({ html_url: 'h', owner: { login: 'bob' }, name: 'r', full_name: 'bob/r' })
|
|
55
|
-
: jsonRes({}, true, 201),
|
|
56
|
-
) as unknown as typeof fetch
|
|
57
|
-
await createRepoAndPush({ token: 'secret', repoName: 'r', files: { a: 'x' }, fetcher })
|
|
58
|
-
const headers = (fetcher as unknown as { mock: { calls: [string, RequestInit][] } }).mock.calls[0][1]?.headers as Record<string, string>
|
|
59
|
-
expect(headers.Authorization).toBe('Bearer secret')
|
|
60
|
-
})
|
|
61
|
-
})
|
package/tests/licenses.test.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
fetchLicenseList,
|
|
4
|
-
fetchLicenseText,
|
|
5
|
-
NO_LICENSE,
|
|
6
|
-
__resetLicenseCaches,
|
|
7
|
-
} from '../src/lib/licenses.js'
|
|
8
|
-
|
|
9
|
-
const LIST = {
|
|
10
|
-
licenses: [
|
|
11
|
-
{ licenseId: 'MIT', name: 'MIT License', isOsiApproved: true, isDeprecatedLicenseId: false },
|
|
12
|
-
{ licenseId: '0BSD', name: 'BSD Zero Clause License', isOsiApproved: true, isDeprecatedLicenseId: false },
|
|
13
|
-
{ licenseId: 'CC0-1.0', name: 'Creative Commons Zero v1.0 Universal', isOsiApproved: false, isDeprecatedLicenseId: false },
|
|
14
|
-
{ licenseId: 'GPL-1.0', name: 'GNU GPL v1.0 (deprecated)', isOsiApproved: false, isDeprecatedLicenseId: true },
|
|
15
|
-
],
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function mockFetch(bodyByUrl: Record<string, { ok?: boolean; json?: unknown; text?: string; status?: number }>) {
|
|
19
|
-
return vi.fn<(url: string) => Promise<Response>>(async (url: string): Promise<Response> => {
|
|
20
|
-
const entry = bodyByUrl[url as string] ?? bodyByUrl['*']
|
|
21
|
-
return {
|
|
22
|
-
ok: entry?.ok ?? true,
|
|
23
|
-
status: entry?.status ?? 200,
|
|
24
|
-
json: async () => entry?.json,
|
|
25
|
-
text: async () => entry?.text ?? '',
|
|
26
|
-
} as Response
|
|
27
|
-
}) as unknown as typeof fetch
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const LIST_URL = 'https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json'
|
|
31
|
-
|
|
32
|
-
beforeEach(() => __resetLicenseCaches())
|
|
33
|
-
|
|
34
|
-
describe('fetchLicenseList', () => {
|
|
35
|
-
it('filters deprecated, sorts, includes NONE, and surfaces popular first', async () => {
|
|
36
|
-
const f = mockFetch({ [LIST_URL]: { json: LIST } })
|
|
37
|
-
const cat = await fetchLicenseList(f)
|
|
38
|
-
|
|
39
|
-
expect(cat.all[0]).toBe(NO_LICENSE)
|
|
40
|
-
const ids = cat.all.map((l) => l.id)
|
|
41
|
-
expect(ids).toContain('MIT')
|
|
42
|
-
expect(ids).toContain('CC0-1.0') // still selectable
|
|
43
|
-
expect(ids).not.toContain('GPL-1.0') // deprecated dropped
|
|
44
|
-
|
|
45
|
-
// popular ordering: the 0BSD default first, CC0 still offered
|
|
46
|
-
const popIds = cat.popular.map((l) => l.id)
|
|
47
|
-
expect(popIds[0]).toBe('0BSD')
|
|
48
|
-
expect(popIds.indexOf('0BSD')).toBeLessThan(popIds.indexOf('MIT'))
|
|
49
|
-
expect(popIds).toContain('CC0-1.0')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('caches the list (one network call across calls)', async () => {
|
|
53
|
-
const f = mockFetch({ [LIST_URL]: { json: LIST } })
|
|
54
|
-
await fetchLicenseList(f)
|
|
55
|
-
await fetchLicenseList(f)
|
|
56
|
-
expect((f as unknown as { mock: { calls: unknown[] } }).mock.calls.length).toBe(1)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('fetchLicenseText', () => {
|
|
61
|
-
it('fetches and caches license text', async () => {
|
|
62
|
-
const url = 'https://raw.githubusercontent.com/spdx/license-list-data/main/text/MIT.txt'
|
|
63
|
-
const f = mockFetch({ [url]: { text: 'MIT LICENSE BODY' } })
|
|
64
|
-
expect(await fetchLicenseText('MIT', f)).toBe('MIT LICENSE BODY')
|
|
65
|
-
await fetchLicenseText('MIT', f)
|
|
66
|
-
expect((f as unknown as { mock: { calls: unknown[] } }).mock.calls.length).toBe(1)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('returns null (no fetch) for the proprietary choice', async () => {
|
|
70
|
-
const f = mockFetch({})
|
|
71
|
-
expect(await fetchLicenseText('NONE', f)).toBeNull()
|
|
72
|
-
expect(await fetchLicenseText('', f)).toBeNull()
|
|
73
|
-
expect((f as unknown as { mock: { calls: unknown[] } }).mock.calls.length).toBe(0)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('throws on a failed fetch', async () => {
|
|
77
|
-
const url = 'https://raw.githubusercontent.com/spdx/license-list-data/main/text/BOGUS.txt'
|
|
78
|
-
const f = mockFetch({ [url]: { ok: false, status: 404 } })
|
|
79
|
-
await expect(fetchLicenseText('BOGUS', f)).rejects.toThrow(/BOGUS/)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
const listOwnedObjects = vi.hoisted(() => vi.fn())
|
|
4
|
-
vi.mock('../src/lib/readClient.js', () => ({
|
|
5
|
-
getReadClient: () => ({ listOwnedObjects }),
|
|
6
|
-
}))
|
|
7
|
-
|
|
8
|
-
import { listMyTokens } from '../src/lib/listMyTokens.js'
|
|
9
|
-
|
|
10
|
-
const PKG = '0x' + 'ab'.repeat(32)
|
|
11
|
-
|
|
12
|
-
describe('listMyTokens', () => {
|
|
13
|
-
it('queries owned TreasuryCaps and parses short- and long-form framework types', async () => {
|
|
14
|
-
listOwnedObjects.mockResolvedValue({
|
|
15
|
-
objects: [
|
|
16
|
-
{ objectId: '0x1', type: `0x2::coin::TreasuryCap<${PKG}::a::A>` },
|
|
17
|
-
{ objectId: '0x2', type: `0x${'0'.repeat(63)}2::coin::TreasuryCap<${PKG}::b::B>` },
|
|
18
|
-
// duplicate coin type and an unrelated type are skipped
|
|
19
|
-
{ objectId: '0x3', type: `0x2::coin::TreasuryCap<${PKG}::a::A>` },
|
|
20
|
-
{ objectId: '0x4', type: '0x2::coin::Coin<0x2::sui::SUI>' },
|
|
21
|
-
],
|
|
22
|
-
})
|
|
23
|
-
const tokens = await listMyTokens('0xowner', 'testnet')
|
|
24
|
-
expect(listOwnedObjects).toHaveBeenCalledWith({ owner: '0xowner', type: '0x2::coin::TreasuryCap' })
|
|
25
|
-
expect(tokens).toEqual([
|
|
26
|
-
{ coinType: `${PKG}::a::A`, packageId: PKG, treasuryCapId: '0x1', label: 'A' },
|
|
27
|
-
{ coinType: `${PKG}::b::B`, packageId: PKG, treasuryCapId: '0x2', label: 'B' },
|
|
28
|
-
])
|
|
29
|
-
})
|
|
30
|
-
})
|
package/tests/template.test.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import init, { deserialize } from '@mysten/move-bytecode-template'
|
|
3
|
-
import { patchTemplateModule } from '../src/lib/template.js'
|
|
4
|
-
import { TEMPLATE_DEFAULTS } from '../src/move-template/template.js'
|
|
5
|
-
|
|
6
|
-
const asStr = (data: number[]) => Buffer.from(data.slice(1)).toString('utf8')
|
|
7
|
-
|
|
8
|
-
interface Decoded {
|
|
9
|
-
identifiers: string[]
|
|
10
|
-
constant_pool: { type_: unknown; data: number[] }[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe('patchTemplateModule', () => {
|
|
14
|
-
it('renames identifiers and patches all five constants, producing a valid module', async () => {
|
|
15
|
-
await (init as unknown as () => Promise<unknown>)()
|
|
16
|
-
|
|
17
|
-
const bytes = await patchTemplateModule({
|
|
18
|
-
moduleName: 'mycoin',
|
|
19
|
-
structName: 'MYCOIN',
|
|
20
|
-
symbol: 'MYC',
|
|
21
|
-
name: 'My Coin',
|
|
22
|
-
description: 'A great coin',
|
|
23
|
-
iconUrl: 'https://example.com/icon.svg',
|
|
24
|
-
decimals: 6,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// valid Move module magic
|
|
28
|
-
expect(Buffer.from(bytes.slice(0, 4)).toString('hex')).toBe('a11ceb0b')
|
|
29
|
-
|
|
30
|
-
const back = deserialize(bytes) as unknown as Decoded
|
|
31
|
-
|
|
32
|
-
// identifiers renamed; template identifiers gone
|
|
33
|
-
expect(back.identifiers).toContain('mycoin')
|
|
34
|
-
expect(back.identifiers).toContain('MYCOIN')
|
|
35
|
-
expect(back.identifiers).not.toContain('sui_token_template')
|
|
36
|
-
expect(back.identifiers).not.toContain('SUI_TOKEN_TEMPLATE')
|
|
37
|
-
|
|
38
|
-
// constants patched
|
|
39
|
-
expect(back.constant_pool.some((c) => c.type_ === 'U8' && c.data[0] === 6)).toBe(true)
|
|
40
|
-
for (const v of ['MYC', 'My Coin', 'A great coin', 'https://example.com/icon.svg']) {
|
|
41
|
-
expect(back.constant_pool.some((c) => c.type_ !== 'U8' && asStr(c.data) === v)).toBe(true)
|
|
42
|
-
}
|
|
43
|
-
// no template defaults remain
|
|
44
|
-
for (const def of [
|
|
45
|
-
TEMPLATE_DEFAULTS.symbol,
|
|
46
|
-
TEMPLATE_DEFAULTS.name,
|
|
47
|
-
TEMPLATE_DEFAULTS.description,
|
|
48
|
-
TEMPLATE_DEFAULTS.iconUrl,
|
|
49
|
-
]) {
|
|
50
|
-
expect(back.constant_pool.some((c) => c.type_ !== 'U8' && asStr(c.data) === def)).toBe(false)
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('supports an empty icon URL', async () => {
|
|
55
|
-
const bytes = await patchTemplateModule({
|
|
56
|
-
moduleName: 'noicon',
|
|
57
|
-
structName: 'NOICON',
|
|
58
|
-
symbol: 'NIC',
|
|
59
|
-
name: 'No Icon',
|
|
60
|
-
description: 'desc',
|
|
61
|
-
iconUrl: '',
|
|
62
|
-
decimals: 9,
|
|
63
|
-
})
|
|
64
|
-
const back = deserialize(bytes) as unknown as Decoded
|
|
65
|
-
// empty string encodes as a single 0x00 length byte
|
|
66
|
-
expect(back.constant_pool.some((c) => c.type_ !== 'U8' && c.data.length === 1 && c.data[0] === 0)).toBe(true)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('rejects out-of-range decimals', async () => {
|
|
70
|
-
await expect(
|
|
71
|
-
patchTemplateModule({
|
|
72
|
-
moduleName: 'x', structName: 'X', symbol: 'X', name: 'X', description: 'X', iconUrl: '', decimals: 300,
|
|
73
|
-
}),
|
|
74
|
-
).rejects.toThrow(/decimals/)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('rejects a string constant with control chars or backslashes (defence in depth)', async () => {
|
|
78
|
-
await expect(
|
|
79
|
-
patchTemplateModule({
|
|
80
|
-
moduleName: 'x', structName: 'X', symbol: 'E\nVIL', name: 'X', description: 'X', iconUrl: '', decimals: 9,
|
|
81
|
-
}),
|
|
82
|
-
).rejects.toThrow(/control characters|quotes|backslashes/)
|
|
83
|
-
|
|
84
|
-
await expect(
|
|
85
|
-
patchTemplateModule({
|
|
86
|
-
moduleName: 'x', structName: 'X', symbol: 'X', name: 'X', description: 'X',
|
|
87
|
-
iconUrl: 'x'.repeat(600), decimals: 9,
|
|
88
|
-
}),
|
|
89
|
-
).rejects.toThrow(/exceeds/)
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// Temporary test to check Walrus blob URL patching
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
-
import init, { deserialize } from '@mysten/move-bytecode-template'
|
|
3
|
-
import { fromBase64 } from '@mysten/sui/utils'
|
|
4
|
-
import {
|
|
5
|
-
TEMPLATE_MODULE_B64,
|
|
6
|
-
TEMPLATE_IDENTIFIERS,
|
|
7
|
-
TEMPLATE_DEFAULTS,
|
|
8
|
-
} from '../src/move-template/template.js'
|
|
9
|
-
|
|
10
|
-
// Provenance guard: the checked-in bytecode (.mv, as base64) and the hand-kept
|
|
11
|
-
// TEMPLATE_IDENTIFIERS / TEMPLATE_DEFAULTS in move-template/template.ts must stay
|
|
12
|
-
// consistent. If regen-template.sh is run but the DEFAULTS aren't updated (or vice
|
|
13
|
-
// versa), the deployer's patch would silently fail to match — this test catches
|
|
14
|
-
// that drift in CI instead of at deploy time.
|
|
15
|
-
|
|
16
|
-
interface Decoded {
|
|
17
|
-
identifiers: string[]
|
|
18
|
-
constant_pool: { type_: unknown; data: number[] }[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// A BCS-encoded String/vector<u8> is a uleb length prefix + utf8 bytes; for these
|
|
22
|
-
// short defaults the prefix is a single byte, so slice(1) recovers the text.
|
|
23
|
-
const asStr = (data: number[]) => Buffer.from(data.slice(1)).toString('utf8')
|
|
24
|
-
|
|
25
|
-
describe('template artifact provenance', () => {
|
|
26
|
-
let decoded: Decoded
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
await (init as unknown as () => Promise<unknown>)()
|
|
30
|
-
decoded = deserialize(fromBase64(TEMPLATE_MODULE_B64)) as unknown as Decoded
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('is a valid v7 Move module (magic a11ceb0b)', () => {
|
|
34
|
-
expect(Buffer.from(fromBase64(TEMPLATE_MODULE_B64).slice(0, 4)).toString('hex')).toBe('a11ceb0b')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('contains the declared template identifiers', () => {
|
|
38
|
-
expect(decoded.identifiers).toContain(TEMPLATE_IDENTIFIERS.module)
|
|
39
|
-
expect(decoded.identifiers).toContain(TEMPLATE_IDENTIFIERS.struct)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('contains the declared decimals default as a U8 constant', () => {
|
|
43
|
-
expect(
|
|
44
|
-
decoded.constant_pool.some((c) => c.type_ === 'U8' && c.data[0] === TEMPLATE_DEFAULTS.decimals),
|
|
45
|
-
).toBe(true)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('contains each declared string default as a distinct vector constant', () => {
|
|
49
|
-
for (const def of [
|
|
50
|
-
TEMPLATE_DEFAULTS.symbol,
|
|
51
|
-
TEMPLATE_DEFAULTS.name,
|
|
52
|
-
TEMPLATE_DEFAULTS.description,
|
|
53
|
-
TEMPLATE_DEFAULTS.iconUrl,
|
|
54
|
-
]) {
|
|
55
|
-
expect(
|
|
56
|
-
decoded.constant_pool.some((c) => c.type_ !== 'U8' && asStr(c.data) === def),
|
|
57
|
-
`default "${def}" not found in the shipped bytecode`,
|
|
58
|
-
).toBe(true)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
3
|
-
import { dirname } from 'node:path'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import templateFiles from '../src/template-src/files.json'
|
|
6
|
-
|
|
7
|
-
// Single-source-of-truth guard. `src/template-src/files.json` is generated from
|
|
8
|
-
// the canonical sui-token-template by scripts/sync-template-src.sh. This test
|
|
9
|
-
// fails if the two drift — i.e. the template changed but the sync script was not
|
|
10
|
-
// re-run — so the downloadable package can never silently diverge from the CLI
|
|
11
|
-
// generator (and, via the shared source, the on-chain bytecode).
|
|
12
|
-
|
|
13
|
-
const files = templateFiles as Record<string, string>
|
|
14
|
-
|
|
15
|
-
// The canonical template ships as the published @meddleware/sui-token-template
|
|
16
|
-
// package (resolved from node_modules); SUI_TOKEN_TEMPLATE_DIR overrides with a
|
|
17
|
-
// local checkout. When neither resolves (e.g. before the package is published),
|
|
18
|
-
// the per-file parity assertions skip — the app still ships the vendored
|
|
19
|
-
// files.json + .mv, so this is a maintainer-time check only.
|
|
20
|
-
function resolveTemplateDir(): string | null {
|
|
21
|
-
if (process.env.SUI_TOKEN_TEMPLATE_DIR) return process.env.SUI_TOKEN_TEMPLATE_DIR
|
|
22
|
-
try {
|
|
23
|
-
return dirname(createRequire(import.meta.url).resolve('@meddleware/sui-token-template/package.json'))
|
|
24
|
-
} catch {
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const T = resolveTemplateDir()
|
|
29
|
-
|
|
30
|
-
// Mirrors the key -> source mapping in scripts/sync-template-src.sh.
|
|
31
|
-
const SOURCE_OF: Record<string, string> = {
|
|
32
|
-
'Move.toml': 'Move.toml',
|
|
33
|
-
'source.move': 'sources/sui_token_template.move',
|
|
34
|
-
'scripts/publish.sh': 'templates/publish.sh',
|
|
35
|
-
'.gitignore': 'templates/gitignore',
|
|
36
|
-
'README.md': 'templates/README.md',
|
|
37
|
-
'deployments.md': 'templates/deployments.md',
|
|
38
|
-
'CLAUDE.md': 'templates/CLAUDE.md',
|
|
39
|
-
'AGENTS.md': 'templates/AGENTS.md',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('template-src parity with the canonical sui-token-template', () => {
|
|
43
|
-
const templatePresent = T !== null && existsSync(`${T}/Move.toml`)
|
|
44
|
-
|
|
45
|
-
it('bundles exactly the expected file keys', () => {
|
|
46
|
-
expect(Object.keys(files).sort()).toEqual(Object.keys(SOURCE_OF).sort())
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
for (const [key, rel] of Object.entries(SOURCE_OF)) {
|
|
50
|
-
it.skipIf(!templatePresent)(`files.json["${key}"] matches ${rel} verbatim`, () => {
|
|
51
|
-
const canonical = readFileSync(`${T as string}/${rel}`, 'utf8')
|
|
52
|
-
expect(files[key]).toBe(canonical)
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
})
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
-
import { useAccessGate } from '../src/composables/useAccessGate.js'
|
|
3
|
-
import type { AccessGateConfig, OwnedObjectsClient } from '../src/lib/accessGate.js'
|
|
4
|
-
|
|
5
|
-
afterEach(() => vi.restoreAllMocks())
|
|
6
|
-
|
|
7
|
-
const GATE = '0x00000000000000000000000000000000000000000000000000000000000000a1'
|
|
8
|
-
const NFT_TYPE = '0xpkg::access_gate::AccessNFT'
|
|
9
|
-
|
|
10
|
-
const gate: AccessGateConfig = {
|
|
11
|
-
packageId: '0xpkg',
|
|
12
|
-
gateId: GATE,
|
|
13
|
-
nftType: NFT_TYPE,
|
|
14
|
-
soulbound: true,
|
|
15
|
-
priceMist: 1000n,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function ownedEntry(uses?: number) {
|
|
19
|
-
return {
|
|
20
|
-
objectId: '0xnft',
|
|
21
|
-
json: {
|
|
22
|
-
data: {
|
|
23
|
-
gate_id: GATE,
|
|
24
|
-
variant:
|
|
25
|
-
uses === undefined
|
|
26
|
-
? { variant: 'UnlimitedPass', fields: {} }
|
|
27
|
-
: { variant: 'SingleUse', fields: { uses_remaining: String(uses) } },
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function clientReturning(objects: unknown[]): OwnedObjectsClient {
|
|
34
|
-
return { core: { listOwnedObjects: vi.fn(async () => ({ objects })) } }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('useAccessGate', () => {
|
|
38
|
-
it('treats the relay as open when no gate is configured', async () => {
|
|
39
|
-
const g = useAccessGate('testnet', { getClient: () => clientReturning([]), gate: null })
|
|
40
|
-
expect(g.gateConfigured).toBe(false)
|
|
41
|
-
expect(g.hasAccess.value).toBe(true)
|
|
42
|
-
await g.checkOwnership('0xowner')
|
|
43
|
-
expect(g.accessGranted.value).toBe(true)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('grants access when the wallet holds the gate NFT', async () => {
|
|
47
|
-
const g = useAccessGate('testnet', { getClient: () => clientReturning([ownedEntry(3)]), gate })
|
|
48
|
-
expect(g.hasAccess.value).toBe(null) // unknown until checked
|
|
49
|
-
await g.checkOwnership('0xowner')
|
|
50
|
-
expect(g.hasAccess.value).toBe(true)
|
|
51
|
-
expect(g.usesRemaining.value).toBe(3)
|
|
52
|
-
expect(g.accessGranted.value).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('denies access when the wallet holds no matching NFT', async () => {
|
|
56
|
-
const g = useAccessGate('testnet', { getClient: () => clientReturning([]), gate })
|
|
57
|
-
await g.checkOwnership('0xowner')
|
|
58
|
-
expect(g.hasAccess.value).toBe(false)
|
|
59
|
-
expect(g.accessGranted.value).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('surfaces an error and denies (does not throw) when the query fails', async () => {
|
|
63
|
-
const g = useAccessGate('testnet', {
|
|
64
|
-
getClient: () => ({
|
|
65
|
-
core: {
|
|
66
|
-
listOwnedObjects: vi.fn(async () => {
|
|
67
|
-
throw new Error('rpc down')
|
|
68
|
-
}),
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
gate,
|
|
72
|
-
})
|
|
73
|
-
await g.checkOwnership('0xowner')
|
|
74
|
-
expect(g.hasAccess.value).toBe(false)
|
|
75
|
-
expect(g.error.value).toMatch(/rpc down/)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('builds a relay access token by signing the challenge', async () => {
|
|
79
|
-
vi.stubGlobal(
|
|
80
|
-
'fetch',
|
|
81
|
-
vi.fn(async () => new Response(JSON.stringify({ nonce: 'the-nonce', expiresAt: 1 }), { status: 200 })),
|
|
82
|
-
)
|
|
83
|
-
const g = useAccessGate('testnet', { getClient: () => clientReturning([]), gate })
|
|
84
|
-
const sign = vi.fn(async (message: Uint8Array) => {
|
|
85
|
-
expect(new TextDecoder().decode(message)).toBe('nft-gate:access:the-nonce')
|
|
86
|
-
return { signature: 'SIG' }
|
|
87
|
-
})
|
|
88
|
-
const token = await g.buildRelayAccessToken({
|
|
89
|
-
relayHost: 'https://relay.example',
|
|
90
|
-
address: '0xowner',
|
|
91
|
-
sign,
|
|
92
|
-
consumeDigest: 'dig',
|
|
93
|
-
})
|
|
94
|
-
const decoded = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'))
|
|
95
|
-
expect(decoded).toEqual({ address: '0xowner', nonce: 'the-nonce', signature: 'SIG', consumeDigest: 'dig' })
|
|
96
|
-
})
|
|
97
|
-
})
|
package/tests/useWallet.test.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { readonly, isReadonly } from 'vue'
|
|
3
|
-
|
|
4
|
-
// Reproduces a real Sui wallet extension: `name`/`icon` are getters backed by an
|
|
5
|
-
// ES private field, so they throw ("attempted to get private field on
|
|
6
|
-
// non-instance") if invoked with a Vue reactive/readonly Proxy as `this`. Plain
|
|
7
|
-
// object literals (like our e2e mock) do NOT have this property, which is why the
|
|
8
|
-
// bug only surfaced with actual installed wallets.
|
|
9
|
-
class PrivateFieldWallet {
|
|
10
|
-
#name = 'Slush'
|
|
11
|
-
#icon = 'data:image/svg+xml;base64,AAAA'
|
|
12
|
-
get name() {
|
|
13
|
-
return this.#name
|
|
14
|
-
}
|
|
15
|
-
get icon() {
|
|
16
|
-
return this.#icon
|
|
17
|
-
}
|
|
18
|
-
version = '1.0.0'
|
|
19
|
-
chains = ['sui:testnet']
|
|
20
|
-
accounts = [
|
|
21
|
-
{
|
|
22
|
-
address: '0x' + '1'.repeat(64),
|
|
23
|
-
publicKey: new Uint8Array(),
|
|
24
|
-
chains: ['sui:testnet'],
|
|
25
|
-
features: ['sui:signTransaction'],
|
|
26
|
-
label: 'Account 1',
|
|
27
|
-
},
|
|
28
|
-
]
|
|
29
|
-
features = {
|
|
30
|
-
'standard:connect': { version: '1.0.0', connect: async () => ({ accounts: this.accounts }) },
|
|
31
|
-
'standard:events': { version: '1.0.0', on: () => () => {} },
|
|
32
|
-
'sui:signTransaction': { version: '2.0.0', signTransaction: async () => ({ bytes: '', signature: '' }) },
|
|
33
|
-
'sui:signPersonalMessage': {
|
|
34
|
-
version: '1.0.0',
|
|
35
|
-
signPersonalMessage: async () => ({ bytes: '', signature: 'PMSIG' }),
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const wallet = new PrivateFieldWallet()
|
|
41
|
-
|
|
42
|
-
vi.mock('@mysten/wallet-standard', () => ({
|
|
43
|
-
getWallets: () => ({ get: () => [wallet], on: () => {} }),
|
|
44
|
-
isWalletWithRequiredFeatureSet: () => true,
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
describe('useWallet with real extension wallets (private-field getters)', () => {
|
|
48
|
-
it('sanity: a private-field getter DOES throw through a Vue readonly Proxy', () => {
|
|
49
|
-
// This is the exact failure the composable must avoid; if this ever stops
|
|
50
|
-
// throwing the guard below is testing nothing.
|
|
51
|
-
const proxied = readonly(new PrivateFieldWallet()) as PrivateFieldWallet
|
|
52
|
-
expect(() => proxied.name).toThrow('Cannot read private member')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('exposes discovered wallets un-proxied so name/icon read safely', async () => {
|
|
56
|
-
const { useWallet } = await import('@meddleware/wallet-adapter')
|
|
57
|
-
const { wallets } = useWallet()
|
|
58
|
-
const list = wallets.value
|
|
59
|
-
expect(list.length).toBe(1)
|
|
60
|
-
// markRaw means Vue never wraps the wallet, so the getters use the real `this`
|
|
61
|
-
expect(isReadonly(list[0])).toBe(false)
|
|
62
|
-
expect(() => list[0].name).not.toThrow()
|
|
63
|
-
expect(list[0].name).toBe('Slush')
|
|
64
|
-
expect(list[0].icon).toContain('data:image')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('signs a personal message via the wallet feature (for the NFT-gate challenge)', async () => {
|
|
68
|
-
const { useWallet } = await import('@meddleware/wallet-adapter')
|
|
69
|
-
const { connect, signPersonalMessage, wallets } = useWallet()
|
|
70
|
-
await connect(wallets.value[0])
|
|
71
|
-
const res = await signPersonalMessage(new TextEncoder().encode('nft-gate:access:abc'))
|
|
72
|
-
expect(res.signature).toBe('PMSIG')
|
|
73
|
-
})
|
|
74
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { useWalrusRelay } from '../src/composables/useWalrusRelay.js'
|
|
4
|
-
|
|
5
|
-
// These tests cover the NFT-gate logic layered onto relay selection. The operator-relay
|
|
6
|
-
// visibility also depends on operator-relay env config (not set in unit tests), so we assert
|
|
7
|
-
// on `accessSatisfied`, which is a pure function of gate config + ownership.
|
|
8
|
-
|
|
9
|
-
describe('useWalrusRelay gating', () => {
|
|
10
|
-
it('is unrestricted when no gate is configured (unchanged behaviour)', () => {
|
|
11
|
-
const relay = useWalrusRelay('testnet')
|
|
12
|
-
expect(relay.accessSatisfied.value).toBe(true)
|
|
13
|
-
// The public relay is always present as a fallback.
|
|
14
|
-
expect(relay.availableRelays.value.some((r) => r.isPublic)).toBe(true)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('reflects ownership when a gate is configured', () => {
|
|
18
|
-
const hasAccess = ref<boolean | null>(null)
|
|
19
|
-
const relay = useWalrusRelay('testnet', { gateConfigured: true, hasAccess })
|
|
20
|
-
expect(relay.accessSatisfied.value).toBe(false) // unknown ⇒ not satisfied
|
|
21
|
-
|
|
22
|
-
hasAccess.value = true
|
|
23
|
-
expect(relay.accessSatisfied.value).toBe(true)
|
|
24
|
-
|
|
25
|
-
hasAccess.value = false
|
|
26
|
-
expect(relay.accessSatisfied.value).toBe(false)
|
|
27
|
-
// Public relay still available regardless of gate access.
|
|
28
|
-
expect(relay.availableRelays.value.some((r) => r.isPublic)).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
})
|