@meddleware/token-deployer-ui 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +11 -118
  2. package/README.md +11 -10
  3. package/package.json +22 -10
  4. package/src/App.vue +1 -1
  5. package/src/component-styles.css +39 -57
  6. package/src/components/GithubPush.vue +2 -2
  7. package/src/components/IconPicker.vue +44 -75
  8. package/src/components/LicensePicker.vue +4 -3
  9. package/src/components/ResultPanel.vue +2 -2
  10. package/src/components/ReviewPanel.vue +11 -3
  11. package/src/components/TokenDeployerView.vue +52 -48
  12. package/src/components/WalletBar.vue +12 -1
  13. package/src/components/WalrusBlobBrowser.vue +4 -4
  14. package/src/composables/useWalrusRelay.ts +1 -2
  15. package/src/config.ts +29 -30
  16. package/src/lib/buildPublishTx.ts +0 -10
  17. package/src/lib/deploy.ts +45 -3
  18. package/src/lib/deployExecutor.ts +19 -44
  19. package/src/lib/generatePackage.ts +12 -12
  20. package/src/lib/licenses.ts +4 -3
  21. package/src/lib/listMyTokens.ts +7 -11
  22. package/src/lib/readClient.ts +15 -7
  23. package/src/lib/template.ts +3 -0
  24. package/src/lib/types.ts +1 -1
  25. package/src/lib/walrus-constants.ts +2 -2
  26. package/src/main.ts +42 -61
  27. package/src/styles.css +22 -0
  28. package/src/template-src/files.json +7 -7
  29. package/.claude/move-language-reference/SKILL.md +0 -445
  30. package/.claude/skills/move-language-reference/SKILL.md +0 -445
  31. package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
  32. package/.claude/sui-move-bootcamp/SKILL.md +0 -128
  33. package/.editorconfig +0 -8
  34. package/.env.example +0 -75
  35. package/.gitattributes +0 -1
  36. package/.github/workflows/docker-publish.yml +0 -348
  37. package/.github/workflows/e2e-realchain.yml +0 -64
  38. package/.github/workflows/node-ci.yml +0 -43
  39. package/.github/workflows/npm-publish.yml +0 -83
  40. package/.htmlvalidate.mjs +0 -30
  41. package/.oxfmtrc.json +0 -5
  42. package/.oxlintrc.json +0 -10
  43. package/.vscode/extensions.json +0 -9
  44. package/AGENTS.md +0 -39
  45. package/CLAUDE.md +0 -159
  46. package/Dockerfile +0 -96
  47. package/eslint.config.ts +0 -63
  48. package/index.html +0 -13
  49. package/playwright.config.ts +0 -30
  50. package/postcss.config.js +0 -8
  51. package/public/CNAME +0 -1
  52. package/public/_headers +0 -12
  53. package/public/_redirects +0 -1
  54. package/public/favicon.ico +0 -0
  55. package/public/favicon.svg +0 -5
  56. package/public/og-image.png +0 -0
  57. package/public/robots.txt +0 -3
  58. package/public/sitemap.xml +0 -8
  59. package/scripts/e2e-browser.mjs +0 -15
  60. package/scripts/e2e-deploy.mjs +0 -222
  61. package/scripts/e2e-localnet.mjs +0 -65
  62. package/scripts/e2e-walrus-browser.mjs +0 -115
  63. package/scripts/gen-og-image.mjs +0 -31
  64. package/scripts/publish-localnet.mjs +0 -55
  65. package/scripts/regen-template.sh +0 -45
  66. package/scripts/sync-template-src.sh +0 -36
  67. package/scripts/verify-bytecode.mjs +0 -49
  68. package/src/__tests__/App.spec.js +0 -11
  69. package/src/composables/useAccessGate.ts +0 -127
  70. package/src/lib/accessGate.ts +0 -141
  71. package/src/lib/walrus.ts +0 -144
  72. package/stylelint.config.js +0 -33
  73. package/test-results/.last-run.json +0 -4
  74. package/tests/accessGate.test.ts +0 -108
  75. package/tests/buildPublishTx.test.ts +0 -243
  76. package/tests/deploy.test.ts +0 -81
  77. package/tests/errors.test.ts +0 -32
  78. package/tests/generatePackage.test.ts +0 -182
  79. package/tests/github.test.ts +0 -61
  80. package/tests/licenses.test.ts +0 -78
  81. package/tests/template.test.ts +0 -93
  82. package/tests/templateArtifact.test.ts +0 -61
  83. package/tests/templateParity.test.ts +0 -55
  84. package/tests/useAccessGate.test.ts +0 -101
  85. package/tests/useWallet.test.ts +0 -74
  86. package/tests/useWalrusRelayGating.test.ts +0 -30
  87. package/tests/validation.test.ts +0 -117
  88. package/tests/walrusUpload.test.ts +0 -93
  89. package/tests/walrus_url_patch.test.ts +0 -26
  90. package/tsconfig.json +0 -19
  91. package/typedoc.json +0 -34
  92. package/vite.config.ts +0 -65
  93. package/vitest.config.ts +0 -10
