Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions src/components/StaticMermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import { estW, LAYOUT, THEMES, type ThemeColors } from './MermaidDiagram'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface FlowNode {
id: string
label: string
}

interface FlowEdge {
from: string
to: string
label: string
}

interface ParsedFlowchart {
nodes: FlowNode[]
edges: FlowEdge[]
}

// ---------------------------------------------------------------------------
// Parser — handles `flowchart TD` blocks
// ---------------------------------------------------------------------------

function parseFlowchart(source: string): ParsedFlowchart {
const lines = source
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('%%'))
const nodes = new Map<string, FlowNode>()
const edges: FlowEdge[] = []

const ensureNode = (id: string, label?: string) => {
if (!nodes.has(id)) {
nodes.set(id, { id, label: label ?? id })
} else if (label) {
const existing = nodes.get(id)
if (existing) existing.label = label
}
}

// Extract id and optional label: id["label"] or just id
const parseNodeRef = (raw: string): { id: string; label?: string } => {
const m = raw.match(/^(\w+)\["(.+?)"\]$/)
if (m) return { id: m[1], label: m[2] }
return { id: raw.trim() }
}

for (const line of lines) {
if (/^flowchart/i.test(line)) continue

// Edge: from -->|"label"| to OR from --> to
const mEdge = line.match(/^(.+?)\s*-->(?:\|"(.+?)"\|)?\s*(.+)$/)
if (mEdge) {
const fromRef = parseNodeRef(mEdge[1].trim())
const toRef = parseNodeRef(mEdge[3].trim())
ensureNode(fromRef.id, fromRef.label)
ensureNode(toRef.id, toRef.label)
edges.push({ from: fromRef.id, to: toRef.id, label: mEdge[2] ?? '' })
continue
}

// Standalone node definition: id["label"]
const mNode = line.match(/^(\w+)\["(.+?)"\]$/)
if (mNode) {
ensureNode(mNode[1], mNode[2])
}
}

return { nodes: Array.from(nodes.values()), edges }
}

// ---------------------------------------------------------------------------
// Layout constants for flowchart
// ---------------------------------------------------------------------------

const FLOW = {
nodeH: 36,
nodePadX: 24,
nodeGapY: 40,
arrowSize: 7,
}

// ---------------------------------------------------------------------------
// SVG renderer
// ---------------------------------------------------------------------------

function renderFlowchart(parsed: ParsedFlowchart, th: ThemeColors): string {
const L = LAYOUT
const F = FLOW
const o: string[] = []

// Measure node widths
const nodeW = parsed.nodes.map((n) => estW(n.label, L.actorFontSize) + F.nodePadX * 2)
const maxW = Math.max(...nodeW)

// Use uniform width for clean vertical alignment
const uniformW = maxW

// Account for edge labels rendered to the right of center
const maxLabelW = parsed.edges.reduce(
(max, e) => (e.label ? Math.max(max, estW(e.label, L.labelFontSize)) : max),
0,
)

// Center x
const padding = L.padding
const cx = padding + uniformW / 2
const totalW = Math.max(uniformW + padding * 2, cx + uniformW / 2 + 10 + maxLabelW + padding)

// Compute node y positions
const nodeY: number[] = []
let y = padding
for (let i = 0; i < parsed.nodes.length; i++) {
nodeY.push(y)
if (i < parsed.nodes.length - 1) {
y += F.nodeH + F.nodeGapY
}
}
const totalH = y + F.nodeH + padding

const nodeIdx = new Map<string, number>()
for (let i = 0; i < parsed.nodes.length; i++) {
nodeIdx.set(parsed.nodes[i].id, i)
}

o.push(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalW} ${totalH}" width="${totalW}" height="${totalH}">`,
)
o.push(`<style>text{font-family:${L.fontFamily}}</style>`)

