@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
|
@@ -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
|
-
}
|
package/src/lib/accessGate.ts
DELETED
|
@@ -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
|
-
}
|
package/stylelint.config.js
DELETED
|
@@ -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
|
-
}
|
package/tests/accessGate.test.ts
DELETED
|
@@ -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
|
-
})
|