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
5 changes: 1 addition & 4 deletions e2e/deposit-to-a-zone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ test('prepare zone access and deposit to Zone A', async ({ page }) => {
timeout: 30000,
})

const authorizeButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i })
.first()
const authorizeButton = page.getByRole('button', { name: 'Authorize Zone A reads' }).first()
await expect(authorizeButton).toBeVisible({ timeout: 30000 })
await expect(authorizeButton).toBeEnabled({ timeout: 90000 })
await authorizeButton.click()

const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first()
Expand Down
10 changes: 2 additions & 8 deletions e2e/send-tokens-across-zones.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,14 @@ test('send pathUSD from Zone A into Zone B', async ({ page }) => {
timeout: 30000,
})

const authorizeSourceButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i })
.first()
const authorizeSourceButton = page.getByRole('button', { name: 'Authorize Zone A reads' }).first()
await expect(authorizeSourceButton).toBeVisible({ timeout: 30000 })
await expect(authorizeSourceButton).toBeEnabled({ timeout: 90000 })
await authorizeSourceButton.click()

const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first()
const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first()
const sendButton = page.getByRole('button', { name: /^Send 25 pathUSD into Zone B$/i }).first()
const authorizeTargetButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone B reads$/i })
.first()
const authorizeTargetButton = page.getByRole('button', { name: 'Authorize Zone B reads' }).first()

await expect
.poll(
Expand Down Expand Up @@ -66,7 +61,6 @@ test('send pathUSD from Zone A into Zone B', async ({ page }) => {
await sendButton.click()

await expect(authorizeTargetButton).toBeVisible({ timeout: 120000 })
await expect(authorizeTargetButton).toBeEnabled({ timeout: 90000 })
await authorizeTargetButton.click()

await expect(
Expand Down
5 changes: 1 addition & 4 deletions e2e/send-tokens-within-a-zone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ test('prepare zone balance and send tokens within Zone A', async ({ page }) => {
timeout: 30000,
})

const authorizeButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i })
.first()
const authorizeButton = page.getByRole('button', { name: 'Authorize Zone A reads' }).first()
await expect(authorizeButton).toBeVisible({ timeout: 30000 })
await expect(authorizeButton).toBeEnabled({ timeout: 90000 })
await authorizeButton.click()

const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first()
Expand Down
10 changes: 2 additions & 8 deletions e2e/swap-across-zones.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,16 @@ test('swap pathUSD from Zone A into betaUSD on Zone B', async ({ page }) => {
timeout: 30000,
})

const authorizeSourceButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i })
.first()
const authorizeSourceButton = page.getByRole('button', { name: 'Authorize Zone A reads' }).first()
await expect(authorizeSourceButton).toBeVisible({ timeout: 30000 })
await expect(authorizeSourceButton).toBeEnabled({ timeout: 90000 })
await authorizeSourceButton.click()

const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first()
const topUpButton = page.getByRole('button', { name: /^Approve \+ top up Zone A$/i }).first()
const swapButton = page
.getByRole('button', { name: /^Swap 25 pathUSD into Zone B betaUSD$/i })
.first()
const authorizeTargetButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone B reads$/i })
.first()
const authorizeTargetButton = page.getByRole('button', { name: 'Authorize Zone B reads' }).first()