@@ -1,127 +0,0 @@
1
- import { computed, ref } from 'vue'
2
- import { ACCESS_GATE } from '../config.js'
3
- import type { WalrusNetwork } from '../config.js'
4
- import {
5
- fetchAccessNfts,
6
- buildPurchaseTx,
7
- buildConsumeTx,
8
- fetchRelayChallenge,
9
- personalMessageForNonce,
10
- buildAccessProofToken,
11
- } from '../lib/accessGate.js'
12
- import type { AccessGateConfig, OwnedObjectsClient } from '../lib/accessGate.js'
13
- import type { Executor } from '../lib/deploy.js'
14
- import type { Transaction } from '@mysten/sui/transactions'
15
-
16
- /** A wallet personal-message signer, as exposed by `useWallet().signPersonalMessage`. */
17
- export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
18
-
19
- /**
20
- * Reactive NFT-gate state for the operator relay.
21
- *
22
- * When no gate is configured for the network (`ACCESS_GATE[network] == null`), the relay is
23
- * treated as OPEN (`hasAccess === true`) and this composable is inert — behaviour is
24
- * unchanged from before gating existed. When a gate IS configured, `checkOwnership` decides
25
- * access with a single `getOwnedObjects` call (the same cheap pattern as the relay health
26
- * check), and `purchase`/`buildRelayAccessToken` drive the buy + prove flows.
27
- */
28
- export function useAccessGate(
29
- network: WalrusNetwork,
30
- deps: {
31
- getClient: (network: WalrusNetwork) => OwnedObjectsClient
32
- /** Override the configured gate (mainly for testing); defaults to `ACCESS_GATE[network]`. */
33
- gate?: AccessGateConfig | null
34
- },
35
- ) {
36
- const gate: AccessGateConfig | null = deps.gate !== undefined ? deps.gate : ACCESS_GATE[network]
37
- const gateConfigured = gate !== null
38
-
39
- // No gate → open. Gate → unknown until checked.
40
- const hasAccess = ref<boolean | null>(gateConfigured ? null : true)
41
- const usesRemaining = ref<number | null>(null)
42
- /** Object id of the held access NFT (for the single-use consume step); null if none. */
43
- const nftId = ref<string | null>(null)
44
- const checking = ref(false)
45
- const error = ref<string | null>(null)
46
-
47
- /** Query whether `address` holds the gate NFT. Cheap; safe to call on connect. */
48
- async function checkOwnership(address: string): Promise<void> {
49
- if (!gate) {
50
- hasAccess.value = true
51
- return
52
- }
53
- checking.value = true
54
- error.value = null
55
- try {
56
- const nfts = await fetchAccessNfts(
57
- deps.getClient(network),
58
- address,
59
- gate.nftType,
60
- gate.gateId,
61
- )
62
- hasAccess.value = nfts.length > 0
63
- usesRemaining.value = nfts.length ? nfts[0].usesRemaining : null
64
- nftId.value = nfts.length ? nfts[0].objectId : null
65
- } catch (e) {
66
- // A failed check must NOT hard-block the user: leave access unknown/false but keep the
67
- // purchase path available (the gateway re-verifies server-side regardless).
68
- hasAccess.value = false
69
- nftId.value = null
70
- error.value = e instanceof Error ? e.message : String(e)
71
- } finally {
72
- checking.value = false
73
- }
74
- }
75
-
76
- /** Purchase access via the connected wallet, then re-check ownership. */
77
- async function purchase(executor: Executor, address: string): Promise<void> {
78
- if (!gate) throw new Error('No access gate configured for this network.')
79
- const tx = buildPurchaseTx(gate)
80
- const res = await executor.signAndExecute(tx)
81
- if (res.digest) await executor.waitForTransaction(res.digest).catch(() => {})
82
- await checkOwnership(address)
83
- }
84
-
85
- /** Build the consume PTB for a single-use NFT (woven into the upload flow before proving). */
86
- function buildConsume(nftId: string, nonce: string): Transaction {
87
- if (!gate) throw new Error('No access gate configured for this network.')
88
- return buildConsumeTx(gate, nftId, nonce)
89
- }
90
-
91
- /**
92
- * Fetch a challenge from the gateway (served at the operator relay host), sign it, and
93
- * return the base64 access-proof token to pass to the Walrus client as the relay auth
94
- * token. For single-use gates supply the on-chain consume `consumeDigest`.
95
- */
96
- async function buildRelayAccessToken(opts: {
97
- relayHost: string
98
- address: string
99
- sign: PersonalMessageSigner
100
- consumeDigest?: string
101
- }): Promise<string> {
102
- const challenge = await fetchRelayChallenge(opts.relayHost)
103
- const { signature } = await opts.sign(personalMessageForNonce(challenge.nonce))
104
- return buildAccessProofToken({
105
- address: opts.address,
106
- nonce: challenge.nonce,
107
- signature,
108
- consumeDigest: opts.consumeDigest,
109
- })
110
- }
111
-
112
- return {
113
- gate,
114
- gateConfigured,
115
- hasAccess,
116
- usesRemaining,
117
- nftId,
118
- checking,
119
- error,
120
- /** Convenience: gate configured AND access confirmed. */
121
- accessGranted: computed(() => !gateConfigured || hasAccess.value === true),
122
- checkOwnership,
123
- purchase,
124
- buildConsume,
125
- buildRelayAccessToken,
126
- }
127
- }
@@ -1,141 +0,0 @@
1
- // Client-side helpers for the `access_gate` NFT primitive that gates the operator's
2
- // upload relay: ownership queries, purchase/consume PTB builders, and the wallet-signed
3
- // access-proof token.
4
- //
5
- // Self-contained (this app is standalone). The wire format mirrors
6
- // `@meddleware/nft-gate-client` and `@meddleware/sui-walrus`'s access helpers; the
7
- // `nft-gate` gateway is the authority that verifies these proofs.
8
- //
9
- // TODO(unify): consume `@meddleware/nft-gate-client` once published and drop the duplication.
10
-
11
- import { Transaction } from '@mysten/sui/transactions'
12
-
13
- /* eslint-disable @typescript-eslint/no-explicit-any */
14
-
15
- /** Deployed gate + the NFT type that satisfies it. */
16
- export interface AccessGateConfig {
17
- packageId: string
18
- gateId: string
19
- /** `<pkg>::access_gate::AccessNFT` or `…::SoulboundAccessNFT`. */
20
- nftType: string
21
- soulbound: boolean
22
- /** Purchase price in MIST. */
23
- priceMist: bigint
24
- }
25
-
26
- export interface OwnedAccessNft {
27
- objectId: string
28
- gateId: string
29
- /** `null` = unlimited pass; otherwise remaining single-use count. */
30
- usesRemaining: number | null
31
- }
32
-
33
- /** Minimal structural subset of the JSON-RPC client used here. */
34
- export interface OwnedObjectsClient {
35
- getOwnedObjects(params: {
36
- owner: string
37
- filter?: { StructType: string }
38
- options?: { showContent?: boolean; showType?: boolean }
39
- }): Promise<{ data: unknown[] }>
40
- }
41
-
42
- function parseUsesRemaining(variant: any): number | null {
43
- if (variant == null) return null
44
- const fields = variant.fields ?? variant
45
- const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
46
- return ur != null ? Number(ur) : null
47
- }
48
-
49
- /** Parse a `getOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
50
- export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
51
- const obj = entry?.data ?? entry
52
- const objectId: string | undefined = obj?.objectId
53
- const inner = obj?.content?.fields?.data?.fields
54
- const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
55
- if (!objectId || !gateId) return null
56
- return { objectId, gateId, usesRemaining: parseUsesRemaining(inner?.variant) }
57
- }
58
-
59
- /** All access NFTs of `nftType` owned by `owner`, optionally restricted to `gateId`. */
60
- export async function fetchAccessNfts(
61
- client: OwnedObjectsClient,
62
- owner: string,
63
- nftType: string,
64
- gateId?: string,
65
- ): Promise<OwnedAccessNft[]> {
66
- const { data } = await client.getOwnedObjects({
67
- owner,
68
- filter: { StructType: nftType },
69
- options: { showContent: true, showType: true },
70
- })
71
- const parsed = (data ?? [])
72
- .map(parseOwnedAccessNft)
73
- .filter((n): n is OwnedAccessNft => n !== null)
74
- return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
75
- }
76
-
77
- /** Build a PTB that buys access (splits the exact price from gas → `purchase`). */
78
- export function buildPurchaseTx(cfg: AccessGateConfig): Transaction {
79
- const tx = new Transaction()
80
- const [payment] = tx.splitCoins(tx.gas, [tx.pure.u64(cfg.priceMist)])
81
- tx.moveCall({
82
- target: `${cfg.packageId}::access_gate::purchase`,
83
- arguments: [tx.object(cfg.gateId), payment],
84
- })
85
- return tx
86
- }
87
-
88
- /** Build a PTB that consumes one single-use, binding it to `nonce`. */
89
- export function buildConsumeTx(cfg: AccessGateConfig, nftId: string, nonce: string): Transaction {
90
- const tx = new Transaction()
91
- const fn = cfg.soulbound ? 'consume_soulbound' : 'consume'
92
- const nonceBytes = Array.from(new TextEncoder().encode(nonce))
93
- tx.moveCall({
94
- target: `${cfg.packageId}::access_gate::${fn}`,
95
- arguments: [tx.object(nftId), tx.object(cfg.gateId), tx.pure.vector('u8', nonceBytes)],
96
- })
97
- return tx
98
- }
99
-
100
- // ── Challenge / proof (mirror of the gateway's wire format) ───────────────────────
101
-
102
- export interface RelayChallenge {
103
- nonce: string
104
- expiresAt: number
105
- }
106
-
107
- export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
108
-
109
- export function personalMessageForNonce(nonce: string): Uint8Array {
110
- return new TextEncoder().encode(`nft-gate:access:${nonce}`)
111
- }
112
-
113
- function toBase64(s: string): string {
114
- return typeof btoa === 'function' ? btoa(s) : Buffer.from(s, 'utf-8').toString('base64')
115
- }
116
-
117
- export function buildAccessProofToken(proof: {
118
- address: string
119
- nonce: string
120
- signature: string
121
- consumeDigest?: string
122
- }): string {
123
- const payload: Record<string, string> = {
124
- address: proof.address,
125
- nonce: proof.nonce,
126
- signature: proof.signature,
127
- }
128
- if (proof.consumeDigest) payload.consumeDigest = proof.consumeDigest
129
- return toBase64(JSON.stringify(payload))
130
- }
131
-
132
- export async function fetchRelayChallenge(
133
- relayHost: string,
134
- opts: { signal?: AbortSignal } = {},
135
- ): Promise<RelayChallenge> {
136
- const res = await fetch(`${relayHost.replace(/\/$/, '')}/v1/challenge`, { signal: opts.signal })
137
- if (!res.ok) throw new Error(`challenge request failed: ${res.status}`)
138
- const data = (await res.json()) as { nonce?: string; expiresAt?: number; expires_at?: number }
139
- if (!data || typeof data.nonce !== 'string') throw new Error('challenge response missing nonce')
140
- return { nonce: data.nonce, expiresAt: data.expiresAt ?? data.expires_at ?? 0 }
141
- }
package/src/lib/walrus.ts DELETED
@@ -1,144 +0,0 @@
1
- // Minimal Walrus client for icon uploads. Mirrors the canonical helpers in the
2
- // `@meddleware/sui-walrus` package (createWalrusClient / createBlobUploadFlow /
3
- // walrusBlobUrl / fetchOwnedWalrusBlobs) but is kept self-contained here so it
4
- // resolves cleanly in the app's Vite build and preserves the lazy-load boundary
5
- // around `@mysten/walrus`. Once `@meddleware/sui-walrus` is published this file
6
- // should be replaced by imports from it (injecting the app's relay host + max tip).
7
- //
8
- // Icons are stored as RAW blobs (not quilts) so `GET /v1/blobs/<blobId>` returns
9
- // the exact image bytes — directly renderable by wallets/explorers. An upload
10
- // relay is REQUIRED (direct-to-storage-node writes fail from browsers). Verified
11
- // end-to-end on testnet (scripts/walrus-upload verification).
12
-
13
- import { SuiGrpcClient } from '@mysten/sui/grpc'
14
- import { walrus, blobIdFromInt } from '@mysten/walrus'
15
- import type { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
16
- import { WALRUS_RELAY_HOSTS, WALRUS_MAX_TIP_MIST, WALRUS_RPC_URLS } from '../config.js'
17
-
18
- export { ICON_EPOCHS } from './walrus-constants.js'
19
-
20
- export type WalrusNetwork = 'testnet' | 'mainnet'
21
-
22
- const AGGREGATOR_HOSTS: Record<WalrusNetwork, string> = {
23
- testnet: 'https://aggregator.walrus-testnet.walrus.space',
24
- mainnet: 'https://aggregator.walrus-mainnet.walrus.space',
25
- }
26
-
27
- /** Public URL that serves the raw blob bytes. */
28
- export function walrusBlobUrl(network: WalrusNetwork, blobId: string): string {
29
- return `${AGGREGATOR_HOSTS[network]}/v1/blobs/${blobId}`
30
- }
31
-
32
- /** Options for {@link createWalrusClient}. */
33
- export interface CreateWalrusClientOptions {
34
- /** Browser WASM URL (Vite `?url` import). */
35
- wasmUrl?: string
36
- /**
37
- * Upload relay host. Defaults to the configured operator/public relay for the
38
- * network ({@link WALRUS_RELAY_HOSTS}); pass a value to honour a user-supplied URL.
39
- */
40
- uploadRelayHost?: string
41
- /**
42
- * Maximum tip (MIST) the client will pay. Defaults to {@link WALRUS_MAX_TIP_MIST}.
43
- * The SDK throws if the relay's tip exceeds this, so keep it above the relay tip.
44
- */
45
- uploadRelayMaxTipMist?: bigint
46
- /**
47
- * Access-proof token for an NFT-gated operator relay (base64 JSON from the access gate).
48
- * Sent as the relay `Authorization: Bearer` header; the `nft-gate` gateway verifies it
49
- * before proxying to the relay. Omit for the public relay or an ungated operator relay.
50
- */
51
- uploadRelayAuthToken?: string
52
- /**
53
- * gRPC fullnode URL for the underlying `SuiGrpcClient`. Defaults to
54
- * {@link WALRUS_RPC_URLS} for the network (operator-overridable via `VITE_WALRUS_RPC_*`).
55
- * NOTE: must be a **gRPC** endpoint — not a JSON-RPC URL (see `WALRUS_RPC_URLS`).
56
- */
57
- rpcUrl?: string
58
- }
59
-
60
- /**
61
- * A Walrus-extended gRPC client configured with an upload relay + tip guard.
62
- *
63
- * The relay's tip address/amount is fetched by the SDK from the relay's
64
- * `/v1/tip-config`; only the `max` (client-side ceiling) is set here. The
65
- * default relay is the operator relay when `VITE_WALRUS_RELAY_*` is set,
66
- * otherwise the public Mysten relay (which earns the operator nothing).
67
- */
68
- export function createWalrusClient(
69
- network: WalrusNetwork,
70
- wasmUrlOrOptions?: string | CreateWalrusClientOptions,
71
- ) {
72
- // Back-compat: a bare string is treated as the WASM URL.
73
- const opts: CreateWalrusClientOptions =
74
- typeof wasmUrlOrOptions === 'string' ? { wasmUrl: wasmUrlOrOptions } : (wasmUrlOrOptions ?? {})
75
- const host = opts.uploadRelayHost ?? WALRUS_RELAY_HOSTS[network]
76
- const maxTip = opts.uploadRelayMaxTipMist ?? WALRUS_MAX_TIP_MIST
77
- const baseUrl = opts.rpcUrl ?? WALRUS_RPC_URLS[network]
78
- return new SuiGrpcClient({ network, baseUrl }).$extend(
79
- walrus({
80
- ...(opts.wasmUrl ? { wasmUrl: opts.wasmUrl } : {}),
81
- uploadRelay: {
82
- host,
83
- sendTip: { max: Number(maxTip) },
84
- ...(opts.uploadRelayAuthToken
85
- ? { headers: { Authorization: `Bearer ${opts.uploadRelayAuthToken}` } }
86
- : {}),
87
- },
88
- }),
89
- )
90
- }
91
-
92
- /** Raw-blob write flow: encode -> register(tx) -> upload -> certify(tx) -> getBlob. */
93
- export function createBlobUploadFlow(
94
- client: ReturnType<typeof createWalrusClient>,
95
- bytes: Uint8Array,
96
- ) {
97
- return client.walrus.writeBlobFlow({ blob: bytes })
98
- }
99
-
100
- /** A Walrus blob object owned by a Sui address. */
101
- export interface OwnedBlob {
102
- objectId: string
103
- /** Aggregator-URL-compatible blob ID string. */
104
- blobId: string
105
- size: number
106
- endEpoch: number
107
- certified: boolean
108
- }
109
-
110
- /**
111
- * Return all Walrus blobs owned by `owner`. Uses `getBlobType()` for dynamic
112
- * package resolution so no hardcoded addresses are needed.
113
- */
114
- export async function fetchOwnedWalrusBlobs(
115
- suiClient: SuiJsonRpcClient,
116
- walrusClient: ReturnType<typeof createWalrusClient>,
117
- owner: string,
118
- ): Promise<OwnedBlob[]> {
119
- const blobType = await walrusClient.walrus.getBlobType()
120
- const { data } = await suiClient.getOwnedObjects({
121
- owner,
122
- filter: { StructType: blobType },
123
- options: { showContent: true },
124
- })
125
- const blobs: OwnedBlob[] = []
126
- for (const item of data) {
127
- if (!item.data) continue
128
- const fields = (item.data.content as { fields?: Record<string, unknown> } | undefined)?.fields
129
- if (!fields) continue
130
- const storage = fields.storage as { fields?: { end_epoch?: unknown } } | undefined
131
- try {
132
- blobs.push({
133
- objectId: item.data.objectId,
134
- blobId: blobIdFromInt(BigInt(fields.blob_id as string)),
135
- size: Number(fields.size),
136
- endEpoch: Number(storage?.fields?.end_epoch ?? 0),
137
- certified: fields.certified_epoch !== null && fields.certified_epoch !== undefined,
138
- })
139
- } catch {
140
- // Skip blobs whose fields cannot be parsed.
141
- }
142
- }
143
- return blobs
144
- }
@@ -1,33 +0,0 @@
1
- // Stylelint over the library's CSS and the <style> blocks of its SFCs.
2
- // postcss-html provides the custom syntax needed to lint Vue single-file components.
3
- export default {
4
- extends: ['stylelint-config-standard'],
5
- overrides: [{ files: ['**/*.vue'], customSyntax: 'postcss-html' }],
6
- rules: {
7
- // The design system leans heavily on custom properties and deliberate
8
- // ordering; these standard rules are too noisy for a token-driven codebase.
9
- 'custom-property-empty-line-before': null,
10
- 'declaration-empty-line-before': null,
11
- 'comment-empty-line-before': null,
12
- 'rule-empty-line-before': null,
13
- 'at-rule-empty-line-before': null,
14
- 'no-descending-specificity': null,
15
- // BEM-style class names (block__element--modifier) are intentional.
16
- 'selector-class-pattern': null,
17
- // Some presentational SFCs use compact single-line multi-declaration rules.
18
- 'declaration-block-single-line-max-declarations': null,
19
- // Preserve camelCase CSS keyword values (e.g. text-rendering: optimizeLegibility).
20
- 'value-keyword-case': null,
21
- // Vue SFC scoped-style pseudo-classes.
22
- 'selector-pseudo-class-no-unknown': [
23
- true,
24
- { ignorePseudoClasses: ['deep', 'slotted', 'global'] },
25
- ],
26
- // Private panel vars use a leading-underscore convention (--_bg, --_text).
27
- 'custom-property-pattern': null,
28
- // Vendor-prefixed properties are emitted by autoprefixer at build time; the
29
- // hand-written -webkit-/-moz- font-smoothing pair is intentional.
30
- 'property-no-vendor-prefix': null,
31
- 'value-no-vendor-prefix': null,
32
- },
33
- }
@@ -1,4 +0,0 @@
1
- {
2
- "status": "failed",
3
- "failedTests": []
4
- }
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest'
2
- import {
3
- parseOwnedAccessNft,
4
- fetchAccessNfts,
5
- buildPurchaseTx,
6
- buildConsumeTx,
7
- personalMessageForNonce,
8
- buildAccessProofToken,
9
- fetchRelayChallenge,
10
- } from '../src/lib/accessGate.js'
11
- import type { AccessGateConfig, OwnedObjectsClient } from '../src/lib/accessGate.js'
12
-
13
- afterEach(() => vi.restoreAllMocks())
14
-
15
- const PKG = '0x00000000000000000000000000000000000000000000000000000000000000aa'
16
- const GATE = '0x00000000000000000000000000000000000000000000000000000000000000a1'
17
- const NFT_TYPE = `${PKG}::access_gate::AccessNFT`
18
-
19
- const cfg: AccessGateConfig = {
20
- packageId: PKG,
21
- gateId: GATE,
22
- nftType: NFT_TYPE,
23
- soulbound: false,
24
- priceMist: 1000n,
25
- }
26
-
27
- function entry(objectId: string, gateId: string, uses?: number) {
28
- return {
29
- data: {
30
- objectId,
31
- content: {
32
- fields: {
33
- data: {
34
- fields: {
35
- gate_id: gateId,
36
- variant:
37
- uses === undefined
38
- ? { variant: 'UnlimitedPass', fields: {} }
39
- : { variant: 'SingleUse', fields: { uses_remaining: String(uses) } },
40
- },
41
- },
42
- },
43
- },
44
- },
45
- }
46
- }
47
-
48
- function commandsJson(tx: { getData: () => unknown }): string {
49
- return JSON.stringify(tx.getData())
50
- }
51
-
52
- describe('accessGate lib', () => {
53
- it('parses unlimited pass and single-use NFTs', () => {
54
- expect(parseOwnedAccessNft(entry('0x1', GATE))).toEqual({
55
- objectId: '0x1',
56
- gateId: GATE,
57
- usesRemaining: null,
58
- })
59
- expect(parseOwnedAccessNft(entry('0x2', GATE, 5))?.usesRemaining).toBe(5)
60
- expect(parseOwnedAccessNft({ data: { objectId: '0x9', content: { fields: {} } } })).toBeNull()
61
- })
62
-
63
- it('fetchAccessNfts filters by gate and passes the StructType filter', async () => {
64
- const client: OwnedObjectsClient = {
65
- getOwnedObjects: vi.fn(async () => ({
66
- data: [entry('0x1', GATE), entry('0x2', '0xother', 1)],
67
- })),
68
- }
69
- const nfts = await fetchAccessNfts(client, '0xowner', NFT_TYPE, GATE)
70
- expect(nfts).toHaveLength(1)
71
- expect(nfts[0].objectId).toBe('0x1')
72
- expect(client.getOwnedObjects).toHaveBeenCalledWith(
73
- expect.objectContaining({ owner: '0xowner', filter: { StructType: NFT_TYPE } }),
74
- )
75
- })
76
-
77
- it('buildPurchaseTx splits gas and calls purchase', () => {
78
- const json = commandsJson(buildPurchaseTx(cfg))
79
- expect(json).toContain('SplitCoins')
80
- expect(json).toContain('"function":"purchase"')
81
- })
82
-
83
- it('buildConsumeTx selects consume vs consume_soulbound', () => {
84
- expect(commandsJson(buildConsumeTx(cfg, '0x1', 'n'))).toContain('"function":"consume"')
85
- expect(commandsJson(buildConsumeTx({ ...cfg, soulbound: true }, '0x1', 'n'))).toContain(
86
- '"function":"consume_soulbound"',
87
- )
88
- })
89
-
90
- it('encodes the proof token and message', () => {
91
- expect(new TextDecoder().decode(personalMessageForNonce('x'))).toBe('nft-gate:access:x')
92
- const token = buildAccessProofToken({ address: '0x1', nonce: 'n', signature: 's', consumeDigest: 'd' })
93
- expect(JSON.parse(Buffer.from(token, 'base64').toString('utf-8'))).toEqual({
94
- address: '0x1',
95
- nonce: 'n',
96
- signature: 's',
97
- consumeDigest: 'd',
98
- })
99
- })
100
-
101
- it('fetchRelayChallenge parses the gateway response', async () => {
102
- vi.stubGlobal(
103
- 'fetch',
104
- vi.fn(async () => new Response(JSON.stringify({ nonce: 'abc', expiresAt: 7 }), { status: 200 })),
105
- )
106
- expect(await fetchRelayChallenge('https://relay.example')).toEqual({ nonce: 'abc', expiresAt: 7 })
107
- })
108
- })