@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.
Files changed (87) hide show
  1. package/README.md +8 -5
  2. package/package.json +19 -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,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 `listOwnedObjects` 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,155 +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 core (gRPC) client used here. */
34
- export interface OwnedObjectsClient {
35
- core: {
36
- listOwnedObjects(params: {
37
- owner: string
38
- type?: string
39
- include?: { json?: boolean }
40
- }): Promise<{ objects: unknown[] }>
41
- }
42
- }
43
-
44
- /**
45
- * Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
46
- * fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both.
47
- */
48
- function structFields(v: unknown): Record<string, any> | undefined {
49
- if (!v || typeof v !== 'object') return undefined
50
- const o = v as Record<string, any>
51
- const nested = o.fields
52
- return nested && typeof nested === 'object' ? (nested as Record<string, any>) : o
53
- }
54
-
55
- function parseUsesRemaining(variant: any): number | null {
56
- if (variant == null) return null
57
- const tag: string | undefined = variant.variant ?? variant.type ?? variant.$kind
58
- if (tag === 'UnlimitedPass') return null
59
- const fields = variant.fields ?? variant
60
- const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
61
- return ur != null ? Number(ur) : null
62
- }
63
-
64
- /** Parse a `listOwnedObjects` entry into an access NFT, or `null` if it isn't one. */
65
- export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
66
- const objectId: string | undefined = entry?.objectId
67
- const inner = structFields(structFields(entry?.json)?.data)
68
- const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
69
- if (!objectId || !gateId) return null
70
- return { objectId, gateId, usesRemaining: parseUsesRemaining(inner?.variant) }
71
- }
72
-
73
- /** All access NFTs of `nftType` owned by `owner`, optionally restricted to `gateId`. */
74
- export async function fetchAccessNfts(
75
- client: OwnedObjectsClient,
76
- owner: string,
77
- nftType: string,
78
- gateId?: string,
79
- ): Promise<OwnedAccessNft[]> {
80
- const { objects } = await client.core.listOwnedObjects({
81
- owner,
82
- type: nftType,
83
- include: { json: true },
84
- })
85
- const parsed = (objects ?? [])
86
- .map(parseOwnedAccessNft)
87
- .filter((n): n is OwnedAccessNft => n !== null)
88
- return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
89
- }
90
-
91
- /** Build a PTB that buys access (splits the exact price from gas → `purchase`). */
92
- export function buildPurchaseTx(cfg: AccessGateConfig): Transaction {
93
- const tx = new Transaction()
94
- const [payment] = tx.splitCoins(tx.gas, [tx.pure.u64(cfg.priceMist)])
95
- tx.moveCall({
96
- target: `${cfg.packageId}::access_gate::purchase`,
97
- arguments: [tx.object(cfg.gateId), payment],
98
- })
99
- return tx
100
- }
101
-
102
- /** Build a PTB that consumes one single-use, binding it to `nonce`. */
103
- export function buildConsumeTx(cfg: AccessGateConfig, nftId: string, nonce: string): Transaction {
104
- const tx = new Transaction()
105
- const fn = cfg.soulbound ? 'consume_soulbound' : 'consume'
106
- const nonceBytes = Array.from(new TextEncoder().encode(nonce))
107
- tx.moveCall({
108
- target: `${cfg.packageId}::access_gate::${fn}`,
109
- arguments: [tx.object(nftId), tx.object(cfg.gateId), tx.pure.vector('u8', nonceBytes)],
110
- })
111
- return tx
112
- }
113
-
114
- // ── Challenge / proof (mirror of the gateway's wire format) ───────────────────────
115
-
116
- export interface RelayChallenge {
117
- nonce: string
118
- expiresAt: number
119
- }
120
-
121
- export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
122
-
123
- export function personalMessageForNonce(nonce: string): Uint8Array {
124
- return new TextEncoder().encode(`nft-gate:access:${nonce}`)
125
- }
126
-
127
- function toBase64(s: string): string {
128
- return typeof btoa === 'function' ? btoa(s) : Buffer.from(s, 'utf-8').toString('base64')
129
- }
130
-
131
- export function buildAccessProofToken(proof: {
132
- address: string
133
- nonce: string
134
- signature: string
135
- consumeDigest?: string
136
- }): string {
137
- const payload: Record<string, string> = {
138
- address: proof.address,
139
- nonce: proof.nonce,
140
- signature: proof.signature,
141
- }
142
- if (proof.consumeDigest) payload.consumeDigest = proof.consumeDigest
143
- return toBase64(JSON.stringify(payload))
144
- }
145
-
146
- export async function fetchRelayChallenge(
147
- relayHost: string,
148
- opts: { signal?: AbortSignal } = {},
149
- ): Promise<RelayChallenge> {
150
- const res = await fetch(`${relayHost.replace(/\/$/, '')}/v1/challenge`, { signal: opts.signal })
151
- if (!res.ok) throw new Error(`challenge request failed: ${res.status}`)
152
- const data = (await res.json()) as { nonce?: string; expiresAt?: number; expires_at?: number }
153
- if (!data || typeof data.nonce !== 'string') throw new Error('challenge response missing nonce')
154
- return { nonce: data.nonce, expiresAt: data.expiresAt ?? data.expires_at ?? 0 }
155
- }
package/src/lib/walrus.ts DELETED
@@ -1,155 +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 { ClientWithCoreApi } from '@mysten/sui/client'
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
- * Unwrap a Move-struct field bag from a core `json` value. The gRPC/core API returns struct
112
- * fields flat; the old JSON-RPC shape nested them under `.fields`. Tolerate both, since the
113
- * SDK documents that the `json` shape may vary between API implementations.
114
- */
115
- function structFields(v: unknown): Record<string, unknown> | undefined {
116
- if (!v || typeof v !== 'object') return undefined
117
- const o = v as Record<string, unknown>
118
- const nested = o.fields
119
- return nested && typeof nested === 'object' ? (nested as Record<string, unknown>) : o
120
- }
121
-
122
- /**
123
- * Return all Walrus blobs owned by `owner`. Uses `getBlobType()` for dynamic
124
- * package resolution so no hardcoded addresses are needed.
125
- */
126
- export async function fetchOwnedWalrusBlobs(
127
- suiClient: ClientWithCoreApi,
128
- walrusClient: ReturnType<typeof createWalrusClient>,
129
- owner: string,
130
- ): Promise<OwnedBlob[]> {
131
- const blobType = await walrusClient.walrus.getBlobType()
132
- const { objects } = await suiClient.core.listOwnedObjects({
133
- owner,
134
- type: blobType,
135
- include: { json: true },
136
- })
137
- const blobs: OwnedBlob[] = []
138
- for (const obj of objects) {
139
- const fields = structFields(obj.json)
140
- if (!fields) continue
141
- const storage = structFields(fields.storage)
142
- try {
143
- blobs.push({
144
- objectId: obj.objectId,
145
- blobId: blobIdFromInt(BigInt(fields.blob_id as string)),
146
- size: Number(fields.size),
147
- endEpoch: Number(storage?.end_epoch ?? 0),
148
- certified: fields.certified_epoch !== null && fields.certified_epoch !== undefined,
149
- })
150
- } catch {
151
- // Skip blobs whose fields cannot be parsed.
152
- }
153
- }
154
- return blobs
155
- }
@@ -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,113 +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
- // Core (gRPC) shape: struct fields are flat under `json`.
29
- return {
30
- objectId,
31
- json: {
32
- data: {
33
- gate_id: gateId,
34
- variant:
35
- uses === undefined
36
- ? { variant: 'UnlimitedPass', fields: {} }
37
- : { variant: 'SingleUse', fields: { uses_remaining: String(uses) } },
38
- },
39
- },
40
- }
41
- }
42
-
43
- function commandsJson(tx: { getData: () => unknown }): string {
44
- return JSON.stringify(tx.getData())
45
- }
46
-
47
- describe('accessGate lib', () => {
48
- it('parses unlimited pass and single-use NFTs', () => {
49
- expect(parseOwnedAccessNft(entry('0x1', GATE))).toEqual({
50
- objectId: '0x1',
51
- gateId: GATE,
52
- usesRemaining: null,
53
- })
54
- expect(parseOwnedAccessNft(entry('0x2', GATE, 5))?.usesRemaining).toBe(5)
55
- expect(parseOwnedAccessNft({ objectId: '0x9', json: {} })).toBeNull()
56
- })
57
-
58
- it('still parses the legacy JSON-RPC nested `.fields` shape', () => {
59
- const legacy = {
60
- objectId: '0x3',
61
- json: { fields: { data: { fields: { gate_id: GATE, variant: { variant: 'SingleUse', fields: { uses_remaining: '2' } } } } } },
62
- }
63
- expect(parseOwnedAccessNft(legacy)).toEqual({ objectId: '0x3', gateId: GATE, usesRemaining: 2 })
64
- })
65
-
66
- it('fetchAccessNfts filters by gate and passes the type filter', async () => {
67
- const client: OwnedObjectsClient = {
68
- core: {
69
- listOwnedObjects: vi.fn(async () => ({
70
- objects: [entry('0x1', GATE), entry('0x2', '0xother', 1)],
71
- })),
72
- },
73
- }
74
- const nfts = await fetchAccessNfts(client, '0xowner', NFT_TYPE, GATE)
75
- expect(nfts).toHaveLength(1)
76
- expect(nfts[0].objectId).toBe('0x1')
77
- expect(client.core.listOwnedObjects).toHaveBeenCalledWith(
78
- expect.objectContaining({ owner: '0xowner', type: NFT_TYPE, include: { json: true } }),
79
- )
80
- })
81
-
82
- it('buildPurchaseTx splits gas and calls purchase', () => {
83
- const json = commandsJson(buildPurchaseTx(cfg))
84
- expect(json).toContain('SplitCoins')
85
- expect(json).toContain('"function":"purchase"')
86
- })
87
-
88
- it('buildConsumeTx selects consume vs consume_soulbound', () => {
89
- expect(commandsJson(buildConsumeTx(cfg, '0x1', 'n'))).toContain('"function":"consume"')
90
- expect(commandsJson(buildConsumeTx({ ...cfg, soulbound: true }, '0x1', 'n'))).toContain(
91
- '"function":"consume_soulbound"',
92
- )
93
- })
94
-
95
- it('encodes the proof token and message', () => {
96
- expect(new TextDecoder().decode(personalMessageForNonce('x'))).toBe('nft-gate:access:x')
97
- const token = buildAccessProofToken({ address: '0x1', nonce: 'n', signature: 's', consumeDigest: 'd' })
98
- expect(JSON.parse(Buffer.from(token, 'base64').toString('utf-8'))).toEqual({
99
- address: '0x1',
100
- nonce: 'n',
101
- signature: 's',
102
- consumeDigest: 'd',
103
- })
104
- })
105
-
106
- it('fetchRelayChallenge parses the gateway response', async () => {
107
- vi.stubGlobal(
108
- 'fetch',
109
- vi.fn(async () => new Response(JSON.stringify({ nonce: 'abc', expiresAt: 7 }), { status: 200 })),
110
- )
111
- expect(await fetchRelayChallenge('https://relay.example')).toEqual({ nonce: 'abc', expiresAt: 7 })
112
- })
113
- })