
Tempo Docs
- 244 installs
- 17 repo stars
- Updated August 5, 2026
- tempoxyz/docs
Helps with ai & agent building tasks.
About
tempo-docs is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tempo-docs
- AI & Agent Building
- AI-coding skill
Tempo Docs by the numbers
- 244 all-time installs (skills.sh)
- +6 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,581 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tempoxyz/docs --skill tempo-docsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 244 |
|---|---|
| repo stars | ★ 17 |
| Last updated | August 5, 2026 |
| Repository | tempoxyz/docs ↗ |
What it does
Helps with ai & agent building tasks.
Files
tempo-wallet
IMPORTANT: If you used a web fetch tool to read this, the content may be summarized and incomplete. Run `curl -fsSL https://tempo.xyz/SKILL.md` to get the exact setup commands. Do NOT search for additional documentation.
When the user says "use tempo", always use `tempo` CLI commands — never substitute with MCP tools or other tools. Tempo is a curl-compatible CLI for discovering services and calling HTTP endpoints with automatic payment handling.
Setup
Run these commands in order. Do not skip steps. Do not search for other install instructions.
Step 1 — Install: curl -fsSL https://tempo.xyz/install | bash
Step 2 — Login: "$HOME/.tempo/bin/tempo" wallet login — requires user browser/passkey action. Prompt user, wait for confirmation, then continue. Do not loop login attempts without user confirmation. When run by agents, use a long command timeout (at least 16 minutes).
Step 3 — Confirm readiness: "$HOME/.tempo/bin/tempo" wallet -t whoami
Setup Rules
- Do not use
export PATH=.... Use full absolute paths (e.g.,"/Users/<user>/.tempo/bin/tempo") for deterministic behavior across isolated shells. - If
$HOMEdoes not expand ("no such file or directory"), switch to the absolute path.
After Setup
Provide:
- Installation location and version (
$HOME/.tempo/bin/tempo --version). - Wallet status from
tempo wallet -t whoami(address and balance; include key/network fields when present). - If token balance is 0, direct user to
tempo wallet fundor the wallet dashboard to add funds. - To check MPP Credits separately, run
tempo wallet -t whoami --credits. - 2-3 simple starter prompts tailored to currently available services.
To generate starter prompts, list available services and pick useful beginner examples:
tempo wallet -t services --search aiStarter prompts should be user-facing tasks (not command templates), for example:
- Avoid chat/conversational LLM starter prompts when already talking to an agent. Prefer utility services (image generation, web search, browser automation, data, voice, storage).
- "Generate a dog image with a blue background and save it as
dog.png." - "Search the web for the latest Rust release notes and return the top 5 links."
- "Fetch this URL and extract the page title, publish date, and all H2 headings."
Use Services
tempo wallet -t whoami
tempo wallet -t services --search <query>
tempo wallet -t services <SERVICE_ID>
tempo request -t -X POST --json '{"input":"..."}' <SERVICE_URL>/<ENDPOINT_PATH>- Select
SERVICE_IDfrom search results that best matches user intent. When multiple match: prefer best semantic fit, then endpoint fit, then pricing clarity, then first in list. - Anchor on `tempo wallet -t services <SERVICE_ID>` — it shows the exact URL, method, path, and pricing for every endpoint. Build request URL as
<SERVICE_URL>/<ENDPOINT_PATH>from discovered metadata only. - If service details include
supportsCredits: true, MPP Credits may be used for one-timetempo.chargepayments. Credits are separate from token balances; check them withtempo wallet -t whoami --creditsand buy them withtempo wallet fund --credits. - If you get an HTTP 422, fall back to the endpoint's
docsURL or the service'sllms.txtfor exact field names. - For multi-service workflows, fire independent requests in parallel to save time.
Request Templates
# JSON POST
tempo request -t --dry-run -X POST --json '{"input":"..."}' <SERVICE_URL>/<ENDPOINT_PATH>
tempo request -t -X POST --json '{"input":"..."}' <SERVICE_URL>/<ENDPOINT_PATH>
# GET
tempo request -t -X GET <SERVICE_URL>/<ENDPOINT_PATH>Response Handling
- Return result payload to user directly when request succeeds.
- If response contains a file URL (e.g., image generation), download it locally:
curl -fsSL "<url>" -o <filename>. - If response is a usage/auth readiness error, run
tempo wallet loginand retry once. - If response indicates payment/funding limit issues, report clearly and stop. For token funding use
tempo wallet fund; for MPP Credits usetempo wallet fund --creditsonly when service details showsupportsCredits: true. - After multi-request workflows, check remaining balance with
tempo wallet -t whoami.
Rules
- Always discover URL/path before request; never guess endpoint paths.
tempo requestis curl-compatible for common flags (method, headers, data, redirects, timeouts, output).- Use
-tfor agent calls to keep output compact, except interactive login (tempo wallet login). - Use
--dry-runbefore potentially expensive requests. - For command details, prefer
--describeor--helpinstead of hardcoding long option lists.
Common Issues
| Issue | Cause | Fix |
|---|---|---|
tempo: command not found | CLI not installed | Run `curl -fsSL https://tempo.xyz/install \ |
| "legacy V1 keychain signature is no longer accepted, use V2" | Outdated tempo launcher or extensions | Reinstall tempo: `curl -fsSL https://tempo.xyz/install \ |
| "access key does not exist" | Key not provisioned on-chain, or stale key after reinstall | Run tempo wallet logout --yes, then tempo wallet login to provision a fresh key. |
ready=false or No wallet configured | Wallet not logged in | Run tempo wallet login, wait for user completion, then rerun tempo wallet -t whoami. |
| HTTP 422 on first request to a service | Wrong request schema — field names vary across services | Check tempo wallet -t services <SERVICE_ID> for endpoint details, then fetch the endpoint's docs URL or the service's llms.txt for exact field names and types. |
| Balance is 0, insufficient funds, or spending limit exceeded | Wallet needs funding or limit hit | Run tempo wallet fund or direct user to the wallet dashboard. Report clearly and stop if limit is exceeded. |
| Token balance is 0 but MPP Credits may be available | Credits are separate from token balances | Run tempo wallet -t whoami --credits. If the service shows supportsCredits: true, credits can be used for one-time charge payments. |
| Need to buy MPP Credits | User wants to fund with card-based credits for eligible services | Run tempo wallet fund --credits, complete checkout in the wallet app, then recheck with tempo wallet -t whoami --credits. |
| Credits are not accepted by a service | MPP Credits only work for eligible Tempo-proxied services | Inspect tempo wallet -t services <SERVICE_ID> and use credits only when supportsCredits: true is present. Otherwise use token funding with tempo wallet fund. |
| Service uses sessions | MPP Credits currently support one-time charges, not sessions | Use token funding for session-based services. |
| Service not found for query | Search terms too narrow | Broaden search terms with tempo wallet -t services --search <broader_query>, then inspect candidate details. |
| Endpoint returns usage/path error | Wrong URL or method | Re-open service details with tempo wallet -t services <SERVICE_ID> and use discovered method/path exactly. |
| Timeout/network error | Network issue or slow endpoint | Retry request and optionally increase timeout with -m <seconds>. |
{
"name": "docs",
"interface": {
"displayName": "Tempo"
},
"plugins": [
{
"name": "tempo",
"source": {
"source": "local",
"path": "./ai/plugins/tempo"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Developer Tools"
}
]
}
{
"name": "claude",
"metadata": {
"description": "Tempo Claude Code plugin marketplace."
},
"owner": {
"name": "Tempo",
"email": "support@tempo.xyz"
},
"plugins": [
{
"name": "tempo",
"description": "Tempo MCP and skills for Tempo docs, developer workflows, wallet, and MPP payments.",
"source": "./ai/providers/claude/plugin",
"category": "development",
"homepage": "https://docs.tempo.xyz/guide/using-tempo-with-ai"
}
]
}
INDEXSUPPLY_API_KEY=
SLACK_FEEDBACK_WEBHOOK= # e.g. https://hooks.slack.com/services/...
VITE_BASE_URL= # e.g. https://docs.tempo.xyz
VITE_GA_MEASUREMENT_ID=
VITE_POSTHOG_HOST=
VITE_POSTHOG_KEY=
VITE_TEMPO_ENV= # testnet|devnet|localnet
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
production:
dependency-type: production
development:
dependency-type: development
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
name: External Links
on:
push:
branches: [main]
pull_request:
schedule:
# Weekly link-rot check, independent of code changes.
- cron: "0 9 * * 1"
workflow_dispatch:
permissions: {}
concurrency:
group: links-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lychee:
name: Lychee
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Clone repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Restore lychee cache
uses: actions/cache@v4
with:
path: .lycheecache
key: lychee-${{ github.sha }}
restore-keys: lychee-
- name: Run lychee
uses: lycheeverse/lychee-action@v2
with:
# README.md is a GitHub-only artifact with pre-existing
# broken references (LICENSE files, lockup paths); excluded
# from this check. Docs site content lives in src/pages/.
args: >-
--root-dir ${{ github.workspace }}
'src/pages/**/*.{md,mdx}'
'vocs.config.ts'
fail: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Open issue on scheduled failure
if: failure() && github.event_name == 'schedule'
uses: peter-evans/create-issue-from-file@v5
with:
title: "Broken external links detected"
content-filepath: ./lychee/out.md
labels: docs, links
name: Verify
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions: {}
concurrency:
group: verify-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Clone repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
run_install: false
version: 10.28.1
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.12.0
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check
run: pnpm run check
- name: Check types
run: pnpm run check:types
- name: Build
run: pnpm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
ci-gate:
name: CI Gate
if: always()
needs: [check, e2e]
runs-on: ubuntu-latest
permissions: {}
steps:
- run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled"
exit 1
fi
e2e:
name: E2E Tests (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.0-noble
options: --user 1001
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- name: Clone repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
run_install: false
version: 10.28.1
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.12.0
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build app for E2E
run: pnpm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
VITE_E2E: "true"
VITE_USE_HTTP: "true"
- name: Run Playwright tests
run: pnpm run test:e2e --shard=${{ matrix.shard }}/3
- name: Upload test results
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
retention-days: 15
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
src/pages.gen.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.vercel
.vocs
.env*.local
src/pages/protocol/tips/tip-*
# Test scratch files
guides/tmp/
# Bundle analysis
.bundle-baseline.json
stats.json
lighthouse-*.json
# Playwright
playwright-report/
test-results/
.zed
Tempo Documentation (docs-next)
Vocs-powered documentation site for Tempo protocol.
Commands
bun run dev- Start development serverbun run build- Build for productionbun run check- Run typecheck
Adding a New Page
1. Create .mdx file in appropriate pages/ subdirectory (match URL path to file path) 2. Add SEO frontmatter at the top of the file (required):
---
title: Page Title Here
description: A concise 150-160 character description for search engines and social sharing.
---- title: Concise, descriptive page title (used in
<title>and OG tags) - description: 150-160 characters, active voice, describes what the page covers
3. Add entry to sidebar in vocs.config.tsx 4. Run bun run dev to verify, then bun run check before committing
SEO Configuration
- Dynamic OG images: Generated via
/api/og.tsxusing title and description from frontmatter - Config:
vocs.config.tsxsetsbaseUrl,ogImageUrl(with%titleand%descriptiontemplate variables), andtitleTemplate - All pages automatically get proper
<title>,<meta description>, Open Graph, and Twitter Card tags from frontmatter
Protocol Concept Naming
- Use literal concept names in user-facing docs. Add the TIP number in parentheses only in the sidebar and when first introducing a concept if it helps disambiguate.
- Use
TIP-20 Tokensfor sidebar labels, page titles, headings, and first-introduction contexts; useTIP-20 tokensin sentence-case prose after that. - Use
Tempo Token Rewardsin sidebar, concept introductions, and rewards-specific resource cards. Do not append(TIP-20). - Keep raw TIP references for technical/spec contexts, e.g.
TIP-20 ABI,TIP-403 policy check, or links titledTIP-20 Specification.
Numbered Steps
When writing step-by-step instructions in guides, use the :::::steps container directive instead of manual ### Step 1, #### Step 2 headings. Each step is a ### heading inside the container. The steps are auto-numbered by the renderer.
:::::steps
### Do the first thing
Content for step 1.
### Do the second thing
Content for step 2.
:::::See https://mpp.dev/guides/multiple-payment-methods for a reference example.
Project Structure
src/pages/- MDX documentation pagessrc/components/- React componentsapi/- Vercel serverless functions (OG image generation)public/- Static assetsvocs.config.ts- Vocs configuration (sidebar, nav, SEO)vercel.json- Vercel deployment config (redirects, rewrites)
TIPs (Tempo Improvement Proposals)
TIPs are stored in src/pages/protocol/tips/ with YAML frontmatter:
---
title: TIP-X Title
description: Short description
status: Draft | Review | Accepted | Implemented
type: Standards | Process | Informational
authors:
- Author Name
---The TipsList component automatically reads TIPs via import.meta.glob and displays them sorted by number.
{
"name": "tempo",
"version": "0.1.0",
"description": "Tempo MCP and skills for Tempo docs, developer workflows, wallet, and MPP payments.",
"author": {
"name": "Tempo",
"url": "https://tempo.xyz"
},
"homepage": "https://docs.tempo.xyz/docs/guide/using-tempo-with-ai",
"repository": "https://github.com/tempoxyz/docs",
"license": "MIT",
"keywords": ["tempo", "mcp", "payments", "wallet", "stablecoins", "mpp"],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "Tempo",
"shortDescription": "Tempo MCP, docs, wallet, and MPP payments.",
"longDescription": "Use Tempo MCP from Codex to search Tempo documentation, plus Tempo skills for developer workflows, wallet setup, service discovery, and MPP paid requests.",
"developerName": "Tempo",
"category": "Developer Tools",
"capabilities": ["Interactive", "Write"],
"websiteURL": "https://tempo.xyz",
"defaultPrompt": [
"Build a Tempo app that sends stablecoin payments.",
"Add Tempo Wallet and passkey accounts to this app.",
"Integrate MPP payments into a Tempo API or agent."
],
"screenshots": [],
"brandColor": "#000000"
}
}
{
"mcpServers": {
"tempo": {
"type": "http",
"url": "https://mcp.tempo.xyz",
"note": "Tempo MCP server for docs search, page discovery, and cleaned page reads."
}
}
}
{
"name": "tempo",
"description": "Tempo development plugin for Claude",
"version": "0.1.0",
"author": {
"name": "Tempo",
"url": "https://tempo.xyz"
},
"homepage": "https://docs.tempo.xyz",
"repository": "https://github.com/tempoxyz/docs",
"license": "MIT",
"keywords": ["tempo", "stablecoins", "mcp", "payments", "mpp"]
}
{
"mcpServers": {
"tempo": {
"type": "http",
"url": "https://mcp.tempo.xyz"
}
}
}
Tempo AI
Agent and editor integration metadata for Tempo.
This directory contains the plugin payloads and skills referenced by the AI marketplace manifests.
Marketplace manifests:
.agents/plugins/marketplace.jsonfor thedocsCodex marketplace.claude-plugin/marketplace.jsonfor theclaudemarketplace
Remote MCP
Use the hosted MCP server:
https://mcp.tempo.xyzThe current server exposes docs search, page discovery, and cleaned page reads. Wallet and paid-request workflows are handled by the tempo-wallet skill using the Tempo CLI.
Feedback from MCP clients should be sent to the shared docs ingress:
POST https://docs.tempo.xyz/api/feedbackUse source: "mcp" plus a short message, and include toolName, relatedResource, or client when available.
Codex
The Codex plugin lives in ai/plugins/tempo.
Claude
The Claude plugin lives in providers/claude/plugin.
Skills
tempo: generic Tempo developer skill placeholder.tempo-wallet: wallet setup, service discovery, and paid HTTP requests withtempo walletandtempo request.
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"quoteStyle": "single",
"trailingCommas": "all"
}
},
"css": {
"parser": {
"tailwindDirectives": true
},
"formatter": {
"enabled": true
},
"linter": {
"enabled": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"nursery": {
"useSortedClasses": {
"level": "warn",
"options": {
"attributes": ["className", "class"],
"functions": ["clsx", "cva", "tw", "cn", "twMerge"]
}
}
}
}
},
"files": {
"ignoreUnknown": false,
"includes": [
"**",
"!dist",
"!node_modules",
"!playwright-report",
"!specs/lib",
"!src/snippets/unformatted",
"!test-results"
]
},
"overrides": [
{
"includes": ["vocs.config.ts"],
"linter": {
"rules": {
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
}
},
{
"includes": ["env.d.ts"],
"linter": {
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "off"
}
}
}
}
]
}
import { defineConfig } from 'cva'
import { twMerge } from 'tailwind-merge'
export const { cva, cx, compose } = defineConfig({
hooks: {
onComplete: (className) => twMerge(className),
},
})
import { expect, test } from '@playwright/test'
test('create a stablecoin', async ({ page }) => {
test.setTimeout(120000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/issuance/create-a-stablecoin')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
// Wait for sign out button (indicates successful sign up)
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
// Wait for "Add more funds" button (indicates funds were added)
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Fill in token details and deploy
// Use label-based selectors to ensure we're filling the right inputs in the demo form
const nameInput = page.getByLabel('Token name').first()
await expect(nameInput).toBeVisible()
await nameInput.fill('TestUSD')
const symbolInput = page.getByLabel('Token symbol').first()
await expect(symbolInput).toBeVisible()
await symbolInput.fill('TEST')
const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
await expect(deployButton).toBeVisible()
await deployButton.click()
// Wait for success - View receipt link
await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'
test('distribute rewards', async ({ page }) => {
test.setTimeout(240000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/issuance/distribute-rewards')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Create a token
// Use label-based selectors to ensure we're filling the right inputs in the demo form
const nameInput = page.getByLabel('Token name').first()
await expect(nameInput).toBeVisible()
await nameInput.fill('RewardTestUSD')
const symbolInput = page.getByLabel('Token symbol').first()
await expect(symbolInput).toBeVisible()
await symbolInput.fill('REWARD')
const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
await expect(deployButton).toBeVisible()
await deployButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
timeout: 90000,
})
// Step 4: Grant issuer role
const grantStep = getDemoStep(page, 'Grant issuer role on RewardTestUSD.')
const grantEnterDetails = grantStep.getByRole('button', { name: 'Enter details' })
await expect(grantEnterDetails).toBeVisible()
await grantEnterDetails.click()
const grantButton = grantStep.getByRole('button', { name: 'Grant' })
await grantButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
timeout: 90000,
})
// Step 5: Mint tokens
const mintStep = getDemoStep(page, 'Mint 100 RewardTestUSD to yourself.')
const mintEnterDetails = mintStep.getByRole('button', { name: 'Enter details' })
await expect(mintEnterDetails).toBeVisible()
await mintEnterDetails.click()
const mintButton = mintStep.getByRole('button', { name: 'Mint' })
await mintButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
timeout: 90000,
})
// Step 6: Opt in to rewards
const optInButton = page.getByRole('button', { name: 'Opt In' }).first()
await expect(optInButton).toBeVisible()
await optInButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(3)).toBeVisible({
timeout: 90000,
})
// Step 7: Start reward
const startButton = page.getByRole('button', { name: 'Start Reward' }).first()
await expect(startButton).toBeVisible()
await startButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(4)).toBeVisible({
timeout: 90000,
})
// Step 8: Claim reward
const claimButton = page.getByRole('button', { name: 'Claim' }).first()
await expect(claimButton).toBeVisible()
await claimButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(5)).toBeVisible({
timeout: 90000,
})
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
test('executing swaps', async ({ page }) => {
test.setTimeout(180000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/stablecoin-dex/executing-swaps')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Execute a swap (Buy AlphaUSD with BetaUSD)
const buyButton = page.getByRole('button', { name: 'Buy' }).first()
await expect(buyButton).toBeVisible()
await buyButton.click()
// Wait for swap receipt
await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
test('fund an address via faucet', async ({ page }) => {
test.setTimeout(120000)
await page.goto('/docs/quickstart/faucet')
// Switch to "Fund an address" tab
const tab = page.getByRole('tab', { name: 'Fund an address' })
await expect(tab).toBeVisible({ timeout: 90000 })
await tab.click()
// Enter an address
const addressInput = page.getByPlaceholder('0x...')
await addressInput.fill('0xbeefcafe54750903ac1c8909323af7beb21ea2cb')
// Click "Add funds" button
await page.getByRole('button', { name: 'Add funds' }).click()
// Confirm "View receipt" link is visible
await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
})
import type { Locator, Page } from '@playwright/test'
export function getDemoStep(page: Page, title: string | RegExp): Locator {
return page.locator('[data-active][data-completed]').filter({ hasText: title }).first()
}
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'
test('manage stablecoin - grant and revoke roles', async ({ page }) => {
test.setTimeout(180000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/issuance/manage-stablecoin')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Create a token
// Use label-based selectors to ensure we're filling the right inputs in the demo form
const nameInput = page.getByLabel('Token name').first()
await expect(nameInput).toBeVisible()
await nameInput.fill('ManageTestUSD')
const symbolInput = page.getByLabel('Token symbol').first()
await expect(symbolInput).toBeVisible()
await symbolInput.fill('MANAGE')
const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
await expect(deployButton).toBeVisible()
await deployButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
timeout: 90000,
})
// Step 4: Grant issuer role
const grantStep = getDemoStep(page, 'Grant issuer role on ManageTestUSD.')
const grantEnterDetails = grantStep.getByRole('button', { name: 'Enter details' })
await expect(grantEnterDetails).toBeVisible()
await grantEnterDetails.click()
const grantButton = grantStep.getByRole('button', { name: 'Grant' })
await expect(grantButton).toBeVisible()
await grantButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
timeout: 90000,
})
// Step 5: Revoke issuer role
const revokeStep = getDemoStep(page, 'Revoke issuer role on ManageTestUSD.')
const revokeEnterDetails = revokeStep.getByRole('button', { name: 'Enter details' })
await expect(revokeEnterDetails).toBeVisible()
await revokeEnterDetails.click()
const revokeButton = revokeStep.getByRole('button', { name: 'Revoke' })
await expect(revokeButton).toBeVisible()
await revokeButton.click()
// Wait for revoke receipt
await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
timeout: 90000,
})
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'
test('mint stablecoins', async ({ page }) => {
test.setTimeout(180000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/issuance/mint-stablecoins')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Create a token (fill form and deploy)
// Use label-based selectors to ensure we're filling the right inputs in the demo form
const nameInput = page.getByLabel('Token name').first()
await expect(nameInput).toBeVisible()
await nameInput.fill('MintTestUSD')
const symbolInput = page.getByLabel('Token symbol').first()
await expect(symbolInput).toBeVisible()
await symbolInput.fill('MINT')
const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
await expect(deployButton).toBeVisible()
await deployButton.click()
// Wait for token to be created (View receipt appears)
await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
timeout: 90000,
})
// Step 4: Grant issuer role - click "Enter details" then "Grant"
const grantStep = getDemoStep(page, 'Grant issuer role on MintTestUSD.')
const grantEnterDetails = grantStep.getByRole('button', { name: 'Enter details' })
await expect(grantEnterDetails).toBeVisible()
await grantEnterDetails.click()
const grantButton = grantStep.getByRole('button', { name: 'Grant' })
await expect(grantButton).toBeVisible()
await grantButton.click()
// Wait for grant receipt
await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
timeout: 90000,
})
// Step 5: Mint tokens - click "Enter details" then "Mint"
const mintStep = getDemoStep(page, 'Mint 100 MintTestUSD to yourself.')
const mintEnterDetails = mintStep.getByRole('button', { name: 'Enter details' })
await expect(mintEnterDetails).toBeVisible()
await mintEnterDetails.click()
const mintButton = mintStep.getByRole('button', { name: 'Mint' })
await expect(mintButton).toBeVisible()
await mintButton.click()
// Wait for mint receipt
await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
timeout: 90000,
})
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { readdirSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect, test } from '@playwright/test'
const __dirname = dirname(fileURLToPath(import.meta.url))
function findMdxFiles(dir: string): string[] {
const results: string[] = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
if (entry.isDirectory() && entry.name !== 'node_modules') {
results.push(...findMdxFiles(full))
} else if (entry.name.endsWith('.mdx')) {
results.push(full)
}
}
return results
}
test('no raw ```mermaid code blocks in MDX files', () => {
const pagesDir = join(__dirname, '..', 'src', 'pages')
const mdxFiles = findMdxFiles(pagesDir)
const violations: string[] = []
for (const file of mdxFiles) {
const content = readFileSync(file, 'utf-8')
if (/^```mermaid\s*$/m.test(content)) {
const relative = file.replace(`${join(__dirname, '..')}/`, '')
violations.push(relative)
}
}
expect(
violations,
[
'Found raw ```mermaid code blocks. Use <StaticMermaidDiagram> instead:',
...violations.map((f) => ` - ${f}`),
].join('\n'),
).toHaveLength(0)
})
import { readdirSync, readFileSync } from 'node:fs'
import { dirname, extname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect, test } from '@playwright/test'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '..')
const srcDir = join(repoRoot, 'src')
const stepsBarrelDir = join(srcDir, 'components', 'guides', 'steps')
const SOURCE_EXTENSIONS = new Set(['.mdx', '.ts', '.tsx'])
function findSourceFiles(dir: string): string[] {
const results: string[] = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
if (entry.isDirectory() && entry.name !== 'node_modules') {
results.push(...findSourceFiles(full))
continue
}
if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
results.push(full)
}
}
return results
}
function findImports(content: string): string[] {
const imports: string[] = []
for (const match of content.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
imports.push(match[1])
}
for (const match of content.matchAll(/import\s+['"]([^'"]+)['"]/g)) {
imports.push(match[1])
}
return imports
}
function resolvesToStepsBarrel(file: string, importPath: string): boolean {
if (!importPath.startsWith('.')) return false
const resolved = resolve(dirname(file), importPath)
return resolved === stepsBarrelDir || resolved === join(stepsBarrelDir, 'index')
}
test('no imports from the guides steps barrel', () => {
const files = findSourceFiles(srcDir)
const violations: string[] = []
for (const file of files) {
const content = readFileSync(file, 'utf-8')
const imports = findImports(content)
for (const importPath of imports) {
if (resolvesToStepsBarrel(file, importPath)) {
violations.push(`${relative(repoRoot, file)} -> ${importPath}`)
}
}
}
expect(
violations,
[
'Found imports from the guides steps barrel. Import the specific step modules instead:',
...violations.map((entry) => ` - ${entry}`),
].join('\n'),
).toHaveLength(0)
})
import { expect, test } from '@playwright/test'
test('providing liquidity - place and query order', async ({ page }) => {
test.setTimeout(180000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/stablecoin-dex/providing-liquidity')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Place order
const placeOrderButton = page.getByRole('button', { name: 'Place order' }).first()
await expect(placeOrderButton).toBeVisible()
await placeOrderButton.click()
// Wait for order to be placed - should see View receipt
await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
timeout: 90000,
})
// Step 4: Query order - button should become enabled after placing
const queryButton = page.getByRole('button', { name: 'Query' }).first()
await expect(queryButton).toBeEnabled({ timeout: 30000 })
await queryButton.click()
// Wait for order details to show (order type indicator)
await expect(page.getByText('Buy').first()).toBeVisible({ timeout: 30000 })
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'
test('send a payment', async ({ page }) => {
test.setTimeout(120000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/payments/send-a-payment')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
// Wait for sign out button (indicates successful sign up)
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
// Wait for "Add more funds" button (indicates funds were added)
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Send payment
const sendPaymentStep = getDemoStep(page, 'Send 100 AlphaUSD to a recipient.')
const enterDetailsButton = sendPaymentStep.getByRole('button', { name: 'Enter details' })
await expect(enterDetailsButton).toBeVisible()
await enterDetailsButton.click()
// Fill in optional memo
const memoInput = page.getByLabel('Memo (optional)').first()
await expect(memoInput).toBeVisible()
await memoInput.fill('test-memo')
// Click send
const sendButton = sendPaymentStep.getByRole('button', { name: 'Send' })
await sendButton.click()
// Wait for transaction receipt link
await expect(page.getByRole('link', { name: 'View receipt' })).toBeVisible({ timeout: 90000 })
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'
test('use stablecoin for fees', async ({ page }) => {
test.setTimeout(240000)
// Set up virtual authenticator via CDP
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
await page.goto('/docs/guide/issuance/use-for-fees')
// Step 1: Sign in
const signUpButton = page.getByRole('button', { name: 'Sign in' }).first()
await expect(signUpButton).toBeVisible({ timeout: 90000 })
await signUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
// Step 2: Add funds
const addFundsButton = page.getByRole('button', { name: 'Add funds' }).first()
await expect(addFundsButton).toBeVisible()
await addFundsButton.click()
await expect(page.getByRole('button', { name: 'Add more funds' }).first()).toBeVisible({
timeout: 90000,
})
// Step 3: Create a token
// Use label-based selectors to ensure we're filling the right inputs in the demo form
const nameInput = page.getByLabel('Token name').first()
await expect(nameInput).toBeVisible()
await nameInput.fill('FeeTestUSD')
const symbolInput = page.getByLabel('Token symbol').first()
await expect(symbolInput).toBeVisible()
await symbolInput.fill('FEE')
const deployButton = page.getByRole('button', { name: 'Deploy' }).first()
await expect(deployButton).toBeVisible()
await deployButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).first()).toBeVisible({
timeout: 90000,
})
// Step 4: Grant issuer role
const grantStep = getDemoStep(page, 'Grant issuer role on FeeTestUSD.')
const grantEnterDetails = grantStep.getByRole('button', { name: 'Enter details' })
await expect(grantEnterDetails).toBeVisible()
await grantEnterDetails.click()
const grantButton = grantStep.getByRole('button', { name: 'Grant' })
await grantButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(1)).toBeVisible({
timeout: 90000,
})
// Step 5: Mint tokens
const mintStep = getDemoStep(page, 'Mint 100 FeeTestUSD to yourself.')
const mintEnterDetails = mintStep.getByRole('button', { name: 'Enter details' })
await expect(mintEnterDetails).toBeVisible()
await mintEnterDetails.click()
const mintButton = mintStep.getByRole('button', { name: 'Mint' })
await mintButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(2)).toBeVisible({
timeout: 90000,
})
// Step 6: Add fee AMM liquidity
const addLiquidityButton = page.getByRole('button', { name: 'Add Liquidity' }).first()
await expect(addLiquidityButton).toBeVisible()
await addLiquidityButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(3)).toBeVisible({
timeout: 90000,
})
// Step 7: Send payment using token as fee
const payStep = getDemoStep(page, 'Send 100 AlphaUSD and pay fees in FeeTestUSD.')
const payEnterDetails = payStep.getByRole('button', { name: 'Enter details' })
await expect(payEnterDetails).toBeVisible()
await payEnterDetails.click()
const sendButton = payStep.getByRole('button', { name: 'Send' })
await expect(sendButton).toBeVisible()
await sendButton.click()
await expect(page.getByRole('link', { name: 'View receipt' }).nth(4)).toBeVisible({
timeout: 90000,
})
// Clean up
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId })
})
import { expect, type Locator, type Page, test } from '@playwright/test'
async function openVirtualAddressesGuide(page: Page): Promise<Locator> {
let lastError: unknown
for (let attempt = 0; attempt < 4; attempt++) {
try {
await page.goto('/docs/guide/payments/virtual-addresses', { waitUntil: 'domcontentloaded' })
await expect(
page.getByRole('heading', { name: 'Use virtual addresses for deposits' }),
).toBeVisible({ timeout: 30000 })
const realRegistrationTab = page.getByRole('tab', { name: 'Real registration' })
await expect(realRegistrationTab).toBeVisible({ timeout: 30000 })
return realRegistrationTab
} catch (error) {
lastError = error
if (attempt === 3) throw error
// Vite can briefly serve SSR while client chunks are still re-optimizing in CI.
await page.waitForTimeout(1000)
}
}
throw lastError
}
test('virtual addresses guide signs in and starts master registration', async ({ page }) => {
test.setTimeout(240000)
const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
const { authenticatorId } = await client.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
})
try {
const realRegistrationTab = await openVirtualAddressesGuide(page)
await realRegistrationTab.click()
const passkeySignUpButton = page.getByRole('button', { name: 'Sign up' }).first()
await expect(passkeySignUpButton).toBeVisible({ timeout: 90000 })
await passkeySignUpButton.click()
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
await expect(page.getByRole('button', { name: 'Sign out' }).first()).toBeVisible({
timeout: 30000,
})
await expect(page.getByText('Connected passkey account')).toBeVisible()
const registerButton = page.getByRole('button', { name: 'Register master id' }).first()
await expect(registerButton).toBeVisible()
await registerButton.click()
await expect
.poll(
async () => {
if (await page.getByRole('button', { name: 'Mining salt…' }).first().isVisible()) {
return 'mining'
}
if (await page.getByRole('button', { name: 'Confirm passkey…' }).first().isVisible()) {
return 'confirm'
}
if (await page.getByRole('button', { name: 'Registering…' }).first().isVisible()) {
return 'registering'
}
if (await page.getByText('registration tx:').isVisible()) return 'registered'
return null
},
{
timeout: 30000,
},
)
.not.toBeNull()
await expect
.poll(
async () => {
if (await page.getByText('hashes tried:').isVisible()) return 'mining'
if (
await page
.getByText('Waiting for the registration transaction to be confirmed.')
.isVisible()
) {
return 'found'
}
if (await page.getByText('registration tx:').isVisible()) return 'registered'
return null
},
{ timeout: 30000 },
)
.not.toBeNull()
} finally {
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }).catch(() => {})
}
})
interface EnvironmentVariables {
readonly NODE_ENV: 'development' | 'production' | 'test'
readonly VITE_BASE_URL: string
readonly VITE_POSTHOG_KEY: string
readonly VITE_POSTHOG_HOST: string
readonly VITE_TEMPO_ENV: 'localnet' | 'devnet' | 'moderato'
readonly INDEXSUPPLY_API_KEY: string
readonly SLACK_FEEDBACK_WEBHOOK: string
readonly VERCEL_URL: string
readonly VERCEL_BRANCH_URL: string
readonly VERCEL_PROJECT_PRODUCTION_URL: string
readonly VERCEL_ENV: 'development' | 'production' | 'preview'
}
declare namespace NodeJS {
interface ProcessEnv extends EnvironmentVariables {}
}
interface ImportMetaEnv extends EnvironmentVariables {}
interface ImportMeta {
readonly env: ImportMetaEnv
}
# lychee configuration for the Tempo docs site.
# Validates external links in markdown, MDX, and config sources.
# See https://lychee.cli.rs/usage/config/
# Network settings
max_redirects = 10
max_concurrency = 16
timeout = 20
retry_wait_time = 2
# Some hosts (Twitter, LinkedIn, Cloudflare-protected sites) reject the default UA.
user_agent = "Mozilla/5.0 (compatible; tempo-docs-link-checker/1.0; +https://docs.tempo.xyz)"
# Auth-walled or rate-limited statuses are still "reachable".
accept = ["200..=299", "401", "403", "429"]
exclude_path = [
"node_modules",
"dist",
".vocs",
".vercel",
"playwright-report",
"test-results",
"patches",
]
exclude = [
# Local / placeholder hosts.
"^https?://localhost",
"^https?://127\\.0\\.0\\.1",
"^https?://0\\.0\\.0\\.0",
"^https?://\\[::1\\]",
"^https?://([a-z0-9-]+\\.)*example\\.(com|org|net)",
# SVG xmlns reference, not a real link.
"^http://www\\.w3\\.org/2000/svg$",
]
# Reuse cached results across runs.
cache = true
max_cache_age = "1d"
no_progress = true
{
"name": "docs-next",
"private": true,
"version": "0.0.0",
"type": "module",
"packageManager": "pnpm@10.28.1",
"scripts": {
"dev": "NODE_OPTIONS='--import tsx' vite",
"build": "tsgo --build && vite build && vite build --config vite.marketing.config.ts",
"check": "biome check --write --unsafe .",
"check:types": "tsgo --project tsconfig.json --noEmit",
"preview": "node dist/preview.js",
"test": "vitest run --dir src",
"test:e2e": "playwright test",
"bundle:analyze": "node --experimental-strip-types scripts/bundle-diff.ts --skip-build",
"bundle:diff": "node --experimental-strip-types scripts/bundle-diff.ts",
"bundle:save": "node --experimental-strip-types scripts/bundle-diff.ts --save",
"lighthouse": "node --experimental-strip-types scripts/lighthouse.ts",
"lighthouse:mobile": "node --experimental-strip-types scripts/lighthouse.ts --mobile",
"og:probe": "node --experimental-strip-types scripts/probe-og.ts"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.102",
"@iconify-json/simple-icons": "^1.2.77",
"@monaco-editor/react": "^4.7.0",
"@takumi-rs/image-response": "0.62.8",
"@takumi-rs/wasm": "0.62.8",
"@tanstack/react-query": "^5.99.0",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"@wagmi/core": "3.4.11",
"abitype": "^1.2.3",
"accounts": "^0.10.7",
"cva": "1.0.0-beta.4",
"hono": "^4.12.26",
"mermaid": "^11.14.0",
"monaco-editor": "^0.55.1",
"ox": "0.14.20",
"posthog-js": "^1.367.0",
"posthog-node": "^5.29.2",
"prool": "^0.2.4",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-server-dom-webpack": "~19.2.6",
"sonner": "^2.0.7",
"sql-formatter": "^15.7.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"unplugin-auto-import": "^21.0.0",
"unplugin-icons": "^23.0.1",
"viem": "^2.52.2",
"vocs": "2.1.1",
"wagmi": "3.6.14",
"waku": "^1.0.0-beta.0",
"webauthx": "~0.1.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
"@playwright/test": "^1.58.0",
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260122.3",
"@vitejs/plugin-react": "^6.0.2",
"anser": "^2.3.5",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^8.0.16",
"vite-plugin-mkcert": "^1.17.12",
"vitest": "^4.1.9"
},
"devEngines": {
"runtime": {
"name": "node",
"version": "24.12.0",
"onFail": "download"
}
}
}
diff --git a/dist/constants.mjs b/dist/constants.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..3f84d3e87ee0e17a8d0c655dca7c8c9bea747855
--- /dev/null
+++ b/dist/constants.mjs
@@ -0,0 +1,8 @@
+export const invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im;
+export const htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g;
+export const htmlCtrlEntityRegex = /&(newline|tab);/gi;
+export const ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim;
+export const urlSchemeRegex = /^.+(:|:)/gim;
+export const whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g;
+export const relativeFirstCharacters = [".", "/"];
+export const BLANK_URL = "about:blank";
diff --git a/dist/index.mjs b/dist/index.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..bfe926a4db36ae8fc0925ab41e185e2332190f65
--- /dev/null
+++ b/dist/index.mjs
@@ -0,0 +1,86 @@
+import {
+ relativeFirstCharacters,
+ ctrlCharactersRegex,
+ htmlEntitiesRegex,
+ htmlCtrlEntityRegex,
+ whitespaceEscapeCharsRegex,
+ urlSchemeRegex,
+ invalidProtocolRegex,
+ BLANK_URL
+} from './constants.mjs';
+
+function isRelativeUrlWithoutProtocol(url) {
+ return relativeFirstCharacters.indexOf(url[0]) > -1;
+}
+
+function decodeHtmlCharacters(str) {
+ var removedNullByte = str.replace(ctrlCharactersRegex, "");
+ return removedNullByte.replace(htmlEntitiesRegex, function (match, dec) {
+ return String.fromCharCode(dec);
+ });
+}
+
+function isValidUrl(url) {
+ return URL.canParse(url);
+}
+
+function decodeURI(uri) {
+ try {
+ return decodeURIComponent(uri);
+ } catch (e) {
+ return uri;
+ }
+}
+
+export function sanitizeUrl(url) {
+ if (!url) {
+ return BLANK_URL;
+ }
+ var charsToDecode;
+ var decodedUrl = decodeURI(url.trim());
+ do {
+ decodedUrl = decodeHtmlCharacters(decodedUrl)
+ .replace(htmlCtrlEntityRegex, "")
+ .replace(ctrlCharactersRegex, "")
+ .replace(whitespaceEscapeCharsRegex, "")
+ .trim();
+ decodedUrl = decodeURI(decodedUrl);
+ charsToDecode =
+ decodedUrl.match(ctrlCharactersRegex) ||
+ decodedUrl.match(htmlEntitiesRegex) ||
+ decodedUrl.match(htmlCtrlEntityRegex) ||
+ decodedUrl.match(whitespaceEscapeCharsRegex);
+ } while (charsToDecode && charsToDecode.length > 0);
+ var sanitizedUrl = decodedUrl;
+ if (!sanitizedUrl) {
+ return BLANK_URL;
+ }
+ if (isRelativeUrlWithoutProtocol(sanitizedUrl)) {
+ return sanitizedUrl;
+ }
+ var trimmedUrl = sanitizedUrl.trimStart();
+ var urlSchemeParseResults = trimmedUrl.match(urlSchemeRegex);
+ if (!urlSchemeParseResults) {
+ return sanitizedUrl;
+ }
+ var urlScheme = urlSchemeParseResults[0].toLowerCase().trim();
+ if (invalidProtocolRegex.test(urlScheme)) {
+ return BLANK_URL;
+ }
+ var backSanitized = trimmedUrl.replace(/\\/g, "/");
+ if (urlScheme === "mailto:" || urlScheme.includes("://")) {
+ return backSanitized;
+ }
+ if (urlScheme === "http:" || urlScheme === "https:") {
+ if (!isValidUrl(backSanitized)) {
+ return BLANK_URL;
+ }
+ var url_1 = new URL(backSanitized);
+ url_1.protocol = url_1.protocol.toLowerCase();
+ url_1.hostname = url_1.hostname.toLowerCase();
+ return url_1.toString();
+ }
+ return backSanitized;
+}
+
+export default { sanitizeUrl };
diff --git a/package.json b/package.json
index 39aca294ea8eacfb2db580b99bd12d21c79e8c15..b41790cd8f42b78c17df66f145146cd6f02f9b35 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,16 @@
"version": "7.1.2",
"description": "A url sanitizer",
"main": "dist/index.js",
+ "module": "dist/index.mjs",
"types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ },
+ "./dist/constants.mjs": "./dist/constants.mjs"
+ },
"author": "",
"scripts": {
"prepublishOnly": "npm run build",
diff --git a/package.json b/package.json
index fb0b8b2a3d27b0f97b0230845efbfb4a31553e65..722701819d3ee81a4e4966e57d47a26bb5eb5d95 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,16 @@
"version": "1.11.20",
"description": "2KB immutable date time library alternative to Moment.js with the same modern API ",
"main": "dayjs.min.js",
+ "module": "esm/index.js",
"types": "index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./esm/index.js",
+ "require": "./dayjs.min.js",
+ "types": "./index.d.ts"
+ },
+ "./*": "./*"
+ },
"scripts": {
"test": "TZ=Pacific/Auckland npm run test-tz && TZ=Europe/London npm run test-tz && TZ=America/Whitehorse npm run test-tz && npm run test-tz && jest --coverage --coverageThreshold='{ \"global\": { \"lines\": 100} }'",
"test-tz": "date && jest test/timezone.test --coverage=false",
import { defineConfig, devices } from '@playwright/test'
const isCI = !!process.env.CI
const webServerUrl = isCI ? 'http://localhost:5173' : 'https://localhost:5173'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 1 : 1, // Retry once due to testnet flakiness
workers: isCI ? 1 : undefined,
timeout: 180000, // 3 min default timeout for testnet transactions
reporter: 'html',
use: {
baseURL: webServerUrl,
ignoreHTTPSErrors: true,
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: isCI
? 'PORT=5173 VITE_E2E=true VITE_USE_HTTP=true node dist/preview.js'
: 'pnpm run dev 2>/dev/null',
url: webServerUrl,
ignoreHTTPSErrors: true,
reuseExistingServer: !isCI,
stdout: 'ignore',
stderr: 'ignore',
},
})
strictDepBuilds: true
blockExoticSubdeps: false
trustPolicy: no-downgrade
trustPolicyExclude:
- reselect@5.1.1
- semver@6.3.1
minimumReleaseAge: 1440
onlyBuiltDependencies:
- core-js
- es5-ext
- esbuild
- vue-demi
ignoredBuiltDependencies:
- protobufjs
minimumReleaseAgeExclude:
- accounts
- mppx
- incur
- ox
- viem
- vocs
- vite
- '@vocs/twoslash-rust'
- '@vocs/twoslash-rust-darwin-arm64'
- '@vocs/twoslash-rust-linux-x64-gnu'
overrides:
protobufjs: '>=7.5.5'
tar: '>=7.5.13'
dompurify: '>=3.4.0'
patchedDependencies:
'@braintree/sanitize-url@7.1.2': patches/@braintree__sanitize-url@7.1.2.patch
dayjs@1.11.20: patches/dayjs@1.11.20.patch
google-site-verification: google6bae3dfb1e9772fa.html<br> <br>
<p align="center"> <a href="https://tempo.xyz"> <picture> <source media="(prefers-color-scheme: dark)" srcset="/public/lockup-dark.svg"> <img alt="tempo lockup" src="/public/lockup-light.svg" width="auto" height="80"> </picture> </a> </p>
<br> <br>
Tempo Documentation
This repository contains documentation for the Tempo blockchain, including the Protocol Specifications, Litepaper, and Getting Started guides.
Usage
pnpm installpnpm dev # Start development server
pnpm check # Run linting + formatting
pnpm check:types # Run type checks
pnpm build # Build for production
pnpm preview # Preview the production buildContributing
Our contributor guidelines can be found in `CONTRIBUTING.md`.
Security
See `SECURITY.md`.
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in these crates by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
#!/usr/bin/env node
/**
* Bundle size analysis and diff tool for the docs site (Vocs/Waku)
*
* Scans built JS files in dist/public/assets/ and measures raw + brotli sizes.
* Groups chunks into: framework, heavy-deps, app.
*
* Usage:
* node --experimental-strip-types scripts/bundle-diff.ts
* node --experimental-strip-types scripts/bundle-diff.ts --save
* node --experimental-strip-types scripts/bundle-diff.ts --ci
*
* CLI flags:
* --ci - Output markdown for GitHub PR comments
* --save - Save current sizes as baseline
* --baseline <file> - Read baseline from specific file path
* --output <file> - Write current stats to file (for caching)
* --skip-build - Skip build step (use existing dist/)
*/
import { execSync } from 'node:child_process'
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { brotliCompressSync, constants } from 'node:zlib'
const ASSETS_DIR = 'dist/public/assets'
const BASELINE_FILE = '.bundle-baseline.json'
const BROTLI_WARNING_KB = 10
const BROTLI_STRONG_WARNING_KB = 25
const HEAVY_DEPS_PATTERNS = [
/^wagmi/i,
/^viem/i,
/^mermaid/i,
/^monaco/i,
/^cytoscape/i,
/^katex/i,
/^accounts/i,
/^tanstack/i,
/^treemap/i,
/^sql-formatter/i,
/^QueryClientProvider/,
/^useQuery/,
// mermaid sub-diagram chunks
/Diagram-/,
/^dagre-/,
/^cose-bilkent/,
/^elk-/,
/^arc-/,
]
const FRAMEWORK_PATTERNS = [
/^Link-/,
/^_layout-/,
/^_mdx-wrapper-/,
/^client-/,
/^context-/,
/^module-/,
/^facade_vocs/,
/^Head-/,
/^layout-/,
/^MdxPageContext-/,
]
interface CIOptions {
ci: boolean
baselinePath: string | null
outputPath: string | null
skipBuild: boolean
save: boolean
}
function parseArgs(args: string[]): CIOptions {
const options: CIOptions = {
ci: false,
baselinePath: null,
outputPath: null,
skipBuild: false,
save: false,
}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg === '--ci') {
options.ci = true
} else if (arg === '--baseline' && args[i + 1]) {
options.baselinePath = args[++i]
} else if (arg === '--output' && args[i + 1]) {
options.outputPath = args[++i]
} else if (arg === '--skip-build') {
options.skipBuild = true
} else if (arg === '--save') {
options.save = true
}
}
return options
}
interface ChunkInfo {
label: string
size: number
brotliSize: number
group: string
}
interface SizeAggregate {
size: number
brotli: number
}
interface GroupStats {
label: string
aggregate: SizeAggregate
}
interface BundleStats {
timestamp: string
total: SizeAggregate
groups: GroupStats[]
chunks: ChunkInfo[]
}
function formatBytes(
bytes: number,
signDisplay: Intl.NumberFormatOptions['signDisplay'] = 'auto',
): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
const value = bytes / k ** i
const formatted = new Intl.NumberFormat('en', {
signDisplay,
maximumFractionDigits: 1,
minimumFractionDigits: 1,
}).format(value)
return `${formatted} ${sizes[i]}`
}
function formatDelta(current: number, baseline: number): string {
return formatBytes(current - baseline, 'exceptZero')
}
function categorize(filename: string): string {
if (HEAVY_DEPS_PATTERNS.some((p) => p.test(filename))) return 'heavy-deps'
if (FRAMEWORK_PATTERNS.some((p) => p.test(filename))) return 'framework'
return 'app'
}
function parseStats(assetsDir: string): BundleStats {
const absDir = resolve(process.cwd(), assetsDir)
const files = readdirSync(absDir).filter((f) => f.endsWith('.js'))
const chunks: ChunkInfo[] = []
for (const file of files) {
const filePath = join(absDir, file)
const raw = readFileSync(filePath)
const rawSize = statSync(filePath).size
const brotli = brotliCompressSync(raw, {
params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY },
})
chunks.push({
label: file,
size: rawSize,
brotliSize: brotli.length,
group: categorize(file),
})
}
chunks.sort((a, b) => b.brotliSize - a.brotliSize)
const aggregate = (group: string): SizeAggregate =>
chunks
.filter((c) => c.group === group)
.reduce(
(acc, chunk) => ({
size: acc.size + chunk.size,
brotli: acc.brotli + chunk.brotliSize,
}),
{ size: 0, brotli: 0 },
)
const total: SizeAggregate = chunks.reduce(
(acc, chunk) => ({
size: acc.size + chunk.size,
brotli: acc.brotli + chunk.brotliSize,
}),
{ size: 0, brotli: 0 },
)
const groupLabels = ['framework', 'heavy-deps', 'app'] as const
const groups: GroupStats[] = groupLabels.map((label) => ({
label,
aggregate: aggregate(label),
}))
return {
timestamp: new Date().toISOString(),
total,
groups,
chunks,
}
}
function findBaselineGroup(baseline: BundleStats, label: string): SizeAggregate | null {
return baseline.groups.find((g) => g.label === label)?.aggregate ?? null
}
function printReport(current: BundleStats, baseline: BundleStats | null, options: CIOptions): void {
if (options.ci) {
printMarkdownReport(current, baseline)
} else {
printTerminalReport(current, baseline)
}
}
function printTerminalReport(current: BundleStats, baseline: BundleStats | null): void {
console.log(`\n${'='.repeat(60)}`)
console.log('Docs Bundle Size Analysis')
console.log('='.repeat(60))
console.log('\nAll sizes are brotli-compressed.\n')
console.log('Current Build:')
console.log(` Total: ${formatBytes(current.total.brotli)}`)
for (const { label, aggregate } of current.groups) {
console.log(` ${label}: ${formatBytes(aggregate.brotli)}`)
}
if (baseline) {
console.log('\nBaseline:')
console.log(` Total: ${formatBytes(baseline.total.brotli)}`)
for (const { label } of current.groups) {
const bg = findBaselineGroup(baseline, label)
console.log(` ${label}: ${bg ? formatBytes(bg.brotli) : '—'}`)
}
console.log('\nDelta:')
console.log(` Total: ${formatDelta(current.total.brotli, baseline.total.brotli)}`)
for (const { label, aggregate } of current.groups) {
const bg = findBaselineGroup(baseline, label)
console.log(` ${label}: ${bg ? formatDelta(aggregate.brotli, bg.brotli) : '—'}`)
}
}
for (const groupLabel of ['framework', 'heavy-deps', 'app'] as const) {
const groupChunks = current.chunks.filter((c) => c.group === groupLabel)
if (groupChunks.length === 0) continue
console.log(`\n ${groupLabel} chunks (${groupChunks.length}):`)
for (const chunk of groupChunks.slice(0, 10)) {
const name = chunk.label.padEnd(50)
console.log(
` ${name} ${formatBytes(chunk.brotliSize).padStart(10)} (raw: ${formatBytes(chunk.size)})`,
)
}
if (groupChunks.length > 10) {
console.log(` ... and ${groupChunks.length - 10} more ${groupLabel} chunks`)
}
}
if (!baseline) {
console.log('\n No baseline found. Run with --save to save current as baseline.')
}
console.log(`\n${'='.repeat(60)}\n`)
}
function printMarkdownReport(current: BundleStats, baseline: BundleStats | null): void {
let output = ''
output += '> All sizes are brotli-compressed.\n\n'
if (baseline) {
const totalDelta = current.total.brotli - baseline.total.brotli
const emoji = totalDelta > 0 ? '📈' : totalDelta < 0 ? '📉' : '➡️'
output += `${emoji} **Total:** ${formatBytes(current.total.brotli)} (${formatBytes(totalDelta, 'exceptZero')})\n\n`
output += '| | Current | Baseline | Delta |\n'
output += '|--|---------|----------|-------|\n'
output += `| Total | ${formatBytes(current.total.brotli)} | ${formatBytes(baseline.total.brotli)} | ${formatDelta(current.total.brotli, baseline.total.brotli)} |\n`
for (const { label, aggregate } of current.groups) {
const bg = findBaselineGroup(baseline, label)
if (bg) {
output += `| ${label} | ${formatBytes(aggregate.brotli)} | ${formatBytes(bg.brotli)} | ${formatDelta(aggregate.brotli, bg.brotli)} |\n`
} else {
output += `| ${label} | ${formatBytes(aggregate.brotli)} | — | — |\n`
}
}
const deltaKb = totalDelta / 1024
if (deltaKb > BROTLI_STRONG_WARNING_KB) {
output += `\n> [!WARNING]\n> Total bundle increased by ${deltaKb.toFixed(1)} KB (exceeds ${BROTLI_STRONG_WARNING_KB} KB)!\n`
} else if (deltaKb > BROTLI_WARNING_KB) {
output += `\n> [!NOTE]\n> Total bundle increased by ${deltaKb.toFixed(1)} KB (exceeds ${BROTLI_WARNING_KB} KB)\n`
}
} else {
output += `**Total:** ${formatBytes(current.total.brotli)}\n\n`
output += '| | Size |\n'
output += '|--|------|\n'
output += `| Total | ${formatBytes(current.total.brotli)} |\n`
for (const { label, aggregate } of current.groups) {
output += `| ${label} | ${formatBytes(aggregate.brotli)} |\n`
}
output += '\n*No baseline available for comparison*\n'
}
for (const groupLabel of ['framework', 'heavy-deps', 'app'] as const) {
const groupChunks = current.chunks.filter((c) => c.group === groupLabel)
if (groupChunks.length === 0) continue
output += `\n<details>\n<summary>${groupLabel} chunks (${groupChunks.length})</summary>\n\n`
output += '| Chunk | Size | Raw |\n'
output += '|-------|------|-----|\n'
for (const chunk of groupChunks) {
const name = chunk.label.length > 45 ? `${chunk.label.slice(0, 42)}...` : chunk.label
output += `| \`${name}\` | ${formatBytes(chunk.brotliSize)} | ${formatBytes(chunk.size)} |\n`
}
output += '\n</details>\n'
}
console.log(output)
}
async function main() {
const args = process.argv.slice(2)
const options = parseArgs(args)
if (!options.skipBuild) {
console.log('Building docs site...')
execSync('pnpm build', { stdio: 'inherit' })
}
const assetsDir = resolve(process.cwd(), ASSETS_DIR)
if (!existsSync(assetsDir)) {
console.error(`Assets directory not found: ${assetsDir}`)
console.error('Make sure to build first: pnpm build')
process.exit(1)
}
const current = parseStats(ASSETS_DIR)
if (options.outputPath) {
writeFileSync(options.outputPath, JSON.stringify(current, null, 2))
console.log(`Stats written to ${options.outputPath}`)
return
}
if (options.save) {
writeFileSync(BASELINE_FILE, JSON.stringify(current, null, 2))
console.log(`Baseline saved to ${BASELINE_FILE}`)
printReport(current, null, options)
return
}
let baseline: BundleStats | null = null
if (options.baselinePath && existsSync(options.baselinePath)) {
baseline = JSON.parse(readFileSync(options.baselinePath, 'utf-8')) as BundleStats
} else if (existsSync(BASELINE_FILE)) {
baseline = JSON.parse(readFileSync(BASELINE_FILE, 'utf-8')) as BundleStats
}
printReport(current, baseline, options)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})
#!/bin/bash
# Download OG images for all docs routes
OUT="/Users/achal/Desktop/og-preview"
BASE="http://localhost:5173"
mkdir -p "$OUT"
download_og() {
local title="$1"
local section="$2"
local subsection="$3"
local filename="$4"
local url="${BASE}/api/og?title=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$title'))")§ion=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$section'))")"
if [ -n "$subsection" ]; then
url="${url}&subsection=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$subsection'))")"
fi
curl -s -o "${OUT}/${filename}.webp" "$url"
echo " Downloaded: ${filename}"
}
echo "=== Landing pages (static) ==="
curl -s -o "${OUT}/landing--home.png" "${BASE}/og-docs.png"
echo " Downloaded: landing--home.png"
echo ""
echo "=== Top-level section pages (TEMPO + section) ==="
download_og "Getting Funds on Tempo" "BUILD" "" "build--getting-funds"
download_og "Create & Use Accounts" "BUILD" "" "build--use-accounts"
download_og "Make Payments" "BUILD" "" "build--payments"
download_og "Issue Stablecoins" "BUILD" "" "build--issuance"
download_og "Exchange Stablecoins" "BUILD" "" "build--stablecoin-dex"
download_og "Make Machine Payments" "BUILD" "" "build--machine-payments"
download_og "Use Tempo Transactions" "BUILD" "" "build--tempo-transaction"
download_og "Developing with LLMs" "BUILD" "" "build--ai"
download_og "Overview" "INTEGRATE" "" "integrate--overview"
download_og "Connect to the Network" "INTEGRATE" "" "integrate--connection-details"
download_og "Get Testnet Faucet Funds" "INTEGRATE" "" "integrate--faucet"
download_og "EVM Differences" "INTEGRATE" "" "integrate--evm-compatibility"
download_og "Predeployed Contracts" "INTEGRATE" "" "integrate--predeployed-contracts"
download_og "Token List Registry" "INTEGRATE" "" "integrate--tokenlist"
download_og "Wallet Developers" "INTEGRATE" "" "integrate--wallet-developers"
download_og "Contract Verification" "INTEGRATE" "" "integrate--verify-contracts"
download_og "Overview" "PROTOCOL" "" "protocol--overview"
download_og "Blockspace" "PROTOCOL" "" "protocol--blockspace"
download_og "Fees" "PROTOCOL" "" "protocol--fees"
download_og "TIPs" "PROTOCOL" "" "protocol--tips"
download_og "Overview" "SDKs" "" "sdk--overview"
download_og "TypeScript" "SDKs" "" "sdk--typescript"
download_og "Go" "SDKs" "" "sdk--go"
download_og "Foundry" "SDKs" "" "sdk--foundry"
download_og "Python" "SDKs" "" "sdk--python"
download_og "Rust" "SDKs" "" "sdk--rust"
download_og "Overview" "CLI" "" "cli--overview"
download_og "Wallet" "CLI" "" "cli--wallet"
download_og "Request" "CLI" "" "cli--request"
download_og "Download" "CLI" "" "cli--download"
download_og "Node" "CLI" "" "cli--node"
download_og "Overview" "ECOSYSTEM" "" "ecosystem--overview"
download_og "Bridges & Exchanges" "ECOSYSTEM" "" "ecosystem--bridges"
download_og "Wallets" "ECOSYSTEM" "" "ecosystem--wallets"
download_og "Overview" "LEARN" "" "learn--overview"
download_og "Partners" "LEARN" "" "learn--partners"
download_og "Stablecoins" "LEARN" "" "learn--stablecoins"
echo ""
echo "=== Subsection pages (section + subsection) ==="
download_og "Embed Passkey Accounts" "BUILD" "ACCOUNTS" "build--accounts--embed-passkeys"
download_og "Connect to Wallets" "BUILD" "ACCOUNTS" "build--accounts--connect-to-wallets"
download_og "Add Funds to Your Balance" "BUILD" "ACCOUNTS" "build--accounts--add-funds"
download_og "Send a Payment" "BUILD" "PAYMENTS" "build--payments--send"
download_og "Accept a Payment" "BUILD" "PAYMENTS" "build--payments--accept"
download_og "Attach a Transfer Memo" "BUILD" "PAYMENTS" "build--payments--transfer-memos"
download_og "Pay Fees in Any Stablecoin" "BUILD" "PAYMENTS" "build--payments--pay-fees"
download_og "Sponsor User Fees" "BUILD" "PAYMENTS" "build--payments--sponsor-user-fees"
download_og "Send Parallel Transactions" "BUILD" "PAYMENTS" "build--payments--parallel"
download_og "Create a Stablecoin" "BUILD" "ISSUANCE" "build--issuance--create"
download_og "Mint Stablecoins" "BUILD" "ISSUANCE" "build--issuance--mint"
download_og "Use Your Stablecoin for Fees" "BUILD" "ISSUANCE" "build--issuance--use-for-fees"
download_og "Distribute Rewards" "BUILD" "ISSUANCE" "build--issuance--distribute-rewards"
download_og "Manage Your Stablecoin" "BUILD" "ISSUANCE" "build--issuance--manage"
download_og "Managing Fee Liquidity" "BUILD" "EXCHANGE" "build--exchange--fee-liquidity"
download_og "Executing Swaps" "BUILD" "EXCHANGE" "build--exchange--executing-swaps"
download_og "View the Orderbook" "BUILD" "EXCHANGE" "build--exchange--orderbook"
download_og "Providing Liquidity" "BUILD" "EXCHANGE" "build--exchange--providing-liquidity"
download_og "Client Quickstart" "BUILD" "MACHINE PAY" "build--machine-pay--client"
download_og "Agent Quickstart" "BUILD" "MACHINE PAY" "build--machine-pay--agent"
download_og "Server Quickstart" "BUILD" "MACHINE PAY" "build--machine-pay--server"
download_og "Accept One-Time Payments" "BUILD" "MACHINE PAY" "build--machine-pay--one-time"
download_og "Accept Pay-as-you-go Payments" "BUILD" "MACHINE PAY" "build--machine-pay--payg"
download_og "Accept Streamed Payments" "BUILD" "MACHINE PAY" "build--machine-pay--streamed"
download_og "Specification" "PROTOCOL" "TIP-20" "protocol--tip20--spec"
download_og "Overview" "PROTOCOL" "TIP-20" "protocol--tip20--overview"
download_og "Specification" "PROTOCOL" "TIP-20 REWARDS" "protocol--tip20-rewards--spec"
download_og "Specification" "PROTOCOL" "TIP-403" "protocol--tip403--spec"
download_og "Specification" "PROTOCOL" "FEES" "protocol--fees--spec"
download_og "Fee AMM" "PROTOCOL" "FEES" "protocol--fees--fee-amm"
download_og "Specification" "PROTOCOL" "TRANSACTIONS" "protocol--transactions--spec"
download_og "EIP-4337 Comparison" "PROTOCOL" "TRANSACTIONS" "protocol--transactions--eip4337"
download_og "EIP-7702 Comparison" "PROTOCOL" "TRANSACTIONS" "protocol--transactions--eip7702"
download_og "Account Keychain Precompile Specification" "PROTOCOL" "TRANSACTIONS" "protocol--transactions--keychain"
download_og "Payment Lane Specification" "PROTOCOL" "BLOCKSPACE" "protocol--blockspace--payment-lane"
download_og "Consensus and Finality" "PROTOCOL" "BLOCKSPACE" "protocol--blockspace--consensus"
download_og "Specification" "PROTOCOL" "DEX" "protocol--dex--spec"
download_og "Quote Tokens" "PROTOCOL" "DEX" "protocol--dex--quote-tokens"
download_og "Executing Swaps" "PROTOCOL" "DEX" "protocol--dex--executing-swaps"
download_og "Providing Liquidity" "PROTOCOL" "DEX" "protocol--dex--providing-liquidity"
download_og "DEX Balance" "PROTOCOL" "DEX" "protocol--dex--balance"
download_og "Handlers" "SDKs" "TYPESCRIPT" "sdk--typescript--handlers"
download_og "compose" "SDKs" "TYPESCRIPT" "sdk--typescript--compose"
download_og "feePayer" "SDKs" "TYPESCRIPT" "sdk--typescript--feePayer"
download_og "keyManager" "SDKs" "TYPESCRIPT" "sdk--typescript--keyManager"
download_og "System Requirements" "BUILD" "NODE" "node--system-requirements"
download_og "Installation" "BUILD" "NODE" "node--installation"
download_og "Running an RPC Node" "BUILD" "NODE" "node--rpc"
download_og "Running a Validator" "BUILD" "NODE" "node--validator"
download_og "Operating Your Validator" "BUILD" "NODE" "node--operate-validator"
download_og "Network Upgrades and Releases" "BUILD" "NODE" "node--network-upgrades"
download_og "Remittances" "LEARN" "USE CASES" "learn--use-cases--remittances"
download_og "Global Payouts" "LEARN" "USE CASES" "learn--use-cases--global-payouts"
download_og "Payroll" "LEARN" "USE CASES" "learn--use-cases--payroll"
download_og "Embedded Finance" "LEARN" "USE CASES" "learn--use-cases--embedded-finance"
download_og "Tokenized Deposits" "LEARN" "USE CASES" "learn--use-cases--tokenized-deposits"
download_og "Microtransactions" "LEARN" "USE CASES" "learn--use-cases--microtransactions"
download_og "Agentic Commerce" "LEARN" "USE CASES" "learn--use-cases--agentic-commerce"
download_og "Native Stablecoins" "LEARN" "TEMPO" "learn--tempo--native-stablecoins"
download_og "Modern Transactions" "LEARN" "TEMPO" "learn--tempo--modern-transactions"
download_og "Performance" "LEARN" "TEMPO" "learn--tempo--performance"
download_og "Onchain FX" "LEARN" "TEMPO" "learn--tempo--fx"
download_og "Privacy" "LEARN" "TEMPO" "learn--tempo--privacy"
download_og "Machine Payments" "LEARN" "TEMPO" "learn--tempo--machine-payments"
echo ""
echo "=== Done ==="
ls -1 "$OUT" | wc -l
echo "images saved to $OUT"
#!/usr/bin/env node
/**
* Lighthouse performance measurement for the docs site.
*
* Usage:
* # Build + preview, then run against preview server:
* pnpm build && pnpm preview &
* pnpm lighthouse --url http://localhost:4173
*
* # Or against dev server (may 500 in headless Chrome):
* pnpm lighthouse --url https://localhost:5173
*
* # Save baseline, make changes, compare:
* pnpm lighthouse --save baseline.json
* pnpm lighthouse --compare baseline.json
*
* # Mobile throttling:
* pnpm lighthouse:mobile
*/
import { execSync } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
const DEFAULT_PAGES = [
'/',
'/guide/issuance/create-a-stablecoin',
'/guide/payments/send-a-payment',
'/guide/stablecoin-dex/executing-swaps',
'/guide/stablecoin-dex/providing-liquidity',
'/guide/machine-payments/client',
]
interface PageResult {
page: string
performance: number
fcp: number
lcp: number
tbt: number
cls: number
tti: number
}
type LighthouseAudit = {
numericValue?: number
}
type LighthouseReport = {
runtimeError?: {
code?: string
message: string
}
categories?: {
performance?: {
score?: number
}
}
audits?: Record<string, LighthouseAudit>
}
type ExecError = Error & {
stderr?: Buffer | string
}
function parseArgs(argv: string[]) {
const args = argv.slice(2)
const flags = {
url: 'https://localhost:5173',
pages: DEFAULT_PAGES,
mobile: false,
json: false,
compare: '',
save: '',
}
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--url':
flags.url = args[++i]
break
case '--pages':
flags.pages = args[++i].split(',')
break
case '--mobile':
flags.mobile = true
break
case '--json':
flags.json = true
break
case '--compare':
flags.compare = args[++i]
break
case '--save':
flags.save = args[++i]
break
}
}
return flags
}
const LH_OUTPUT = '/tmp/lighthouse-result.json'
function runLighthouse(url: string, mobile: boolean): LighthouseReport | null {
const preset = mobile ? 'perf' : 'desktop'
// Run npx from /tmp to avoid devEngines conflicts in the docs package.json
// Use --output-path to avoid pipe truncation on large JSON output
const cmd = `npx lighthouse "${url}" --output=json --output-path=${LH_OUTPUT} --chrome-flags="--headless --no-sandbox --ignore-certificate-errors" --preset=${preset} --quiet`
try {
execSync(cmd, {
encoding: 'utf-8',
maxBuffer: 50 * 1024 * 1024,
stdio: ['pipe', 'pipe', 'pipe'],
cwd: '/tmp',
})
const raw = readFileSync(LH_OUTPUT, 'utf-8')
const report = JSON.parse(raw) as LighthouseReport
// Check for runtime errors (e.g., page returned 500)
if (report.runtimeError?.code) {
console.error(` ✗ ${report.runtimeError.message.split('.')[0]}`)
return null
}
return report
} catch (err) {
console.error(` ✗ Lighthouse failed for ${url}`)
if (err instanceof Error) {
const stderr = (err as ExecError).stderr?.toString() || ''
const meaningful = stderr
.split('\n')
.filter((l: string) => !l.includes('npm warn') && l.trim())
.slice(0, 3)
.join('\n ')
if (meaningful) console.error(` ${meaningful}`)
}
return null
}
}
function extractMetrics(report: LighthouseReport, page: string): PageResult {
const score = Math.round((report.categories?.performance?.score ?? 0) * 100)
const audits = report.audits ?? {}
return {
page,
performance: score,
fcp: audits['first-contentful-paint']?.numericValue ?? 0,
lcp: audits['largest-contentful-paint']?.numericValue ?? 0,
tbt: audits['total-blocking-time']?.numericValue ?? 0,
cls: audits['cumulative-layout-shift']?.numericValue ?? 0,
tti: audits.interactive?.numericValue ?? 0,
}
}
function formatMs(ms: number): string {
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`
return `${Math.round(ms)}ms`
}
function formatCls(cls: number): string {
return cls.toFixed(3)
}
function pad(str: string, len: number, right = false): string {
if (right) return str.padStart(len)
return str.padEnd(len)
}
function printTable(results: PageResult[]) {
const pageWidth = Math.max(40, ...results.map((r) => r.page.length + 2))
const header = `${pad('Page', pageWidth)}${pad('Perf', 7, true)}${pad('FCP', 9, true)}${pad('LCP', 9, true)}${pad('TBT', 9, true)}${pad('CLS', 8, true)}`
console.log(header)
console.log('─'.repeat(header.length))
for (const r of results) {
const line = `${pad(r.page, pageWidth)}${pad(String(r.performance), 7, true)}${pad(formatMs(r.fcp), 9, true)}${pad(formatMs(r.lcp), 9, true)}${pad(formatMs(r.tbt), 9, true)}${pad(formatCls(r.cls), 8, true)}`
console.log(line)
}
}
function colorDelta(value: number, lowerIsBetter: boolean): string {
const sign = value > 0 ? '+' : ''
const formatted = `${sign}${value.toFixed(1)}`
// Green = improvement, Red = regression
const improved = lowerIsBetter ? value < 0 : value > 0
const regressed = lowerIsBetter ? value > 0 : value < 0
if (improved) return `\x1b[32m${formatted}\x1b[0m`
if (regressed) return `\x1b[31m${formatted}\x1b[0m`
return formatted
}
function printComparison(current: PageResult[], baseline: PageResult[]) {
const baselineMap = new Map(baseline.map((r) => [r.page, r]))
const pageWidth = Math.max(40, ...current.map((r) => r.page.length + 2))
const header = `${pad('Page', pageWidth)}${pad('Perf', 12, true)}${pad('FCP', 16, true)}${pad('LCP', 16, true)}${pad('TBT', 16, true)}${pad('CLS', 14, true)}`
console.log(header)
console.log('─'.repeat(header.length))
for (const r of current) {
const b = baselineMap.get(r.page)
if (!b) {
console.log(`${pad(r.page, pageWidth)} (new page, no baseline)`)
continue
}
const perfDelta = r.performance - b.performance
const fcpDelta = r.fcp - b.fcp
const lcpDelta = r.lcp - b.lcp
const tbtDelta = r.tbt - b.tbt
const clsDelta = r.cls - b.cls
const line =
`${pad(r.page, pageWidth)}` +
`${pad(`${r.performance} (${colorDelta(perfDelta, false)})`, 12, true)}` +
`${pad(`${formatMs(r.fcp)} (${colorDelta(fcpDelta, true)})`, 16, true)}` +
`${pad(`${formatMs(r.lcp)} (${colorDelta(lcpDelta, true)})`, 16, true)}` +
`${pad(`${formatMs(r.tbt)} (${colorDelta(tbtDelta, true)})`, 16, true)}` +
`${pad(`${formatCls(r.cls)} (${colorDelta(clsDelta, true)})`, 14, true)}`
console.log(line)
}
}
function main() {
const flags = parseArgs(process.argv)
console.log(`\n🔦 Lighthouse Performance Audit`)
console.log(` Base URL: ${flags.url}`)
console.log(` Mode: ${flags.mobile ? 'mobile' : 'desktop'}`)
console.log(` Pages: ${flags.pages.length}\n`)
const results: PageResult[] = []
for (const page of flags.pages) {
const fullUrl = `${flags.url.replace(/\/$/, '')}${page}`
process.stdout.write(` Testing ${page} ... `)
const report = runLighthouse(fullUrl, flags.mobile)
if (!report) {
results.push({ page, performance: 0, fcp: 0, lcp: 0, tbt: 0, cls: 0, tti: 0 })
continue
}
const metrics = extractMetrics(report, page)
results.push(metrics)
console.log(`✓ ${metrics.performance}/100`)
}
console.log('')
if (flags.compare) {
const baseline: PageResult[] = JSON.parse(readFileSync(flags.compare, 'utf-8'))
printComparison(results, baseline)
} else {
printTable(results)
}
if (flags.save) {
writeFileSync(flags.save, JSON.stringify(results, null, 2))
console.log(`\n💾 Results saved to ${flags.save}`)
}
if (flags.json) {
console.log(`\n${JSON.stringify(results, null, 2)}`)
}
}
main()
/**
* OG image coverage probe.
*
* Walks every route file under `src/pages/`, replicates the `ogImageUrl`
* logic from `vocs.config.ts` to derive each route's (section, subsection),
* collapses to the unique buckets, then probes each bucket against a
* running server and verifies it returns a valid image response.
*
* Two layers of validation:
* 1. Mapping coverage — fails if any route hits the auto-uppercase
* fallback path instead of an explicit map entry.
* 2. Runtime — every static landing image, every dynamic bucket, and a
* handful of title edge cases must return 2xx with image/* content-type.
*
* Usage (against a `pnpm dev` server):
* VITE_USE_HTTP=true pnpm dev --port 5181
* PREVIEW_URL=http://localhost:5181 pnpm og:probe
*
* Keep the maps below in sync with vocs.config.ts.
*/
import { readdirSync, statSync, writeFileSync } from 'node:fs'
import { join, relative } from 'node:path'
const PREVIEW = process.env.PREVIEW_URL ?? 'https://tempo-docs-k6tznt1fw-tempoxyz.vercel.app'
const PAGES_DIR = 'src/pages'
const sectionMap: Record<string, string> = {
quickstart: 'INTEGRATE',
guide: 'BUILD',
protocol: 'PROTOCOL',
sdk: 'SDKs',
cli: 'CLI',
ecosystem: 'ECOSYSTEM',
learn: 'LEARN',
wallet: 'WALLET',
accounts: 'ACCOUNTS',
}
const subsectionMap: Record<string, string> = {
'use-accounts': 'ACCOUNTS',
payments: 'PAYMENTS',
issuance: 'ISSUANCE',
'stablecoin-dex': 'EXCHANGE',
'machine-payments': 'MACHINE PAY',
'tempo-transaction': 'TRANSACTIONS',
tip20: 'TIP-20',
'tip20-rewards': 'REWARDS',
tip403: 'TIP-403',
fees: 'FEES',
transactions: 'TRANSACTIONS',
blockspace: 'BLOCKSPACE',
exchange: 'DEX',
tips: 'TIPS',
node: 'NODE',
typescript: 'TYPESCRIPT',
go: 'GO',
foundry: 'FOUNDRY',
python: 'PYTHON',
rust: 'RUST',
stablecoins: 'STABLECOINS',
'use-cases': 'USE CASES',
tempo: 'TEMPO',
upgrades: 'UPGRADES',
api: 'API',
guides: 'GUIDES',
rpc: 'RPC',
server: 'SERVER',
wagmi: 'WAGMI',
}
const LANDING = new Set(['/', '/learn', '/changelog'])
function deriveOgParams(path: string) {
if (LANDING.has(path)) return { kind: 'static' as const, url: '/og-docs.png' }
const segments = path.split('/').filter(Boolean)
const firstSeg = segments[0] || ''
const secondSeg = segments[1] || ''
const sectionMapped = firstSeg in sectionMap
const section = sectionMap[firstSeg] || firstSeg.toUpperCase().replace(/-/g, ' ')
const subsectionMapped = segments.length >= 3 && secondSeg in subsectionMap
const subsection =
segments.length >= 3 && subsectionMap[secondSeg]
? subsectionMap[secondSeg]
: segments.length >= 3
? secondSeg.toUpperCase().replace(/-/g, ' ')
: ''
return {
kind: 'dynamic' as const,
section,
subsection,
sectionMapped,
subsectionMapped,
firstSeg,
secondSeg,
hasSubsection: segments.length >= 3,
}
}
function fileToRoute(filePath: string) {
const rel = relative(PAGES_DIR, filePath).replace(/\.(mdx|md|tsx)$/, '')
if (rel.startsWith('_')) return null
if (rel.includes('/_')) return null
if (rel === 'index') return '/'
if (rel.endsWith('/index')) return `/${rel.slice(0, -'/index'.length)}`
return `/${rel}`
}
function walk(dir: string): string[] {
const out: string[] = []
for (const entry of readdirSync(dir)) {
const full = join(dir, entry)
const s = statSync(full)
if (s.isDirectory()) out.push(...walk(full))
else if (/\.(mdx|md|tsx)$/.test(entry)) out.push(full)
}
return out
}
const allFiles = walk(PAGES_DIR)
const routes = allFiles
.map(fileToRoute)
.filter((r): r is string => !!r)
.filter((r) => !r.startsWith('/_api/'))
const buckets = new Map<string, { route: string; section: string; subsection: string }>()
const fallbackSections = new Map<string, string[]>()
const fallbackSubsections = new Map<string, string[]>()
let staticCount = 0
for (const route of routes) {
const og = deriveOgParams(route)
if (og.kind === 'static') {
staticCount++
continue
}
const key = `${og.section}::${og.subsection}`
if (!buckets.has(key)) {
buckets.set(key, { route, section: og.section, subsection: og.subsection })
}
if (!og.sectionMapped) {
if (!fallbackSections.has(og.firstSeg)) fallbackSections.set(og.firstSeg, [])
fallbackSections.get(og.firstSeg)?.push(route)
}
if (og.hasSubsection && !og.subsectionMapped) {
if (!fallbackSubsections.has(og.secondSeg)) fallbackSubsections.set(og.secondSeg, [])
fallbackSubsections.get(og.secondSeg)?.push(route)
}
}
console.log(
`Discovered ${routes.length} routes, ${staticCount} landing (static), ${buckets.size} unique dynamic OG buckets.\n`,
)
console.log('=== Mapping coverage check ===')
if (fallbackSections.size === 0 && fallbackSubsections.size === 0) {
console.log(' 100% coverage: every section and subsection has an explicit map entry.\n')
} else {
if (fallbackSections.size > 0) {
console.log(' Sections falling through to auto-uppercase:')
for (const [seg, paths] of fallbackSections) {
console.log(
` ${seg.padEnd(20)} → "${seg.toUpperCase().replace(/-/g, ' ')}" (${paths.length} routes, e.g. ${paths[0]})`,
)
}
}
if (fallbackSubsections.size > 0) {
console.log(' Subsections falling through to auto-uppercase:')
for (const [seg, paths] of fallbackSubsections) {
console.log(
` ${seg.padEnd(20)} → "${seg.toUpperCase().replace(/-/g, ' ')}" (${paths.length} routes, e.g. ${paths[0]})`,
)
}
}
console.log('')
}
type ProbeResult = {
label: string
url: string
status: number
ct: string
bytes: number
ms: number
error?: string
}
async function probe(label: string, url: string): Promise<ProbeResult> {
const t0 = Date.now()
try {
const r = await fetch(url, { method: 'GET', redirect: 'follow' })
const ct = r.headers.get('content-type') ?? ''
const buf = await r.arrayBuffer()
const ms = Date.now() - t0
return { label, url, status: r.status, ct, bytes: buf.byteLength, ms }
} catch (e) {
return { label, url, status: -1, ct: '', bytes: 0, ms: Date.now() - t0, error: String(e) }
}
}
type AnnotatedResult = ProbeResult & {
kind: 'static' | 'bucket' | 'edge'
section?: string
subsection?: string
sampleRoute?: string
desc?: string
}
const results: AnnotatedResult[] = []
console.log('=== Static landing OG ===')
for (const path of ['/og-docs.png']) {
const r = await probe('static og-docs.png', `${PREVIEW}${path}`)
results.push({ kind: 'static', ...r })
console.log(` ${r.status} ${r.ct.padEnd(20)} ${r.bytes} bytes ${r.ms}ms ${r.url}`)
}
console.log('\n=== Dynamic OG (one per (section,subsection) bucket) ===')
const sample = Array.from(buckets.values())
let i = 0
for (const b of sample) {
i++
const params = new URLSearchParams({
title: 'Sample Title For OG Verification',
description: 'Probe',
section: b.section,
...(b.subsection ? { subsection: b.subsection } : {}),
})
const url = `${PREVIEW}/api/og?${params.toString()}`
const r = await probe(`bucket ${b.section}/${b.subsection || '-'} (${b.route})`, url)
results.push({
kind: 'bucket',
section: b.section,
subsection: b.subsection,
sampleRoute: b.route,
...r,
})
console.log(
` [${String(i).padStart(2)}/${sample.length}] ${r.status} ${r.ct.padEnd(20)} ${String(r.bytes).padStart(7)}B ${String(r.ms).padStart(5)}ms ${b.section}${b.subsection ? `/${b.subsection}` : ''}`,
)
}
console.log('\n=== Title edge cases ===')
const edge = [
{ title: 'X', desc: 'one-char' },
{ title: 'API', desc: 'short three-letter' },
{
title:
'A truly absurdly long page title that will exercise the balanceLines path with three lines and possible wrapping',
desc: 'very long',
},
{ title: 'Émoji & “smart quotes” — café', desc: 'unicode' },
{ title: 'tip-1017: account abstraction', desc: 'mixed case + colon' },
{ title: '<script>alert(1)</script>', desc: 'html-ish' },
]
for (const e of edge) {
const params = new URLSearchParams({
title: e.title,
description: 'edge',
section: 'PROTOCOL',
subsection: 'TIPS',
})
const url = `${PREVIEW}/api/og?${params.toString()}`
const r = await probe(`edge:${e.desc}`, url)
results.push({ kind: 'edge', desc: e.desc, ...r })
console.log(
` ${r.status} ${r.ct.padEnd(20)} ${String(r.bytes).padStart(7)}B ${String(r.ms).padStart(5)}ms ${e.desc}`,
)
}
const failed = results.filter((r) => r.status !== 200 || !r.ct.startsWith('image/'))
console.log(`\n=== Summary ===`)
console.log(`Total probes: ${results.length}`)
console.log(`Failed: ${failed.length}`)
if (failed.length) {
console.log('\nFailures:')
for (const f of failed) console.log(` ${f.kind} ${f.status} ${f.ct} ${f.url}`)
}
writeFileSync(
'og-probe-results.json',
JSON.stringify(
{ preview: PREVIEW, totalRoutes: routes.length, buckets: buckets.size, results },
null,
2,
),
)
console.log('\nFull results written to og-probe-results.json')
process.exit(failed.length ? 1 : 0)
import type { SVGProps } from 'react'
import SimpleIconsClaude from '~icons/simple-icons/claude'
import SimpleIconsOpenai from '~icons/simple-icons/openai'
export function ClaudeLogo(props: SVGProps<SVGSVGElement>) {
return <SimpleIconsClaude {...props} />
}
export function CodexLogo(props: SVGProps<SVGSVGElement>) {
return <SimpleIconsOpenai {...props} />
}
export function AmpLogo(props: SVGProps<SVGSVGElement>) {
return (
// biome-ignore lint/a11y/noSvgWithoutTitle: Decorative product mark paired with visible tab text.
<svg viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M3.76879 18.3015L8.49839 13.505L10.2196 20.0399L12.72 19.3561L10.2288 9.86749L0.890876 7.33844L0.22594 9.89331L6.65134 11.6388L1.94138 16.4282L3.76879 18.3015Z"
fill="currentColor"
/>
<path
d="M17.4074 12.7414L19.9078 12.0575L17.4167 2.56897L8.07873 0.0399246L7.4138 2.5948L15.2992 4.73685L17.4074 12.7414Z"
fill="currentColor"
/>
<path
d="M13.8184 16.3883L16.3188 15.7044L13.8276 6.21588L4.48971 3.68683L3.82477 6.24171L11.7101 8.38376L13.8184 16.3883Z"
fill="currentColor"
/>
</svg>
)
}
const variants = {
red: 'bg-red3 text-red11',
amber: 'bg-amber3 text-amber11',
green: 'bg-green3 text-green11',
blue: 'bg-accentTint text-accent',
violet: 'bg-violet3 text-violet11',
gray: 'bg-gray3 text-gray11',
} as const
type BadgeVariant = keyof typeof variants
export function Badge({
variant = 'gray',
children,
}: {
variant?: BadgeVariant
children: React.ReactNode
}) {
return (
<span
className={`inline-flex h-[19px] items-center justify-center rounded-[30px] px-1.5 text-center font-medium text-[9px] uppercase leading-none tracking-[2%] ${variants[variant]}`}
>
{children}
</span>
)
}
export default Badge
'use client'
import * as React from 'react'
import { tempo } from 'viem/chains'
import { useChains, useConnect, useConnection, useConnectors, useSwitchChain } from 'wagmi'
import { Button, Logout } from './guides/Demo'
import { filterSupportedInjectedConnectors } from './lib/wallets'
const mainnetParams = {
chainId: tempo.id,
addEthereumChainParameter: {
chainName: tempo.name,
nativeCurrency: { name: 'USD', decimals: 18, symbol: 'USD' },
rpcUrls: [tempo.rpcUrls.default.http[0]],
blockExplorerUrls: ['https://explore.tempo.xyz'],
},
}
export function ConnectWallet({
showAddChain = true,
network = 'testnet',
}: {
showAddChain?: boolean
network?: 'mainnet' | 'testnet'
}) {
const { address, chain, connector } = useConnection()
const connect = useConnect()
const connectors = useConnectors()
const injectedConnectors = React.useMemo(
() => filterSupportedInjectedConnectors(connectors),
[connectors],
)
const switchChain = useSwitchChain()
const chains = useChains()
const targetChainId = network === 'mainnet' ? tempo.id : chains[0].id
const isSupported =
network === 'mainnet' ? chain?.id === targetChainId : chains.some((c) => c.id === chain?.id)
if (!injectedConnectors.length)
return (
<div className="flex items-center text-[14px] -tracking-[2%]">No browser wallets found.</div>
)
if (!address || connector?.id === 'webAuthn')
return (
<div className="flex gap-2">
{injectedConnectors.map((connector) => (
<Button
variant="default"
className="flex items-center gap-1.5"
key={connector.id}
onClick={() => connect.connect({ connector })}
>
{connector.icon ? (
<img className="size-5" src={connector.icon} alt={connector.name} />
) : (
<div />
)}
{connector.name}
</Button>
))}
</div>
)
const switchParams =
network === 'mainnet'
? mainnetParams
: {
chainId: chains[0].id,
addEthereumChainParameter: {
nativeCurrency: { name: 'USD', decimals: 18, symbol: 'USD' },
blockExplorerUrls: ['https://explore.testnet.tempo.xyz'],
},
}
return (
<div className="flex flex-col gap-2">
<Logout />
{showAddChain && !isSupported && (
<Button
className="w-fit"
variant="accent"
onClick={() => switchChain.switchChain(switchParams)}
>
Add Tempo to {connector?.name ?? 'Wallet'}
</Button>
)}
{switchChain.isSuccess && (
<div className="flex items-center font-normal text-[14px] -tracking-[2%]">
Added Tempo to {connector?.name ?? 'Wallet'}!
</div>
)}
</div>
)
}
'use client'
export function Container(
props: React.PropsWithChildren<{
headerLeft?: React.ReactNode
headerRight?: React.ReactNode
footer?: React.ReactNode
}>,
) {
const { children, headerLeft, headerRight, footer } = props
// Note: styling of this container mimics Vocs styles.
return (
<div className="divide-y divide-gray4 rounded border border-gray4">
{(headerLeft || headerRight) && (
<header className="flex h-[44px] items-center justify-between px-4">
{headerLeft}
{headerRight}
</header>
)}
<div className="p-4">{children}</div>
{footer && (
<footer className="flex min-h-8 items-center px-2.5 text-[13px] text-gray10">
{footer}
</footer>
)}
</div>
)
}
'use client'
import { createContext, type ReactNode, useCallback, useContext, useState } from 'react'
import type { Address, PrivateKeyAccount, TransactionReceipt } from 'viem'
import { useConnectionEffect } from 'wagmi'
// Define your allowed keys and their types here
export interface DemoData {
tokenAddress: Address
tokenReceipt: TransactionReceipt
sponsorAccount: PrivateKeyAccount
transferId: string
policyId: bigint
orderId: bigint
rewardId: bigint
}
interface DemoContextValue {
setData: <K extends keyof DemoData>(key: K, value: DemoData[K]) => void
getData: <K extends keyof DemoData>(key: K) => DemoData[K] | undefined
clearData: <K extends keyof DemoData>(key?: K) => void
checkFlowDependencies: (keys: (keyof DemoData)[]) => boolean
data: Partial<DemoData>
}
const DemoContext = createContext<DemoContextValue | undefined>(undefined)
interface DemoContextProviderProps {
children: ReactNode
}
export function DemoContextProvider({ children }: DemoContextProviderProps) {
const [data, setDataState] = useState<Partial<DemoData>>({})
const setData = useCallback(<K extends keyof DemoData>(key: K, value: DemoData[K]) => {
setDataState((prev) => ({
...prev,
[key]: value,
}))
}, [])
const getData = useCallback(
<K extends keyof DemoData>(key: K): DemoData[K] | undefined => {
return data[key]
},
[data],
)
const clearData = useCallback(<K extends keyof DemoData>(key?: K) => {
setDataState((prev) => {
if (key === undefined) {
return {}
}
const { [key]: _, ...rest } = prev
return rest
})
}, [])
const checkFlowDependencies = useCallback(
(keys: (keyof DemoData)[]): boolean => {
return keys.every((key) => data[key] !== undefined)
},
[data],
)
// Clear all data when account disconnects
useConnectionEffect({
onDisconnect() {
setDataState({})
},
})
const value: DemoContextValue = {
setData,
getData,
clearData,
checkFlowDependencies,
data,
}
return <DemoContext.Provider value={value}>{children}</DemoContext.Provider>
}
export function useDemoContext(): DemoContextValue {
const context = useContext(DemoContext)
if (context === undefined) {
throw new Error('useDemoContext must be used within a DemoContextProvider')
}
return context
}
import type * as React from 'react'
import { cx } from '../../cva.config'
export function DocsLinkButton({
children,
className,
href,
}: {
children: React.ReactNode
className?: string
href: string
}) {
return (
<a
className={cx(
'relative my-6 flex h-[32px] w-fit cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md border bg-invert px-[14px] font-normal text-[14px] text-invert no-underline transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring dark:border-dashed',
className,
)}
href={href}
>
{children}
</a>
)
}
'use client'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useConfig } from 'vocs'
import { resolveSidebarItems, SidebarNodes, usePathname } from './DocsHeader'
/**
* Mobile-only docs sidebar drawer.
*
* Injects a hamburger toggle into the sticky "On this page" outline row (Vocs'
* `[data-v-outline-mobile]` bar) so the docs sidebar stays reachable while
* scrolling, then slides the sidebar tree in from the left. On pages without an
* outline row the full-screen menu's "Docs" section remains the fallback.
*/
export default function DocsSidebarDrawer() {
const pathname = usePathname()
const config = useConfig()
const items = resolveSidebarItems(config?.sidebar, pathname)
const [host, setHost] = useState<HTMLElement | null>(null)
const [open, setOpen] = useState(false)
// Mount a portal host inside the outline row, re-attaching whenever the route
// changes (Vocs re-creates the row per page).
useEffect(() => {
let span: HTMLElement | null = null
const attach = () => {
const row = document.querySelector('[data-v-outline-mobile] > div')
if (!row || span) return Boolean(span)
span = document.createElement('span')
span.dataset.docsSidebarToggle = ''
span.style.display = 'inline-flex'
span.style.marginRight = '0.5rem'
row.prepend(span)
setHost(span)
return true
}
if (attach()) {
return () => {
span?.remove()
setHost(null)
}
}
const observer = new MutationObserver(() => {
if (attach()) observer.disconnect()
})
observer.observe(document.body, { childList: true, subtree: true })
return () => {
observer.disconnect()
span?.remove()
setHost(null)
}
}, [])
// Close on route change.
// biome-ignore lint/correctness/useExhaustiveDependencies: close drawer when the path changes.
useEffect(() => {
setOpen(false)
}, [pathname])
// Close on Escape + lock body scroll while open.
useEffect(() => {
if (!open) return
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false)
}
document.addEventListener('keydown', onKey)
const prevOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = prevOverflow
}
}, [open])
if (items.length === 0) return null
const toggle = host
? createPortal(
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-label="Open docs navigation"
aria-expanded={open}
className="flex items-center gap-1.5 font-sans text-foreground/70 transition-colors hover:text-foreground"
>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden>
<title>Docs navigation</title>
<path
d="M3 5h14M3 10h14M3 15h14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
<span>Menu</span>
</button>,
host,
)
: null
return (
<>
{toggle}
<div
className={`fixed inset-0 z-[60] lg:hidden ${open ? '' : 'pointer-events-none'}`}
aria-hidden={!open}
>
{/* Backdrop */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: backdrop dismiss. */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Escape handled globally. */}
<div
onClick={() => setOpen(false)}
className={`absolute inset-0 bg-black/40 transition-opacity duration-200 ${
open ? 'opacity-100' : 'opacity-0'
}`}
/>
{/* Panel */}
<div
className={`absolute top-0 left-0 flex h-full w-[82%] max-w-[320px] flex-col border-line border-r bg-background transition-transform duration-200 ease-out ${
open ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between border-line border-b px-5 py-4">
<span className="font-sans text-[15px] text-foreground tracking-[0]">
Documentation
</span>
<button
type="button"
onClick={() => setOpen(false)}
aria-label="Close docs navigation"
className="grid size-8 place-items-center text-foreground/70 transition-colors hover:text-foreground"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden>
<title>Close</title>
<path
d="M5 5l10 10M15 5L5 15"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</button>
</div>
<div className="flex flex-1 flex-col overflow-y-auto px-5 py-3">
<SidebarNodes
nodes={items}
pathname={pathname}
depth={0}
onNavigate={() => setOpen(false)}
/>
</div>
</div>
</div>
</>
)
}
interface FAQItem {
question: string
answer: string
}
interface FAQSchemaProps {
items: readonly FAQItem[]
}
export function FAQSchema({ items }: FAQSchemaProps) {
const mainEntity = items
.filter((i): i is FAQItem => Boolean(i?.question?.trim()) && Boolean(i?.answer?.trim()))
.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
}))
if (mainEntity.length === 0) return null
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity,
}
const safeSchema = JSON.stringify(schema)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD requires innerHTML; content is escaped above
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: safeSchema }} />
}
'use client'
import { useEffect } from 'react'
declare global {
interface Window {
dataLayer: unknown[]
gtag: (...args: unknown[]) => void
}
}
function GoogleAnalyticsInit({ id }: { id: string }) {
useEffect(() => {
if (typeof window.gtag !== 'undefined') return
const init = () => {
window.dataLayer = window.dataLayer || []
window.gtag = function gtag() {
// biome-ignore lint/complexity/noArguments: gtag API requires arguments object
window.dataLayer.push(arguments)
}
window.gtag('js', new Date())
window.gtag('config', id)
const script = document.createElement('script')
script.async = true
script.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
document.head.appendChild(script)
}
if ('requestIdleCallback' in window) {
window.requestIdleCallback(init)
} else {
setTimeout(init, 1)
}
}, [id])
return null
}
export default function GoogleAnalytics() {
const id = import.meta.env.VITE_GA_MEASUREMENT_ID
if (!id) return null
return <GoogleAnalyticsInit id={id} />
}
'use client'
import { useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { parseUnits } from 'viem'
import { useConnection, useConnectionEffect } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import { useDemoContext } from '../../../DemoContext'
import { Button, ExplorerLink, Step } from '../../Demo'
import { alphaUsd } from '../../tokens'
import type { DemoStepProps } from '../types'
const validatorToken = alphaUsd
export function BurnFeeAmmLiquidity(props: DemoStepProps) {
const { stepNumber, last = false } = props
const { address } = useConnection()
const { getData } = useDemoContext()
const queryClient = useQueryClient()
const tokenAddress = getData('tokenAddress')
const { data: lpBalance } = Hooks.amm.useLiquidityBalance({
address,
userToken: tokenAddress,
validatorToken,
})
const { data: metadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const { data: validatorMetadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const burnLiquidity = Hooks.amm.useBurnSync({
mutation: {
onSettled() {
queryClient.refetchQueries({ queryKey: ['getPool'] })
queryClient.refetchQueries({ queryKey: ['getLiquidityBalance'] })
},
},
})
useConnectionEffect({
onDisconnect() {
burnLiquidity.reset()
},
})
const hasSufficientBalance =
lpBalance && lpBalance >= parseUnits('10', validatorMetadata?.decimals || 6)
const active = React.useMemo(() => {
return Boolean(address && tokenAddress && hasSufficientBalance)
}, [address, tokenAddress, hasSufficientBalance])
return (
<Step
active={active && (last ? true : !burnLiquidity.isSuccess)}
completed={burnLiquidity.isSuccess}
actions={
<Button
variant={active ? (burnLiquidity.isSuccess ? 'default' : 'accent') : 'default'}
disabled={!active}
onClick={() => {
if (!address || !tokenAddress) return
burnLiquidity.mutate({
userToken: tokenAddress,
validatorToken,
liquidity: parseUnits('10', validatorMetadata?.decimals || 6),
to: address,
feeToken: alphaUsd,
})
}}
type="button"
className="font-normal text-[14px] -tracking-[2%]"
>
Burn Liquidity
</Button>
}
number={stepNumber}
title={`Burn 10 LP tokens from ${metadata ? metadata.name : 'your token'} pool.`}
>
{burnLiquidity.data && (
<div className="mx-6 flex flex-col gap-3 pb-4">
<div className="border-gray4 border-s-2 ps-5">
<ExplorerLink hash={burnLiquidity.data.receipt.transactionHash} />
</div>
</div>
)}
</Step>
)
}
'use client'
import * as React from 'react'
import { formatUnits } from 'viem'
import { useConnection } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import { useDemoContext } from '../../../DemoContext'
import { Step } from '../../Demo'
import { alphaUsd } from '../../tokens'
import type { DemoStepProps } from '../types'
const validatorToken = alphaUsd
export function CheckFeeAmmPool(props: DemoStepProps) {
const { stepNumber } = props
const { address } = useConnection()
const { getData } = useDemoContext()
const tokenAddress = getData('tokenAddress')
const { data: pool } = Hooks.amm.usePool({
userToken: tokenAddress,
validatorToken,
})
const { data: lpBalance } = Hooks.amm.useLiquidityBalance({
address,
userToken: tokenAddress,
validatorToken,
})
const { data: metadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const { data: validatorMetadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const active = React.useMemo(() => {
return Boolean(address && tokenAddress && pool && lpBalance && lpBalance > 0n)
}, [address, tokenAddress, pool, lpBalance])
return (
<Step
active={active}
completed={active}
number={stepNumber}
title={`View Fee AMM pool for ${metadata ? metadata.name : 'your token'}.`}
>
{active && pool && lpBalance && (
<div className="mx-6 flex flex-col gap-3 pb-4">
<div className="border-gray4 border-s-2 ps-5">
<div className="mt-2 mb-3 rounded-lg bg-gray2 p-3 text-[13px] -tracking-[1%]">
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="font-medium text-gray10">Your LP Balance</span>
<span className="text-gray12">
{formatUnits(lpBalance, validatorMetadata?.decimals || 6)} LP tokens
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium text-gray10">Validator Token Reserves</span>
<span className="text-gray12">
{formatUnits(pool.reserveValidatorToken, validatorMetadata?.decimals || 6)}{' '}
AlphaUSD
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium text-gray10">User Token Reserves</span>
<span className="text-gray12">
{formatUnits(pool.reserveUserToken, metadata?.decimals || 6)}{' '}
{metadata?.symbol || ''}
</span>
</div>
</div>
</div>
</div>
</div>
)}
</Step>
)
}
'use client'
import { useQueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { parseUnits } from 'viem'
import { useConnection, useConnectionEffect } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import LucideCheck from '~icons/lucide/check'
import LucideCircle from '~icons/lucide/circle'
import { useDemoContext } from '../../../DemoContext'
import { Button, ExplorerLink, Step } from '../../Demo'
import { alphaUsd, pathUsd } from '../../tokens'
import type { DemoStepProps } from '../types'
export function MintFeeAmmLiquidity(props: DemoStepProps & { waitForBalance?: boolean }) {
const { stepNumber, last = false, waitForBalance = true } = props
const { address } = useConnection()
const { getData } = useDemoContext()
const queryClient = useQueryClient()
const tokenAddress = getData('tokenAddress')
const { data: metadata } = Hooks.token.useGetMetadata({
token: tokenAddress,
})
const { data: tokenBalance } = Hooks.token.useGetBalance({
account: address,
token: tokenAddress,
})
const [pathUsdMinted, setPathUsdMinted] = React.useState(false)
const [alphaUsdMinted, setAlphaUsdMinted] = React.useState(false)
const [pathUsdTxHash, setPathUsdTxHash] = React.useState<string>()
const [alphaUsdTxHash, setAlphaUsdTxHash] = React.useState<string>()
const mintFeeLiquidity = Hooks.amm.useMintSync({
mutation: {
onSettled() {
queryClient.refetchQueries({ queryKey: ['getPool'] })
queryClient.refetchQueries({ queryKey: ['getLiquidityBalance'] })
},
},
})
useConnectionEffect({
onDisconnect() {
mintFeeLiquidity.reset()
setPathUsdMinted(false)
setAlphaUsdMinted(false)
setPathUsdTxHash(undefined)
setAlphaUsdTxHash(undefined)
},
})
const handleMintAll = React.useCallback(async () => {
if (!address || !tokenAddress) return
if (!pathUsdMinted) {
await new Promise<void>((resolve) => {
mintFeeLiquidity.mutate(
{
userTokenAddress: tokenAddress,
validatorTokenAddress: pathUsd,
validatorTokenAmount: parseUnits('100', 6),
to: address,
feeToken: alphaUsd,
},
{
onSuccess(data) {
setPathUsdMinted(true)
setPathUsdTxHash(data.receipt.transactionHash)
resolve()
},
onError() {
resolve()
},
},
)
})
}
if (!alphaUsdMinted) {
mintFeeLiquidity.mutate(
{
userTokenAddress: tokenAddress,
validatorTokenAddress: alphaUsd,
validatorTokenAmount: parseUnits('100', 6),
to: address,
feeToken: alphaUsd,
},
{
onSuccess(data) {
setAlphaUsdMinted(true)
setAlphaUsdTxHash(data.receipt.transactionHash)
},
},
)
}
}, [address, tokenAddress, pathUsdMinted, alphaUsdMinted, mintFeeLiquidity])
const active = React.useMemo(() => {
const balanceCheck = waitForBalance ? Boolean(tokenBalance && tokenBalance > 0n) : true
return Boolean(address && tokenAddress && balanceCheck)
}, [address, tokenAddress, tokenBalance, waitForBalance])
const allMinted = pathUsdMinted && alphaUsdMinted
const someMinted = pathUsdMinted || alphaUsdMinted
return (
<Step
active={active && (last ? true : !allMinted)}
completed={allMinted}
actions={
<Button
variant={active ? (allMinted ? 'default' : 'accent') : 'default'}
disabled={!active || mintFeeLiquidity.isPending}
onClick={handleMintAll}
type="button"
className="font-normal text-[14px] -tracking-[2%]"
>
{mintFeeLiquidity.isPending
? 'Adding...'
: allMinted
? 'Done'
: someMinted
? 'Continue Adding'
: 'Add Liquidity'}
</Button>
}
error={mintFeeLiquidity.error}
number={stepNumber}
title={`Add fee liquidity for ${metadata ? metadata.name : 'your token'}.`}
>
{someMinted && (
<div className="mx-6 flex flex-col gap-2 pb-4">
<div className="border-gray4 border-s-2 ps-5">
<div className="mt-2 space-y-1">
<div className="flex items-center gap-2 text-[13px]">
{pathUsdMinted ? (
<LucideCheck className="size-4 text-green9" />
) : (
<LucideCircle className="size-4 text-gray9" />
)}
<span className="w-20 font-mono">pathUSD</span>
{pathUsdTxHash && (
<span className="-mt-1">
<ExplorerLink hash={pathUsdTxHash} />
</span>
)}
</div>
<div className="flex items-center gap-2 text-[13px]">
{alphaUsdMinted ? (
<LucideCheck className="size-4 text-green9" />
) : (
<LucideCircle className="size-4 text-gray9" />
)}
<span className="w-20 font-mono">AlphaUSD</span>
{alphaUsdTxHash && (
<span className="-mt-1">
<ExplorerLink hash={alphaUsdTxHash} />
</span>
)}
</div>
</div>
</div>
</div>
)}
</Step>
)
}
'use client'
import { useConnection } from 'wagmi'
import { Login, Logout, Step } from '../../Demo'
import type { DemoStepProps } from '../types'
export function Connect(props: DemoStepProps) {
const { stepNumber = 1 } = props
const { address } = useConnection()
return (
<Step
active={!address}
completed={Boolean(address)}
actions={address ? <Logout /> : <Login />}
number={stepNumber}
title="Create an account, or use an existing one."
/>
)
}
'use client'
import * as React from 'react'
import { parseUnits } from 'viem'
import { Addresses } from 'viem/tempo'
import { useConnection, useConnectionEffect } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import { Button, ExplorerLink, Step } from '../../Demo'
import { alphaUsd, pathUsd } from '../../tokens'
import type { DemoStepProps } from '../types'
export function ApproveSpend(props: DemoStepProps) {
const { stepNumber, last = false } = props
const { address } = useConnection()
const approve = Hooks.token.useApproveSync()
useConnectionEffect({
onDisconnect() {
approve.reset()
},
})
const amount = parseUnits('100', 6)
const active = React.useMemo(() => {
return !!address
}, [address])
return (
<Step
active={active && (last ? true : !approve.isSuccess)}
completed={approve.isSuccess}
actions={
<Button
variant={active ? (approve.isSuccess ? 'default' : 'accent') : 'default'}
disabled={!active}
onClick={() => {
approve.mutate({
amount,
spender: Addresses.stablecoinDex,
token: pathUsd,
feeToken: alphaUsd,
})
}}
type="button"
className="font-normal text-[14px] -tracking-[2%]"
>
{approve.isPending ? 'Approving...' : 'Approve Spend'}
</Button>
}
number={stepNumber}
title="Approve the Stablecoin DEX to spend pathUSD"
>
{approve.data && (
<div className="mx-6 flex flex-col gap-3 pb-4">
<div className="border-gray4 border-s-2 ps-5">
<ExplorerLink hash={approve.data.receipt.transactionHash} />
</div>
</div>
)}
</Step>
)
}