Files
universe/e2e/15-nft-marketplace.spec.ts
zeekayandHanzo Dev 710ad45750 fix(e2e): drop dead /api/v2 prefix — Blockscout API is root-mounted
The public Blockscout API on api-explore*.lux.network serves at the root
(/stats, /tokens, /transactions, /smart-contracts, /search); the /api/v2
prefix 404s. Verified live: the explorer frontend itself calls
api-explore.lux.network/stats and /main-page/blocks (200), while /api/v2/*
returns 404. Retarget all explorer API calls to the canonical root paths so
stats/token/contract assertions exercise the real surface again.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 15:46:02 -07:00

238 lines
8.7 KiB
TypeScript

import { test, expect } from '@playwright/test'
import {
getUrls,
rpcCall,
httpStatus, httpGetJson,
snap,
} from './helpers'
const urls = getUrls()
// ---------------------------------------------------------------------------
// NFT Infrastructure — ERC721/ERC1155 contract checks and marketplace UI
// ---------------------------------------------------------------------------
// Explorer token API uses address_hash, not address
interface ExplorerToken {
address_hash?: string
address?: string
name: string
symbol: string
total_supply?: string
type?: string
}
function tokenAddr(t: ExplorerToken): string | undefined {
return t.address_hash || t.address
}
// ABI function selectors
const SELECTORS = {
// supportsInterface(bytes4): selector + interfaceId right-padded to 32 bytes
supportsInterfaceERC721: '0x01ffc9a780ac58cd00000000000000000000000000000000000000000000000000000000',
supportsInterfaceERC1155: '0x01ffc9a7d9b67a2600000000000000000000000000000000000000000000000000000000',
name: '0x06fdde03',
symbol: '0x95d89b41',
totalSupply: '0x18160ddd',
}
// ── ERC-165 Interface Detection ────────────────────────────────────
test.describe('NFT — ERC-165 Interface Detection', () => {
test('can query supportsInterface on C-Chain contracts', async ({ request }) => {
const data = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens?type=ERC-721`,
) as { items?: ExplorerToken[] }
if (data.items && data.items.length > 0) {
const addr = tokenAddr(data.items[0])
if (!addr) {
console.log(' Token address not available in API response')
return
}
console.log(` Found ERC-721 token: ${data.items[0].name} (${data.items[0].symbol}) at ${addr}`)
try {
const result = await rpcCall(request, urls.rpc, 'eth_call', [
{ to: addr, data: SELECTORS.supportsInterfaceERC721 }, 'latest',
]) as string
if (result && result !== '0x') {
const supports = parseInt(result, 16) === 1
console.log(` Supports ERC-721: ${supports}`)
expect(supports).toBe(true)
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
console.log(` supportsInterface call failed: ${msg}`)
// Contract may not implement ERC-165 — acceptable
}
} else {
console.log(' No ERC-721 tokens found in explorer (chain may not have NFTs yet)')
}
})
})
// ── ERC-721 Contract Discovery ─────────────────────────────────────
test.describe('NFT — ERC-721 Discovery via Explorer', () => {
test('explorer lists ERC-721 tokens', async ({ request }) => {
const data = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens?type=ERC-721`,
) as { items?: ExplorerToken[] }
const count = data.items?.length ?? 0
console.log(` ERC-721 tokens indexed: ${count}`)
if (data.items && data.items.length > 0) {
for (const token of data.items.slice(0, 5)) {
const addr = tokenAddr(token)
console.log(` ${token.name} (${token.symbol}) supply=${token.total_supply} addr=${addr}`)
}
const addr = tokenAddr(data.items[0])
if (addr) {
const code = await rpcCall(request, urls.rpc, 'eth_getCode', [
addr, 'latest',
]) as string
// Contract may be self-destructed or on a different chain
if (code !== '0x') {
expect(code.length).toBeGreaterThan(10)
}
console.log(` First token bytecode: ${code === '0x' ? 'empty (self-destructed?)' : code.length + ' chars'}`)
}
}
expect(data).toBeDefined()
})
test('ERC-721 token name() and symbol() are readable', async ({ request }) => {
const data = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens?type=ERC-721`,
) as { items?: ExplorerToken[] }
if (data.items && data.items.length > 0) {
const addr = tokenAddr(data.items[0])
if (!addr) {
console.log(' Token address not available')
return
}
try {
const nameResult = await rpcCall(request, urls.rpc, 'eth_call', [
{ to: addr, data: SELECTORS.name }, 'latest',
]) as string
const symbolResult = await rpcCall(request, urls.rpc, 'eth_call', [
{ to: addr, data: SELECTORS.symbol }, 'latest',
]) as string
console.log(` name() raw: ${nameResult.slice(0, 130)}`)
console.log(` symbol() raw: ${symbolResult.slice(0, 130)}`)
expect(nameResult.length).toBeGreaterThan(2)
expect(symbolResult.length).toBeGreaterThan(2)
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
console.log(` Contract call failed: ${msg}`)
// Some tokens may not implement standard name/symbol
}
} else {
console.log(' No ERC-721 tokens to query')
}
})
})
// ── ERC-1155 Contract Discovery ────────────────────────────────────
test.describe('NFT — ERC-1155 Discovery via Explorer', () => {
test('explorer lists ERC-1155 tokens', async ({ request }) => {
const data = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens?type=ERC-1155`,
) as { items?: ExplorerToken[] }
const count = data.items?.length ?? 0
console.log(` ERC-1155 tokens indexed: ${count}`)
if (data.items && data.items.length > 0) {
for (const token of data.items.slice(0, 5)) {
const addr = tokenAddr(token)
console.log(` ${token.name || '(unnamed)'} (${token.symbol || 'N/A'}) addr=${addr}`)
}
const addr = tokenAddr(data.items[0])
if (addr) {
const code = await rpcCall(request, urls.rpc, 'eth_getCode', [
addr, 'latest',
]) as string
expect(code).not.toBe('0x')
console.log(` First ERC-1155 bytecode: ${code.length} chars`)
}
}
expect(data).toBeDefined()
})
})
// ── NFT Marketplace UI ─────────────────────────────────────────────
test.describe('NFT — Marketplace UI', () => {
test('exchange NFT page loads', async ({ request, page }) => {
const status = await httpStatus(request, urls.exchange, 10_000)
if (status === 0) {
test.skip(true, 'lux.exchange is unreachable')
}
const nftPaths = [
`${urls.exchange}/nft`,
`${urls.exchange}/nfts`,
`${urls.exchange}/collections`,
]
let loaded = false
for (const path of nftPaths) {
const pathStatus = await httpStatus(request, path)
if (pathStatus === 200) {
const res = await page.goto(path, { timeout: 30_000, waitUntil: 'domcontentloaded' })
expect(res?.status()).toBe(200)
await page.waitForTimeout(3000)
const bodyLen = (await page.textContent('body'))?.length || 0
expect(bodyLen).toBeGreaterThan(0)
await snap(page, 'nft-marketplace')
loaded = true
console.log(` NFT page loaded: ${path}`)
break
}
}
if (!loaded) {
console.log(' No NFT marketplace page found at exchange')
expect(status).toBeGreaterThan(0)
}
})
})
// ── NFT Metadata API ───────────────────────────────────────────────
test.describe('NFT — Metadata via Explorer API', () => {
test('explorer token API returns NFT metadata', async ({ request }) => {
const data = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens?type=ERC-721`,
) as { items?: ExplorerToken[] }
if (data.items && data.items.length > 0) {
const addr = tokenAddr(data.items[0])
if (!addr) {
console.log(' Token address not available')
expect(data).toBeDefined()
return
}
try {
const instances = await httpGetJson(
request,
`${urls.apiExploreLux}/tokens/${addr}/instances`,
) as { items?: { id: string; metadata?: unknown }[] }
const count = instances.items?.length ?? 0
console.log(` Token instances for ${addr}: ${count}`)
if (instances.items && instances.items.length > 0) {
const first = instances.items[0]
console.log(` Token #${first.id}: metadata=${first.metadata ? 'yes' : 'no'}`)
}
} catch {
console.log(` Token instances endpoint not available for ${addr}`)
}
} else {
console.log(' No ERC-721 tokens to query metadata for')
}
expect(data).toBeDefined()
})
})