// Edges (draw before nodes so lines go behind)
for (const e of parsed.edges) {
const fi = nodeIdx.get(e.from)
const ti = nodeIdx.get(e.to)
if (fi === undefined || ti === undefined) continue

const fromY = nodeY[fi] + F.nodeH
const toY = nodeY[ti]
const sz = F.arrowSize

// Vertical line
o.push(
`<line x1="${cx}" y1="${fromY}" x2="${cx}" y2="${toY - sz}" stroke="${th.line}" stroke-width="${L.messageStroke}"/>`,
)

// Arrow
o.push(
`<polygon points="${cx},${toY} ${cx - sz / 2},${toY - sz} ${cx + sz / 2},${toY - sz}" fill="${th.line}" stroke="${th.line}" stroke-width="1" stroke-linejoin="round"/>`,
)

// Edge label
if (e.label) {
const midY = (fromY + toY) / 2
o.push(
`<text x="${cx + 10}" y="${midY}" text-anchor="start" dy="0.35em" font-size="${L.labelFontSize}" font-weight="${L.labelFontWeight}" fill="${th.textMuted}">${esc(e.label)}</text>`,
)
}
}

// Nodes
for (let i = 0; i < parsed.nodes.length; i++) {
const n = parsed.nodes[i]
const nw = uniformW
const nx = cx - nw / 2
const ny = nodeY[i]

o.push(
`<rect x="${nx}" y="${ny}" width="${nw}" height="${F.nodeH}" rx="4" fill="${th.actorFill}" stroke="${th.actorStroke}" stroke-width="1"/>`,
)
o.push(
`<text x="${cx}" y="${ny + F.nodeH / 2}" text-anchor="middle" dy="0.35em" font-size="${L.actorFontSize}" font-weight="${L.actorFontWeight}" fill="${th.text}">${esc(n.label)}</text>`,
)
}

o.push('</svg>')
return o.join('\n')
}

function esc(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}

// ---------------------------------------------------------------------------
// React component
// ---------------------------------------------------------------------------

export function StaticMermaidDiagram({ chart }: { chart: string }) {
const svgRef = useRef<HTMLDivElement>(null)
const [isDark, setIsDark] = useState(false)

useEffect(() => {
const check = () =>
setIsDark(
document.documentElement.style.colorScheme === 'dark' ||
document.documentElement.classList.contains('dark'),
)
check()
const obs = new MutationObserver(check)
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
})
return () => obs.disconnect()
}, [])

useEffect(() => {
const el = svgRef.current
if (!el) return
const parsed = parseFlowchart(chart)
const th = isDark ? THEMES.dark : THEMES.light
el.innerHTML = renderFlowchart(parsed, th)
const svg = el.querySelector('svg')
if (svg) {
svg.style.maxWidth = '100%'
svg.style.height = 'auto'
svg.style.display = 'block'
svg.style.margin = '0 auto'
}
}, [chart, isDark])

return (
<div
className="mermaid-diagram"
style={{
margin: '2rem 0',
padding: '1.5rem 1rem',
borderRadius: '12px',
overflow: 'hidden',
overflowX: 'auto',
minHeight: '100px',
position: 'relative',
}}
>
<div ref={svgRef} />
</div>
)
}
33 changes: 20 additions & 13 deletions src/pages/accounts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: Set up the Tempo Accounts SDK to create, manage, and interact with
---

import { Cards, Card } from 'vocs'
import { StaticMermaidDiagram } from '../../components/StaticMermaidDiagram'
import * as Demo from '../../components/guides/Demo.tsx'
import * as Step from '../../components/guides/steps'
import IconGitHub from '~icons/simple-icons/github'
Expand All @@ -14,19 +15,11 @@ import IconGitHub from '~icons/simple-icons/github'

The Tempo Accounts SDK is a TypeScript library for applications and wallets to create, manage, and interact with accounts on Tempo.

<div className="flex gap-2">
<a
href="https://github.com/tempoxyz/accounts"
target="_blank"
rel="noopener noreferrer"
className="bg-surfaceTint px-2 py-1 rounded-lg border border-primary no-underline flex items-center gap-2"
>
<span className="flex items-center gap-2 text-sm text-primary font-[450]">
<IconGitHub className="size-3" />
GitHub
</span>
</a>
</div>
<StaticMermaidDiagram chart={`flowchart TD
app["App"] -->|"EIP-5792 requests"| wallet["Tempo Wallet + WebAuthn Passkey"]
wallet -->|"delegates to"| sdk["Accounts SDK"]
sdk -->|"built on"| infra["Wagmi + Viem"]
`} />

