mirror of
https://github.com/luxfi/wallet.git
synced 2026-07-27 03:37:41 +00:00
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:
+2
-10
@@ -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}`
|
||||
}
|
||||
@@ -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 />} />,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'")
|
||||
}
|
||||
@@ -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 = "🔓"
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
}
|
||||
Generated
+26
-412
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user