await expect
.poll(
Expand Down Expand Up @@ -68,7 +63,6 @@ test('swap pathUSD from Zone A into betaUSD on Zone B', async ({ page }) => {
await swapButton.click()

await expect(authorizeTargetButton).toBeVisible({ timeout: 120000 })
await expect(authorizeTargetButton).toBeEnabled({ timeout: 90000 })
await authorizeTargetButton.click()

await expect(
Expand Down
5 changes: 1 addition & 4 deletions e2e/withdraw-from-a-zone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ test('prepare zone balance and withdraw from Zone A', async ({ page }) => {
timeout: 20000,
})

const authorizeButton = page
.getByRole('button', { name: /^Authoriz(?:e|ing) Zone A reads$/i })
.first()
const authorizeButton = page.getByRole('button', { name: 'Authorize Zone A reads' }).first()
await expect(authorizeButton).toBeVisible({ timeout: 30000 })
await expect(authorizeButton).toBeEnabled({ timeout: 90000 })
await authorizeButton.click()

const getFundsButton = page.getByRole('button', { name: /^Get testnet pathUSD$/i }).first()
Expand Down
3 changes: 0 additions & 3 deletions src/components/guides/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,6 @@ export function Button(
) {
const { className, disabled, render, size, static: static_, variant, ...rest } = props
const Element = render ? (p: typeof props) => React.cloneElement(render, p) : 'button'
const accessibilityProps = render ? { 'aria-disabled': disabled || undefined } : { disabled }

return (
<Element
className={buttonClassName({
Expand All @@ -609,7 +607,6 @@ export function Button(
static: static_,
variant,
})}
{...accessibilityProps}
{...rest}
/>
)
Expand Down
5 changes: 2 additions & 3 deletions src/components/guides/zones/SendTokensAcrossZones.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
} from '../../../lib/private-zones.ts'
import { useRootWebAuthnAccount } from '../../../lib/useRootWebAuthnAccount.ts'
import { useZoneAuthorization, type ZoneAuthClientLike } from '../../../lib/useZoneAuthorization.ts'
import { Button, ExplorerLink, Logout, ReceiptHash, Step } from '../Demo'
import { SignInButtons } from '../EmbedPasskeys'
import { Button, ExplorerLink, Login, Logout, ReceiptHash, Step } from '../Demo'
import { pathUsd } from '../tokens'
import { useStickyStepCompletion } from './useStickyStepCompletion.ts'

Expand Down Expand Up @@ -78,7 +77,7 @@ export function SendTokensAcrossZones() {
<Step
active={!connected}
completed={connected}
actions={connected ? <Logout /> : <SignInButtons />}
actions={connected ? <Logout /> : <Login />}
error={undefined}
number={1}
title="Create or use a passkey account on the public chain."
Expand Down
93 changes: 10 additions & 83 deletions src/lib/useRootWebAuthnAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,30 @@

import { useQuery } from '@tanstack/react-query'
import { Account } from 'viem/tempo'
import type { WebAuthnP256 } from 'viem/tempo'
import { useConnection } from 'wagmi'
import { config, webAuthnRpId } from '../wagmi.config'
import { config, webAuthnRpId } from '../wagmi.config.ts'

type RootWebAuthnAccount = ReturnType<typeof Account.fromWebAuthnP256>
type RootWebAuthnCredential = WebAuthnP256.P256Credential
type RootWebAuthnAccountProvider = {
getAccount: (options: {
accessKey?: boolean | undefined
address?: `0x${string}` | undefined
signable?: boolean | undefined
}) => RootWebAuthnAccount
request: (args: { method: 'eth_accounts' }) => Promise<readonly `0x${string}`[]>
}
type RootWebAuthnCredential = Parameters<typeof Account.fromWebAuthnP256>[0]

export function useRootWebAuthnAccount() {
const { address, connector } = useConnection()

return useQuery({
enabled: Boolean(address && connector?.id === 'webAuthn'),
queryKey: ['root-webauthn-account', address],
enabled: Boolean(address && connector?.id === 'webAuthn' && webAuthnRpId),
queryKey: ['root-webauthn-account', address, webAuthnRpId],
queryFn: async () => {
if (!address) throw new Error('account address not ready')
if (!connector) throw new Error('connector not ready')

const provider = await connector.getProvider()
if (isRootWebAuthnAccountProvider(provider)) {
await waitForProviderAccount(provider, address as `0x${string}`)
if (!webAuthnRpId) throw new Error('webauthn RP ID is not configured')

return provider.getAccount({
accessKey: false,
address: address as `0x${string}`,
signable: true,
})
}
const credential = await config.storage?.getItem('webAuthn.activeCredential')
if (!credential) throw new Error('webauthn credential not available')

const credential = await waitForStoredCredential(address as `0x${string}`)
return accountFromCredential(credential)
return Account.fromWebAuthnP256(credential as RootWebAuthnCredential, {
rpId: webAuthnRpId,
})
},
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
staleTime: Number.POSITIVE_INFINITY,
})
}

function accountFromCredential(credential: RootWebAuthnCredential) {
return Account.fromWebAuthnP256(credential, webAuthnRpId ? { rpId: webAuthnRpId } : undefined)
}

function isRootWebAuthnAccountProvider(value: unknown): value is RootWebAuthnAccountProvider {
return Boolean(
value &&
typeof value === 'object' &&
'getAccount' in value &&
typeof value.getAccount === 'function' &&
'request' in value &&
typeof value.request === 'function',
)
}

async function waitForProviderAccount(
provider: RootWebAuthnAccountProvider,
address: `0x${string}`,
timeoutMs = 5_000,
) {
const deadline = Date.now() + timeoutMs
const normalizedAddress = address.toLowerCase()

while (Date.now() < deadline) {
const accounts = await provider.request({ method: 'eth_accounts' })
if (accounts.some((account) => account.toLowerCase() === normalizedAddress)) return

await new Promise((resolve) => setTimeout(resolve, 100))
}

throw new Error(`webauthn account ${address} not ready`)
}

async function waitForStoredCredential(
address: `0x${string}`,
timeoutMs = 5_000,
): Promise<RootWebAuthnCredential> {
const deadline = Date.now() + timeoutMs
const normalizedAddress = address.toLowerCase()

while (Date.now() < deadline) {
const credential = await config.storage?.getItem('webAuthn.activeCredential')
if (credential) {
const account = accountFromCredential(credential as RootWebAuthnCredential)
if (account.address.toLowerCase() === normalizedAddress) {
return credential as RootWebAuthnCredential
}
}

await new Promise((resolve) => setTimeout(resolve, 100))
}

throw new Error(`webauthn credential for ${address} not ready`)
}
26 changes: 3 additions & 23 deletions src/lib/useZoneAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import type { Hex } from 'viem'
import { Storage as ZoneStorage } from 'viem/tempo'

const zoneAuthorizationInfoTimeoutMs = 5_000

export type ZoneAuthClientLike = {
zone: {
getAuthorizationTokenInfo: () => Promise<{
Expand Down Expand Up @@ -46,10 +44,7 @@ export function useZoneAuthorization(parameters: {
if (accountToken) await storage.setItem(chainStorageKey, accountToken)

try {
const info = await withTimeout(
zoneClient.zone.getAuthorizationTokenInfo(),
zoneAuthorizationInfoTimeoutMs,
)
const info = await zoneClient.zone.getAuthorizationTokenInfo()
const expired = info.expiresAt <= BigInt(Math.floor(Date.now() / 1000))
const matchesAccount = info.account.toLowerCase() === lowerAddress

Expand Down Expand Up @@ -94,32 +89,17 @@ export function useZoneAuthorization(parameters: {
authorizeMutation,
error: authorizeMutation.error ?? statusQuery.error,
isAuthorized: statusQuery.data !== null && statusQuery.data !== undefined,
isChecking: statusQuery.fetchStatus === 'fetching',
isChecking: statusQuery.isPending,
statusQuery,
}
}

function withTimeout<T>(promise: Promise<T>, timeoutMs: number) {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
const timeout = setTimeout(() => {
const error = new Error('zone authorization info request timed out')
error.name = 'TimeoutError'
reject(error)
}, timeoutMs)

promise.finally(() => clearTimeout(timeout))
}),
])
}

function isZoneAuthorizationError(error: unknown) {
const status = getErrorStatus(error)
if (status === 401 || status === 403) return true

const name = getErrorName(error)
if (name === 'HttpRequestError' || name === 'TimeoutError') return true
if (name === 'HttpRequestError') return true

const message = getErrorMessage(error)
return /authorization token/i.test(message)
Expand Down
33 changes: 16 additions & 17 deletions src/pages/protocol/zones/proving.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ title: Zone Proving
description: Batch submission and proof verification for Tempo zones, including the state transition function, ZK and TEE deployment modes, and ancestry proofs.
---

import { StaticMermaidDiagram } from '../../../components/StaticMermaidDiagram'

# Zone Proving

:::info
Expand Down Expand Up @@ -73,22 +71,23 @@ pub fn prove_zone_batch(witness: BatchWitness) -> Result<BatchOutput, Error>

### Execution Flow

<StaticMermaidDiagram chart={`flowchart TD
A["Batch witness"] --> B["Verify Tempo state proofs"]
B --> C["Initialize zone state from previous block hash"]
C --> D{"Next zone block"}
D --> E["Check parent hash and block number"]
E --> F["Verify beneficiary is the sequencer"]
F --> G["Execute advanceTempo system transaction if present"]
G --> H["Execute user transactions via revm"]
H --> I{"Final block in batch?"}
I -- No --> J["Compute simplified zone block hash"]
```mermaid
flowchart TD
A[Batch witness] --> B[Verify Tempo state proofs]
B --> C[Initialize zone state from previous block hash]
C --> D{Next zone block}
D --> E[Check parent hash and block number]
E --> F[Verify beneficiary is the sequencer]
F --> G[Execute advanceTempo system transaction if present]
G --> H[Execute user transactions via revm]
H --> I{Final block in batch?}
I -- No --> J[Compute simplified zone block hash]
J --> D
I -- Yes --> K["Execute finalizeWithdrawalBatch"]
K --> L["Compute simplified zone block hash"]
L --> M["Extract output commitments"]
M --> N["Return batch output for verification"]
`} />
I -- Yes --> K[Execute finalizeWithdrawalBatch]
K --> L[Compute simplified zone block hash]
L --> M[Extract output commitments]
M --> N[Return batch output for verification]
```

1. **Verify Tempo state proofs.** Validate MPT proofs for all Tempo storage reads against Tempo state roots.
2. **Initialize zone state.** Load the zone state from the witness, binding the initial state root to the previous block hash.
Expand Down
Loading