### Demo

Expand Down Expand Up @@ -281,3 +274,17 @@ export default defineConfig({
Have questions or building something cool with the Accounts SDK?

Join the Telegram group to chat with the team and other devs: [@mpp_devs](https://t.me/mpp_devs)

<div className="flex gap-2 mt-4">
<a
href="https://github.com/tempoxyz/accounts"
target="_blank"
rel="noopener noreferrer"
className="bg-surfaceTint px-2 py-1 rounded-lg border border-primary no-underline flex items-center gap-2"
>
<span className="flex items-center gap-2 text-sm text-primary font-[450]">
<IconGitHub className="size-3" />
GitHub
</span>
</a>
</div>
55 changes: 17 additions & 38 deletions src/wagmi.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { QueryClient } from '@tanstack/react-query'
import { Expiry } from 'accounts'
import { tempoWallet, webAuthn as webAuthnAccounts } from 'accounts/wagmi'
import { tempoWallet, webAuthn } from 'accounts/wagmi'
import * as React from 'react'
import { parseUnits } from 'viem'
import { tempoDevnet, tempoLocalnet, tempoModerato } from 'viem/chains'
Expand All @@ -13,7 +13,6 @@ import {
useConnectors,
webSocket,
} from 'wagmi'
import { KeyManager, webAuthn } from 'wagmi/tempo'
import { alphaUsd, betaUsd, pathUsd, thetaUsd } from './components/guides/tokens'

const feeToken = '0x20c0000000000000000000000000000000000001'
Expand All @@ -25,13 +24,6 @@ const chain =
? tempoDevnet.extend({ feeToken })
: tempoModerato.extend({ feeToken })

const rpId = (() => {
const hostname = globalThis.location?.hostname
if (!hostname) return undefined
const parts = hostname.split('.')
return parts.length > 2 ? parts.slice(-2).join('.') : hostname
})()

export function getConfig(options: getConfig.Options = {}) {
const { multiInjectedProviderDiscovery = false } = options
return createConfig({
Expand All @@ -40,35 +32,22 @@ export function getConfig(options: getConfig.Options = {}) {
},
chains: [chain],
connectors: [
...(import.meta.env.VITE_E2E === 'true'
? [
webAuthnAccounts({
authUrl: 'https://keys.tempo.xyz',
rdns: 'webAuthn',
}),
]
: [
tempoWallet({
authorizeAccessKey: () => ({
expiry: Expiry.days(1),
limits: [
{ token: pathUsd, limit: parseUnits('500', 6) },
{ token: alphaUsd, limit: parseUnits('500', 6) },
{ token: betaUsd, limit: parseUnits('500', 6) },
{ token: thetaUsd, limit: parseUnits('500', 6) },
],
}),
feePayerUrl: 'https://sponsor.moderato.tempo.xyz',
}),
webAuthn({
grantAccessKey: {
// @ts-expect-error - TODO: migrate to webAuthn on Accounts SDK
chainId: BigInt(chain.id),
},
keyManager: KeyManager.http('https://keys.tempo.xyz'),
rpId,
}),
]),
tempoWallet({
authorizeAccessKey: () => ({
expiry: Expiry.days(1),
limits: [
{ token: pathUsd, limit: parseUnits('500', 6) },
{ token: alphaUsd, limit: parseUnits('500', 6) },
{ token: betaUsd, limit: parseUnits('500', 6) },
{ token: thetaUsd, limit: parseUnits('500', 6) },
],
}),
feePayerUrl: 'https://sponsor.moderato.tempo.xyz',
}),
webAuthn({
authUrl: 'https://keys.tempo.xyz',
rdns: 'webAuthn',
}),
],
multiInjectedProviderDiscovery,
storage: createStorage({
Expand Down
Loading