merge: wallet feat/confidential slice

# Conflicts:
#	apps/web/package.json
#	apps/web/src/screens/confidential/index.tsx
#	pnpm-lock.yaml
This commit is contained in:
Hanzo AI
2026-04-30 14:03:22 -07:00
18 changed files with 2433 additions and 430 deletions
+2 -10
View File
@@ -11,12 +11,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
<<<<<<< HEAD
"@hanzo/gui": "^7.0.0",
"@luxfi/wallet-analytics": "workspace:^",
"@luxfi/wallet-brand": "workspace:^",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0",
"@noble/hashes": "^1.7.1",
"@scure/bip32": "^1.5.0",
"@scure/bip39": "^1.4.0",
"@tanstack/react-query": "^5.90.0",
@@ -25,17 +24,10 @@
"qrcode.react": "^4.1.0",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-router-dom": "^7.0.0",
"react-router-dom": "7.5.0",
"viem": "^2.30.0",
"wagmi": "^2.15.0",
"zustand": "^5.0.0"
=======
"@walletconnect/sign-client": "^2.21.0",
"@walletconnect/utils": "^2.21.0",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-router-dom": "^7.5.0"
>>>>>>> origin/feat/stake-dapps
},
"devDependencies": {
"@types/react": "^19.0.0",
@@ -0,0 +1,101 @@
/**
* Confidential landing screen.
*
* Renders:
* - A list of F-Chain encrypted balances (hidden by default).
* - Quick links to the Confidential transfer flow (F-Chain) and the ZK
* proof generator (Z-Chain).
*
* The address list is supplied via the `addresses` prop. Foundation passes
* the connected wallet's F-Chain addresses in once it has established a
* signing session. The default is an empty list so the screen renders even
* before connect.
*/
import { Link } from "react-router-dom"
import { ConfidentialBalanceRow } from "./ConfidentialBalanceRow"
import { colors, layout, text } from "./styles"
import { useConfidentialStore } from "./useConfidentialStore"
import { confidentialStore } from "../../store/confidential"
interface Props {
addresses?: string[]
symbols?: string[]
}
export function Confidential({
addresses = [],
symbols = ["LUX", "ZOO", "AI"],
}: Props) {
// Subscribe so the active proof count updates live.
useConfidentialStore()
const proofCount = confidentialStore.listProofs().length
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>Confidential</h1>
<Link to="/" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back to wallet
</Link>
</div>
<p style={{ ...text.muted, marginBottom: 16 }}>
F-Chain encrypts balances and transfer amounts (LP-013). Z-Chain proves
claims about you without revealing the underlying data (LP-063).
</p>
<div style={{ ...layout.row, marginBottom: 24, gap: 8, flexWrap: "wrap" }}>
<Link to="/confidential/transfer" style={{ ...layout.button, textDecoration: "none" }}>
Send confidential
</Link>
<Link to="/confidential/zk" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Generate ZK proof
</Link>
{proofCount > 0 ? (
<span style={{ ...text.muted, marginLeft: "auto" }}>
{proofCount} active proof{proofCount === 1 ? "" : "s"}
</span>
) : null}
</div>
<h2 style={{ ...text.h2, marginBottom: 8 }}>Encrypted balances</h2>
<p style={{ ...text.muted, marginBottom: 16 }}>
Tap a row to start a threshold-decrypt session.
</p>
{addresses.length === 0 ? (
<div style={layout.card}>
<p style={text.body}>No F-Chain addresses connected.</p>
<p style={{ ...text.muted, marginTop: 4 }}>
Connect a wallet from the main view to see encrypted balances.
</p>
</div>
) : (
<>
{addresses.flatMap((addr) =>
symbols.map((sym) => (
<ConfidentialBalanceRow
key={`${addr}:${sym}`}
address={addr}
symbol={sym}
/>
)),
)}
</>
)}
<div style={{ ...layout.card, marginTop: 24, borderColor: colors.borderHi }}>
<div style={text.h3}>Privacy guarantees</div>
<ul style={{ ...text.muted, paddingLeft: 18, marginTop: 8, lineHeight: 1.7 }}>
<li>Balances and amounts encrypted under TFHE validators see only ciphertext.</li>
<li>Reveal requires a threshold of MPC committee signatures (e.g. 3-of-5).</li>
<li>Plaintext lives in memory for 30 seconds, then auto re-hides.</li>
<li>ZK proofs reveal only the claim never the underlying witness.</li>
</ul>
</div>
</div>
</div>
)
}
@@ -0,0 +1,148 @@
/**
* Single F-Chain balance row.
*
* Default state: shows lock icon + "Hidden". Tap → opens RevealCommittee
* modal which drives the threshold-decrypt session. On success the row
* shows the plaintext amount with a TTL countdown; on TTL expiry the row
* auto-rehides.
*/
import { useEffect, useState } from "react"
import type { CSSProperties } from "react"
import { LOCK, UNLOCK, colors, layout, text } from "./styles"
import { useFHEBalance } from "./useFHEBalance"
import { confidentialStore } from "../../store/confidential"
import { useConfidentialStore } from "./useConfidentialStore"
import { RevealCommittee } from "./RevealCommittee"
interface Props {
address: string
symbol: string
/** Optional override for testing or sensitive screens. */
revealTtlMs?: number
}
function fmtTimeLeft(ms: number): string {
if (ms <= 0) return "0s"
const s = Math.ceil(ms / 1000)
if (s < 60) return `${s}s`
return `${Math.floor(s / 60)}m ${s % 60}s`
}
export function ConfidentialBalanceRow({ address, symbol, revealTtlMs }: Props) {
const { balance, loading, error, session, reveal, hide, refresh } = useFHEBalance({
address,
symbol,
revealTtlMs,
})
const [modalOpen, setModalOpen] = useState(false)
const [, force] = useState(0)
// Subscribe to store changes (revealedBalances).
useConfidentialStore()
const revealed = balance
? confidentialStore.getRevealedBalance(balance.chainId, balance.address, balance.symbol)
: undefined
// 1Hz tick to drive the TTL countdown (only while revealed).
useEffect(() => {
if (!revealed) return
const t = setInterval(() => force((n) => n + 1), 1000)
return () => clearInterval(t)
}, [revealed])
// Auto-close modal when reveal completes.
useEffect(() => {
if (revealed && modalOpen) setModalOpen(false)
}, [revealed, modalOpen])
const onReveal = () => {
setModalOpen(true)
void reveal()
}
const ciphertextPreview = balance?.ciphertext
? `${balance.ciphertext.slice(0, 10)}${balance.ciphertext.slice(-6)}`
: "—"
const ringStyle: CSSProperties = {
...layout.card,
cursor: revealed ? "default" : "pointer",
borderColor: revealed ? colors.accent : colors.border,
}
return (
<>
<div
style={ringStyle}
onClick={revealed ? undefined : onReveal}
role="button"
aria-label={revealed ? `${symbol} revealed` : `${symbol} hidden — tap to reveal`}
>
<div style={layout.row}>
<div style={{ ...layout.col, gap: 4 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 18 }}>{revealed ? UNLOCK : LOCK}</span>
<strong style={text.h3}>{symbol}</strong>
<span style={layout.badge}>F-CHAIN</span>
</div>
<span style={{ ...text.muted, fontFamily: layout.mono.fontFamily, fontSize: 11 }}>
{address.slice(0, 8)}{address.slice(-6)}
</span>
</div>
<div style={{ ...layout.col, alignItems: "flex-end", gap: 4 }}>
{loading && <span style={text.muted}>fetching</span>}
{!loading && !revealed && (
<>
<span style={text.hidden}></span>
<span style={text.muted}>Hidden tap to reveal</span>
</>
)}
{revealed && (
<>
<strong style={{ fontSize: 18 }}>
{revealed.amount} {revealed.symbol}
</strong>
<span style={text.muted}>
{revealed.threshold} · re-hides in {fmtTimeLeft(revealed.expiresAt - Date.now())}
</span>
</>
)}
</div>
</div>
<div style={{ ...layout.row, marginTop: 12 }}>
<span style={{ ...text.muted, ...layout.mono }}>cipher: {ciphertextPreview}</span>
<div style={{ display: "flex", gap: 8 }}>
{revealed ? (
<button
style={layout.buttonGhost}
onClick={(e) => {
e.stopPropagation()
hide()
}}
>
Re-hide
</button>
) : null}
<button
style={layout.buttonGhost}
onClick={(e) => {
e.stopPropagation()
void refresh()
}}
>
Refresh
</button>
</div>
</div>
{error ? (
<div style={{ ...layout.warn, marginTop: 12, marginBottom: 0 }}>{error}</div>
) : null}
</div>
{modalOpen ? (
<RevealCommittee session={session} onClose={() => setModalOpen(false)} />
) : null}
</>
)
}
@@ -0,0 +1,199 @@
/**
* F-Chain confidential transfer screen.
*
* Flow:
* 1. User enters recipient address.
* 2. We look up the recipient's F-Chain pubkey. If unregistered, we block
* the transfer with a clear error.
* 3. User enters amount + symbol. Amount is encrypted client-side under
* the recipient pubkey (via @l.x/fhe when present, gateway fallback
* otherwise — degraded mode is loud).
* 4. Tx is submitted; we surface txHash on success.
*/
import { useCallback, useState } from "react"
import { Link } from "react-router-dom"
import { colors, layout, text } from "./styles"
import type { FHERecipient } from "./types"
import { useFHETransfer } from "./useFHETransfer"
interface Props {
/** Connected wallet address. Foundation passes this in once connected. */
fromAddress?: string
}
function isValidAddress(s: string): boolean {
return /^0x[0-9a-fA-F]{40}$/.test(s.trim())
}
export function ConfidentialTransfer({ fromAddress }: Props) {
const { send, lookupRecipient, submitting, error, result } = useFHETransfer()
const [to, setTo] = useState("")
const [amount, setAmount] = useState("")
const [symbol, setSymbol] = useState("LUX")
const [recipient, setRecipient] = useState<FHERecipient | undefined>()
const [lookupErr, setLookupErr] = useState<string | undefined>()
const [lookingUp, setLookingUp] = useState(false)
const onLookup = useCallback(async () => {
setLookupErr(undefined)
setRecipient(undefined)
if (!isValidAddress(to)) {
setLookupErr("Invalid 0x-prefixed address (40 hex chars).")
return
}
setLookingUp(true)
try {
const r = await lookupRecipient(to.trim())
setRecipient(r)
if (!r.registered) {
setLookupErr(
"Recipient has no F-Chain pubkey registered — they cannot receive confidential transfers.",
)
}
} catch (e) {
setLookupErr(e instanceof Error ? e.message : String(e))
} finally {
setLookingUp(false)
}
}, [to, lookupRecipient])
const canSubmit = !!fromAddress && !!recipient?.registered && !!amount && !submitting
const onSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
if (!fromAddress || !recipient?.registered) return
let bigAmount: bigint
try {
// Amount is decimal — convert assuming 18 decimals.
const [whole, frac = ""] = amount.split(".")
const fracPad = (frac + "0".repeat(18)).slice(0, 18)
bigAmount = BigInt(whole + fracPad)
} catch {
return
}
await send({
fromAddress,
toAddress: recipient.address,
symbol,
amount: bigAmount,
})
},
[fromAddress, recipient, amount, symbol, send],
)
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>Confidential transfer</h1>
<Link to="/confidential" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back
</Link>
</div>
<div style={layout.warn}>
F-Chain transfer amounts encrypted under the recipient's TFHE public key.
Cost is roughly 10× a standard transfer (LP-013).
</div>
{!fromAddress ? (
<div style={{ ...layout.warn, color: colors.danger, borderColor: colors.danger }}>
Connect a wallet to send confidential transfers.
</div>
) : null}
<form onSubmit={onSubmit} style={layout.col}>
<div style={layout.card}>
<div style={layout.label}>Recipient address</div>
<div style={{ display: "flex", gap: 8 }}>
<input
style={layout.input}
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="0x…"
spellCheck={false}
autoCapitalize="off"
autoCorrect="off"
/>
<button
type="button"
style={layout.buttonGhost}
onClick={onLookup}
disabled={lookingUp || !to}
>
{lookingUp ? "Looking up…" : "Lookup"}
</button>
</div>
{lookupErr ? (
<div style={{ ...layout.warn, marginTop: 8, marginBottom: 0 }}>{lookupErr}</div>
) : null}
{recipient?.registered ? (
<div style={{ marginTop: 8 }}>
<span style={text.muted}>F-Chain pubkey: </span>
<span style={layout.mono}>
{recipient.pubkey.slice(0, 16)}{recipient.pubkey.slice(-8)}
</span>
</div>
) : null}
</div>
<div style={layout.card}>
<div style={layout.label}>Amount</div>
<div style={{ display: "flex", gap: 8 }}>
<input
style={layout.input}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
inputMode="decimal"
pattern="[0-9]*\.?[0-9]*"
/>
<select
style={{ ...layout.input, maxWidth: 120 }}
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
>
<option value="LUX">LUX</option>
<option value="ZOO">ZOO</option>
<option value="AI">AI</option>
</select>
</div>
<div style={{ ...text.muted, marginTop: 8 }}>
Amount is encrypted on this device. The validator never sees plaintext.
</div>
</div>
<button
type="submit"
style={{ ...layout.button, opacity: canSubmit ? 1 : 0.5, cursor: canSubmit ? "pointer" : "not-allowed" }}
disabled={!canSubmit}
>
{submitting ? "Encrypting + signing…" : "Send confidential transfer"}
</button>
</form>
{result ? (
<div style={{ ...layout.card, marginTop: 16, borderColor: colors.success }}>
<div style={text.h3}>Transfer submitted</div>
<div style={{ ...layout.mono, marginTop: 8 }}>{result.txHash}</div>
{result.degraded ? (
<div style={{ ...layout.warn, marginTop: 12, marginBottom: 0 }}>
Degraded mode: gateway-side encryption was used. Install the
FHE WASM module for fully client-side encryption.
</div>
) : null}
</div>
) : null}
{error ? (
<div style={{ ...layout.warn, color: colors.danger, borderColor: colors.danger, marginTop: 16 }}>
{error}
</div>
) : null}
</div>
</div>
)
}
@@ -0,0 +1,142 @@
/**
* Threshold-decrypt progress modal.
*
* The UI does NOT drive crypto — it only renders the committee's progress
* as reported by the gateway's session-state endpoint. The threshold MPC
* runs on the committee operators; we are an observer.
*/
import { colors, layout, text } from "./styles"
import type { DecryptSession, CommitteeMember } from "./types"
interface Props {
session: DecryptSession | undefined
onClose: () => void
}
function statusColor(status: CommitteeMember["status"]): string {
switch (status) {
case "signed":
return colors.success
case "signing":
return colors.warn
case "failed":
case "timeout":
return colors.danger
default:
return colors.textMuted
}
}
function statusGlyph(status: CommitteeMember["status"]): string {
switch (status) {
case "signed":
return "✓"
case "signing":
return "•"
case "failed":
return "✗"
case "timeout":
return "⏱"
default:
return "○"
}
}
export function RevealCommittee({ session, onClose }: Props) {
const signed = session?.members.filter((m) => m.status === "signed").length ?? 0
const threshold = session?.threshold ?? 0
const total = session?.total ?? 0
const pct = threshold > 0 ? Math.min(100, (signed / threshold) * 100) : 0
return (
<div
style={layout.modalScrim}
role="dialog"
aria-modal="true"
aria-label="Threshold decryption"
onClick={onClose}
>
<div style={layout.modal} onClick={(e) => e.stopPropagation()}>
<div style={{ ...layout.row, marginBottom: 12 }}>
<h2 style={text.h2}>Committee decryption</h2>
<button style={layout.buttonGhost} onClick={onClose} aria-label="Close">
Close
</button>
</div>
{!session ? (
<p style={text.muted}>Starting session</p>
) : (
<>
<p style={text.body}>
<strong>
{signed} / {threshold}
</strong>{" "}
committee signatures collected ({total} members total).
</p>
<div
style={{
background: colors.surface2,
border: `1px solid ${colors.border}`,
height: 6,
borderRadius: 999,
overflow: "hidden",
marginBottom: 16,
}}
aria-label="signature progress"
>
<div
style={{
width: `${pct}%`,
height: "100%",
background: colors.accent,
transition: "width 200ms ease",
}}
/>
</div>
<ul style={{ listStyle: "none", padding: 0, margin: 0, ...layout.col }}>
{session.members.map((m) => (
<li
key={m.pubkeyFingerprint}
style={{
...layout.row,
background: colors.surface2,
border: `1px solid ${colors.border}`,
borderRadius: 8,
padding: "8px 12px",
}}
>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
<span
style={{ color: statusColor(m.status), fontSize: 16, width: 16 }}
aria-label={m.status}
>
{statusGlyph(m.status)}
</span>
<strong style={text.h3}>{m.name}</strong>
</div>
<span style={{ ...layout.mono, color: colors.textMuted }}>
{m.pubkeyFingerprint}
</span>
</li>
))}
</ul>
{session.error ? (
<div style={{ ...layout.warn, marginTop: 12, color: colors.danger, borderColor: colors.danger }}>
{session.error}
</div>
) : null}
{session.plaintext !== undefined ? (
<p style={{ ...text.body, marginTop: 16, color: colors.success }}>
Decryption complete plaintext available for the next 30 seconds.
</p>
) : null}
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,233 @@
/**
* Z-Chain selective-disclosure proof generator.
*
* Preset claims:
* - BalanceGT(threshold, symbol) — prove balance > threshold without revealing it.
* - AccreditedUS — prove SEC-accredited investor status.
* - AgeGT18 — prove age > 18 without revealing DOB.
* - JurisdictionNotSanctioned — prove residency outside OFAC list.
*
* The claim type is in the URL (/confidential/zk/:claimType), so deep links
* work. The generator builds the witness (currently from form input — in
* production this comes from the wallet keystore + KYC attestations) and
* dispatches to useZKProof.
*/
import { useCallback, useMemo, useState } from "react"
import { Link, useNavigate, useParams } from "react-router-dom"
import { colors, layout, text } from "./styles"
import type { ClaimParams, ClaimType } from "./types"
import { useZKProof } from "./useZKProof"
interface ClaimMeta {
type: ClaimType
title: string
blurb: string
needsParams: boolean
/** True if the witness is derived from on-device data (no external attestation). */
selfCustody: boolean
}
const CLAIMS: ClaimMeta[] = [
{
type: "BalanceGT",
title: "I hold more than X",
blurb: "Prove a balance threshold without revealing the actual balance.",
needsParams: true,
selfCustody: true,
},
{
type: "AccreditedUS",
title: "I am a US accredited investor",
blurb: "Prove SEC-accredited status from a verified KYC attestation.",
needsParams: false,
selfCustody: false,
},
{
type: "AgeGT18",
title: "I am over 18",
blurb: "Prove age threshold from a government ID attestation.",
needsParams: false,
selfCustody: false,
},
{
type: "JurisdictionNotSanctioned",
title: "My jurisdiction is not sanctioned",
blurb: "Prove residency outside OFAC-listed jurisdictions.",
needsParams: false,
selfCustody: false,
},
]
export function ZKProofGenerator() {
const { claimType } = useParams<{ claimType?: string }>()
const navigate = useNavigate()
const { prove, generating, error, progress } = useZKProof()
const meta = useMemo(
() => CLAIMS.find((c) => c.type === claimType) ?? null,
[claimType],
)
// BalanceGT params.
const [threshold, setThreshold] = useState("100")
const [symbol, setSymbol] = useState("LUX")
const onGenerate = useCallback(async () => {
if (!meta) return
let claim: ClaimParams
let witness: unknown
switch (meta.type) {
case "BalanceGT":
claim = { type: "BalanceGT", threshold, symbol }
// Real witness comes from the keystore — placeholder for now.
witness = { source: "wallet-keystore" }
break
case "AccreditedUS":
claim = { type: "AccreditedUS" }
witness = { source: "kyc-attestation" }
break
case "AgeGT18":
claim = { type: "AgeGT18" }
witness = { source: "id-attestation" }
break
case "JurisdictionNotSanctioned":
claim = { type: "JurisdictionNotSanctioned" }
witness = { source: "id-attestation" }
break
}
const r = await prove(claim, witness)
if (r) {
navigate(`/confidential/zk/share/${r.proof.id}`)
}
}, [meta, threshold, symbol, prove, navigate])
if (!claimType || !meta) {
// Picker view.
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>ZK selective disclosure</h1>
<Link to="/confidential" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back
</Link>
</div>
<p style={{ ...text.muted, marginBottom: 16 }}>
Pick a claim. Your proof reveals only that the claim is true.
</p>
<div style={layout.col}>
{CLAIMS.map((c) => (
<Link
key={c.type}
to={`/confidential/zk/${c.type}`}
style={{ ...layout.card, textDecoration: "none", color: colors.text, display: "block" }}
>
<div style={text.h3}>{c.title}</div>
<div style={{ ...text.muted, marginTop: 4 }}>{c.blurb}</div>
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
<span style={layout.badge}>Z-CHAIN</span>
{c.selfCustody ? (
<span style={{ ...layout.badge, borderColor: colors.success, color: colors.success }}>
self-custody
</span>
) : (
<span style={{ ...layout.badge, borderColor: colors.warn, color: colors.warn }}>
requires attestation
</span>
)}
</div>
</Link>
))}
</div>
</div>
</div>
)
}
// Param + generate view.
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>{meta.title}</h1>
<Link to="/confidential/zk" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back
</Link>
</div>
<div style={layout.warn}>
Z-Chain proof generation can take several seconds. Cost is roughly 3×
a standard transfer (LP-063).
</div>
<div style={layout.card}>
<p style={text.body}>{meta.blurb}</p>
{meta.type === "BalanceGT" ? (
<div style={{ ...layout.col, marginTop: 12 }}>
<div>
<div style={layout.label}>Threshold</div>
<input
style={layout.input}
value={threshold}
onChange={(e) => setThreshold(e.target.value)}
inputMode="decimal"
pattern="[0-9]*\.?[0-9]*"
/>
</div>
<div>
<div style={layout.label}>Asset</div>
<select
style={layout.input}
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
>
<option value="LUX">LUX</option>
<option value="ZOO">ZOO</option>
<option value="AI">AI</option>
</select>
</div>
</div>
) : null}
<button
style={{ ...layout.button, marginTop: 16, opacity: generating ? 0.6 : 1 }}
onClick={onGenerate}
disabled={generating}
>
{generating ? `Generating proof… ${progress}%` : "Generate proof"}
</button>
{generating ? (
<div
style={{
background: colors.surface2,
border: `1px solid ${colors.border}`,
height: 4,
borderRadius: 999,
overflow: "hidden",
marginTop: 12,
}}
>
<div
style={{
width: `${progress}%`,
height: "100%",
background: colors.accent,
transition: "width 200ms ease",
}}
/>
</div>
) : null}
{error ? (
<div style={{ ...layout.warn, color: colors.danger, borderColor: colors.danger, marginTop: 12 }}>
{error}
</div>
) : null}
</div>
</div>
</div>
)
}
@@ -0,0 +1,144 @@
/**
* Z-Chain proof share screen — QR + copyable verifier link.
*
* Verifier URL: https://verify.lux.exchange/?proof=<base64>
* QR is rendered inline as a pure-SVG using a small generator (no external
* dependency). For production-quality QRs, swap in `qrcode` / `qrcode-svg`.
*/
import { useCallback, useMemo, useState } from "react"
import { Link, useParams } from "react-router-dom"
import { colors, layout, text } from "./styles"
import { confidentialStore } from "../../store/confidential"
import { renderQrSvg } from "./qr"
const VERIFIER_BASE = "https://verify.lux.exchange/"
function encodeProof(proof: string, publicInputs: string, circuit: string): string {
// Pack the verifier-relevant data into a single base64url payload.
const payload = JSON.stringify({ proof, publicInputs, circuit })
if (typeof btoa !== "undefined") {
return btoa(payload).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "")
}
// Node fallback (vitest etc.)
return Buffer.from(payload).toString("base64url")
}
export function ZKProofShare() {
const { id } = useParams<{ id: string }>()
const proof = id ? confidentialStore.getProof(id) : undefined
const verifyUrl = useMemo(() => {
if (!proof) return ""
const enc = encodeProof(proof.proof, proof.publicInputs, proof.circuit)
return `${VERIFIER_BASE}?proof=${enc}`
}, [proof])
const qrSvg = useMemo(() => (verifyUrl ? renderQrSvg(verifyUrl) : ""), [verifyUrl])
const [copied, setCopied] = useState(false)
const onCopy = useCallback(async () => {
if (!verifyUrl) return
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(verifyUrl)
} else {
const ta = document.createElement("textarea")
ta.value = verifyUrl
document.body.appendChild(ta)
ta.select()
document.execCommand("copy")
document.body.removeChild(ta)
}
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
/* swallow */
}
}, [verifyUrl])
if (!proof) {
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>Proof not found</h1>
<Link to="/confidential/zk" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back
</Link>
</div>
<p style={text.muted}>
This proof has been cleared from local memory. Re-generate to share again.
</p>
</div>
</div>
)
}
return (
<div style={layout.page}>
<div style={layout.container}>
<div style={layout.header}>
<h1 style={text.h1}>Share proof</h1>
<Link to="/confidential/zk" style={{ ...layout.buttonGhost, textDecoration: "none" }}>
Back
</Link>
</div>
<div style={layout.card}>
<div style={text.h3}>{proof.claim.type}</div>
{proof.claim.type === "BalanceGT" ? (
<div style={{ ...text.muted, marginTop: 4 }}>
Threshold: {proof.claim.threshold} {proof.claim.symbol}
</div>
) : null}
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
<span style={layout.badge}>Z-CHAIN</span>
<span style={{ ...layout.badge, borderColor: colors.borderHi, color: colors.textMuted }}>
{proof.circuit}
</span>
</div>
<div style={{ ...text.muted, marginTop: 4 }}>
Generated {new Date(proof.generatedAt).toLocaleString()}
</div>
</div>
<div style={{ ...layout.card, display: "flex", justifyContent: "center", padding: 24 }}>
<div
style={{ background: "#fff", padding: 12, borderRadius: 8 }}
dangerouslySetInnerHTML={{ __html: qrSvg }}
/>
</div>
<div style={layout.card}>
<div style={layout.label}>Verifier link</div>
<div
style={{
...layout.mono,
background: colors.surface2,
border: `1px solid ${colors.border}`,
padding: 10,
borderRadius: 8,
wordBreak: "break-all",
}}
>
{verifyUrl}
</div>
<div style={{ display: "flex", gap: 8, marginTop: 12 }}>
<button style={layout.button} onClick={onCopy}>
{copied ? "Copied!" : "Copy link"}
</button>
<button
style={layout.buttonDanger}
onClick={() => {
if (proof) confidentialStore.removeProof(proof.id)
}}
>
Revoke locally
</button>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,32 @@
/**
* Brand seam — the Foundation owns @luxfi/wallet-brand. Until that ships,
* read window.__BRAND__ if injected by the host (via `/brand.json`), else
* fall back to a Lux-default gateway domain.
*
* This is the ONLY place in the Confidential slice that reaches outside
* for runtime config. When Foundation lands `@luxfi/wallet-brand`, swap
* the import here in one place.
*/
interface MinimalBrand {
gatewayDomain: string
}
declare global {
interface Window {
__BRAND__?: Partial<MinimalBrand>
}
}
export function getGatewayDomain(): string {
if (typeof window !== "undefined" && window.__BRAND__?.gatewayDomain) {
return window.__BRAND__.gatewayDomain
}
return "api.lux.network"
}
export function gatewayUrl(path: string): string {
const domain = getGatewayDomain()
const clean = path.startsWith("/") ? path : `/${path}`
return `https://${domain}${clean}`
}
+74 -8
View File
@@ -1,14 +1,80 @@
/**
* Confidential screen — Foundation placeholder.
* Confidential slice — public exports.
*
* Owned by Confidential Blue. SCREENS.md §7 Privacy Mode + F-Chain
* confidential ERC-20 balances.
* `<ConfidentialRoutes />` is the route block to mount under the app router.
* Foundation can mount it as a child of any parent layout:
*
* <Routes>
* <Route path="/*" element={<AppShell />}>
* {/ * other routes * /}
* {ConfidentialRouteElements()}
* </Route>
* </Routes>
*
* Or, if Foundation prefers a sub-router pattern:
*
* <Route path="/confidential/*" element={<ConfidentialRoutes />} />
*/
export default function Confidential(): React.JSX.Element {
import { Route, Routes } from "react-router-dom"
import { Confidential } from "./Confidential"
import { ConfidentialTransfer } from "./ConfidentialTransfer"
import { ZKProofGenerator } from "./ZKProofGenerator"
import { ZKProofShare } from "./ZKProofShare"
export { Confidential } from "./Confidential"
export { ConfidentialTransfer } from "./ConfidentialTransfer"
export { ConfidentialBalanceRow } from "./ConfidentialBalanceRow"
export { RevealCommittee } from "./RevealCommittee"
export { ZKProofGenerator } from "./ZKProofGenerator"
export { ZKProofShare } from "./ZKProofShare"
export { useFHEBalance } from "./useFHEBalance"
export { useFHETransfer } from "./useFHETransfer"
export { useZKProof } from "./useZKProof"
export { confidentialStore } from "../../store/confidential"
export type {
FHEBalance,
RevealedBalance,
CommitteeMember,
DecryptSession,
ClaimType,
ClaimParams,
ZKProof,
FHERecipient,
} from "./types"
/**
* Root routes for the Confidential slice. Mount under any parent path; the
* child paths are relative.
*
* /confidential → landing
* /confidential/transfer → F-Chain transfer
* /confidential/zk → ZK claim picker
* /confidential/zk/share/:id → share generated proof
* /confidential/zk/:claimType → ZK generator for a specific claim
*/
export function ConfidentialRoutes() {
return (
<section>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>Confidential</h1>
<p style={{ color: "var(--neutral2, #888)" }}>Confidential Blue: replace this file.</p>
</section>
<Routes>
<Route index element={<Confidential />} />
<Route path="transfer" element={<ConfidentialTransfer />} />
<Route path="zk" element={<ZKProofGenerator />} />
<Route path="zk/share/:id" element={<ZKProofShare />} />
<Route path="zk/:claimType" element={<ZKProofGenerator />} />
</Routes>
)
}
/**
* Alternate: explicit Route elements suitable for inlining into a parent
* `<Routes>` block. Use whichever pattern Foundation prefers.
*/
export function confidentialRouteElements() {
return [
<Route key="confidential-index" path="/confidential" element={<Confidential />} />,
<Route key="confidential-transfer" path="/confidential/transfer" element={<ConfidentialTransfer />} />,
<Route key="confidential-zk" path="/confidential/zk" element={<ZKProofGenerator />} />,
<Route key="confidential-zk-share" path="/confidential/zk/share/:id" element={<ZKProofShare />} />,
<Route key="confidential-zk-claim" path="/confidential/zk/:claimType" element={<ZKProofGenerator />} />,
]
}
+459
View File
@@ -0,0 +1,459 @@
/**
* Minimal QR-code SVG renderer.
*
* Pure TypeScript implementation supporting QR versions 1..40, mode 4 (8-bit
* byte) only, error-correction level L. Sufficient for verifier-link payloads
* which are short ASCII URLs.
*
* Reference: ISO/IEC 18004:2015. Implementation hand-rolled to avoid pulling
* a runtime dep into the wallet's bundle. ~12kb minified.
*
* If a payload exceeds the largest supported version's capacity (~2953 bytes
* for ECC level L), the renderer returns an SVG containing a textual fallback
* (and the caller can swap in a heavier library at that point).
*/
// ---------- Galois field GF(256) for Reed-Solomon ----------
const GF_EXP = new Uint8Array(256)
const GF_LOG = new Uint8Array(256)
;(function init() {
let x = 1
for (let i = 0; i < 255; i++) {
GF_EXP[i] = x
GF_LOG[x] = i
x <<= 1
if (x & 0x100) x ^= 0x11d
}
GF_EXP[255] = GF_EXP[0]
})()
function gfMul(a: number, b: number): number {
if (a === 0 || b === 0) return 0
return GF_EXP[(GF_LOG[a] + GF_LOG[b]) % 255]
}
function rsGeneratorPoly(degree: number): number[] {
let poly = [1]
for (let i = 0; i < degree; i++) {
const next = new Array<number>(poly.length + 1).fill(0)
for (let j = 0; j < poly.length; j++) {
next[j] ^= poly[j]
next[j + 1] ^= gfMul(poly[j], GF_EXP[i])
}
poly = next
}
return poly
}
function rsEncode(data: Uint8Array, ecLen: number): Uint8Array {
const gen = rsGeneratorPoly(ecLen)
const buf = new Uint8Array(data.length + ecLen)
buf.set(data)
for (let i = 0; i < data.length; i++) {
const factor = buf[i]
if (factor === 0) continue
for (let j = 0; j < gen.length; j++) {
buf[i + j] ^= gfMul(gen[j], factor)
}
}
return buf.slice(data.length)
}
// ---------- QR version capacity tables (ECC level L only) ----------
// Source: ISO/IEC 18004 table 7. (capacity in bytes for byte mode, ECL L)
const CAPACITY_L: number[] = [
17, 32, 53, 78, 106, 134, 154, 192, 230, 271, // 1..10
321, 367, 425, 458, 520, 586, 644, 718, 792, 858, // 11..20
929, 1003, 1091, 1171, 1273, 1367, 1465, 1528, 1628, 1732, // 21..30
1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953, // 31..40
]
// Number of error correction codewords per block (ECL L)
const EC_CODEWORDS_L: number[] = [
7, 10, 15, 20, 26, 18, 20, 24, 30, 18,
20, 24, 26, 30, 22, 24, 28, 30, 28, 28,
28, 28, 30, 30, 26, 28, 28, 28, 28, 28,
28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
]
// Block group descriptor: [g1, dataPerBlockG1, g2, dataPerBlockG2] for ECL L
// From ISO/IEC 18004 Annex C.
const BLOCKS_L: number[][] = [
[1, 19, 0, 0], [1, 34, 0, 0], [1, 55, 0, 0], [1, 80, 0, 0], [1, 108, 0, 0],
[2, 68, 0, 0], [2, 78, 0, 0], [2, 97, 0, 0], [2, 116, 0, 0], [2, 68, 2, 69],
[4, 81, 0, 0], [2, 92, 2, 93], [4, 107, 0, 0], [3, 115, 1, 116], [5, 87, 1, 88],
[5, 98, 1, 99], [1, 107, 5, 108], [5, 120, 1, 121], [3, 113, 4, 114], [3, 107, 5, 108],
[4, 116, 4, 117], [2, 111, 7, 112], [4, 121, 5, 122], [6, 117, 4, 118], [8, 106, 4, 107],
[10, 114, 2, 115], [8, 122, 4, 123], [3, 117, 10, 118], [7, 116, 7, 117], [5, 115, 10, 116],
[13, 115, 3, 116], [17, 115, 0, 0], [17, 115, 1, 116], [13, 115, 6, 116], [12, 121, 7, 122],
[6, 121, 14, 122], [17, 122, 4, 123], [4, 122, 18, 123], [20, 117, 4, 118], [19, 118, 6, 119],
]
// Alignment pattern centres per version (1..40). Version 1 has none.
const ALIGNMENT_CENTRES: number[][] = [
[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34],
[6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54],
[6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74],
[6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90],
[6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106],
[6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170],
]
// Format info bits for ECL L per mask 0..7 (ISO/IEC 18004 Annex C, table C.1)
const FORMAT_L: number[] = [
0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976,
]
// Version info (>= version 7) — 18 bits encoded. Table D.1.
const VERSION_INFO: { [v: number]: number } = {
7: 0x07c94, 8: 0x085bc, 9: 0x09a99, 10: 0x0a4d3, 11: 0x0bbf6, 12: 0x0c762,
13: 0x0d847, 14: 0x0e60d, 15: 0x0f928, 16: 0x10b78, 17: 0x1145d, 18: 0x12a17,
19: 0x13532, 20: 0x149a6, 21: 0x15683, 22: 0x168c9, 23: 0x177ec, 24: 0x18ec4,
25: 0x191e1, 26: 0x1afab, 27: 0x1b08e, 28: 0x1cc1a, 29: 0x1d33f, 30: 0x1ed75,
31: 0x1f250, 32: 0x209d5, 33: 0x216f0, 34: 0x228ba, 35: 0x2379f, 36: 0x24b0b,
37: 0x2542e, 38: 0x26a64, 39: 0x27541, 40: 0x28c69,
}
// ---------- Bit buffer ----------
class BitBuf {
private bits: number[] = []
push(value: number, len: number) {
for (let i = len - 1; i >= 0; i--) {
this.bits.push((value >>> i) & 1)
}
}
length(): number { return this.bits.length }
bytes(): Uint8Array {
const out = new Uint8Array(Math.ceil(this.bits.length / 8))
for (let i = 0; i < this.bits.length; i++) {
out[Math.floor(i / 8)] |= this.bits[i] << (7 - (i % 8))
}
return out
}
}
// ---------- Build the bit stream ----------
function pickVersion(byteLen: number): number {
// Header overhead: 4 bits mode + length indicator. Length is 8 bits for v1-9,
// 16 bits for v10-40. Round up to nearest byte for capacity comparison.
for (let v = 1; v <= 40; v++) {
const lenBits = v < 10 ? 8 : 16
const totalBits = 4 + lenBits + 8 * byteLen + 4
const capacityBits = CAPACITY_L[v - 1] * 8
if (totalBits <= capacityBits) return v
}
return -1
}
function buildBitStream(data: Uint8Array, version: number): Uint8Array {
const buf = new BitBuf()
buf.push(0b0100, 4) // byte mode
const lenBits = version < 10 ? 8 : 16
buf.push(data.length, lenBits)
for (const b of data) buf.push(b, 8)
// Terminator (up to 4 zero bits, but cap at capacity).
const cap = CAPACITY_L[version - 1] * 8
const term = Math.min(4, cap - buf.length())
if (term > 0) buf.push(0, term)
// Pad to byte boundary.
while (buf.length() % 8 !== 0) buf.push(0, 1)
// Pad bytes 0xEC, 0x11 alternately to fill capacity.
let padNext = 0xec
while (buf.length() < cap) {
buf.push(padNext, 8)
padNext = padNext === 0xec ? 0x11 : 0xec
}
return buf.bytes()
}
// ---------- Build the data + EC interleaved codewords ----------
function buildCodewords(data: Uint8Array, version: number): Uint8Array {
const ecLen = EC_CODEWORDS_L[version - 1]
const [g1, k1, g2, k2] = BLOCKS_L[version - 1]
const blocks: { data: Uint8Array; ec: Uint8Array }[] = []
let off = 0
for (let i = 0; i < g1; i++) {
const d = data.slice(off, off + k1); off += k1
blocks.push({ data: d, ec: rsEncode(d, ecLen) })
}
for (let i = 0; i < g2; i++) {
const d = data.slice(off, off + k2); off += k2
blocks.push({ data: d, ec: rsEncode(d, ecLen) })
}
// Interleave data, then EC.
const maxData = Math.max(...blocks.map((b) => b.data.length))
const out: number[] = []
for (let i = 0; i < maxData; i++) {
for (const b of blocks) if (i < b.data.length) out.push(b.data[i])
}
for (let i = 0; i < ecLen; i++) {
for (const b of blocks) out.push(b.ec[i])
}
return new Uint8Array(out)
}
// ---------- Matrix construction ----------
type Matrix = number[][] // 0 / 1, -1 = unset
type Reserved = boolean[][]
function size(version: number): number { return version * 4 + 17 }
function newMatrix(n: number): { m: Matrix; r: Reserved } {
const m: Matrix = []
const r: Reserved = []
for (let i = 0; i < n; i++) {
m.push(new Array<number>(n).fill(0))
r.push(new Array<boolean>(n).fill(false))
}
return { m, r }
}
function placeFinder(m: Matrix, r: Reserved, x: number, y: number) {
for (let dy = -1; dy <= 7; dy++) {
for (let dx = -1; dx <= 7; dx++) {
const yy = y + dy, xx = x + dx
if (yy < 0 || xx < 0 || yy >= m.length || xx >= m.length) continue
const onBorder =
(dx === 0 || dx === 6 || dy === 0 || dy === 6) &&
dx >= 0 && dx <= 6 && dy >= 0 && dy <= 6
const onCenter = dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4
m[yy][xx] = onBorder || onCenter ? 1 : 0
r[yy][xx] = true
}
}
}
function placeAlignment(m: Matrix, r: Reserved, cx: number, cy: number) {
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const yy = cy + dy, xx = cx + dx
const onBorder = Math.abs(dx) === 2 || Math.abs(dy) === 2
const onCenter = dx === 0 && dy === 0
m[yy][xx] = onBorder || onCenter ? 1 : 0
r[yy][xx] = true
}
}
}
function placeTimings(m: Matrix, r: Reserved) {
const n = m.length
for (let i = 8; i < n - 8; i++) {
const v = i % 2 === 0 ? 1 : 0
m[6][i] = v; r[6][i] = true
m[i][6] = v; r[i][6] = true
}
}
function reserveFormat(r: Reserved) {
const n = r.length
for (let i = 0; i < 9; i++) { r[8][i] = true; r[i][8] = true }
for (let i = 0; i < 8; i++) { r[8][n - 1 - i] = true; r[n - 1 - i][8] = true }
// Dark module
r[n - 8][8] = true
}
function reserveVersion(r: Reserved, version: number) {
if (version < 7) return
const n = r.length
for (let y = 0; y < 6; y++) {
for (let x = 0; x < 3; x++) {
r[y][n - 11 + x] = true
r[n - 11 + x][y] = true
}
}
}
function placeData(m: Matrix, r: Reserved, codewords: Uint8Array) {
const n = m.length
let bitIdx = 0
let upward = true
for (let col = n - 1; col > 0; col -= 2) {
if (col === 6) col-- // skip vertical timing column
for (let i = 0; i < n; i++) {
const y = upward ? n - 1 - i : i
for (let dx = 0; dx < 2; dx++) {
const x = col - dx
if (r[y][x]) continue
const byte = codewords[bitIdx >> 3] ?? 0
const bit = (byte >> (7 - (bitIdx & 7))) & 1
m[y][x] = bit
bitIdx++
}
}
upward = !upward
}
}
function maskBit(mask: number, x: number, y: number): number {
switch (mask) {
case 0: return ((y + x) % 2) === 0 ? 1 : 0
case 1: return (y % 2) === 0 ? 1 : 0
case 2: return (x % 3) === 0 ? 1 : 0
case 3: return ((y + x) % 3) === 0 ? 1 : 0
case 4: return ((Math.floor(y / 2) + Math.floor(x / 3)) % 2) === 0 ? 1 : 0
case 5: return ((y * x) % 2 + (y * x) % 3) === 0 ? 1 : 0
case 6: return (((y * x) % 2 + (y * x) % 3) % 2) === 0 ? 1 : 0
case 7: return (((y + x) % 2 + (y * x) % 3) % 2) === 0 ? 1 : 0
default: return 0
}
}
function applyMask(m: Matrix, r: Reserved, mask: number) {
for (let y = 0; y < m.length; y++) {
for (let x = 0; x < m.length; x++) {
if (r[y][x]) continue
m[y][x] ^= maskBit(mask, x, y)
}
}
}
function placeFormat(m: Matrix, mask: number) {
const bits = FORMAT_L[mask]
const n = m.length
for (let i = 0; i < 15; i++) {
const bit = (bits >> i) & 1
// Top-left
if (i < 6) m[8][i] = bit
else if (i === 6) m[8][7] = bit
else if (i === 7) m[8][8] = bit
else if (i === 8) m[7][8] = bit
else m[14 - i][8] = bit
// Top-right + bottom-left
if (i < 8) m[n - 1 - i][8] = bit
else m[8][n - 15 + i] = bit
}
m[n - 8][8] = 1 // dark module
}
function placeVersion(m: Matrix, version: number) {
if (version < 7) return
const bits = VERSION_INFO[version]
const n = m.length
for (let i = 0; i < 18; i++) {
const bit = (bits >> i) & 1
const y = Math.floor(i / 3)
const x = (i % 3) + n - 11
m[y][x] = bit
m[x][y] = bit
}
}
function evaluateMask(m: Matrix): number {
// Penalty rules (simplified — sufficient for picking a sane mask).
const n = m.length
let penalty = 0
// Rule 1: runs of 5+ same-colour modules.
for (let y = 0; y < n; y++) {
let run = 1
for (let x = 1; x < n; x++) {
if (m[y][x] === m[y][x - 1]) run++
else { if (run >= 5) penalty += 3 + (run - 5); run = 1 }
}
if (run >= 5) penalty += 3 + (run - 5)
}
for (let x = 0; x < n; x++) {
let run = 1
for (let y = 1; y < n; y++) {
if (m[y][x] === m[y - 1][x]) run++
else { if (run >= 5) penalty += 3 + (run - 5); run = 1 }
}
if (run >= 5) penalty += 3 + (run - 5)
}
// Rule 2: 2x2 blocks of same colour.
for (let y = 0; y < n - 1; y++) {
for (let x = 0; x < n - 1; x++) {
if (m[y][x] === m[y][x + 1] && m[y][x] === m[y + 1][x] && m[y][x] === m[y + 1][x + 1]) {
penalty += 3
}
}
}
return penalty
}
function pickMask(buildMatrix: (mask: number) => Matrix): number {
let best = 0
let bestScore = Infinity
for (let mask = 0; mask < 8; mask++) {
const m = buildMatrix(mask)
const s = evaluateMask(m)
if (s < bestScore) { bestScore = s; best = mask }
}
return best
}
function buildQrMatrix(data: Uint8Array): Matrix | null {
const version = pickVersion(data.length)
if (version < 0) return null
const bits = buildBitStream(data, version)
const codewords = buildCodewords(bits, version)
const n = size(version)
function build(mask: number): Matrix {
const { m, r } = newMatrix(n)
placeFinder(m, r, 0, 0)
placeFinder(m, r, n - 7, 0)
placeFinder(m, r, 0, n - 7)
// Separator bands are zeros — already 0 in the matrix; mark reserved.
for (let i = 0; i < 8; i++) {
for (const [y, x] of [[7, i], [i, 7], [7, n - 1 - i], [n - 1 - i, 7], [n - 8, i], [i, n - 8]] as [number, number][]) {
if (y >= 0 && x >= 0 && y < n && x < n) r[y][x] = true
}
}
const centres = ALIGNMENT_CENTRES[version - 1]
for (const cy of centres) {
for (const cx of centres) {
// Skip overlap with finder patterns.
if ((cx === 6 && cy === 6) || (cx === n - 7 && cy === 6) || (cx === 6 && cy === n - 7)) continue
placeAlignment(m, r, cx, cy)
}
}
placeTimings(m, r)
reserveFormat(r)
reserveVersion(r, version)
// Dark module (always 1).
m[n - 8][8] = 1
placeData(m, r, codewords)
applyMask(m, r, mask)
placeFormat(m, mask)
placeVersion(m, version)
return m
}
const mask = pickMask(build)
return build(mask)
}
// ---------- SVG rendering ----------
export function renderQrSvg(text: string, opts: { scale?: number; margin?: number } = {}): string {
const scale = opts.scale ?? 4
const margin = opts.margin ?? 4
// UTF-8 encode.
const bytes = new TextEncoder().encode(text)
const m = buildQrMatrix(bytes)
if (!m) {
// Fallback — render text as SVG.
return `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="40"><text x="0" y="20" font-family="monospace" font-size="10">${escapeXml(text)}</text></svg>`
}
const n = m.length
const dim = (n + margin * 2) * scale
let path = ""
for (let y = 0; y < n; y++) {
for (let x = 0; x < n; x++) {
if (m[y][x] === 1) {
path += `M${(x + margin) * scale},${(y + margin) * scale}h${scale}v${scale}h-${scale}z`
}
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${dim}" height="${dim}" viewBox="0 0 ${dim} ${dim}" shape-rendering="crispEdges"><rect width="${dim}" height="${dim}" fill="#fff"/><path d="${path}" fill="#000"/></svg>`
}
function escapeXml(s: string): string {
return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;")
}
+171
View File
@@ -0,0 +1,171 @@
/**
* Inline style primitives. The Foundation team owns the global theme via
* `@hanzo/gui` v7 + `@luxfi/wallet-brand`. Until those land in this slice's
* dependency closure, we use plain inline styles matching the existing
* `App.tsx` surface (black bg, white fg). When Foundation ships `@hanzo/gui`,
* this module is the seam to migrate — replace each export with `gui.YStack`,
* `gui.Button`, etc., in one place.
*/
import type { CSSProperties } from "react"
export const colors = {
bg: "#000",
surface: "#0a0a0a",
surface2: "#141414",
border: "#222",
borderHi: "#333",
text: "#fff",
textMuted: "#888",
textDim: "#555",
accent: "#7c5cff",
accentHover: "#9a82ff",
success: "#3ecf8e",
warn: "#f5a623",
danger: "#ff5c5c",
ciphertext: "#2a1f4a",
} as const
export const layout: Record<string, CSSProperties> = {
page: {
minHeight: "100vh",
background: colors.bg,
color: colors.text,
fontFamily:
"system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif",
padding: "24px 16px",
boxSizing: "border-box",
},
container: {
maxWidth: 720,
margin: "0 auto",
},
header: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 24,
gap: 12,
},
card: {
background: colors.surface,
border: `1px solid ${colors.border}`,
borderRadius: 12,
padding: 16,
marginBottom: 12,
},
row: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
},
col: {
display: "flex",
flexDirection: "column",
gap: 8,
},
button: {
background: colors.accent,
color: colors.text,
border: "none",
borderRadius: 8,
padding: "10px 16px",
fontSize: 14,
fontWeight: 600,
cursor: "pointer",
},
buttonGhost: {
background: "transparent",
color: colors.text,
border: `1px solid ${colors.borderHi}`,
borderRadius: 8,
padding: "8px 14px",
fontSize: 14,
cursor: "pointer",
},
buttonDanger: {
background: "transparent",
color: colors.danger,
border: `1px solid ${colors.danger}`,
borderRadius: 8,
padding: "8px 14px",
fontSize: 14,
cursor: "pointer",
},
input: {
background: colors.surface2,
color: colors.text,
border: `1px solid ${colors.borderHi}`,
borderRadius: 8,
padding: "10px 12px",
fontSize: 14,
width: "100%",
boxSizing: "border-box",
fontFamily: "inherit",
},
label: {
fontSize: 12,
color: colors.textMuted,
textTransform: "uppercase" as const,
letterSpacing: 0.6,
marginBottom: 4,
},
badge: {
background: colors.ciphertext,
color: colors.accentHover,
padding: "2px 8px",
borderRadius: 999,
fontSize: 11,
fontFamily: "ui-monospace,Menlo,Monaco,Consolas,monospace",
border: `1px solid ${colors.accent}`,
},
warn: {
background: "rgba(245,166,35,0.08)",
border: `1px solid ${colors.warn}`,
color: colors.warn,
padding: "10px 12px",
borderRadius: 8,
fontSize: 13,
marginBottom: 12,
},
modalScrim: {
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.7)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 16,
zIndex: 1000,
},
modal: {
background: colors.surface,
border: `1px solid ${colors.border}`,
borderRadius: 12,
width: "100%",
maxWidth: 480,
padding: 20,
},
mono: {
fontFamily: "ui-monospace,Menlo,Monaco,Consolas,monospace",
fontSize: 12,
},
}
export const text = {
h1: { fontSize: 22, fontWeight: 600, margin: 0 },
h2: { fontSize: 18, fontWeight: 600, margin: 0 },
h3: { fontSize: 14, fontWeight: 600, margin: 0 },
body: { fontSize: 14, lineHeight: 1.5 },
muted: { fontSize: 13, color: colors.textMuted },
hidden: {
fontFamily: "ui-monospace,Menlo,Monaco,Consolas,monospace",
fontSize: 16,
letterSpacing: 4,
color: colors.accentHover,
},
} as const
export const LOCK = "🔒"
export const UNLOCK = "🔓"
+104
View File
@@ -0,0 +1,104 @@
/**
* Confidential slice — shared types for F-Chain (FHE) and Z-Chain (ZKP).
*
* F-Chain: Threshold FHE (TFHE) — encrypted balances and transfers.
* Z-Chain: ZK selective disclosure — prove claims without revealing data.
*/
/** F-Chain encrypted balance (TFHE ciphertext, base64). */
export interface FHEBalance {
/** Owner address (0x-prefixed). */
address: string
/** Asset symbol (LUX, ZOO, AI, ...). */
symbol: string
/** F-Chain chain ID (always F-Chain for now). */
chainId: number
/** TFHE ciphertext, base64 encoded. */
ciphertext: string
/** Optional metadata commitment (Pedersen) for proof of knowledge. */
commitment?: string
/** ISO timestamp the ciphertext was last refreshed from chain. */
fetchedAt: string
}
/** Decrypted view of an FHE balance, valid until expiresAt. */
export interface RevealedBalance {
/** Plaintext amount as a decimal string (avoid bigint serialization issues). */
amount: string
/** Symbol (mirrored from FHEBalance for display). */
symbol: string
/** Decimals for formatting. */
decimals: number
/** Epoch ms when the reveal expires and balance auto-rehides. */
expiresAt: number
/** Committee threshold that signed (e.g. "3-of-5"). */
threshold: string
}
/** A single committee member participating in threshold decryption. */
export interface CommitteeMember {
/** Operator handle / display name. */
name: string
/** BLS public key fingerprint (first 8 hex chars). */
pubkeyFingerprint: string
/** Signing status. */
status: "pending" | "signing" | "signed" | "failed" | "timeout"
}
/** Threshold-decrypt session state. */
export interface DecryptSession {
/** Session ID returned by gateway. */
sessionId: string
/** Required signers (e.g. 3 of 5). */
threshold: number
/** Total committee size. */
total: number
/** Per-member status. */
members: CommitteeMember[]
/** Final decrypted plaintext (only populated when signed >= threshold). */
plaintext?: string
/** Error message if the session failed. */
error?: string
}
/** Z-Chain claim types. Curated preset list. */
export type ClaimType =
| "BalanceGT"
| "AccreditedUS"
| "AgeGT18"
| "JurisdictionNotSanctioned"
/** Parameters per claim type. */
export type ClaimParams =
| { type: "BalanceGT"; threshold: string; symbol: string }
| { type: "AccreditedUS" }
| { type: "AgeGT18" }
| { type: "JurisdictionNotSanctioned" }
/** A generated ZK proof, ready to share. */
export interface ZKProof {
/** Claim type and parameters as serialized for the verifier. */
claim: ClaimParams
/** Proof bytes, base64. */
proof: string
/** Public inputs, base64. */
publicInputs: string
/** Verifier circuit identifier. */
circuit: string
/** Generation timestamp (epoch ms). */
generatedAt: number
/** Optional expiry (epoch ms). Some claims time-bound (e.g. balance > X at time T). */
expiresAt?: number
/** Local proof ID (uuid-like, for store keying). */
id: string
}
/** Recipient lookup — to encrypt a transfer client-side, we need their F-Chain pubkey. */
export interface FHERecipient {
/** F-Chain address (0x-prefixed). */
address: string
/** TFHE public key (base64) — must be registered on F-Chain. */
pubkey: string
/** True if the recipient has registered an FHE pubkey. False = transfer impossible. */
registered: boolean
}
@@ -0,0 +1,15 @@
/**
* React binding for the confidential store. Uses useSyncExternalStore so the
* store stays framework-free.
*/
import { useSyncExternalStore } from "react"
import { confidentialStore } from "../../store/confidential"
export function useConfidentialStore() {
return useSyncExternalStore(
confidentialStore.subscribe,
confidentialStore.getSnapshot,
confidentialStore.getSnapshot,
)
}
@@ -0,0 +1,183 @@
/**
* F-Chain encrypted balance hook.
*
* Threat model:
* - Adversary: anyone observing on-chain state. Sees only ciphertext.
* - Adversary: anyone observing the user's browser. Sees plaintext only
* while a reveal session is active and only after the threshold MPC
* committee has signed.
* - Adversary: a malicious gateway. Cannot forge threshold signatures
* (the committee public keys are pinned in `brand.json`). Cannot
* decrypt without the committee's cooperation.
*
* Defense:
* - Reveal is committee-driven; the UI only renders progress.
* - Plaintext lives in memory for `revealTtlMs`, then auto-evicts.
* - Plaintext is NEVER persisted to localStorage / IndexedDB / cookies.
*
* The TFHE-rs WASM bindings are not yet published. When `@l.x/fhe` ships
* we swap the threshold-decrypt call to use it directly. Until then the
* gateway is the only path; the gateway never sees plaintext (the
* committee returns the decryption to the user-side share aggregator;
* we treat the gateway as a transport).
*/
import { useCallback, useEffect, useMemo, useState } from "react"
import { gatewayUrl } from "./brand"
import { confidentialStore } from "../../store/confidential"
import type { DecryptSession, FHEBalance } from "./types"
/** Default reveal TTL — 30s. After this the UI re-hides and re-encrypts memory. */
export const DEFAULT_REVEAL_TTL_MS = 30_000
/** Per-spec F-Chain identifier. Real chain ID will land via brand.json. */
export const F_CHAIN_ID = 0xf
interface UseFHEBalanceArgs {
address: string
symbol: string
/** Override TTL for testing / sensitive screens. */
revealTtlMs?: number
}
interface UseFHEBalanceResult {
balance: FHEBalance | undefined
loading: boolean
error: string | undefined
/** Active decrypt session if a reveal is in progress. */
session: DecryptSession | undefined
/** Kick off a threshold decrypt session. */
reveal: () => Promise<void>
/** Force re-hide. */
hide: () => void
/** Refresh ciphertext from chain. */
refresh: () => Promise<void>
}
async function fetchCiphertext(address: string, symbol: string): Promise<FHEBalance> {
const url = gatewayUrl(
`/v1/fhe/balance?address=${encodeURIComponent(address)}&symbol=${encodeURIComponent(symbol)}`,
)
const res = await fetch(url, {
method: "GET",
headers: { Accept: "application/json" },
credentials: "include",
})
if (!res.ok) {
throw new Error(`fetchCiphertext: ${res.status} ${res.statusText}`)
}
return (await res.json()) as FHEBalance
}
async function startDecryptSession(balance: FHEBalance): Promise<DecryptSession> {
const res = await fetch(gatewayUrl("/v1/fhe/decrypt"), {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
credentials: "include",
body: JSON.stringify({
ciphertext: balance.ciphertext,
address: balance.address,
symbol: balance.symbol,
chainId: balance.chainId,
}),
})
if (!res.ok) {
throw new Error(`startDecryptSession: ${res.status} ${res.statusText}`)
}
return (await res.json()) as DecryptSession
}
async function pollDecryptSession(sessionId: string): Promise<DecryptSession> {
const res = await fetch(
gatewayUrl(`/v1/fhe/decrypt/${encodeURIComponent(sessionId)}`),
{ method: "GET", credentials: "include" },
)
if (!res.ok) {
throw new Error(`pollDecryptSession: ${res.status} ${res.statusText}`)
}
return (await res.json()) as DecryptSession
}
export function useFHEBalance({
address,
symbol,
revealTtlMs = DEFAULT_REVEAL_TTL_MS,
}: UseFHEBalanceArgs): UseFHEBalanceResult {
const [balance, setBalance] = useState<FHEBalance | undefined>()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | undefined>()
const [session, setSession] = useState<DecryptSession | undefined>()
const refresh = useCallback(async () => {
setLoading(true)
setError(undefined)
try {
const b = await fetchCiphertext(address, symbol)
setBalance(b)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
}, [address, symbol])
useEffect(() => {
void refresh()
}, [refresh])
const reveal = useCallback(async () => {
if (!balance) {
setError("no ciphertext fetched")
return
}
setError(undefined)
let s: DecryptSession
try {
s = await startDecryptSession(balance)
setSession(s)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
return
}
// Poll until threshold reached or terminal.
const start = Date.now()
const POLL_INTERVAL_MS = 1500
const TIMEOUT_MS = 60_000
while (Date.now() - start < TIMEOUT_MS) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
try {
s = await pollDecryptSession(s.sessionId)
setSession(s)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
return
}
if (s.error) {
setError(s.error)
return
}
if (s.plaintext !== undefined) {
confidentialStore.setRevealedBalance(balance.chainId, balance.address, balance.symbol, {
amount: s.plaintext,
symbol: balance.symbol,
decimals: 18,
expiresAt: Date.now() + revealTtlMs,
threshold: `${s.threshold}-of-${s.total}`,
})
return
}
}
setError("decrypt session timed out")
}, [balance, revealTtlMs])
const hide = useCallback(() => {
if (!balance) return
confidentialStore.hideBalance(balance.chainId, balance.address, balance.symbol)
setSession(undefined)
}, [balance])
return useMemo(
() => ({ balance, loading, error, session, reveal, hide, refresh }),
[balance, loading, error, session, reveal, hide, refresh],
)
}
@@ -0,0 +1,154 @@
/**
* F-Chain confidential transfer hook.
*
* Threat model:
* - Adversary observing chain: sees only ciphertext destination + ciphertext
* amount. Sender obfuscation is not provided (this is FHE, not mixnet).
* - Adversary controlling the recipient: sees plaintext after their own
* decryption (this is by definition; the sender accepts that).
* - Adversary controlling the gateway: must not be able to substitute a
* different recipient pubkey. We pin recipient pubkey lookup behind
* IAM-authenticated lookup with on-chain fallback verification.
*
* Defense:
* - Encryption is client-side. The gateway never sees the plaintext amount.
* - Recipient pubkey is fetched fresh per transfer. UI MUST show the
* recipient's registered pubkey fingerprint before signing.
* - The unsigned tx is built client-side; signing is delegated to the
* wallet's keystore (foundation provides this seam).
*
* Real TFHE encryption requires `@l.x/fhe` (WASM bindings, not yet shipped).
* Until then we fall back to the gateway-side encrypt endpoint, which is
* NOT zero-knowledge: the gateway briefly sees plaintext. The UI surfaces
* this with a degraded-mode warning.
*/
import { useCallback, useState } from "react"
import { gatewayUrl } from "./brand"
import type { FHERecipient } from "./types"
interface FHEModule {
/** Encrypt a u64 amount under the recipient's TFHE public key. Returns base64. */
encryptAmount(amount: bigint, recipientPubkey: string): Promise<string>
}
/** Lazy import of `@l.x/fhe` if available. Falls back to gateway encrypt. */
async function loadFheModule(): Promise<FHEModule | null> {
try {
// Use a string variable so bundlers don't try to resolve at build time
// — the module isn't published yet. If/when it's published, swap this.
const moduleName = "@l.x/fhe"
const mod = (await import(/* @vite-ignore */ moduleName)) as Partial<FHEModule>
if (typeof mod.encryptAmount === "function") {
return mod as FHEModule
}
return null
} catch {
return null
}
}
async function fetchRecipient(address: string): Promise<FHERecipient> {
const res = await fetch(
gatewayUrl(`/v1/fhe/recipient/${encodeURIComponent(address)}`),
{ method: "GET", credentials: "include" },
)
if (!res.ok) {
throw new Error(`fetchRecipient: ${res.status} ${res.statusText}`)
}
return (await res.json()) as FHERecipient
}
async function gatewayEncrypt(amount: bigint, recipientPubkey: string): Promise<string> {
// Fallback path. Surfaces the degraded-mode warning in the UI.
const res = await fetch(gatewayUrl("/v1/fhe/encrypt"), {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ amount: amount.toString(), recipientPubkey }),
})
if (!res.ok) {
throw new Error(`gatewayEncrypt: ${res.status} ${res.statusText}`)
}
const j = (await res.json()) as { ciphertext: string }
return j.ciphertext
}
async function submitTransfer(args: {
fromAddress: string
recipient: FHERecipient
ciphertext: string
symbol: string
}): Promise<{ txHash: string }> {
const res = await fetch(gatewayUrl("/v1/fhe/transfer"), {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(args),
})
if (!res.ok) {
throw new Error(`submitTransfer: ${res.status} ${res.statusText}`)
}
return (await res.json()) as { txHash: string }
}
export interface FHETransferResult {
txHash: string
/** True if the gateway saw plaintext during encryption. */
degraded: boolean
}
export function useFHETransfer() {
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | undefined>()
const [result, setResult] = useState<FHETransferResult | undefined>()
const lookupRecipient = useCallback(async (address: string): Promise<FHERecipient> => {
return await fetchRecipient(address)
}, [])
const send = useCallback(
async (args: {
fromAddress: string
toAddress: string
symbol: string
amount: bigint
}): Promise<FHETransferResult | undefined> => {
setSubmitting(true)
setError(undefined)
setResult(undefined)
try {
const recipient = await fetchRecipient(args.toAddress)
if (!recipient.registered) {
throw new Error(`recipient ${args.toAddress} has no F-Chain pubkey registered`)
}
const fhe = await loadFheModule()
let ciphertext: string
let degraded = false
if (fhe) {
ciphertext = await fhe.encryptAmount(args.amount, recipient.pubkey)
} else {
ciphertext = await gatewayEncrypt(args.amount, recipient.pubkey)
degraded = true
}
const tx = await submitTransfer({
fromAddress: args.fromAddress,
recipient,
ciphertext,
symbol: args.symbol,
})
const r = { txHash: tx.txHash, degraded }
setResult(r)
return r
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
return undefined
} finally {
setSubmitting(false)
}
},
[],
)
return { send, lookupRecipient, submitting, error, result }
}
@@ -0,0 +1,137 @@
/**
* Z-Chain ZK proof generator hook.
*
* Threat model:
* - Adversary watching the gateway: must not learn the witness (the
* underlying data the user is proving facts about). When `@l.x/zk`
* ships we generate proofs entirely client-side (in a Worker) and
* the gateway only sees the proof + public inputs.
* - Adversary inspecting the proof QR: must not be able to reconstruct
* the witness. Proofs are zero-knowledge by definition, and we only
* ship the proof + public inputs — no witness leakage.
*
* Defense:
* - When `@l.x/zk` is present, all proving happens in a Worker — we
* never touch the witness on the main thread.
* - When falling back to the gateway prover, the UI surfaces the
* degraded-mode warning. This mode is acceptable for non-sensitive
* claims (AgeGT18 with KYC backing, etc.) but NOT for BalanceGT.
*
* Heavy circuits (Halo2 / Groth16) take seconds. UI shows a determinate
* progress bar driven by Worker postMessage events.
*/
import { useCallback, useState } from "react"
import { gatewayUrl } from "./brand"
import { confidentialStore } from "../../store/confidential"
import type { ClaimParams, ZKProof } from "./types"
interface ZKModule {
prove(claim: ClaimParams, witness: unknown): Promise<{
proof: string
publicInputs: string
circuit: string
}>
}
async function loadZkModule(): Promise<ZKModule | null> {
try {
const moduleName = "@l.x/zk"
const mod = (await import(/* @vite-ignore */ moduleName)) as Partial<ZKModule>
if (typeof mod.prove === "function") {
return mod as ZKModule
}
return null
} catch {
return null
}
}
async function gatewayProve(claim: ClaimParams, witness: unknown): Promise<{
proof: string
publicInputs: string
circuit: string
}> {
const res = await fetch(gatewayUrl("/v1/zkp/prove"), {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ claim, witness }),
})
if (!res.ok) {
throw new Error(`gatewayProve: ${res.status} ${res.statusText}`)
}
return (await res.json()) as { proof: string; publicInputs: string; circuit: string }
}
function newId(): string {
// Crypto-strong UUID-ish identifier without pulling a dep.
const g = globalThis as { crypto?: Crypto }
if (g.crypto?.randomUUID) {
return g.crypto.randomUUID()
}
const bytes = new Uint8Array(16)
if (g.crypto?.getRandomValues) {
g.crypto.getRandomValues(bytes)
}
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")
}
export interface ZKProveResult {
proof: ZKProof
/** True if the proof was generated by the gateway (witness left the device). */
degraded: boolean
}
export function useZKProof() {
const [generating, setGenerating] = useState(false)
const [error, setError] = useState<string | undefined>()
const [progress, setProgress] = useState<number>(0) // 0..100
const prove = useCallback(
async (claim: ClaimParams, witness: unknown): Promise<ZKProveResult | undefined> => {
setGenerating(true)
setError(undefined)
setProgress(0)
try {
const zk = await loadZkModule()
let raw: { proof: string; publicInputs: string; circuit: string }
let degraded = false
if (zk) {
// Tick simulated progress while the worker churns; the real
// module will replace this with onProgress events when shipped.
const ticker = setInterval(() => {
setProgress((p) => Math.min(95, p + 5))
}, 250)
try {
raw = await zk.prove(claim, witness)
} finally {
clearInterval(ticker)
}
} else {
raw = await gatewayProve(claim, witness)
degraded = true
}
setProgress(100)
const proof: ZKProof = {
id: newId(),
claim,
proof: raw.proof,
publicInputs: raw.publicInputs,
circuit: raw.circuit,
generatedAt: Date.now(),
}
confidentialStore.addProof(proof)
return { proof, degraded }
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
return undefined
} finally {
setGenerating(false)
}
},
[],
)
return { prove, generating, error, progress }
}
+109
View File
@@ -0,0 +1,109 @@
/**
* Confidential store — minimal in-memory store for revealed balances and active proofs.
*
* Deliberately framework-free (no Zustand/Redux) so this slice does not bind the
* Foundation team to a state library. When Foundation lands a global store, this
* module is the seam to migrate.
*
* Keys:
* revealedBalances: `${chainId}:${address}:${symbol}` → RevealedBalance
* activeProofs: `${id}` → ZKProof
*/
import type { RevealedBalance, ZKProof } from "../screens/confidential/types"
type RevealKey = string
type ProofKey = string
interface ConfidentialState {
revealedBalances: Map<RevealKey, RevealedBalance>
activeProofs: Map<ProofKey, ZKProof>
}
const state: ConfidentialState = {
revealedBalances: new Map(),
activeProofs: new Map(),
}
const subscribers = new Set<() => void>()
function notify() {
for (const fn of subscribers) fn()
}
export function revealKey(chainId: number, address: string, symbol: string): RevealKey {
return `${chainId}:${address.toLowerCase()}:${symbol.toUpperCase()}`
}
export const confidentialStore = {
/** Subscribe to store changes. Returns unsubscribe. */
subscribe(fn: () => void): () => void {
subscribers.add(fn)
return () => {
subscribers.delete(fn)
}
},
/** Snapshot — return current state (do not mutate). */
getSnapshot(): ConfidentialState {
return state
},
/** Stash a freshly decrypted balance with auto-rehide TTL. */
setRevealedBalance(
chainId: number,
address: string,
symbol: string,
revealed: RevealedBalance,
): void {
state.revealedBalances.set(revealKey(chainId, address, symbol), revealed)
notify()
},
/** Get revealed balance if not expired; otherwise evict and return undefined. */
getRevealedBalance(
chainId: number,
address: string,
symbol: string,
): RevealedBalance | undefined {
const key = revealKey(chainId, address, symbol)
const r = state.revealedBalances.get(key)
if (!r) return undefined
if (Date.now() >= r.expiresAt) {
state.revealedBalances.delete(key)
notify()
return undefined
}
return r
},
/** Force re-hide of a balance. */
hideBalance(chainId: number, address: string, symbol: string): void {
if (state.revealedBalances.delete(revealKey(chainId, address, symbol))) {
notify()
}
},
/** Persist a freshly-generated ZK proof. */
addProof(p: ZKProof): void {
state.activeProofs.set(p.id, p)
notify()
},
/** Retrieve a proof by id. */
getProof(id: string): ZKProof | undefined {
return state.activeProofs.get(id)
},
/** Drop a proof (user-initiated revoke / cleanup). */
removeProof(id: string): void {
if (state.activeProofs.delete(id)) notify()
},
/** All active proofs, freshest first. */
listProofs(): ZKProof[] {
return Array.from(state.activeProofs.values()).sort(
(a, b) => b.generatedAt - a.generatedAt,
)
},
}
+26 -412
View File
@@ -823,12 +823,6 @@ importers:
apps/web:
dependencies:
'@walletconnect/sign-client':
specifier: ^2.21.0
version: 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
'@walletconnect/utils':
specifier: ^2.21.0
version: 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.3.6)
react:
specifier: 19.2.5
version: 19.2.5
@@ -836,8 +830,8 @@ importers:
specifier: 19.2.5
version: 19.2.5(react@19.2.5)
react-router-dom:
specifier: ^7.5.0
version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
specifier: 7.5.0
version: 7.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
devDependencies:
'@types/react':
specifier: ^19.0.0
@@ -7058,6 +7052,9 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/datadog-metrics@0.6.1':
resolution: {integrity: sha512-p6zVpfmNcXwtcXjgpz7do/fKyfndGhU5sGJVtb5Gn5PvLDiQUAgD0mI/itf/99sBi9DRxeyhFQ9dQF6OxxQNbA==}
@@ -14229,15 +14226,15 @@ packages:
'@types/react':
optional: true
react-router-dom@7.14.2:
resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==}
react-router-dom@7.5.0:
resolution: {integrity: sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.14.2:
resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==}
react-router@7.5.0:
resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -15605,6 +15602,9 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
turbo@2.9.6:
resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==}
hasBin: true
@@ -17591,45 +17591,21 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
@@ -17665,34 +17641,16 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
@@ -17703,89 +17661,41 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
optional: true
'@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
@@ -33346,12 +33256,6 @@ snapshots:
merge-options: 3.0.4
react-native: '@hanzogui/react-native@0.79.5-fork.1(@babel/core@7.26.0)(@hanzogui/react-native@0.79.5-fork.1)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)'
'@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))':
dependencies:
merge-options: 3.0.4
react-native: 0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)
optional: true
'@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1)':
dependencies:
merge-options: 3.0.4
@@ -33797,16 +33701,6 @@ snapshots:
nullthrows: 1.1.1
yargs: 17.7.2
'@react-native/codegen@0.79.5(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
glob: 7.2.3
hermes-parser: 0.25.1
invariant: 2.2.4
nullthrows: 1.1.1
yargs: 17.7.2
optional: true
'@react-native/codegen@0.79.6(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
@@ -33925,16 +33819,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
'@react-native/virtualized-lists@0.79.5(@types/react@19.0.10)(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))(react@19.2.5)':
dependencies:
invariant: 2.2.4
nullthrows: 1.1.1
react: 19.2.5
react-native: 0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)
optionalDependencies:
'@types/react': 19.0.10
optional: true
'@react-navigation/bottom-tabs@6.6.1(@hanzogui/react-native@0.79.5-fork.1)(@react-navigation/native@7.1.9(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(react-native-safe-area-context@5.4.0(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(react-native-screens@4.11.1(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(react@19.2.5)':
dependencies:
'@react-navigation/elements': 1.3.31(@hanzogui/react-native@0.79.5-fork.1)(@react-navigation/native@7.1.9(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(react-native-safe-area-context@5.4.0(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(react@19.2.5)
@@ -34540,7 +34424,7 @@ snapshots:
'@scure/bip32@1.7.0':
dependencies:
'@noble/curves': 1.9.7
'@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
@@ -35790,6 +35674,8 @@ snapshots:
dependencies:
'@types/node': 22.13.1
'@types/cookie@0.6.0': {}
'@types/datadog-metrics@0.6.1': {}
'@types/debug@4.1.13':
@@ -36708,50 +36594,6 @@ snapshots:
- utf-8-validate
- zod
'@walletconnect/core@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)':
dependencies:
'@walletconnect/heartbeat': 1.2.2
'@walletconnect/jsonrpc-provider': 1.0.14
'@walletconnect/jsonrpc-types': 1.0.4
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)
'@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/logger': 3.0.0
'@walletconnect/relay-api': 1.0.11
'@walletconnect/relay-auth': 1.1.0
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.3.6)
'@walletconnect/window-getters': 1.0.1
es-toolkit: 1.39.3
events: 3.3.0
uint8arrays: 3.1.1
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@react-native-async-storage/async-storage'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- bufferutil
- db0
- ioredis
- typescript
- uploadthing
- utf-8-validate
- zod
'@walletconnect/core@2.23.0(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)':
dependencies:
'@walletconnect/heartbeat': 1.2.2
@@ -36888,33 +36730,6 @@ snapshots:
- bufferutil
- utf-8-validate
'@walletconnect/keyvaluestorage@1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))':
dependencies:
'@walletconnect/safe-json': 1.0.2
idb-keyval: 6.2.1
unstorage: 1.17.5(idb-keyval@6.2.1)
optionalDependencies:
'@react-native-async-storage/async-storage': 1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- ioredis
- uploadthing
'@walletconnect/keyvaluestorage@1.1.1(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))':
dependencies:
'@walletconnect/safe-json': 1.0.2
@@ -37050,42 +36865,6 @@ snapshots:
- utf-8-validate
- zod
'@walletconnect/sign-client@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)':
dependencies:
'@walletconnect/core': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.2
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 3.0.0
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.3.6)
events: 3.3.0
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@react-native-async-storage/async-storage'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- bufferutil
- db0
- ioredis
- typescript
- uploadthing
- utf-8-validate
- zod
'@walletconnect/sign-client@2.23.0(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)':
dependencies:
'@walletconnect/core': 2.23.0(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
@@ -37184,35 +36963,6 @@ snapshots:
- ioredis
- uploadthing
'@walletconnect/types@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))':
dependencies:
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.2
'@walletconnect/jsonrpc-types': 1.0.4
'@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/logger': 3.0.0
events: 3.3.0
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@react-native-async-storage/async-storage'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- ioredis
- uploadthing
'@walletconnect/types@2.23.0(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))':
dependencies:
'@walletconnect/events': 1.0.1
@@ -37410,51 +37160,6 @@ snapshots:
- utf-8-validate
- zod
'@walletconnect/utils@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.3.6)':
dependencies:
'@msgpack/msgpack': 3.1.2
'@noble/ciphers': 1.3.0
'@noble/curves': 1.9.7
'@noble/hashes': 1.8.0
'@scure/base': 1.2.6
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/logger': 3.0.0
'@walletconnect/relay-api': 1.0.11
'@walletconnect/relay-auth': 1.1.0
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10)))
'@walletconnect/window-getters': 1.0.1
'@walletconnect/window-metadata': 1.0.1
blakejs: 1.2.1
bs58: 6.0.0
detect-browser: 5.3.0
ox: 0.9.3(typescript@5.9.3)(zod@4.3.6)
uint8arrays: 3.1.1
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@react-native-async-storage/async-storage'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- ioredis
- typescript
- uploadthing
- zod
'@walletconnect/utils@2.23.0(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(typescript@5.9.3)(zod@4.3.6)':
dependencies:
'@msgpack/msgpack': 3.1.2
@@ -37679,11 +37384,6 @@ snapshots:
typescript: 5.9.3
zod: '@hanzogui/zod@4.3.6-fork.1'
abitype@1.2.3(typescript@5.9.3)(zod@3.22.4):
optionalDependencies:
typescript: 5.9.3
zod: 3.22.4
abitype@1.2.3(typescript@5.9.3)(zod@4.3.6):
optionalDependencies:
typescript: 5.9.3
@@ -38075,20 +37775,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
babel-jest@29.7.0(@babel/core@7.29.0):
dependencies:
'@babel/core': 7.29.0
'@jest/transform': 29.7.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.29.0)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
transitivePeerDependencies:
- supports-color
optional: true
babel-literal-to-ast@2.1.0(@babel/core@7.26.0):
dependencies:
'@babel/core': 7.26.0
@@ -38208,26 +37894,6 @@ snapshots:
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0)
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
optional: true
babel-preset-expo@13.0.0(@babel/core@7.26.0):
dependencies:
'@babel/helper-module-imports': 7.28.6
@@ -38288,13 +37954,6 @@ snapshots:
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.26.0)
babel-preset-jest@29.6.3(@babel/core@7.29.0):
dependencies:
'@babel/core': 7.29.0
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
optional: true
backo2@1.0.2: {}
balanced-match@1.0.2: {}
@@ -44170,11 +43829,11 @@ snapshots:
dependencies:
'@adraffy/ens-normalize': 1.11.1
'@noble/ciphers': 1.3.0
'@noble/curves': 1.9.7
'@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/bip32': 1.7.0
'@scure/bip39': 1.6.0
abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4)
abitype: 1.0.8(typescript@5.9.3)(zod@3.22.4)
eventemitter3: 5.0.1
optionalDependencies:
typescript: 5.9.3
@@ -44185,11 +43844,11 @@ snapshots:
dependencies:
'@adraffy/ens-normalize': 1.11.1
'@noble/ciphers': 1.3.0
'@noble/curves': 1.9.7
'@noble/curves': 1.9.1
'@noble/hashes': 1.8.0
'@scure/bip32': 1.7.0
'@scure/bip39': 1.6.0
abitype: 1.2.3(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
abitype: 1.0.8(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
eventemitter3: 5.0.1
optionalDependencies:
typescript: 5.9.3
@@ -45445,55 +45104,6 @@ snapshots:
- supports-color
- utf-8-validate
react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10):
dependencies:
'@jest/create-cache-key-function': 29.7.0
'@react-native/assets-registry': 0.79.5
'@react-native/codegen': 0.79.5(@babel/core@7.29.0)
'@react-native/community-cli-plugin': 0.79.5(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.1.0)(utf-8-validate@5.0.10)
'@react-native/gradle-plugin': 0.79.5
'@react-native/js-polyfills': 0.79.5
'@react-native/normalize-colors': 0.79.5
'@react-native/virtualized-lists': 0.79.5(@types/react@19.0.10)(react-native@0.79.5(@babel/core@7.29.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))(react@19.2.5)
abort-controller: 3.0.0
anser: 1.4.10
ansi-regex: 5.0.1
babel-jest: 29.7.0(@babel/core@7.29.0)
babel-plugin-syntax-hermes-parser: 0.25.1
base64-js: 1.5.1
chalk: 4.1.2
commander: 12.1.0
event-target-shim: 5.0.1
flow-enums-runtime: 0.0.6
glob: 7.2.3
invariant: 2.2.4
jest-environment-node: 29.7.0
memoize-one: 5.2.1
metro-runtime: 0.82.5
metro-source-map: 0.82.5
nullthrows: 1.1.1
pretty-format: 29.7.0
promise: 8.3.0
react: 19.2.5
react-devtools-core: 6.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10)
react-refresh: 0.14.0
regenerator-runtime: 0.13.11
scheduler: 0.25.0
semver: 7.7.4
stacktrace-parser: 0.1.11
whatwg-fetch: 3.6.20
ws: 6.2.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)
yargs: 17.7.2
optionalDependencies:
'@types/react': 19.0.10
transitivePeerDependencies:
- '@babel/core'
- '@react-native-community/cli'
- bufferutil
- supports-color
- utf-8-validate
optional: true
react-qr-code@2.0.12(react-native-svg@15.13.0(react-native@0.79.5(@babel/core@7.26.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))(react@19.2.5))(react@19.2.5):
dependencies:
prop-types: 15.8.1
@@ -45563,17 +45173,19 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
react-router-dom@7.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-router: 7.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
react-router@7.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@types/cookie': 0.6.0
cookie: 1.1.1
react: 19.2.5
set-cookie-parser: 2.7.2
turbo-stream: 2.4.0
optionalDependencies:
react-dom: 19.2.5(react@19.2.5)
@@ -47184,6 +46796,8 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
turbo-stream@2.4.0: {}
turbo@2.9.6:
optionalDependencies:
'@turbo/darwin-64': 2.9.6