@meddleware/token-deployer-ui 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +8 -5
  2. package/package.json +22 -8
  3. package/src/App.vue +1 -1
  4. package/src/component-styles.css +39 -57
  5. package/src/components/GithubPush.vue +2 -2
  6. package/src/components/IconPicker.vue +44 -75
  7. package/src/components/LicensePicker.vue +2 -2
  8. package/src/components/ResultPanel.vue +2 -2
  9. package/src/components/ReviewPanel.vue +11 -3
  10. package/src/components/TokenDeployerView.vue +52 -48
  11. package/src/components/WalletBar.vue +12 -1
  12. package/src/components/WalrusBlobBrowser.vue +4 -4
  13. package/src/composables/useWalrusRelay.ts +1 -2
  14. package/src/config.ts +19 -17
  15. package/src/lib/buildPublishTx.ts +0 -10
  16. package/src/lib/deploy.ts +9 -3
  17. package/src/lib/deployExecutor.ts +18 -45
  18. package/src/lib/template.ts +3 -0
  19. package/src/lib/walrus-constants.ts +2 -2
  20. package/src/main.ts +4 -6
  21. package/src/styles.css +22 -0
  22. package/.claude/move-language-reference/SKILL.md +0 -445
  23. package/.claude/skills/move-language-reference/SKILL.md +0 -445
  24. package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
  25. package/.claude/sui-move-bootcamp/SKILL.md +0 -128
  26. package/.editorconfig +0 -8
  27. package/.env.example +0 -72
  28. package/.gitattributes +0 -1
  29. package/.github/workflows/docker-publish.yml +0 -348
  30. package/.github/workflows/e2e-realchain.yml +0 -64
  31. package/.github/workflows/node-ci.yml +0 -43
  32. package/.github/workflows/npm-publish.yml +0 -83
  33. package/.htmlvalidate.mjs +0 -30
  34. package/.oxfmtrc.json +0 -5
  35. package/.oxlintrc.json +0 -10
  36. package/.vscode/extensions.json +0 -9
  37. package/AGENTS.md +0 -39
  38. package/CLAUDE.md +0 -166
  39. package/Dockerfile +0 -96
  40. package/eslint.config.ts +0 -63
  41. package/index.html +0 -22
  42. package/playwright.config.ts +0 -30
  43. package/postcss.config.js +0 -8
  44. package/public/CNAME +0 -1
  45. package/public/_headers +0 -12
  46. package/public/_redirects +0 -1
  47. package/public/favicon.ico +0 -0
  48. package/public/favicon.svg +0 -5
  49. package/public/og-image.png +0 -0
  50. package/public/robots.txt +0 -3
  51. package/public/sitemap.xml +0 -8
  52. package/scripts/e2e-browser.mjs +0 -15
  53. package/scripts/e2e-deploy.mjs +0 -227
  54. package/scripts/e2e-localnet.mjs +0 -67
  55. package/scripts/e2e-walrus-browser.mjs +0 -112
  56. package/scripts/gen-og-image.mjs +0 -31
  57. package/scripts/publish-localnet.mjs +0 -56
  58. package/scripts/regen-template.sh +0 -45
  59. package/scripts/sync-template-src.sh +0 -36
  60. package/scripts/verify-bytecode.mjs +0 -49
  61. package/src/__tests__/App.spec.js +0 -11
  62. package/src/composables/useAccessGate.ts +0 -127
  63. package/src/lib/accessGate.ts +0 -155
  64. package/src/lib/walrus.ts +0 -155
  65. package/stylelint.config.js +0 -33
  66. package/test-results/.last-run.json +0 -4
  67. package/tests/accessGate.test.ts +0 -113
  68. package/tests/buildPublishTx.test.ts +0 -243
  69. package/tests/deploy.test.ts +0 -149
  70. package/tests/errors.test.ts +0 -32
  71. package/tests/generatePackage.test.ts +0 -193
  72. package/tests/github.test.ts +0 -61
  73. package/tests/licenses.test.ts +0 -81
  74. package/tests/listMyTokens.test.ts +0 -30
  75. package/tests/template.test.ts +0 -93
  76. package/tests/templateArtifact.test.ts +0 -61
  77. package/tests/templateParity.test.ts +0 -55
  78. package/tests/useAccessGate.test.ts +0 -97
  79. package/tests/useWallet.test.ts +0 -74
  80. package/tests/useWalrusRelayGating.test.ts +0 -30
  81. package/tests/validation.test.ts +0 -117
  82. package/tests/walrusUpload.test.ts +0 -93
  83. package/tests/walrus_url_patch.test.ts +0 -26
  84. package/tsconfig.json +0 -19
  85. package/typedoc.json +0 -34
  86. package/vite.config.ts +0 -73
  87. package/vitest.config.ts +0 -10
@@ -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
- })
@@ -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
- })
@@ -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
- })
@@ -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
- })