From dc080740c811990fc6a75de3a8c87351ce84666a Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Mon, 2 Mar 2026 18:28:07 +0200 Subject: [PATCH 1/7] docs: add TIP-1023 address format page and convert docs to tempo1 addresses Adds documentation for the Tempo Address Format (TIP-1023) with an interactive bech32m converter tool, and migrates user-facing 0x addresses throughout the docs to the new tempo1 bech32m format. - Add bech32m encode/decode/validate library (src/lib/bech32m.ts) - Add interactive AddressConverter component with conversion and substitution error detection demo - Add dedicated /protocol/addresses page with converter, format overview, and links to TIP-1023 spec - Convert address tables (predeployed contracts, faucet, token lists) to tempo1 format - Convert deployment addresses in protocol specs (DEX, FeeManager, TIP20Factory, TIP403Registry, AccountKeychain) to tempo1 - Convert token address references in guides and SDK examples to tempo1 - Update sidebar to include Address Format page Code examples using Solidity/Rust/Go and low-level protocol byte values are intentionally left as 0x since the EVM operates on hex addresses. Companion to tempoxyz/tempo#2925 Made-with: Cursor --- src/components/AddressConverter.tsx | 316 ++++++++++++++++++ src/lib/bech32m.ts | 134 ++++++++ .../guide/issuance/create-a-stablecoin.mdx | 2 +- .../payments/pay-fees-in-any-stablecoin.mdx | 4 +- src/pages/guide/payments/send-a-payment.mdx | 2 +- src/pages/guide/use-accounts/add-funds.mdx | 8 +- src/pages/protocol/addresses.mdx | 115 +++++++ src/pages/protocol/exchange/index.mdx | 2 +- src/pages/protocol/exchange/quote-tokens.mdx | 2 +- src/pages/protocol/exchange/spec.mdx | 2 +- src/pages/protocol/fees/spec-fee-amm.mdx | 2 +- src/pages/protocol/tip20/spec.mdx | 2 +- src/pages/protocol/tip403/spec.mdx | 2 +- .../protocol/transactions/AccountKeychain.mdx | 2 +- .../transactions/spec-tempo-transaction.mdx | 2 +- src/pages/quickstart/faucet.mdx | 8 +- .../quickstart/predeployed-contracts.mdx | 20 +- src/pages/quickstart/tokenlist.mdx | 2 +- src/pages/quickstart/wallet-developers.mdx | 2 +- .../sdk/typescript/server/handler.compose.mdx | 2 +- .../typescript/server/handler.feePayer.mdx | 6 +- src/pages/sdk/typescript/server/handlers.mdx | 2 +- vocs.config.ts | 4 + 23 files changed, 606 insertions(+), 37 deletions(-) create mode 100644 src/components/AddressConverter.tsx create mode 100644 src/lib/bech32m.ts create mode 100644 src/pages/protocol/addresses.mdx diff --git a/src/components/AddressConverter.tsx b/src/components/AddressConverter.tsx new file mode 100644 index 00000000..76a4d80d --- /dev/null +++ b/src/components/AddressConverter.tsx @@ -0,0 +1,316 @@ +'use client' +import * as React from 'react' +import LucideArrowDownUp from '~icons/lucide/arrow-down-up' +import LucideCheck from '~icons/lucide/check' +import LucideCopy from '~icons/lucide/copy' +import LucideShieldAlert from '~icons/lucide/shield-alert' +import LucideShieldCheck from '~icons/lucide/shield-check' +import { + decodeTempoAddress, + encodeTempoAddress, + formatTempoAddress, + validateTempoAddress, +} from '../lib/bech32m' +import { Container } from './Container' + +const EXAMPLE_HEX = '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' +const EXAMPLE_TEMPO = 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0' + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = React.useState(false) + + const handleCopy = () => { + navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( + + ) +} + +function ConverterSection() { + const [hexInput, setHexInput] = React.useState(EXAMPLE_HEX) + const [tempoInput, setTempoInput] = React.useState('') + const [direction, setDirection] = React.useState<'to-tempo' | 'to-hex'>('to-tempo') + const [result, setResult] = React.useState<{ value: string; error?: string }>({ value: '' }) + + React.useEffect(() => { + try { + if (direction === 'to-tempo') { + if (!hexInput.trim()) { + setResult({ value: '' }) + return + } + const tempo = encodeTempoAddress(hexInput.trim()) + setResult({ value: tempo }) + } else { + if (!tempoInput.trim()) { + setResult({ value: '' }) + return + } + const hex = decodeTempoAddress(tempoInput.trim()) + setResult({ value: hex }) + } + } catch (e) { + setResult({ value: '', error: e instanceof Error ? e.message : 'Invalid input' }) + } + }, [hexInput, tempoInput, direction]) + + const toggleDirection = () => { + if (direction === 'to-tempo' && result.value && !result.error) { + setTempoInput(result.value) + setDirection('to-hex') + } else if (direction === 'to-hex' && result.value && !result.error) { + setHexInput(result.value) + setDirection('to-tempo') + } else { + setDirection((d) => (d === 'to-tempo' ? 'to-hex' : 'to-tempo')) + } + } + + const inputValue = direction === 'to-tempo' ? hexInput : tempoInput + const setInputValue = direction === 'to-tempo' ? setHexInput : setTempoInput + const inputLabel = direction === 'to-tempo' ? '0x Address' : 'tempo1 Address' + const outputLabel = direction === 'to-tempo' ? 'tempo1 Address' : '0x Address' + + return ( +
+
+ +
+ setInputValue(e.target.value)} + placeholder={direction === 'to-tempo' ? '0x...' : 'tempo1...'} + spellCheck={false} + className="w-full bg-transparent font-mono text-[13px] text-gray12 outline-none placeholder:text-gray8" + /> + {inputValue && } +
+
+ +
+ +
+ +
+ +
+ {result.error ? ( + {result.error} + ) : result.value ? ( +
+ + {result.value} + + +
+ ) : ( + Enter an address above + )} +
+ {result.value && direction === 'to-tempo' && ( +

+ Display: {formatTempoAddress(result.value)} +

+ )} +
+
+ ) +} + +const SUBSTITUTION_PRESETS = [ + { + label: '2-char substitution', + description: 'Positions 8 and 20', + changes: [ + { pos: 8, to: 'm' }, + { pos: 20, to: '6' }, + ], + }, + { + label: '4-char substitution', + description: 'Positions 8, 15, 25, 35', + changes: [ + { pos: 8, to: 'm' }, + { pos: 15, to: 'e' }, + { pos: 25, to: 'u' }, + { pos: 35, to: 'a' }, + ], + }, +] as const + +function SubstitutionDemo() { + const [tampered, setTampered] = React.useState(EXAMPLE_TEMPO) + const [activePreset, setActivePreset] = React.useState(null) + const validation = validateTempoAddress(tampered) + + const handlePreset = (idx: number) => { + const preset = SUBSTITUTION_PRESETS[idx] + let addr = EXAMPLE_TEMPO + for (const { pos, to } of preset.changes) { + addr = addr.slice(0, pos) + to + addr.slice(pos + 1) + } + setTampered(addr) + setActivePreset(idx) + } + + const handleReset = () => { + setTampered(EXAMPLE_TEMPO) + setActivePreset(null) + } + + const diffChars = React.useMemo(() => { + const result: number[] = [] + for (let i = 0; i < Math.max(tampered.length, EXAMPLE_TEMPO.length); i++) { + if (tampered[i] !== EXAMPLE_TEMPO[i]) result.push(i) + } + return new Set(result) + }, [tampered]) + + return ( +
+
+
+ +
+
+ {EXAMPLE_TEMPO} +
+
+ +
+
+ +
+ {SUBSTITUTION_PRESETS.map((preset, idx) => ( + + ))} + +
+
+
+ { + setTampered(e.target.value) + setActivePreset(null) + }} + spellCheck={false} + className="w-full bg-transparent font-mono text-[13px] text-gray12 outline-none" + /> +
+ + {diffChars.size > 0 && ( +
+
+ {Array.from(tampered).map((ch, i) => ( + + {ch} + + ))} +
+
+ {diffChars.size} character{diffChars.size !== 1 ? 's' : ''} changed from original +
+
+ )} +
+ +
+ {validation.valid ? ( + <> + + Valid tempo1 address + + ) : ( + <> + + + Rejected: {validation.error} + + + )} +
+
+ ) +} + +export function AddressConverter() { + const [tab, setTab] = React.useState<'convert' | 'detect'>('convert') + + return ( + + + + + } + > + {tab === 'convert' ? : } + + ) +} + +export default AddressConverter diff --git a/src/lib/bech32m.ts b/src/lib/bech32m.ts new file mode 100644 index 00000000..7087917d --- /dev/null +++ b/src/lib/bech32m.ts @@ -0,0 +1,134 @@ +const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' +const BECH32M_CONST = 0x2bc830a3 +const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + +function polymod(values: number[]): number { + let chk = 1 + for (const v of values) { + const b = chk >> 25 + chk = ((chk & 0x1ffffff) << 5) ^ v + for (let i = 0; i < 5; i++) { + chk ^= (b >> i) & 1 ? GEN[i] : 0 + } + } + return chk +} + +function hrpExpand(hrp: string): number[] { + const ret: number[] = [] + for (const c of hrp) ret.push(c.charCodeAt(0) >> 5) + ret.push(0) + for (const c of hrp) ret.push(c.charCodeAt(0) & 31) + return ret +} + +function verifyChecksum(hrp: string, data: number[]): boolean { + return polymod([...hrpExpand(hrp), ...data]) === BECH32M_CONST +} + +function createChecksum(hrp: string, data: number[]): number[] { + const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0] + const mod = polymod(values) ^ BECH32M_CONST + return Array.from({ length: 6 }, (_, i) => (mod >> (5 * (5 - i))) & 31) +} + +function convertBits(data: number[], fromBits: number, toBits: number, pad: boolean): number[] { + let acc = 0 + let bits = 0 + const maxv = (1 << toBits) - 1 + const ret: number[] = [] + + for (const value of data) { + acc = (acc << fromBits) | value + bits += fromBits + while (bits >= toBits) { + bits -= toBits + ret.push((acc >> bits) & maxv) + } + } + + if (pad) { + if (bits > 0) ret.push((acc << (toBits - bits)) & maxv) + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) !== 0) { + throw new Error('Invalid padding') + } + + return ret +} + +function bech32mEncode(hrp: string, dataBytes: number[]): string { + const data5 = convertBits(dataBytes, 8, 5, true) + const checksum = createChecksum(hrp, data5) + return hrp + '1' + [...data5, ...checksum].map((d) => CHARSET[d]).join('') +} + +function bech32mDecode(addr: string): { hrp: string; data: number[] } { + if (addr.length > 90) throw new Error('Address exceeds 90 characters') + + for (const c of addr) { + const code = c.charCodeAt(0) + if (code < 33 || code > 126) throw new Error('Character outside printable ASCII range') + } + + const hasLower = /[a-z]/.test(addr) + const hasUpper = /[A-Z]/.test(addr) + if (hasLower && hasUpper) throw new Error('Mixed case rejected per BIP-350') + + addr = addr.toLowerCase() + const pos = addr.lastIndexOf('1') + if (pos < 1) throw new Error('No separator character found') + + const hrp = addr.slice(0, pos) + const dataPart = addr.slice(pos + 1) + if (dataPart.length < 6) throw new Error('Data part too short for checksum') + + const data5 = Array.from(dataPart, (c) => CHARSET.indexOf(c)) + if (data5.some((d) => d === -1)) throw new Error('Invalid character in data part') + + if (!verifyChecksum(hrp, data5)) throw new Error('Invalid checksum — address is corrupted') + + const payload = convertBits(data5.slice(0, -6), 5, 8, false) + return { hrp, data: payload } +} + +const CURRENT_VERSION = 0 + +export function encodeTempoAddress(hexAddress: string): string { + const hex = hexAddress.replace(/^0x/i, '') + if (hex.length !== 40 || !/^[0-9a-fA-F]{40}$/.test(hex)) + throw new Error('Invalid hex address: must be 20 bytes (40 hex chars)') + + const rawBytes = Array.from({ length: 20 }, (_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16)) + const data = [CURRENT_VERSION, ...rawBytes] + return bech32mEncode('tempo', data) +} + +export function decodeTempoAddress(address: string): string { + const { hrp, data } = bech32mDecode(address) + + if (hrp !== 'tempo') throw new Error(`Invalid HRP: expected "tempo", got "${hrp}"`) + if (data.length < 1) throw new Error('Address data too short') + + const version = data[0] + if (version !== CURRENT_VERSION) throw new Error(`Unsupported version: ${version}`) + + const rawAddress = data.slice(1) + if (rawAddress.length !== 20) + throw new Error(`Invalid address length: ${rawAddress.length} bytes (expected 20)`) + + return '0x' + rawAddress.map((b) => b.toString(16).padStart(2, '0')).join('') +} + +export function validateTempoAddress(address: string): { valid: boolean; error?: string } { + try { + decodeTempoAddress(address) + return { valid: true } + } catch (e) { + return { valid: false, error: e instanceof Error ? e.message : 'Unknown error' } + } +} + +export function formatTempoAddress(address: string): string { + if (address.length < 12) return address + return `${address.slice(0, 6)} ${address.slice(6).replace(/(.{5})/g, '$1 ')}`.trim() +} diff --git a/src/pages/guide/issuance/create-a-stablecoin.mdx b/src/pages/guide/issuance/create-a-stablecoin.mdx index c3f2c7fa..5952bc97 100644 --- a/src/pages/guide/issuance/create-a-stablecoin.mdx +++ b/src/pages/guide/issuance/create-a-stablecoin.mdx @@ -37,7 +37,7 @@ Ensure that you have set up your project with Wagmi and integrated accounts by f Before we send off a transaction to deploy our stablecoin to the Tempo testnet, we need to make sure our account is funded with a stablecoin to cover the transaction fee. -As we have configured our project to use `AlphaUSD` (`0x20c000…0001`) +As we have configured our project to use `AlphaUSD` (`tempo1qqsv…9xgnd`) as the [default fee token](/quickstart/integrate-tempo#default-fee-token), we will need to add some `AlphaUSD` to our account. Luckily, the built-in Tempo testnet faucet supports funding accounts with `AlphaUSD`. diff --git a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx index c3b3dd28..8aa4ab6f 100644 --- a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx +++ b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx @@ -88,7 +88,7 @@ Ensure that you have set up your project with Wagmi and integrated accounts by f ### Add testnet funds¹ -Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`) and paying fees in `BetaUSD` (`0x20c000…0002`). +Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`tempo1qqsv…9xgnd`) and paying fees in `BetaUSD` (`tempo1qqsv…mqzz2`). The built-in Tempo testnet faucet includes `AlphaUSD` and `BetaUSD` when funding. @@ -272,7 +272,7 @@ For simplicity of the guide, this example uses a Private Key (Secp256k1) account ### Add testnet funds¹ -Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`) and paying fees in `BetaUSD` (`0x20c000…0002`). +Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`tempo1qqsv…9xgnd`) and paying fees in `BetaUSD` (`tempo1qqsv…mqzz2`). The built-in Tempo testnet faucet includes `AlphaUSD` and `BetaUSD` when funding. diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/guide/payments/send-a-payment.mdx index 7eaaa891..1f25b0d8 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/guide/payments/send-a-payment.mdx @@ -33,7 +33,7 @@ Ensure that you have set up your project with Wagmi and integrated accounts by f ### Add testnet funds¹ -Before you can send a payment, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`). +Before you can send a payment, you need to fund your account. In this guide you will be sending `AlphaUSD` (`tempo1qqsv…9xgnd`). The built-in Tempo testnet faucet funds accounts with `AlphaUSD`. diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index fcced800..0b716cd0 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -82,10 +82,10 @@ The faucet funds the following assets. | Asset | Address |Amount| |-------|---------|----:| -| [pathUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `0x20c0000000000000000000000000000000000000` | `1M` | -| [AlphaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `0x20c0000000000000000000000000000000000001` | `1M` | -| [BetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `0x20c0000000000000000000000000000000000002` | `1M` | -| [ThetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `0x20c0000000000000000000000000000000000003` | `1M` | +| [pathUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh` | `1M` | +| [AlphaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd` | `1M` | +| [BetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgjmqzz2` | `1M` | +| [ThetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqva3zyds` | `1M` | ## Verify Your Balance diff --git a/src/pages/protocol/addresses.mdx b/src/pages/protocol/addresses.mdx new file mode 100644 index 00000000..88328bca --- /dev/null +++ b/src/pages/protocol/addresses.mdx @@ -0,0 +1,115 @@ +--- +title: Tempo Address Format +description: Convert between 0x and tempo1 address formats. Tempo uses bech32m encoding (BIP-350) for human-readable addresses with built-in error detection. +--- + +import { AddressConverter } from '../../components/AddressConverter' + +# Tempo Address Format + +Tempo addresses use [bech32m encoding](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki) (BIP-350) to provide a human-readable format that is instantly distinguishable from addresses on other chains. All Tempo addresses begin with the `tempo1` prefix and are exactly 46 characters long. + +``` +tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0 +``` + +## Address Converter + +Convert between standard `0x` hex addresses and `tempo1` bech32m addresses. Use the **Error Detection** tab to see how bech32m catches character substitution errors. + + + +## Format Overview + +A Tempo address encodes a 20-byte Ethereum-style address with a version byte, a human-readable prefix, and a 6-character checksum: + +``` +┌─────────┬───┬──────────────────────────────────────────────────────┐ +│ HRP │ 1 │ Base32-encoded data + checksum │ +│ "tempo" │sep│ │ +└─────────┴───┴──────────────────────────────────────────────────────┘ + │ + ┌───────────────────┴──────────────────────────┐ + │ Data (base32) │ + ├─────────┬──────────────┬─────────────────────┤ + │ Version │ Raw Address │ Bech32m Checksum │ + │ 1 byte │ 20 bytes │ 6 chars │ + └─────────┴──────────────┴─────────────────────┘ +``` + +| Property | Value | +|----------|-------| +| HRP (Human-Readable Part) | `tempo` | +| Full prefix | `tempo1` | +| Version byte | `0x00` (current) | +| Raw address | 20 bytes (Ethereum-style) | +| Total length | 46 characters | +| Encoding | bech32m (BIP-350) | + +## Error Detection + +The bech32m checksum guarantees detection of: + +- **Up to 4 character substitutions** anywhere in the address +- **Insertions and deletions** via checksum (≥ 1 − 2⁻³⁰ probability) plus fixed-length validation + +This means if a user accidentally mistypes up to 4 characters in a Tempo address, the error will always be caught before any funds are sent. + +## Case Handling + +Per BIP-350, Tempo addresses are case-insensitive: + +- Encoders produce **lowercase** (`tempo1qp6z...`) +- Decoders accept all-lowercase or all-uppercase +- **Mixed case is rejected** — `TeMpO1qp6z...` is invalid + +## Display Recommendations + +For readability, UIs may display addresses with visual spacing: + +``` +tempo1 qp6z6 dwvvc 6vq5e fyk3m s39un e6etu 4a9qt j2kk0 +``` + +Spaces are for display only. Copy buttons must always use the canonical form without spaces. + +## JavaScript / TypeScript Usage + +The `src/lib/bech32m.ts` module provides encode, decode, and validate functions: + +```typescript +import { + encodeTempoAddress, + decodeTempoAddress, + validateTempoAddress, +} from './lib/bech32m' + +// 0x → tempo1 +const tempo = encodeTempoAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28') +// → "tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0" + +// tempo1 → 0x +const hex = decodeTempoAddress('tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0') +// → "0x742d35cc6634c0532925a3b844bc9e7595f2bd28" + +// Validate without throwing +const result = validateTempoAddress('tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kkq') +// → { valid: false, error: "Invalid checksum — address is corrupted" } +``` + +## Specification + +The full specification is defined in [TIP-1023](/protocol/tips/tip-1023), which covers: + +- Complete encoding and decoding algorithms +- Version byte semantics and extensibility +- Validation rules (8 steps) +- Invariants (9 properties) +- Python reference implementation +- Test vectors for valid addresses, invalid versions, incorrect lengths, and substitution detection + +## Source Code + +- [TIP-1023 Specification](https://github.com/tempoxyz/tempo/pull/2925) — PR in `tempoxyz/tempo` +- [TypeScript Implementation](https://github.com/tempoxyz/docs/blob/main/src/lib/bech32m.ts) — `bech32m.ts` in this docs repo +- [BIP-350 (bech32m)](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki) — The underlying encoding standard diff --git a/src/pages/protocol/exchange/index.mdx b/src/pages/protocol/exchange/index.mdx index aa48036a..10e47835 100644 --- a/src/pages/protocol/exchange/index.mdx +++ b/src/pages/protocol/exchange/index.mdx @@ -8,7 +8,7 @@ import { Cards, Card } from 'vocs' Tempo features an enshrined decentralized exchange (DEX) designed specifically for trading between stablecoins of the same underlying asset (e.g., USDC to USDT). The exchange provides optimal pricing for cross-stablecoin payments while minimizing chain load from excessive market activity. -The exchange operates as a singleton precompiled contract at address `0xdec0000000000000000000000000000000000000`. It maintains an orderbook with separate queues for each price tick, using price-time priority for order matching. +The exchange operates as a singleton precompiled contract at address `tempo1qr0vqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvml629`. It maintains an orderbook with separate queues for each price tick, using price-time priority for order matching. Trading pairs are determined by each token's quote token. All TIP-20 tokens specify a quote token for trading on the exchange. See [Quote Tokens](/protocol/exchange/quote-tokens) for more information on quote token selection and the optional [pathUSD](/protocol/exchange/quote-tokens#pathusd) stablecoin. See the [Stablecoin DEX Specification](/protocol/exchange/spec) for detailed information on the exchange structure. diff --git a/src/pages/protocol/exchange/quote-tokens.mdx b/src/pages/protocol/exchange/quote-tokens.mdx index 15091e0b..bafd6b79 100644 --- a/src/pages/protocol/exchange/quote-tokens.mdx +++ b/src/pages/protocol/exchange/quote-tokens.mdx @@ -22,7 +22,7 @@ pathUSD is a predeployed [TIP-20](/protocol/tip20/spec) at genesis. Since it is | Property | Value | | -------------- | -------------------------------------------- | -| address | `0x20c0000000000000000000000000000000000000` | +| address | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh` | | `name()` | `"pathUSD"` | | `symbol()` | `"pathUSD"` | | `currency()` | `"USD"` | diff --git a/src/pages/protocol/exchange/spec.mdx b/src/pages/protocol/exchange/spec.mdx index a940eee2..4dea2620 100644 --- a/src/pages/protocol/exchange/spec.mdx +++ b/src/pages/protocol/exchange/spec.mdx @@ -24,7 +24,7 @@ Another design goal is to avoid fragmentation of liquidity across many different ### Contract and scope -The exchange is a singleton contract deployed at `0xdec0000000000000000000000000000000000000`. It exposes functions to create trading pairs, place and cancel orders (including flip orders), execute swaps, produce quotes, and manage internal balances. +The exchange is a singleton contract deployed at `tempo1qr0vqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvml629`. It exposes functions to create trading pairs, place and cancel orders (including flip orders), execute swaps, produce quotes, and manage internal balances. ### Key concepts diff --git a/src/pages/protocol/fees/spec-fee-amm.mdx b/src/pages/protocol/fees/spec-fee-amm.mdx index b74bef4d..5f0efecc 100644 --- a/src/pages/protocol/fees/spec-fee-amm.mdx +++ b/src/pages/protocol/fees/spec-fee-amm.mdx @@ -140,7 +140,7 @@ Verifies sufficient validator token reserves for the fee swap. Calculates `maxAm #### 2. FeeManager Contract -Tempo introduces a precompiled contract, the `FeeManager`, at the address `0xfeec000000000000000000000000000000000000`. +Tempo introduces a precompiled contract, the `FeeManager`, at the address `tempo1qrlwcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqx3a39m`. The `FeeManager` is a singleton contract that implements all the functions of the Fee AMM for every pool. It handles the collection and refunding of fees during each transaction, executes fee swaps immediately, stores fee token preferences for users and validators, and accumulates fees for validators to claim via `distributeFees()`. diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/protocol/tip20/spec.mdx index 026414e1..45a4de8e 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/protocol/tip20/spec.mdx @@ -445,7 +445,7 @@ System level functions `systemTransferFrom`, `transferFeePreTx`, and `transferFe See [rewards distribution](/protocol/tip20-rewards/spec) for more information. ## TIP20Factory -The `TIP20Factory` contract is the canonical entrypoint for creating new TIP-20 tokens on Tempo. The factory derives deterministic deployment addresses using a caller-provided salt, combined with the caller's address, under a fixed 12-byte TIP-20 prefix. This ensures that every TIP-20 token exists at a predictable, collision-free address. The `TIP20Factory` precompile is deployed at `0x20Fc000000000000000000000000000000000000`. +The `TIP20Factory` contract is the canonical entrypoint for creating new TIP-20 tokens on Tempo. The factory derives deterministic deployment addresses using a caller-provided salt, combined with the caller's address, under a fixed 12-byte TIP-20 prefix. This ensures that every TIP-20 token exists at a predictable, collision-free address. The `TIP20Factory` precompile is deployed at `tempo1qqs0cqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkm2wnu`. Newly created TIP-20 addresses are deployed to a deterministic address derived from `TIP20_PREFIX || lowerBytes`, where: - `TIP20_PREFIX` is the 12-byte prefix `20C000000000000000000000` diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/protocol/tip403/spec.mdx index d60e5b8b..3ef38f99 100644 --- a/src/pages/protocol/tip403/spec.mdx +++ b/src/pages/protocol/tip403/spec.mdx @@ -20,7 +20,7 @@ TIP-403 addresses this by providing a centralized registry that tokens can refer The TIP-403 registry stores policies that TIP-20 tokens check against on any token transfer. Policies are associated with a unique `policyId`, can either be a blacklist or a whitelist policy, and contain a list of addresses. This list of addresses can be updated by the policy `admin`. -The TIP403Registry is deployed at address `0x403c000000000000000000000000000000000000`. +The TIP403Registry is deployed at address `tempo1qpqrcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqr08uee`. ## Built-in Policies diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/protocol/transactions/AccountKeychain.mdx index 71f0438b..734b7544 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/protocol/transactions/AccountKeychain.mdx @@ -4,7 +4,7 @@ description: Technical specification for the Account Keychain precompile managin # Account Keychain Precompile -**Address:** `0xAAAAAAAA00000000000000000000000000000000` +**Address:** `tempo1qz424242qqqqqqqqqqqqqqqqqqqqqqqqqqgsxxqr` ## Overview diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 38742ca3..d30d0edf 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -587,7 +587,7 @@ Note: `expiry` and `limits` use RLP trailing field semantics - they can be omitt #### Keychain Precompile -The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits. +The Account Keychain precompile (deployed at address `tempo1qz424242qqqqqqqqqqqqqqqqqqqqqqqqqqgsxxqr`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits. **See the [Account Keychain Specification](./AccountKeychain) for complete interface details, storage layout, and implementation.** diff --git a/src/pages/quickstart/faucet.mdx b/src/pages/quickstart/faucet.mdx index 26e6b5a6..dbaa1d64 100644 --- a/src/pages/quickstart/faucet.mdx +++ b/src/pages/quickstart/faucet.mdx @@ -83,7 +83,7 @@ The faucet funds the following assets. | Asset | Address |Amount| |-------|---------|----:| -| [pathUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `0x20c0000000000000000000000000000000000000` | `1M` | -| [AlphaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `0x20c0000000000000000000000000000000000001` | `1M` | -| [BetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `0x20c0000000000000000000000000000000000002` | `1M` | -| [ThetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `0x20c0000000000000000000000000000000000003` | `1M` | +| [pathUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh` | `1M` | +| [AlphaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd` | `1M` | +| [BetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgjmqzz2` | `1M` | +| [ThetaUSD](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqva3zyds` | `1M` | diff --git a/src/pages/quickstart/predeployed-contracts.mdx b/src/pages/quickstart/predeployed-contracts.mdx index b3648038..36401525 100644 --- a/src/pages/quickstart/predeployed-contracts.mdx +++ b/src/pages/quickstart/predeployed-contracts.mdx @@ -10,11 +10,11 @@ Core protocol contracts that power Tempo's features. | Contract | Address | Description | |----------|---------|-------------| -| [**TIP-20 Factory**](/protocol/tip20/overview) | `0x20fc000000000000000000000000000000000000` | Create new TIP-20 tokens | -| [**Fee Manager**](/protocol/fees/spec-fee-amm#2-feemanager-contract) | `0xfeec000000000000000000000000000000000000` | Handle fee payments and conversions | -| [**Stablecoin DEX**](/protocol/exchange) | `0xdec0000000000000000000000000000000000000` | Enshrined DEX for stablecoin swaps | -| [**TIP-403 Registry**](/protocol/tip403/spec) | `0x403c000000000000000000000000000000000000` | Transfer policy registry | -| [**pathUSD**](/protocol/exchange/quote-tokens#pathusd) | `0x20c0000000000000000000000000000000000000` | First stablecoin deployed | +| [**TIP-20 Factory**](/protocol/tip20/overview) | `tempo1qqs0cqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkm2wnu` | Create new TIP-20 tokens | +| [**Fee Manager**](/protocol/fees/spec-fee-amm#2-feemanager-contract) | `tempo1qrlwcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqx3a39m` | Handle fee payments and conversions | +| [**Stablecoin DEX**](/protocol/exchange) | `tempo1qr0vqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvml629` | Enshrined DEX for stablecoin swaps | +| [**TIP-403 Registry**](/protocol/tip403/spec) | `tempo1qpqrcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqr08uee` | Transfer policy registry | +| [**pathUSD**](/protocol/exchange/quote-tokens#pathusd) | `tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh` | First stablecoin deployed | ## Standard Utilities @@ -22,11 +22,11 @@ Popular Ethereum contracts deployed for convenience. | Contract | Address | Description | |----------|---------|-------------| -| [**Multicall3**](https://www.multicall3.com/) | `0xcA11bde05977b3631167028862bE2a173976CA11` | Batch multiple calls in one transaction | -| [**CreateX**](https://github.com/pcaversaccio/createx) | `0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed` | Deterministic contract deployment | -| [**Permit2**](https://docs.uniswap.org/contracts/permit2/overview) | `0x000000000022d473030f116ddee9f6b43ac78ba3` | Token approvals and transfers | -| [**Arachnid Create2 Factory**](https://github.com/Arachnid/deterministic-deployment-proxy) | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | CREATE2 deployment proxy | -| [**Safe Deployer**](https://github.com/safe-fndn/safe-singleton-factory) | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | Safe deployer contract | +| [**Multicall3**](https://www.multicall3.com/) | `tempo1qr9pr00qt9mmxcc3vupgsc479gtnjak2zyetjw2l` | Batch multiple calls in one transaction | +| [**CreateX**](https://github.com/pcaversaccio/createx) | `tempo1qza9a5yevv7nkvf7f40hhhqnqhfu9za9a5kzeeav` | Deterministic contract deployment | +| [**Permit2**](https://docs.uniswap.org/contracts/permit2/overview) | `tempo1qqqqqqqqqq3dgucrpugkmhhf766r43ut5vkwg6pn` | Token approvals and transfers | +| [**Arachnid Create2 Factory**](https://github.com/Arachnid/deterministic-deployment-proxy) | `tempo1qp89ndzgg7ehj4u93zfqefu0hunvpdy4dsxdzh93` | CREATE2 deployment proxy | +| [**Safe Deployer**](https://github.com/safe-fndn/safe-singleton-factory) | `tempo1qzg56llvd2kge42zuu4u579nqegdg4jr6u5aqam2` | Safe deployer contract | ## Contract ABIs diff --git a/src/pages/quickstart/tokenlist.mdx b/src/pages/quickstart/tokenlist.mdx index 1ec420fb..631ed057 100644 --- a/src/pages/quickstart/tokenlist.mdx +++ b/src/pages/quickstart/tokenlist.mdx @@ -20,7 +20,7 @@ As an example, here's Tempo Testnet's tokenlist, fetched from [tokenlist.tempo.x [`/list/{chain_id}`](https://tokenlist.tempo.xyz/list/42431) | Token list for a chain | [`/asset/{chain_id}/{id}`](https://tokenlist.tempo.xyz/asset/42431/pathUSD) | Get a single token by symbol or address​ [`/icon/{chain_id}`](https://tokenlist.tempo.xyz/icon/42431) | Chain icon (SVG) | -[`/icon/{chain_id}/{address}`](https://tokenlist.tempo.xyz/icon/42431/0x20c0000000000000000000000000000000000000) | Token icon (SVG) | +[`/icon/{chain_id}/{address}`](https://tokenlist.tempo.xyz/icon/42431/tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh) | Token icon (SVG) | ## Adding a New Token diff --git a/src/pages/quickstart/wallet-developers.mdx b/src/pages/quickstart/wallet-developers.mdx index c2ddab9d..bf47de2c 100644 --- a/src/pages/quickstart/wallet-developers.mdx +++ b/src/pages/quickstart/wallet-developers.mdx @@ -116,7 +116,7 @@ Set the user's default fee token preference. This will be used for all transacti import { setUserToken } from 'viem/tempo' await client.fee.setUserTokenSync({ - token: '0x20c0000000000000000000000000000000000001', + token: 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd', // AlphaUSD }) ``` diff --git a/src/pages/sdk/typescript/server/handler.compose.mdx b/src/pages/sdk/typescript/server/handler.compose.mdx index f72682ac..8f58bcf2 100644 --- a/src/pages/sdk/typescript/server/handler.compose.mdx +++ b/src/pages/sdk/typescript/server/handler.compose.mdx @@ -20,7 +20,7 @@ const handler = Handler.compose([ Handler.feePayer({ account: privateKeyToAccount('0x...'), chain: tempoModerato.extend({ - feeToken: '0x20c0...0001' + feeToken: 'tempo1qqsv…9xgnd' // AlphaUSD }), transport: http(), path: '/fee-payer', diff --git a/src/pages/sdk/typescript/server/handler.feePayer.mdx b/src/pages/sdk/typescript/server/handler.feePayer.mdx index 6344d28c..47119f9f 100644 --- a/src/pages/sdk/typescript/server/handler.feePayer.mdx +++ b/src/pages/sdk/typescript/server/handler.feePayer.mdx @@ -20,7 +20,7 @@ import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.feePayer({ account: privateKeyToAccount('0x...'), - chain: tempoModerato.extend({ feeToken: '0x20c0...0001' }), + chain: tempoModerato.extend({ feeToken: 'tempo1qqsv…9xgnd' }), // AlphaUSD path: '/fee-payer', transport: http(), }) @@ -44,8 +44,8 @@ const client = createClient({ const receipt = await client.token.transferSync({ amount: parseUnits('10', 6), feePayer: true, - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', + to: 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu97hvwnw3n3', + token: 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd', // AlphaUSD }) ``` diff --git a/src/pages/sdk/typescript/server/handlers.mdx b/src/pages/sdk/typescript/server/handlers.mdx index 98e80414..ffac1dbb 100644 --- a/src/pages/sdk/typescript/server/handlers.mdx +++ b/src/pages/sdk/typescript/server/handlers.mdx @@ -19,7 +19,7 @@ import { account, client } from './config' const handler = Handler.feePayer({ account, client, - feeToken: '0x20c0…0001' + feeToken: 'tempo1qqsv…9xgnd' // AlphaUSD path: '/fee-payer', }) diff --git a/vocs.config.ts b/vocs.config.ts index 9ebdd607..930567e5 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -436,6 +436,10 @@ export default defineConfig({ }, ], }, + { + text: 'Address Format', + link: '/protocol/addresses', + }, { text: 'TIPs', link: '/protocol/tips', From dd597ff7d18b508d418c52ba6dde64eb67621a51 Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Mon, 2 Mar 2026 19:05:05 +0200 Subject: [PATCH 2/7] docs: add tempo1 address integration step to wallet developer guide Add a dedicated step for displaying and accepting tempo1 addresses, with code examples for encode/decode and a link to display recommendations. Also adds it to the pre-launch checklist and learning resources. Made-with: Cursor --- src/pages/quickstart/wallet-developers.mdx | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/pages/quickstart/wallet-developers.mdx b/src/pages/quickstart/wallet-developers.mdx index bf47de2c..20091c0f 100644 --- a/src/pages/quickstart/wallet-developers.mdx +++ b/src/pages/quickstart/wallet-developers.mdx @@ -58,6 +58,28 @@ As a wallet developer, you can set the fee token for your user at the account le If you don't, Tempo uses a cascading fee token selection algorithm to determine the fee token for a transaction – learn more about [Fee Token Preferences](/protocol/fees/spec-fee#fee-token-preferences). ::: +### Display Tempo addresses + +Tempo uses a human-readable address format based on [bech32m encoding](/protocol/addresses) (BIP-350). All Tempo addresses start with the `tempo1` prefix and are exactly 46 characters long, providing instant chain recognition and built-in error detection for up to 4 character substitutions. + +Your wallet should display and accept `tempo1` addresses throughout the UI. Use the [bech32m library](/protocol/addresses#javascript--typescript-usage) to convert between the internal `0x` hex format used by the EVM and the user-facing `tempo1` format: + +```ts +import { encodeTempoAddress, decodeTempoAddress } from './lib/bech32m' + +// Display: convert internal hex to user-facing tempo1 +const display = encodeTempoAddress(account.address) +// → "tempo1qp6z6dwvvc6vq5efyk3ms39une6etu97hvwnw3n3" + +// Input: convert user-pasted tempo1 back to hex for transactions +const hex = decodeTempoAddress(userInput) +// → "0x742d35cc6634c0532925a3b844bc9e7595f0bebb" +``` + +:::tip +Display addresses with optional visual spacing for readability (e.g., `tempo1 qp6z6 dwvvc …`), but always copy the canonical form without spaces. See [Display Recommendations](/protocol/addresses#display-recommendations). +::: + ### Display token and network assets Tempo provides a public tokenlist service that hosts token and network assets. You can pull these assets from our public tokenlist service to display in your UI. @@ -130,12 +152,19 @@ Before launching Tempo support, ensure your wallet: - [ ] Hides or removes native balance display for Tempo - [ ] Displays `USD` as the currency symbol for gas - [ ] Quotes gas prices in the user's fee token +- [ ] Displays and accepts `tempo1` addresses ([format details](/protocol/addresses)) - [ ] Pulls token/network assets from Tempo's tokenlist - [ ] (Recommended) Integrates Tempo Transactions for enhanced UX ## Learning Resources + Date: Mon, 2 Mar 2026 19:12:55 +0200 Subject: [PATCH 3/7] docs: implement real bech32m error correction in address tool Replace the string-comparison-based substitution demo with actual BCH error correction that recovers corrupted addresses with no knowledge of the original: - Add correctTempoAddress() to bech32m.ts that performs brute-force BCH decoding: 1-error search (1,280 candidates) then 2-error search (~800K candidates) - Redesign Error Correction tab to show recovered address, error positions, and corrected characters - Presets for 1, 2, 3, and 5 errors demonstrate the boundary between correctable (1-2) and detection-only (3+) - Update addresses.mdx to document correction capabilities Made-with: Cursor --- src/components/AddressConverter.tsx | 209 ++++++++++++++++------------ src/lib/bech32m.ts | 154 ++++++++++++++++++++ src/pages/protocol/addresses.mdx | 13 +- 3 files changed, 286 insertions(+), 90 deletions(-) diff --git a/src/components/AddressConverter.tsx b/src/components/AddressConverter.tsx index 76a4d80d..45197c85 100644 --- a/src/components/AddressConverter.tsx +++ b/src/components/AddressConverter.tsx @@ -6,6 +6,8 @@ import LucideCopy from '~icons/lucide/copy' import LucideShieldAlert from '~icons/lucide/shield-alert' import LucideShieldCheck from '~icons/lucide/shield-check' import { + type CorrectionResult, + correctTempoAddress, decodeTempoAddress, encodeTempoAddress, formatTempoAddress, @@ -136,75 +138,139 @@ function ConverterSection() { ) } -const SUBSTITUTION_PRESETS = [ +const ERROR_PRESETS = [ { - label: '2-char substitution', - description: 'Positions 8 and 20', + label: '1 error', + changes: [{ pos: 12, to: 'x' }], + }, + { + label: '2 errors', changes: [ { pos: 8, to: 'm' }, { pos: 20, to: '6' }, ], }, { - label: '4-char substitution', - description: 'Positions 8, 15, 25, 35', + label: '3 errors', + changes: [ + { pos: 8, to: 'm' }, + { pos: 20, to: '6' }, + { pos: 30, to: 'q' }, + ], + }, + { + label: '5 errors', changes: [ { pos: 8, to: 'm' }, { pos: 15, to: 'e' }, - { pos: 25, to: 'u' }, - { pos: 35, to: 'a' }, + { pos: 22, to: 'x' }, + { pos: 30, to: 'q' }, + { pos: 38, to: 'z' }, ], }, ] as const -function SubstitutionDemo() { - const [tampered, setTampered] = React.useState(EXAMPLE_TEMPO) +function StatusBadge({ result }: { result: CorrectionResult }) { + if (result.status === 'valid') + return ( +
+ + Valid address — no errors +
+ ) + + if (result.status === 'invalid_format') + return ( +
+ + {result.error} +
+ ) + + if (result.status === 'corrected') + return ( +
+
+ + + Corrected — recovered the original address by locating{' '} + {result.errors!.length} error{result.errors!.length !== 1 ? 's' : ''} + {result.searchedErrors === 2 && result.errors!.length === 2 && ' (2-error search)'} + +
+
+
+ {result.corrected} + +
+
+ {result.errors!.map((e) => ( +
+ position {e.position}: {e.was} →{' '} + {e.correctedTo} +
+ ))} +
+
+
+ ) + + return ( +
+ + + Detected — {result.error} + +
+ ) +} + +function ErrorCorrectionDemo() { + const [input, setInput] = React.useState(EXAMPLE_TEMPO) const [activePreset, setActivePreset] = React.useState(null) - const validation = validateTempoAddress(tampered) + const [correction, setCorrection] = React.useState(null) + const [computing, setComputing] = React.useState(false) + + React.useEffect(() => { + if (!input.trim()) { + setCorrection(null) + return + } + setComputing(true) + // defer to keep UI responsive during 2-error search + const id = setTimeout(() => { + setCorrection(correctTempoAddress(input.trim())) + setComputing(false) + }, 10) + return () => clearTimeout(id) + }, [input]) - const handlePreset = (idx: number) => { - const preset = SUBSTITUTION_PRESETS[idx] + const applyPreset = (idx: number) => { let addr = EXAMPLE_TEMPO - for (const { pos, to } of preset.changes) { + for (const { pos, to } of ERROR_PRESETS[idx].changes) { addr = addr.slice(0, pos) + to + addr.slice(pos + 1) } - setTampered(addr) + setInput(addr) setActivePreset(idx) } - const handleReset = () => { - setTampered(EXAMPLE_TEMPO) - setActivePreset(null) - } - - const diffChars = React.useMemo(() => { - const result: number[] = [] - for (let i = 0; i < Math.max(tampered.length, EXAMPLE_TEMPO.length); i++) { - if (tampered[i] !== EXAMPLE_TEMPO[i]) result.push(i) - } - return new Set(result) - }, [tampered]) - return (
-
-
- -
-
- {EXAMPLE_TEMPO} -
-
+

+ Paste or type any corrupted tempo1 address. The + bech32m checksum will detect the error, and for 1–2 substitutions the algorithm can locate + and recover the original address with no prior knowledge of it. +

- +
- {SUBSTITUTION_PRESETS.map((preset, idx) => ( + {ERROR_PRESETS.map((preset, idx) => (
{ - setTampered(e.target.value) + setInput(e.target.value) setActivePreset(null) }} + placeholder="tempo1..." spellCheck={false} - className="w-full bg-transparent font-mono text-[13px] text-gray12 outline-none" + className="w-full bg-transparent font-mono text-[13px] text-gray12 outline-none placeholder:text-gray8" />
- - {diffChars.size > 0 && ( -
-
- {Array.from(tampered).map((ch, i) => ( - - {ch} - - ))} -
-
- {diffChars.size} character{diffChars.size !== 1 ? 's' : ''} changed from original -
-
- )}
-
- {validation.valid ? ( - <> - - Valid tempo1 address - - ) : ( - <> - - - Rejected: {validation.error} - - - )} -
+ {computing && ( +
Searching for corrections...
+ )} + {!computing && correction && }
) } export function AddressConverter() { - const [tab, setTab] = React.useState<'convert' | 'detect'>('convert') + const [tab, setTab] = React.useState<'convert' | 'correct'>('convert') return ( } > - {tab === 'convert' ? : } + {tab === 'convert' ? : } ) } diff --git a/src/lib/bech32m.ts b/src/lib/bech32m.ts index 7087917d..7510c833 100644 --- a/src/lib/bech32m.ts +++ b/src/lib/bech32m.ts @@ -132,3 +132,157 @@ export function formatTempoAddress(address: string): string { if (address.length < 12) return address return `${address.slice(0, 6)} ${address.slice(6).replace(/(.{5})/g, '$1 ')}`.trim() } + +export type CorrectionResult = { + status: 'valid' | 'corrected' | 'detected' | 'invalid_format' + corrected?: string + errors?: { position: number; was: string; correctedTo: string }[] + candidates?: number + searchedErrors?: number + error?: string +} + +/** + * Attempts to correct a corrupted tempo1 address using brute-force BCH + * decoding. Tries single-error correction first (1,280 candidates), then + * two-error correction (~800K candidates). Returns the unique valid address + * if one is found, or reports detection-only if correction is ambiguous or + * the search space is exceeded. + */ +export function correctTempoAddress(address: string): CorrectionResult { + const addr = address.toLowerCase() + + if (addr.length > 90) + return { status: 'invalid_format', error: 'Address exceeds 90 characters' } + if (/[A-Z]/.test(address) && /[a-z]/.test(address)) + return { status: 'invalid_format', error: 'Mixed case rejected per BIP-350' } + + const pos = addr.lastIndexOf('1') + if (pos < 1) return { status: 'invalid_format', error: 'No separator found' } + + const hrp = addr.slice(0, pos) + const dataPart = addr.slice(pos + 1) + if (dataPart.length < 6) + return { status: 'invalid_format', error: 'Data part too short' } + + const data5 = Array.from(dataPart, (c) => CHARSET.indexOf(c)) + if (data5.some((d) => d === -1)) + return { status: 'invalid_format', error: 'Invalid character in data part' } + + const expanded = hrpExpand(hrp) + + if (polymod([...expanded, ...data5]) === BECH32M_CONST) { + try { + decodeTempoAddress(address) + return { status: 'valid' } + } catch (e) { + return { status: 'invalid_format', error: e instanceof Error ? e.message : 'Invalid' } + } + } + + // 1-error correction: try each position × each of 32 characters + const dataLen = dataPart.length + const found: { addr: string; errors: { position: number; was: string; correctedTo: string }[] }[] = [] + + for (let i = 0; i < dataLen; i++) { + const original = data5[i] + for (let c = 0; c < 32; c++) { + if (c === original) continue + data5[i] = c + if (polymod([...expanded, ...data5]) === BECH32M_CONST) { + const fixed = addr.slice(0, pos + 1 + i) + CHARSET[c] + addr.slice(pos + 1 + i + 1) + try { + decodeTempoAddress(fixed) + found.push({ + addr: fixed, + errors: [{ position: pos + 1 + i, was: CHARSET[original], correctedTo: CHARSET[c] }], + }) + } catch { + // valid checksum but fails higher-level validation (e.g. wrong version) + } + } + data5[i] = original + } + } + + if (found.length === 1) { + return { + status: 'corrected', + corrected: found[0].addr, + errors: found[0].errors, + candidates: 1, + searchedErrors: 1, + } + } + if (found.length > 1) { + return { + status: 'detected', + candidates: found.length, + searchedErrors: 1, + error: `${found.length} candidates found — correction is ambiguous`, + } + } + + // 2-error correction: try each pair of positions × each pair of characters + for (let i = 0; i < dataLen; i++) { + const origI = data5[i] + for (let j = i + 1; j < dataLen; j++) { + const origJ = data5[j] + for (let ci = 0; ci < 32; ci++) { + if (ci === origI) continue + data5[i] = ci + for (let cj = 0; cj < 32; cj++) { + if (cj === origJ) continue + data5[j] = cj + if (polymod([...expanded, ...data5]) === BECH32M_CONST) { + const fixed = + addr.slice(0, pos + 1 + i) + + CHARSET[ci] + + addr.slice(pos + 1 + i + 1, pos + 1 + j) + + CHARSET[cj] + + addr.slice(pos + 1 + j + 1) + try { + decodeTempoAddress(fixed) + found.push({ + addr: fixed, + errors: [ + { position: pos + 1 + i, was: CHARSET[origI], correctedTo: CHARSET[ci] }, + { position: pos + 1 + j, was: CHARSET[origJ], correctedTo: CHARSET[cj] }, + ], + }) + } catch { + // valid checksum but fails higher-level validation + } + } + data5[j] = origJ + } + data5[i] = origI + } + } + if (found.length > 5) break // early exit if ambiguous + } + + if (found.length === 1) { + return { + status: 'corrected', + corrected: found[0].addr, + errors: found[0].errors, + candidates: 1, + searchedErrors: 2, + } + } + if (found.length > 1) { + return { + status: 'detected', + candidates: found.length, + searchedErrors: 2, + error: `${found.length} candidates found — correction is ambiguous`, + } + } + + return { + status: 'detected', + searchedErrors: 2, + error: 'More than 2 errors — detected but cannot correct', + } +} diff --git a/src/pages/protocol/addresses.mdx b/src/pages/protocol/addresses.mdx index 88328bca..3e583807 100644 --- a/src/pages/protocol/addresses.mdx +++ b/src/pages/protocol/addresses.mdx @@ -15,7 +15,7 @@ tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0 ## Address Converter -Convert between standard `0x` hex addresses and `tempo1` bech32m addresses. Use the **Error Detection** tab to see how bech32m catches character substitution errors. +Convert between standard `0x` hex addresses and `tempo1` bech32m addresses. Use the **Error Correction** tab to see how the bech32m checksum detects corrupted addresses and recovers the original by locating errors — with no prior knowledge of the correct address. @@ -46,14 +46,21 @@ A Tempo address encodes a 20-byte Ethereum-style address with a version byte, a | Total length | 46 characters | | Encoding | bech32m (BIP-350) | -## Error Detection +## Error Detection and Correction The bech32m checksum guarantees detection of: - **Up to 4 character substitutions** anywhere in the address - **Insertions and deletions** via checksum (≥ 1 − 2⁻³⁰ probability) plus fixed-length validation -This means if a user accidentally mistypes up to 4 characters in a Tempo address, the error will always be caught before any funds are sent. +Beyond detection, the BCH code structure enables **error correction** — the algorithm can locate the corrupted characters and recover the original address with no prior knowledge of it: + +- **1 substitution**: always correctable (unique solution found) +- **2 substitutions**: correctable in most cases (searches ~800K candidates) +- **3–4 substitutions**: detected but too many candidates to correct reliably +- **5+ substitutions**: probabilistic detection (≥ 1 − 2⁻³⁰) + +Try it in the **Error Correction** tab above to see recovery in action. ## Case Handling From 25eebf3c9fe09f749f8bfe45968c851f0095559e Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Mon, 2 Mar 2026 19:24:37 +0200 Subject: [PATCH 4/7] Replace brute-force error correction with syndrome-based BCH decoding Port Pieter Wuille's algebraic GF(1024) syndrome decoder from sipa/bech32. Error positions are now found via syndrome computation + GF(1024) log/exp tables, then values are corrected by targeted search at known positions only. Performance: 1-error 0.8ms (was ~50ms), 2-error 6ms (was ~3s), 3+ errors detected instantly (was ~3s scanning before giving up). Made-with: Cursor --- src/lib/bech32m.ts | 358 +++++++++++++++++++++++++++++++++------------ 1 file changed, 266 insertions(+), 92 deletions(-) diff --git a/src/lib/bech32m.ts b/src/lib/bech32m.ts index 7510c833..d574fbab 100644 --- a/src/lib/bech32m.ts +++ b/src/lib/bech32m.ts @@ -133,6 +133,199 @@ export function formatTempoAddress(address: string): string { return `${address.slice(0, 6)} ${address.slice(6).replace(/(.{5})/g, '$1 ')}`.trim() } +// --------------------------------------------------------------------------- +// Syndrome-based BCH error correction over GF(1024) +// +// Ported from Pieter Wuille's reference implementation (sipa/bech32). +// The bech32m BCH code's syndromes live in GF(1024) = GF(32²). Error +// positions are found algebraically, then values are corrected by targeted +// search at the known positions — O(n) total instead of O(n²×32²) brute force. +// --------------------------------------------------------------------------- + +// prettier-ignore +const GF1024_EXP = [ + 1,32,311,139,206,553,934,180,537,145,910,131,462,373,652,927,675,840,938, + 308,235,958,948,756,1007,979,356,172,281,124,240,222,41,23,736,367,460,309, + 203,649,831,570,454,117,464,693,392,1010,115,272,348,667,383,972,644,671, + 511,610,129,398,818,922,515,977,292,747,15,480,386,690,360,300,1003,851, + 202,681,520,689,264,604,630,513,913,867,1021,403,146,1006,1011,83,39,471, + 597,854,106,560,134,366,492,2,64,583,278,412,370,620,321,315,267,572,262, + 924,707,56,567,102,944,628,577,470,629,609,225,766,687,712,344,539,209,457, + 405,82,7,224,734,920,579,406,50,887,381,908,195,905,99,784,749,207,521,657, + 63,727,696,40,55,983,484,258,796,877,573,294,683,584,246,30,960,772,109,720, + 600,758,943,404,114,304,107,528,433,485,290,555,998,755,783,269,764,751,143, + 78,903,419,933,212,361,268,732,984,4,128,430,517,785,717,504,642,607,534, + 369,524,561,166,89,359,204,617,481,418,901,483,482,450,245,126,176,665,319, + 395,914,771,141,14,448,181,569,422,773,77,999,723,568,390,562,198,809,250, + 414,306,43,87,167,121,80,71,679,968,516,817,1018,371,588,118,432,453,21, + 672,808,218,169,441,229,638,769,205,585,214,297,843,970,580,374,748,239,830, + 538,241,254,286,156,558,838,618,385,722,536,177,697,8,256,860,298,811,186, + 985,36,439,293,715,312,363,332,155,718,408,498,962,836,554,966,964,900,451, + 213,329,59,599,790,557,806,282,28,896,323,379,844,810,154,750,175,377,780, + 365,396,882,477,789,589,86,135,334,219,137,142,110,688,296,875,765,719,440, + 197,841,906,3,96,880,413,338,859,458,501,802,410,434,389,594,950,692,424, + 709,248,478,885,317,459,469,533,273,380,940,500,770,173,313,331,123,16,512, + 945,596,886,349,699,72,839,586,182,601,726,664,287,188,793,973,676,936,372, + 684,680,552,902,387,658,95,423,805,378,876,541,17,544,646,735,952,884,285, + 252,350,731,824,730,792,1005,915,803,442,133,270,668,415,274,284,220,105, + 592,1014,243,190,857,394,946,564,6,192,1001,787,653,959,916,963,868,797,845, + 778,429,613,97,848,170,473,917,995,595,918,899,291,523,721,632,961,804,346, + 603,662,223,9,288,619,417,997,659,127,144,942,436,325,443,165,57,535,337, + 827,698,104,624,705,120,112,368,556,774,45,151,846,874,733,1016,307,11,352, + 44,183,633,993,531,465,661,191,889,189,825,762,559,870,861,266,540,49,791, + 525,529,401,210,425,741,463,341,955,788,621,353,12,384,754,815,58,631,545, + 678,1000,819,954,820,858,490,194,937,340,923,547,742,431,549,550,582,310, + 171,505,674,872,669,447,37,407,18,576,502,834,746,47,215,265,636,833,650, + 863,330,91,295,651,895,125,208,489,162,217,201,713,376,812,90,263,956,1012, + 179,761,591,22,704,88,327,507,738,303,907,35,343,1019,339,891,253,382,1004, + 947,532,305,75,807,314,299,779,397,850,234,926,643,639,801,506,706,24,768, + 237,894,93,487,354,108,752,879,637,865,957,980,388,626,641,575,358,236,862, + 362,364,428,581,342,987,100,1008,51,855,74,775,13,416,965,932,244,94,391, + 530,497,930,52,951,660,159,590,54,1015,211,393,978,324,411,402,178,729,888, + 157,526,625,737,335,251,446,5,160,153,654,991,228,606,566,70,647,767,655, + 1023,467,725,760,623,289,587,150,878,605,598,822,794,941,468,565,38,503,866, + 989,164,25,800,474,1013,147,974,708,216,233,1022,499,994,627,673,776,493,34, + 375,716,472,949,724,728,856,426,645,703,200,745,79,935,148,814,26,832,682, + 616,449,149,782,301,971,612,65,615,33,279,444,69,743,399,786,685,648,799, + 781,333,187,1017,275,316,491,226,670,479,853,10,320,283,60,695,456,437,357, + 140,46,247,62,759,911,163,249,510,578,438,261,1020,435,421,869,829,634,897, + 355,76,967,996,691,328,27,864,925,739,271,700,168,409,466,757,975,740,495, + 98,816,986,68,711,184,921,611,161,185,953,852,42,119,400,242,158,622,257, + 892,29,928,116,496,898,259,828,602,694,488,130,494,66,519,849,138,238,798, + 813,122,48,823,826,666,351,763,527,593,982,452,53,919,931,20,640,543,81,103, + 912,835,714,280,92,455,85,231,574,326,475,981,420,837,522,753,847,842,1002, + 883,509,546,710,152,686,744,111,656,31,992,563,230,542,113,336,795,909,227, + 702,232,990,196,873,701,136,174,345,571,486,322,347,635,929,84,199,777,461, + 277,508,514,1009,19,608,193,969,548,518,881,445,101,976,260,988,132,302,939, + 276,476,821,890,221,73,871,893,61,663,255,318,427,677,904,67,551,614, +] + +// prettier-ignore +const GF1024_LOG = [ + -1,0,99,363,198,726,462,132,297,495,825,528,561,693,231,66,396,429,594,990, + 924,264,627,33,660,759,792,858,330,891,165,957,1,804,775,635,304,592,754,90, + 153,32,883,248,530,521,834,599,911,547,138,689,703,921,708,154,113,508,565, + 324,828,1013,836,150,100,802,903,1020,874,807,734,253,403,1010,691,646,853, + 237,189,788,252,927,131,89,982,935,347,249,629,212,620,607,933,664,698,423, + 364,476,871,144,687,998,115,928,513,453,94,176,667,168,353,955,517,962,174, + 48,893,43,261,884,516,251,910,395,29,611,223,501,199,58,901,11,1002,446,96, + 348,973,351,906,3,833,230,352,188,502,9,86,763,790,797,745,522,952,728,336, + 311,288,719,887,706,727,879,614,839,758,507,211,250,864,268,478,586,27,392, + 974,338,224,295,716,624,7,233,406,531,876,880,302,816,411,539,457,537,463, + 992,575,142,970,360,243,983,786,616,74,38,214,273,4,147,612,128,552,710,193, + 322,275,600,766,615,267,350,452,1009,31,494,133,122,821,966,731,270,960,936, + 968,767,653,20,679,662,907,282,30,285,886,456,697,222,164,835,380,840,245, + 724,436,640,286,1015,298,889,157,896,1000,844,110,621,78,601,545,108,195, + 185,447,862,49,387,450,818,1005,986,102,805,932,28,329,827,451,435,287,410, + 496,743,180,485,64,306,161,608,355,276,300,649,71,799,1003,633,175,645,247, + 527,19,37,585,2,308,393,648,107,819,383,1016,226,826,106,978,332,713,505, + 938,630,857,323,606,394,310,815,349,723,963,510,367,638,577,556,685,636,126, + 975,491,979,50,401,437,915,529,560,666,852,26,832,678,213,70,194,681,309, + 682,341,97,35,518,208,104,259,416,13,280,776,618,339,426,333,388,140,641,52, + 562,292,68,421,674,374,241,699,46,711,459,227,342,651,59,809,885,551,715,85, + 173,130,137,593,313,865,372,714,103,366,246,449,694,498,217,191,941,847,235, + 424,378,553,783,1017,683,474,200,581,262,178,373,846,504,831,843,305,359, + 269,445,506,806,997,725,591,232,796,221,321,920,263,42,934,830,129,369,384, + 36,985,12,555,44,535,866,739,752,385,119,91,778,479,761,939,1006,344,381, + 823,67,216,220,219,156,179,977,665,900,613,574,820,98,774,902,870,894,701, + 314,769,390,370,596,755,204,587,658,631,987,949,841,56,397,81,988,62,256, + 201,995,904,76,148,943,486,209,549,720,917,177,550,700,534,644,386,207,509, + 294,8,284,127,546,428,961,926,430,567,950,579,994,582,583,1021,419,5,317, + 181,519,327,289,542,95,210,242,959,461,753,733,114,240,234,41,976,109,160, + 937,677,595,118,842,136,279,684,584,101,163,274,405,744,260,346,707,626,454, + 918,375,482,399,92,748,325,170,407,898,492,79,747,732,206,991,121,57,878, + 801,475,1022,803,795,215,291,497,105,559,888,742,514,721,675,771,117,120,80, + 566,488,532,850,980,602,670,271,656,925,676,205,655,54,784,431,735,812,39, + 604,609,14,466,729,737,956,149,422,500,705,536,493,1014,409,225,914,51,448, + 590,822,55,265,772,588,16,414,1018,568,254,418,75,794,162,417,811,953,124, + 354,77,69,856,377,45,899,829,152,296,512,402,863,972,967,785,628,515,659, + 112,765,379,951,875,125,617,931,307,777,203,312,358,169,487,293,239,780,740, + 408,151,781,717,440,438,196,525,134,432,34,722,632,861,869,554,580,808,954, + 787,598,65,281,146,337,187,668,944,563,183,23,867,171,837,741,625,541,916, + 186,357,123,736,661,272,391,229,167,236,520,692,773,984,473,650,340,814,798, + 184,145,202,810,465,558,345,326,548,441,412,750,964,158,471,908,813,760,657, + 371,444,490,425,328,647,266,244,335,301,619,909,791,564,872,257,60,570,572, + 1007,749,912,439,540,913,511,897,849,283,40,793,603,597,930,316,942,290,404, + 17,361,946,277,334,472,523,945,477,905,652,73,882,824,93,690,782,458,573, + 368,299,544,680,605,859,671,756,83,470,848,543,1011,589,971,524,356,427,159, + 746,669,365,996,343,948,434,382,400,139,718,538,1008,639,890,1012,663,610, + 331,851,895,484,320,218,420,190,1019,143,362,634,141,965,10,838,929,82,228, + 443,468,480,483,922,135,877,61,578,111,860,654,15,892,981,702,923,696,192,6, + 789,415,576,18,1004,389,751,503,172,116,398,460,643,22,779,376,704,433,881, + 571,557,622,672,21,467,166,489,315,469,319,695,318,854,255,993,278,800,53, + 413,764,868,999,63,712,25,673,940,919,155,197,303,873,686,1001,757,969,730, + 958,533,770,481,855,499,182,238,569,464,947,72,642,442,87,24,688,989,47,88, + 623,762,455,709,526,817,258,637,845,84,768,738, +] + +/** + * Maps a 30-bit polymod residue to three 10-bit syndromes in GF(1024). + * The syndrome transform is specific to the bech32/bech32m generator polynomial. + */ +function computeSyndrome(residue: number): number { + const low = residue & 0x1f + let syn = low ^ (low << 10) ^ (low << 20) + /* prettier-ignore */ + const BITS = [ + 0x3d0195bd, 0x2a932b53, 0x072653af, 0x0cdc067e, 0x19ac89f5, + 0x15922369, 0x29b443f2, 0x03f826ed, 0x0574c8fa, 0x087935dd, + 0x2c6dcf4f, 0x0acfbfbe, 0x158bfa75, 0x2993d5e3, 0x03b70fc6, + 0x051e8fd6, 0x08b99aa5, 0x1167b06a, 0x205f60d4, 0x12aae581, + 0x04296874, 0x0846f4c1, 0x108d4d82, 0x210ebf04, 0x1299fb28, + ] + for (let i = 0; i < 25; i++) { + if ((residue >> (i + 5)) & 1) syn ^= BITS[i] + } + return syn +} + +/** + * Locates up to 2 error positions in the data part using syndrome decoding + * over GF(1024). Returns positions as offsets from the END of the data + * (position 0 = last character). Returns [] if errors cannot be located. + */ +function locateErrors(residue: number, length: number): number[] { + if (residue === 0) return [] + + const syn = computeSyndrome(residue) + const s0 = syn & 0x3ff + const s1 = (syn >> 10) & 0x3ff + const s2 = syn >> 20 + const l_s0 = GF1024_LOG[s0] + const l_s1 = GF1024_LOG[s1] + const l_s2 = GF1024_LOG[s2] + + if (l_s0 !== -1 && l_s1 !== -1 && l_s2 !== -1) { + if ((2 * l_s1 - l_s2 - l_s0 + 2046) % 1023 === 0) { + const p1 = (l_s1 - l_s0 + 1023) % 1023 + if (p1 < length) { + const l_e1 = l_s0 + (1023 - 997) * p1 + if (l_e1 % 33 === 0) return [p1] + } + } + } + + for (let p1 = 0; p1 < length; p1++) { + const s2_s1p1 = s2 ^ (s1 === 0 ? 0 : GF1024_EXP[(l_s1 + p1) % 1023]) + if (s2_s1p1 === 0) continue + const s1_s0p1 = s1 ^ (s0 === 0 ? 0 : GF1024_EXP[(l_s0 + p1) % 1023]) + if (s1_s0p1 === 0) continue + const l_s1_s0p1 = GF1024_LOG[s1_s0p1] + const p2 = (GF1024_LOG[s2_s1p1] - l_s1_s0p1 + 1023) % 1023 + if (p2 >= length || p1 === p2) continue + const s1_s0p2 = s1 ^ (s0 === 0 ? 0 : GF1024_EXP[(l_s0 + p2) % 1023]) + if (s1_s0p2 === 0) continue + const inv_p1_p2 = 1023 - GF1024_LOG[GF1024_EXP[p1] ^ GF1024_EXP[p2]] + const l_e2 = l_s1_s0p1 + inv_p1_p2 + (1023 - 997) * p2 + if (l_e2 % 33 !== 0) continue + const l_e1 = GF1024_LOG[s1_s0p2] + inv_p1_p2 + (1023 - 997) * p1 + if (l_e1 % 33 !== 0) continue + return p1 < p2 ? [p1, p2] : [p2, p1] + } + + return [] +} + export type CorrectionResult = { status: 'valid' | 'corrected' | 'detected' | 'invalid_format' corrected?: string @@ -143,11 +336,10 @@ export type CorrectionResult = { } /** - * Attempts to correct a corrupted tempo1 address using brute-force BCH - * decoding. Tries single-error correction first (1,280 candidates), then - * two-error correction (~800K candidates). Returns the unique valid address - * if one is found, or reports detection-only if correction is ambiguous or - * the search space is exceeded. + * Attempts to correct a corrupted tempo1 address using syndrome-based BCH + * decoding (ported from sipa/bech32). Algebraically locates error positions + * via GF(1024) syndromes, then tests values only at those positions. + * O(n) for position finding + O(32^k) for k located errors. */ export function correctTempoAddress(address: string): CorrectionResult { const addr = address.toLowerCase() @@ -157,11 +349,11 @@ export function correctTempoAddress(address: string): CorrectionResult { if (/[A-Z]/.test(address) && /[a-z]/.test(address)) return { status: 'invalid_format', error: 'Mixed case rejected per BIP-350' } - const pos = addr.lastIndexOf('1') - if (pos < 1) return { status: 'invalid_format', error: 'No separator found' } + const sepPos = addr.lastIndexOf('1') + if (sepPos < 1) return { status: 'invalid_format', error: 'No separator found' } - const hrp = addr.slice(0, pos) - const dataPart = addr.slice(pos + 1) + const hrp = addr.slice(0, sepPos) + const dataPart = addr.slice(sepPos + 1) if (dataPart.length < 6) return { status: 'invalid_format', error: 'Data part too short' } @@ -170,8 +362,9 @@ export function correctTempoAddress(address: string): CorrectionResult { return { status: 'invalid_format', error: 'Invalid character in data part' } const expanded = hrpExpand(hrp) + const residue = polymod([...expanded, ...data5]) ^ BECH32M_CONST - if (polymod([...expanded, ...data5]) === BECH32M_CONST) { + if (residue === 0) { try { decodeTempoAddress(address) return { status: 'valid' } @@ -180,109 +373,90 @@ export function correctTempoAddress(address: string): CorrectionResult { } } - // 1-error correction: try each position × each of 32 characters + const errorPositions = locateErrors(residue, dataPart.length) + + if (errorPositions.length === 0) { + return { + status: 'detected', + searchedErrors: 2, + error: 'Errors detected but positions could not be determined (likely 3+ substitutions)', + } + } + + // Convert from end-relative positions to data-part indices const dataLen = dataPart.length - const found: { addr: string; errors: { position: number; was: string; correctedTo: string }[] }[] = [] + const indices = errorPositions.map((p) => dataLen - 1 - p) - for (let i = 0; i < dataLen; i++) { - const original = data5[i] + // Try all character values at the located positions + const originals = indices.map((i) => data5[i]) + + if (indices.length === 1) { + const [idx] = indices + const orig = originals[0] for (let c = 0; c < 32; c++) { - if (c === original) continue - data5[i] = c + if (c === orig) continue + data5[idx] = c if (polymod([...expanded, ...data5]) === BECH32M_CONST) { - const fixed = addr.slice(0, pos + 1 + i) + CHARSET[c] + addr.slice(pos + 1 + i + 1) + const fixed = addr.slice(0, sepPos + 1 + idx) + CHARSET[c] + addr.slice(sepPos + 1 + idx + 1) + data5[idx] = orig try { decodeTempoAddress(fixed) - found.push({ - addr: fixed, - errors: [{ position: pos + 1 + i, was: CHARSET[original], correctedTo: CHARSET[c] }], - }) + return { + status: 'corrected', + corrected: fixed, + errors: [{ position: sepPos + 1 + idx, was: CHARSET[orig], correctedTo: CHARSET[c] }], + candidates: 1, + searchedErrors: 1, + } } catch { - // valid checksum but fails higher-level validation (e.g. wrong version) + return { status: 'detected', error: 'Position located but no valid Tempo address found' } } } - data5[i] = original } + data5[idx] = orig } - if (found.length === 1) { - return { - status: 'corrected', - corrected: found[0].addr, - errors: found[0].errors, - candidates: 1, - searchedErrors: 1, - } - } - if (found.length > 1) { - return { - status: 'detected', - candidates: found.length, - searchedErrors: 1, - error: `${found.length} candidates found — correction is ambiguous`, - } - } - - // 2-error correction: try each pair of positions × each pair of characters - for (let i = 0; i < dataLen; i++) { - const origI = data5[i] - for (let j = i + 1; j < dataLen; j++) { - const origJ = data5[j] - for (let ci = 0; ci < 32; ci++) { - if (ci === origI) continue - data5[i] = ci - for (let cj = 0; cj < 32; cj++) { - if (cj === origJ) continue - data5[j] = cj - if (polymod([...expanded, ...data5]) === BECH32M_CONST) { - const fixed = - addr.slice(0, pos + 1 + i) + - CHARSET[ci] + - addr.slice(pos + 1 + i + 1, pos + 1 + j) + - CHARSET[cj] + - addr.slice(pos + 1 + j + 1) - try { - decodeTempoAddress(fixed) - found.push({ - addr: fixed, - errors: [ - { position: pos + 1 + i, was: CHARSET[origI], correctedTo: CHARSET[ci] }, - { position: pos + 1 + j, was: CHARSET[origJ], correctedTo: CHARSET[cj] }, - ], - }) - } catch { - // valid checksum but fails higher-level validation + if (indices.length === 2) { + const [idx0, idx1] = indices + const [orig0, orig1] = originals + for (let c0 = 0; c0 < 32; c0++) { + if (c0 === orig0) continue + data5[idx0] = c0 + for (let c1 = 0; c1 < 32; c1++) { + if (c1 === orig1) continue + data5[idx1] = c1 + if (polymod([...expanded, ...data5]) === BECH32M_CONST) { + const parts = [...addr] + parts[sepPos + 1 + idx0] = CHARSET[c0] + parts[sepPos + 1 + idx1] = CHARSET[c1] + const fixed = parts.join('') + data5[idx0] = orig0 + data5[idx1] = orig1 + try { + decodeTempoAddress(fixed) + return { + status: 'corrected', + corrected: fixed, + errors: [ + { position: sepPos + 1 + idx0, was: CHARSET[orig0], correctedTo: CHARSET[c0] }, + { position: sepPos + 1 + idx1, was: CHARSET[orig1], correctedTo: CHARSET[c1] }, + ], + candidates: 1, + searchedErrors: 2, } + } catch { + return { status: 'detected', error: 'Positions located but no valid Tempo address found' } } - data5[j] = origJ } - data5[i] = origI } } - if (found.length > 5) break // early exit if ambiguous - } - - if (found.length === 1) { - return { - status: 'corrected', - corrected: found[0].addr, - errors: found[0].errors, - candidates: 1, - searchedErrors: 2, - } - } - if (found.length > 1) { - return { - status: 'detected', - candidates: found.length, - searchedErrors: 2, - error: `${found.length} candidates found — correction is ambiguous`, - } + data5[idx0] = orig0 + data5[idx1] = orig1 } return { status: 'detected', searchedErrors: 2, - error: 'More than 2 errors — detected but cannot correct', + error: 'Errors detected but correction failed', } } From fc534f12e0281af4979d1ee0fdcc968ee871651a Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Tue, 3 Mar 2026 17:03:16 +0200 Subject: [PATCH 5/7] Fix spaced address example using a code block with a copy button The markdown code fence gave the example a Vocs-generated copy button that copied the address with spaces, contradicting the text saying copy must use the canonical form. Switch to a plain

so there is no copy affordance on the display-only example. Made-with: Cursor --- src/pages/protocol/addresses.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/protocol/addresses.mdx b/src/pages/protocol/addresses.mdx index 3e583807..a8b23277 100644 --- a/src/pages/protocol/addresses.mdx +++ b/src/pages/protocol/addresses.mdx @@ -74,11 +74,9 @@ Per BIP-350, Tempo addresses are case-insensitive: For readability, UIs may display addresses with visual spacing: -``` -tempo1 qp6z6 dwvvc 6vq5e fyk3m s39un e6etu 4a9qt j2kk0 -``` +

tempo1 qp6z6 dwvvc 6vq5e fyk3m s39un e6etu 4a9qt j2kk0

-Spaces are for display only. Copy buttons must always use the canonical form without spaces. +Spaces are for **display only** — copy buttons and APIs must always use the canonical form without spaces. ## JavaScript / TypeScript Usage From d2b25470568bf58ae3728329a4b6f50e84428b07 Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Tue, 3 Mar 2026 17:48:07 +0200 Subject: [PATCH 6/7] Remove unused validateTempoAddress import Made-with: Cursor --- src/components/AddressConverter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AddressConverter.tsx b/src/components/AddressConverter.tsx index 45197c85..0acad580 100644 --- a/src/components/AddressConverter.tsx +++ b/src/components/AddressConverter.tsx @@ -11,7 +11,6 @@ import { decodeTempoAddress, encodeTempoAddress, formatTempoAddress, - validateTempoAddress, } from '../lib/bech32m' import { Container } from './Container' From c613a9e24a77cc04bbf49ec0dbded2185faf9f8b Mon Sep 17 00:00:00 2001 From: Dankrad Feist Date: Tue, 3 Mar 2026 19:38:05 +0200 Subject: [PATCH 7/7] Fix Biome lint errors in AddressConverter - Replace non-null assertions with optional chaining / nullish coalescing - Add htmlFor/id associations for label+input pairs - Use span for output label (no associated input) - Auto-format per Biome rules Made-with: Cursor --- src/components/AddressConverter.tsx | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/AddressConverter.tsx b/src/components/AddressConverter.tsx index 0acad580..9358748e 100644 --- a/src/components/AddressConverter.tsx +++ b/src/components/AddressConverter.tsx @@ -86,9 +86,12 @@ function ConverterSection() { return (
- +
setInputValue(e.target.value)} @@ -112,7 +115,7 @@ function ConverterSection() {
- + {outputLabel}
{result.error ? ( {result.error} @@ -193,17 +196,17 @@ function StatusBadge({ result }: { result: CorrectionResult }) { Corrected — recovered the original address by locating{' '} - {result.errors!.length} error{result.errors!.length !== 1 ? 's' : ''} - {result.searchedErrors === 2 && result.errors!.length === 2 && ' (2-error search)'} + {result.errors?.length} error{result.errors?.length !== 1 ? 's' : ''} + {result.searchedErrors === 2 && result.errors?.length === 2 && ' (2-error search)'}
{result.corrected} - +
- {result.errors!.map((e) => ( + {result.errors?.map((e) => (
position {e.position}: {e.was} →{' '} {e.correctedTo} @@ -256,14 +259,16 @@ function ErrorCorrectionDemo() { return (

- Paste or type any corrupted tempo1 address. The - bech32m checksum will detect the error, and for 1–2 substitutions the algorithm can locate - and recover the original address with no prior knowledge of it. + Paste or type any corrupted tempo1 address. The bech32m + checksum will detect the error, and for 1–2 substitutions the algorithm can locate and + recover the original address with no prior knowledge of it.

- +
{ERROR_PRESETS.map((preset, idx) => (
- {computing && ( -
Searching for corrections...
- )} + {computing &&
Searching for corrections...
} {!computing && correction && }
) @@ -324,7 +328,7 @@ export function AddressConverter() {