diff --git a/Makefile b/Makefile index ee5b47a..3191870 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ include .env export ELECTROBUN_DEVELOPER_ID ELECTROBUN_TEAMID ELECTROBUN_APPLEID ELECTROBUN_APPLEIDPASS endif -.PHONY: install dev dev-hmr build build-stable build-canary build-signed prune-bundle dmg clean help vault sign-check verify publish release upload-dmg submodules modules-install modules-build modules-clean audit +.PHONY: install dev dev-hmr build build-stable build-canary build-signed prune-bundle dmg clean help vault sign-check verify publish release upload-dmg submodules modules-install modules-build modules-clean audit build-zcash-cli build-zcash-cli-debug # --- Submodules (auto-init on fresh worktrees/clones) --- @@ -29,6 +29,14 @@ modules-clean: cd modules/proto-tx-builder && rm -rf dist node_modules cd modules/hdwallet && yarn clean 2>/dev/null || (rm -rf packages/*/dist node_modules) +# --- Zcash CLI Sidecar (Rust) --- + +build-zcash-cli: + cd $(PROJECT_DIR)/zcash-cli && cargo build --release + +build-zcash-cli-debug: + cd $(PROJECT_DIR)/zcash-cli && cargo build + # --- Vault --- install: modules-build @@ -80,7 +88,7 @@ dmg: echo "Verifying extracted app..."; \ codesign --verify --deep --strict "$$APP" || (echo "ERROR: codesign verification failed"; exit 1); \ ln -s /Applications "$$STAGING/Applications"; \ - DMG_OUT="$(PROJECT_DIR)/artifacts/$(DMG_NAME)"; \ + DMG_OUT="$$(pwd)/$(PROJECT_DIR)/artifacts/$(DMG_NAME)"; \ rm -f "$$DMG_OUT"; \ echo "Creating DMG..."; \ hdiutil create -volname "KeepKey Vault" -srcfolder "$$STAGING" -ov -format UDZO "$$DMG_OUT"; \ @@ -96,7 +104,7 @@ dmg: echo "DMG ready: $$DMG_OUT" clean: modules-clean - cd $(PROJECT_DIR) && rm -rf dist node_modules build artifacts + cd $(PROJECT_DIR) && rm -rf dist node_modules build _build artifacts # --- Audit & SBOM --- @@ -118,8 +126,8 @@ sign-check: @security find-identity -v -p codesigning | grep "$$ELECTROBUN_DEVELOPER_ID" || echo "WARNING: Certificate not found in keychain" verify: - @APP=$$(find $(PROJECT_DIR)/build -name "*.app" -maxdepth 2 | head -1); \ - if [ -z "$$APP" ]; then echo "No .app bundle found in build/"; exit 1; fi; \ + @APP=$$(find $(PROJECT_DIR)/_build -name "*.app" -maxdepth 2 | head -1); \ + if [ -z "$$APP" ]; then echo "No .app bundle found in _build/"; exit 1; fi; \ echo "Verifying: $$APP"; \ echo "--- codesign ---"; \ codesign --verify --deep --strict "$$APP" && echo "codesign: PASS" || echo "codesign: FAIL"; \ @@ -184,6 +192,8 @@ help: @echo " make dmg - Create DMG from existing build artifacts" @echo " make modules-build - Build hdwallet + proto-tx-builder from source" @echo " make modules-clean - Clean module build artifacts" + @echo " make build-zcash-cli - Build Zcash CLI sidecar (release)" + @echo " make build-zcash-cli-debug - Build Zcash CLI sidecar (debug)" @echo " make audit - Generate dependency manifest + SBOM" @echo " make sign-check - Verify signing env vars are configured" @echo " make verify - Verify .app bundle signature + Gatekeeper" diff --git a/modules/device-protocol b/modules/device-protocol index ce10ea7..1b4db72 160000 --- a/modules/device-protocol +++ b/modules/device-protocol @@ -1 +1 @@ -Subproject commit ce10ea79a000f2e20e87fbbab3a0c4f7a07f6f0e +Subproject commit 1b4db72e4ad4d0dc619fe8ad2824cdd4a2698d3f diff --git a/modules/hdwallet b/modules/hdwallet index 15b6b23..9d6ed95 160000 --- a/modules/hdwallet +++ b/modules/hdwallet @@ -1 +1 @@ -Subproject commit 15b6b23e4dd96b780b71a2382dde30bc369968ab +Subproject commit 9d6ed95ea30f2dbdb2a08adf605982fdf0c99ea1 diff --git a/projects/keepkey-vault/electrobun.config.ts b/projects/keepkey-vault/electrobun.config.ts index d2954be..a04dce2 100644 --- a/projects/keepkey-vault/electrobun.config.ts +++ b/projects/keepkey-vault/electrobun.config.ts @@ -4,10 +4,11 @@ export default { app: { name: "keepkey-vault", identifier: "com.keepkey.vault", - version: "1.1.1", + version: "1.1.2", urlSchemes: ["keepkey"], }, build: { + buildFolder: "_build", bun: { // Mark native addons and protobuf-dependent packages as external // so Bun loads them at runtime instead of bundling them. @@ -29,7 +30,7 @@ export default { copy: { "dist/index.html": "views/mainview/index.html", "dist/assets": "views/mainview/assets", - "build/_ext_modules": "node_modules", + "_build/_ext_modules": "node_modules", }, mac: { bundleCEF: false, diff --git a/projects/keepkey-vault/package.json b/projects/keepkey-vault/package.json index cf9f6c1..f14e9c2 100644 --- a/projects/keepkey-vault/package.json +++ b/projects/keepkey-vault/package.json @@ -1,6 +1,6 @@ { "name": "keepkey-vault", - "version": "1.1.1", + "version": "1.1.2", "description": "KeepKey Vault - Desktop hardware wallet management powered by Electrobun", "scripts": { "dev": "vite build && bun scripts/collect-externals.ts && electrobun build && electrobun dev", @@ -16,6 +16,7 @@ "dependencies": { "@chakra-ui/react": "^3.33.0", "@emotion/react": "^11.14.0", + "@keepkey/device-protocol": "file:../../modules/device-protocol", "@keepkey/hdwallet-core": "file:../../modules/hdwallet/packages/hdwallet-core", "@keepkey/hdwallet-keepkey": "file:../../modules/hdwallet/packages/hdwallet-keepkey", "@keepkey/hdwallet-keepkey-nodehid": "file:../../modules/hdwallet/packages/hdwallet-keepkey-nodehid", @@ -54,6 +55,7 @@ "tiny-secp256k1" ], "overrides": { + "@keepkey/device-protocol": "file:../../modules/device-protocol", "@keepkey/proto-tx-builder": "file:../../modules/proto-tx-builder", "@keepkey/hdwallet-core": "file:../../modules/hdwallet/packages/hdwallet-core", "@keepkey/hdwallet-keepkey": "file:../../modules/hdwallet/packages/hdwallet-keepkey", diff --git a/projects/keepkey-vault/scripts/collect-externals.ts b/projects/keepkey-vault/scripts/collect-externals.ts index 0af4e76..1bc47f4 100644 --- a/projects/keepkey-vault/scripts/collect-externals.ts +++ b/projects/keepkey-vault/scripts/collect-externals.ts @@ -23,7 +23,7 @@ const EXTERNALS = [ const projectRoot = join(import.meta.dir, '..') const nmSource = join(projectRoot, 'node_modules') -const nmDest = join(projectRoot, 'build', '_ext_modules') +const nmDest = join(projectRoot, '_build', '_ext_modules') // Resolve file: linked packages to their actual source directories. // Bun's file: resolution can leave broken stubs in node_modules (empty dir with only node_modules/). @@ -621,6 +621,24 @@ if (DEVELOPER_ID) { console.log(`[collect-externals] ELECTROBUN_DEVELOPER_ID not set, skipping native binary signing`) } +// Remove dangling symlinks (left behind after pruning/stripping) +function removeDanglingSymlinks(dirPath: string) { + try { + const entries = readdirSync(dirPath, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = join(dirPath, entry.name) + if (entry.isSymbolicLink()) { + try { statSync(fullPath) } catch { + rmSync(fullPath) + } + } else if (entry.isDirectory()) { + removeDanglingSymlinks(fullPath) + } + } + } catch {} +} +removeDanglingSymlinks(nmDest) + // Report final size const { stdout } = Bun.spawnSync(['du', '-sh', nmDest]) console.log(`[collect-externals] Final size: ${stdout.toString().trim().split('\t')[0]}`) diff --git a/projects/keepkey-vault/src/bun/engine-controller.ts b/projects/keepkey-vault/src/bun/engine-controller.ts index e216a1f..a752553 100644 --- a/projects/keepkey-vault/src/bun/engine-controller.ts +++ b/projects/keepkey-vault/src/bun/engine-controller.ts @@ -235,6 +235,16 @@ export class EngineController extends EventEmitter { }) }, delay) } + + // Same for needs_passphrase — device has passphrase protection but PIN is + // already cached (or disabled). We must call promptPin() → getPublicKeys() + // so the device sends PASSPHRASE_REQUEST; without it, the UI overlay shows + // but sendPassphrase() has no pending device request to respond to. + if (state === 'needs_passphrase' && !this.promptPinActive) { + this.promptPin().catch(err => { + console.warn('[Engine] Auto prompt-passphrase failed:', err?.message) + }) + } } // ── Firmware Manifest ────────────────────────────────────────────────── @@ -419,11 +429,12 @@ export class EngineController extends EventEmitter { this.rebootPollCount++ if (this.rebootPollCount >= EngineController.MAX_REBOOT_POLLS) { console.warn('[Engine] Reboot poll: max attempts reached (5 min), stopping') - this.updatePhase = null + this.updatePhase = 'idle' this.stopRebootPoll() + this.updateState('disconnected') return } - console.log(`[Engine] Reboot poll ${this.rebootPollCount}/${EngineController.MAX_REBOOT_POLLS}: calling syncState()`) + if (this.rebootPollCount % 6 === 0) console.log(`[Engine] Reboot poll ${this.rebootPollCount}/${EngineController.MAX_REBOOT_POLLS}: calling syncState()`) this.syncState() }, 5000) } @@ -881,7 +892,11 @@ export class EngineController extends EventEmitter { if (opts.autoLockDelayMs !== undefined) settings.autoLockDelayMs = opts.autoLockDelayMs await this.wallet.applySettings(settings) this.cachedFeatures = await this.wallet.getFeatures() - this.emit('state-change', this.getDeviceState()) + // Route through updateState so needs_passphrase triggers promptPin() → + // getPublicKeys() → PASSPHRASE_REQUEST. Previously this emitted directly, + // so enabling passphrase from settings showed the overlay but the device + // never received the passphrase (no pending request to respond to). + this.updateState(this.deriveState(this.cachedFeatures)) } async changePin() { diff --git a/projects/keepkey-vault/src/bun/index.ts b/projects/keepkey-vault/src/bun/index.ts index 9b6cd8a..cf7ab88 100644 --- a/projects/keepkey-vault/src/bun/index.ts +++ b/projects/keepkey-vault/src/bun/index.ts @@ -13,7 +13,9 @@ import { startRestApi, type RestApiCallbacks } from "./rest-api" import { AuthStore } from "./auth" import { getPioneer, getPioneerApiBase, resetPioneer } from "./pioneer" import { buildTx, broadcastTx } from "./txbuilder" -import { CHAINS, customChainToChainDef } from "../shared/chains" +import { initializeOrchardFromDevice, scanOrchardNotes, getShieldedBalance, sendShielded } from "./txbuilder/zcash-shielded" +import { isSidecarReady, startSidecar, stopSidecar, hasFvkLoaded, getCachedFvk, setCachedFvk, onScanProgress } from "./zcash-sidecar" +import { CHAINS, customChainToChainDef, isChainSupported } from "../shared/chains" import type { ChainDef } from "../shared/chains" import { BtcAccountManager } from "./btc-accounts" import { EvmAddressManager, evmAddressPath } from "./evm-addresses" @@ -250,11 +252,13 @@ const rpc = BrowserView.defineRPC({ analyzeFirmware: async (params) => { if (params.data.length > 10_000_000) throw new Error('Firmware data too large (max ~7.5MB)') const buf = Buffer.from(params.data, 'base64') + if (buf.length > 7_500_000) throw new Error('Decoded firmware exceeds 7.5MB limit') return engine.analyzeFirmware(buf) }, flashCustomFirmware: async (params) => { if (params.data.length > 10_000_000) throw new Error('Firmware data too large (max ~7.5MB)') const buf = Buffer.from(params.data, 'base64') + if (buf.length > 7_500_000) throw new Error('Decoded firmware exceeds 7.5MB limit') await engine.flashCustomFirmware(buf) }, resetDevice: async (params) => { await engine.resetDevice(params) }, @@ -1138,6 +1142,57 @@ const rpc = BrowserView.defineRPC({ return Object.fromEntries(map) }, + // ── Zcash Shielded (Orchard) ──────────────────────────── + zcashShieldedStatus: async () => { + const zcashDef = CHAINS.find(c => c.id === 'zcash-shielded') + if (!zcashDef || !isChainSupported(zcashDef, engine.state?.firmwareVersion)) { + return { ready: false, fvk_loaded: false, address: null, fvk: null } + } + // Lazy-start sidecar on first status check + if (!isSidecarReady()) { + try { await startSidecar() } catch { /* binary not found — will show not_running */ } + } + const cached = getCachedFvk() + return { + ready: isSidecarReady(), + fvk_loaded: hasFvkLoaded(), + address: cached?.address ?? null, + fvk: cached?.fvk ?? null, + } + }, + zcashShieldedInit: async (params) => { + const zcashDef = CHAINS.find(c => c.id === 'zcash-shielded') + if (!zcashDef || !isChainSupported(zcashDef, engine.state?.firmwareVersion)) { + throw new Error('Zcash shielded requires firmware >= 7.11.0') + } + // If FVK is already loaded from DB, return it immediately + const cached = getCachedFvk() + if (cached) return cached + // Otherwise get from device + if (!engine.wallet) throw new Error('No device connected') + const result = await initializeOrchardFromDevice(engine.wallet as any, params?.account ?? 0) + setCachedFvk(result.address, result.fvk) + return result + }, + zcashShieldedScan: async (params) => { + return await scanOrchardNotes(params?.startHeight, params?.fullRescan) + }, + zcashShieldedBalance: async () => { + return await getShieldedBalance() + }, + zcashShieldedSend: async (params) => { + if (!engine.wallet) throw new Error('No device connected') + const zcashDef = CHAINS.find(c => c.id === 'zcash-shielded') + if (!zcashDef || !isChainSupported(zcashDef, engine.state?.firmwareVersion)) { + throw new Error('Zcash shielded requires firmware >= 7.11.0') + } + return await sendShielded(engine.wallet as any, { + recipient: params.recipient, + amount: params.amount, + memo: params.memo, + }) + }, + // ── Camera / QR scanning ───────────────────────────────── startQrScan: async () => { startCamera( @@ -1450,6 +1505,9 @@ engine.on('state-change', (state) => { engine.on('firmware-progress', (progress) => { try { rpc.send['firmware-progress'](progress) } catch { /* webview not ready yet */ } }) +onScanProgress((progress) => { + try { rpc.send['scan-progress'](progress) } catch { /* webview not ready yet */ } +}) engine.on('pin-request', (req) => { try { rpc.send['pin-request'](req) } catch { /* webview not ready yet */ } }) @@ -1629,6 +1687,8 @@ if (process.platform === 'win32') { // Start engine (USB event listeners + initial device sync) await engine.start() +// Zcash sidecar is started lazily on first zcash RPC call (requires firmware >= 7.11.0) + // Cache app version for REST health endpoint Updater.localInfo.version().then(v => { appVersionCache = v }).catch(() => {}) @@ -1666,6 +1726,7 @@ function cleanupAndQuit() { if (quitting) return quitting = true stopCamera() + stopSidecar() engine.stop() restServer?.stop() Utils.quit() diff --git a/projects/keepkey-vault/src/bun/reports.ts b/projects/keepkey-vault/src/bun/reports.ts index a0161c6..11d9686 100644 --- a/projects/keepkey-vault/src/bun/reports.ts +++ b/projects/keepkey-vault/src/bun/reports.ts @@ -17,6 +17,24 @@ import { getPioneerApiBase } from './pioneer' const REPORT_TIMEOUT_MS = 60_000 +/** Section title prefixes — shared with tax-export.ts for reliable extraction. */ +export const SECTION_TITLES = { + TX_DETAILS: 'Transaction Details', + TX_HISTORY: 'Transaction History', +} as const + +/** Safely round a satoshi string/number to integer, guarding against values beyond Number.MAX_SAFE_INTEGER. */ +function safeRoundSats(value: unknown): number { + if (value === undefined || value === null) return 0 + const n = Number(value) + if (!Number.isFinite(n)) return 0 + if (Math.abs(n) > Number.MAX_SAFE_INTEGER) { + console.warn(`[Report] safeRoundSats: value ${value} exceeds MAX_SAFE_INTEGER, clamping`) + return n > 0 ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER + } + return Math.round(n) +} + function getPioneerQueryKey(): string { return process.env.PIONEER_API_KEY || `key:public-${Date.now()}` } @@ -45,7 +63,12 @@ async function fetchPubkeyInfo(baseUrl: string, xpub: string): Promise { ) if (!resp.ok) throw new Error(`PubkeyInfo ${resp.status}`) const json = await resp.json() - return json.data || json + const result = json.data || json + if (typeof result !== 'object' || result === null) { + console.warn('[Report] fetchPubkeyInfo: unexpected response shape, returning empty object') + return {} + } + return result } async function fetchTxHistory(baseUrl: string, xpub: string, caip: string): Promise { @@ -60,6 +83,10 @@ async function fetchTxHistory(baseUrl: string, xpub: string, caip: string): Prom ) if (!resp.ok) throw new Error(`TxHistory ${resp.status}`) const json = await resp.json() + if (typeof json !== 'object' || json === null) { + console.warn('[Report] fetchTxHistory: unexpected response shape, returning empty array') + return [] + } const histories = json.histories || json.data?.histories || [] return histories[0]?.transactions || [] } @@ -257,9 +284,9 @@ async function buildBtcSections( xpub: x.xpub, scriptType: x.scriptType, label: `${x.scriptType}`, - balance: Math.round(Number(info.balance || 0)), - totalReceived: Math.round(Number(info.totalReceived || 0)), - totalSent: Math.round(Number(info.totalSent || 0)), + balance: safeRoundSats(info.balance), + totalReceived: safeRoundSats(info.totalReceived), + totalSent: safeRoundSats(info.totalSent), txCount: info.txs || 0, usedAddresses: used.map((t: any) => ({ name: t.name, path: t.path, transfers: t.transfers, @@ -380,7 +407,7 @@ async function buildBtcSections( }) sections.push({ - title: `Transaction History (${allTxs.length})`, + title: `${SECTION_TITLES.TX_HISTORY} (${allTxs.length})`, type: 'table', data: { headers: ['#', 'Dir', 'TXID', 'Block', 'Date', 'Value (BTC)', 'Fee (BTC)'], @@ -412,7 +439,7 @@ async function buildBtcSections( const detailTxs = allTxs.slice(0, MAX_TX_DETAILS) if (detailTxs.length > 0) { sections.push({ - title: `Transaction Details (${Math.min(allTxs.length, MAX_TX_DETAILS)}${allTxs.length > MAX_TX_DETAILS ? ` of ${allTxs.length}` : ''})`, + title: `${SECTION_TITLES.TX_DETAILS} (${Math.min(allTxs.length, MAX_TX_DETAILS)}${allTxs.length > MAX_TX_DETAILS ? ` of ${allTxs.length}` : ''})`, type: 'table', data: { headers: ['TXID', 'Dir', 'Block', 'Date', 'Value (BTC)', 'Fee (BTC)', 'From', 'To'], @@ -440,7 +467,7 @@ async function buildBtcSections( sections.push({ title: 'Note', type: 'text', data: `Showing ${MAX_TX_DETAILS} of ${allTxs.length} transactions.` }) } } else { - sections.push({ title: 'Transaction History', type: 'text', data: 'No transactions found' }) + sections.push({ title: SECTION_TITLES.TX_HISTORY, type: 'text', data: 'No transactions found' }) } // 3. Address flow analysis from tx data diff --git a/projects/keepkey-vault/src/bun/rest-api.ts b/projects/keepkey-vault/src/bun/rest-api.ts index 3762eb5..0412b00 100644 --- a/projects/keepkey-vault/src/bun/rest-api.ts +++ b/projects/keepkey-vault/src/bun/rest-api.ts @@ -3,7 +3,12 @@ import type { AuthStore } from './auth' import { HttpError } from './auth' import type { SigningRequestInfo, ApiLogEntry, EIP712DecodedInfo } from '../shared/types' import { decodeEIP712 } from './eip712-decoder' -import { CHAINS } from '../shared/chains' +import { CHAINS, isChainSupported } from '../shared/chains' +import { + initializeOrchardFromDevice, scanOrchardNotes, getShieldedBalance, + buildShieldedTx, finalizeShieldedTx, broadcastShieldedTx, +} from './txbuilder/zcash-shielded' +import { isSidecarReady } from './zcash-sidecar' import { readFileSync } from 'fs' import { join } from 'path' import * as S from './schemas' @@ -888,6 +893,10 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 outputs: body.outputs, version: body.version ?? 1, locktime: body.locktime ?? 0, + ...(body.overwintered !== undefined ? { overwintered: body.overwintered } : {}), + ...(body.expiry !== undefined ? { expiry: body.expiry } : {}), + ...(body.versionGroupId !== undefined ? { versionGroupId: body.versionGroupId } : {}), + ...(body.branchId !== undefined ? { branchId: body.branchId } : {}), }) return json(validateResponse(result, S.UtxoSignTransactionResponse, path)) } @@ -1447,6 +1456,77 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 return json({ success: true }) } + // ── Zcash Shielded (Orchard) ──────────────────────────────── + + const zcashShieldedDef = CHAINS.find(c => c.id === 'zcash-shielded') + const zcashFwSupported = zcashShieldedDef && isChainSupported(zcashShieldedDef, engine.state?.firmwareVersion) + + if (path === '/api/zcash/shielded/status' && method === 'GET') { + if (!zcashFwSupported) return json({ ready: false, error: 'Zcash requires firmware >= 7.11.0' }) + return json({ ready: isSidecarReady() }) + } + + // All mutating zcash endpoints require firmware support + if (path.startsWith('/api/zcash/shielded/') && path !== '/api/zcash/shielded/status' && !zcashFwSupported) { + return json({ error: 'Zcash requires firmware >= 7.11.0' }, 503) + } + + if (path === '/api/zcash/shielded/init' && method === 'POST') { + auth.requireAuth(req) + const body = await req.json() as { seed_hex?: string; from_device?: boolean; account?: number } + if (body.from_device) { + const wallet = requireWallet(engine) + const result = await initializeOrchardFromDevice(wallet, body.account ?? 0) + return json(result) + } + // seed_hex path is dev/test only — reject in production builds + return json({ error: 'seed_hex init disabled — use from_device: true' }, 403) + } + + if (path === '/api/zcash/shielded/scan' && method === 'POST') { + auth.requireAuth(req) + const body = await req.json() as { start_height?: number } + const result = await scanOrchardNotes(body.start_height) + return json(result) + } + + if (path === '/api/zcash/shielded/balance' && method === 'GET') { + auth.requireAuth(req) + const result = await getShieldedBalance() + return json(result) + } + + if (path === '/api/zcash/shielded/build' && method === 'POST') { + auth.requireAuth(req) + const body = await req.json() as { recipient: string; amount: number; account?: number; memo?: string } + if (!body.recipient || !body.amount) return json({ error: 'Missing recipient or amount' }, 400) + if (typeof body.amount !== 'number' || body.amount <= 0 || body.amount > 2_100_000_000_000_000) { + return json({ error: 'Amount must be > 0 and <= 21M ZEC in zatoshis' }, 400) + } + const result = await buildShieldedTx(body) + return json(result) + } + + if (path === '/api/zcash/shielded/finalize' && method === 'POST') { + auth.requireAuth(req) + const body = await req.json() as { signatures: string[] } + if (!body.signatures?.length) return json({ error: 'Missing signatures' }, 400) + const hexRe = /^[0-9a-fA-F]{128}$/ + if (!body.signatures.every(s => hexRe.test(s))) { + return json({ error: 'Each signature must be 128 hex chars (64 bytes)' }, 400) + } + const result = await finalizeShieldedTx(body.signatures) + return json(result) + } + + if (path === '/api/zcash/shielded/broadcast' && method === 'POST') { + auth.requireAuth(req) + const body = await req.json() as { raw_tx: string } + if (!body.raw_tx) return json({ error: 'Missing raw_tx' }, 400) + const result = await broadcastShieldedTx(body.raw_tx) + return json(result) + } + // ── Catch-all ──────────────────────────────────────────────── // Sequential if/else routing is fine for ~60 localhost-only endpoints. // A Map-based router adds complexity with no measurable perf gain here. diff --git a/projects/keepkey-vault/src/bun/tax-export.ts b/projects/keepkey-vault/src/bun/tax-export.ts index ae5e7bd..0f29b8c 100644 --- a/projects/keepkey-vault/src/bun/tax-export.ts +++ b/projects/keepkey-vault/src/bun/tax-export.ts @@ -10,6 +10,7 @@ */ import type { ReportData, ReportSection } from '../shared/types' +import { SECTION_TITLES } from './reports' // ── Internal canonical transaction model ──────────────────────────── @@ -52,14 +53,14 @@ export function extractTransactionsFromReport(data: ReportData): TaxTransaction[ // Find the detailed transaction table first (has From/To) let txSection = data.sections.find( - s => s.type === 'table' && s.title.startsWith('Transaction Details'), + s => s.type === 'table' && s.title.startsWith(SECTION_TITLES.TX_DETAILS), ) let useDetailed = true // Fallback to Transaction History (less detail) if (!txSection) { txSection = data.sections.find( - s => s.type === 'table' && s.title.startsWith('Transaction History'), + s => s.type === 'table' && s.title.startsWith(SECTION_TITLES.TX_HISTORY), ) useDetailed = false } @@ -72,6 +73,7 @@ export function extractTransactionsFromReport(data: ReportData): TaxTransaction[ for (const row of rows) { if (useDetailed) { // Transaction Details: [TXID, Dir, Block, Date, Value (BTC), Fee (BTC), From, To] + if (row.length < 8) { console.warn(`[TaxExport] Skipping row with ${row.length} cols, expected 8`); continue } const txid = String(row[0] || '') const dir = String(row[1] || '').toLowerCase() const date = String(row[3] || '') @@ -83,6 +85,7 @@ export function extractTransactionsFromReport(data: ReportData): TaxTransaction[ txs.push(buildBtcTx(txid, dir, date, value, fee, from, to)) } else { // Transaction History: [#, Dir, TXID, Block, Date, Value (BTC), Fee (BTC)] + if (row.length < 7) { console.warn(`[TaxExport] Skipping row with ${row.length} cols, expected 7`); continue } const txid = String(row[2] || '') const dir = String(row[1] || '').toLowerCase() const date = String(row[4] || '') diff --git a/projects/keepkey-vault/src/bun/txbuilder/index.ts b/projects/keepkey-vault/src/bun/txbuilder/index.ts index e02f5f2..af99603 100644 --- a/projects/keepkey-vault/src/bun/txbuilder/index.ts +++ b/projects/keepkey-vault/src/bun/txbuilder/index.ts @@ -7,6 +7,7 @@ import { buildUtxoTx, type BuildUtxoParams } from './utxo' import { buildEvmTx, type BuildEvmParams } from './evm' import { buildCosmosTx, type BuildCosmosParams } from './cosmos' import { buildXrpTx, type BuildXrpParams } from './xrp' +import { sendShielded, type ShieldedSendParams } from './zcash-shielded' export type { BuildTxParams } @@ -145,6 +146,10 @@ export async function signTx( return wallet.rippleSignTx(unsignedTx) case 'solana': return wallet.solanaSignTx(unsignedTx) + case 'zcash-shielded': + // Shielded signing is handled by the zcash-shielded module (sidecar + device) + // The full flow is orchestrated by sendShielded() — this should not be called directly + throw new Error('Zcash shielded transactions use sendShielded() — not the standard sign flow') default: throw new Error(`Cannot sign for chain family: ${chain.chainFamily}`) } diff --git a/projects/keepkey-vault/src/bun/txbuilder/utxo.ts b/projects/keepkey-vault/src/bun/txbuilder/utxo.ts index c3d010f..6be5afc 100644 --- a/projects/keepkey-vault/src/bun/txbuilder/utxo.ts +++ b/projects/keepkey-vault/src/bun/txbuilder/utxo.ts @@ -47,11 +47,12 @@ const DEFAULT_FEES: Record = { - bitcoin: 0, litecoin: 2, dogecoin: 3, dash: 5, bitcoincash: 145, + bitcoin: 0, litecoin: 2, dogecoin: 3, dash: 5, bitcoincash: 145, zcash: 133, } // Purpose by scriptType @@ -77,6 +78,15 @@ function addressToScriptPubKeyHex(address: string): string | undefined { if (program.length === 32) return `0020${hex}` // p2wsh return undefined } + // Zcash t1... (P2PKH) and t3... (P2SH) — 2-byte version prefix + if (address.startsWith('t1') || address.startsWith('t3')) { + const payload = bs58check.decode(address) + if (payload.length < 22) return undefined + const hash = Buffer.from(payload.slice(2)).toString('hex') + if (address.startsWith('t1')) return `76a914${hash}88ac` + if (address.startsWith('t3')) return `a914${hash}87` + return undefined + } // Base58Check addresses (1..., 3..., L..., D..., X..., etc.) const payload = bs58check.decode(address) const version = payload[0] @@ -413,8 +423,14 @@ export async function buildUtxoTx( coin: chain.coin, inputs: preparedInputs, outputs: preparedOutputs, - version: 1, // keepkey-desktop always passes these explicitly + version: chain.coin === 'Zcash' ? 4 : 1, locktime: 0, + ...(chain.coin === 'Zcash' ? { + overwintered: true, + versionGroupId: 0x892F2085, + branchId: 0x4dec4df0, // NU6.1 (current Zcash mainnet) + expiry: 0, + } : {}), fee: String(fee / 10 ** chain.decimals), memo, // opReturnData at top-level for v1 server contract diff --git a/projects/keepkey-vault/src/bun/txbuilder/zcash-shielded.ts b/projects/keepkey-vault/src/bun/txbuilder/zcash-shielded.ts new file mode 100644 index 0000000..5655ba2 --- /dev/null +++ b/projects/keepkey-vault/src/bun/txbuilder/zcash-shielded.ts @@ -0,0 +1,308 @@ +/** + * Zcash Orchard shielded transaction builder. + * + * Orchestrates the three-way flow: sidecar (crypto) + device (signing) + sidecar (finalize). + * + * Data flow: + * 1. Sidecar builds PCZT, extracts signing request + * 2. Electrobun sends signing fields to device via hdwallet/protobuf + * 3. Device returns RedPallas signatures + * 4. Sidecar applies signatures, generates binding sig, serializes v5 tx + * 5. Sidecar (or Pioneer API) broadcasts + */ + +import { sendCommand, isSidecarReady, startSidecar } from "../zcash-sidecar" + +export interface ShieldedSendParams { + /** Hex-encoded Orchard recipient address (43 bytes) */ + recipient: string + /** Amount in zatoshis */ + amount: number + /** Account index (default 0) */ + account?: number + /** Optional memo */ + memo?: string +} + +export interface SigningRequest { + n_actions: number + account: number + branch_id: number + sighash: string + digests: { + header: string + transparent: string + sapling: string + orchard: string + } + bundle_meta: { + flags: number + value_balance: number + anchor: string + } + actions: Array<{ + index: number + alpha: string + cv_net: string + nullifier: string + cmx: string + epk: string + enc_compact: string + enc_memo: string + enc_noncompact: string + rk: string + out_ciphertext: string + value: number + is_spend: boolean + }> + display: { + amount: string + fee: string + to: string + } +} + +/** + * Initialize the sidecar with a seed (testing/dev only — seed should not leave device in production). + * + * @param seedHex - 64-byte master seed (hex) + * @param account - Account index (default 0) + */ +export async function initializeOrchardFromSeed(seedHex: string, account: number = 0): Promise<{ + fvk: { ak: string; nk: string; rivk: string } + address: string +}> { + if (!isSidecarReady()) { + await startSidecar() + } + + const result = await sendCommand("derive_fvk", { seed_hex: seedHex, account }) + return { fvk: result.fvk, address: result.address } +} + +/** @deprecated Use initializeOrchardFromDevice for production */ +export const initializeOrchard = initializeOrchardFromSeed + +/** + * Initialize Orchard from device-exported FVK. + * + * This is the production path — the seed never leaves the device. + * The device exports {ak, nk, rivk} via the ZcashGetOrchardFVK protobuf message. + * + * @param wallet - hdwallet instance with zcashGetOrchardFvk method + * @param account - Account index (default 0) + */ +export async function initializeOrchardFromDevice(wallet: any, account: number = 0): Promise<{ + fvk: { ak: string; nk: string; rivk: string } + address: string +}> { + if (!isSidecarReady()) { + await startSidecar() + } + + if (typeof wallet.zcashGetOrchardFVK !== "function") { + throw new Error( + "hdwallet does not support zcashGetOrchardFVK — " + + "ensure keepkey-firmware with Zcash/Orchard support is flashed" + ) + } + + // Request FVK from device — device derives internally, seed never leaves + console.log("[zcash-shielded] Requesting Orchard FVK from device...") + const deviceResult = await wallet.zcashGetOrchardFVK(account) + const { ak, nk, rivk } = deviceResult + + if (!ak || !nk || !rivk) { + throw new Error("Device returned incomplete FVK — missing ak, nk, or rivk") + } + + // Convert Uint8Array to hex strings for sidecar IPC + const toHex = (buf: Uint8Array | Buffer) => + Buffer.from(buf).toString("hex") + const akHex = toHex(ak) + const nkHex = toHex(nk) + const rivkHex = toHex(rivk) + + // Send FVK components to sidecar + console.log("[zcash-shielded] Setting FVK on sidecar...") + const result = await sendCommand("set_fvk", { ak: akHex, nk: nkHex, rivk: rivkHex }) + return { fvk: result.fvk, address: result.address } +} + +/** + * Scan the Zcash chain for Orchard notes. + * Resumes from last scan position automatically. + */ +export async function scanOrchardNotes(startHeight?: number, fullRescan?: boolean): Promise<{ + balance: number + notes_found: number + synced_to: number +}> { + if (!isSidecarReady()) { + throw new Error("Sidecar not initialized — call initializeOrchard() first") + } + + const params: Record = {} + if (startHeight !== undefined) params.start_height = startHeight + if (fullRescan) params.full_rescan = true + + return await sendCommand("scan", params) +} + +/** + * Get the current shielded balance (in zatoshis). + */ +export async function getShieldedBalance(): Promise<{ + confirmed: number + pending: number +}> { + if (!isSidecarReady()) { + throw new Error("Sidecar not initialized — call initializeOrchard() first") + } + + return await sendCommand("balance") +} + +/** + * Build a shielded transaction and get the signing request for the device. + * + * The caller must then send the signing request to the device, + * collect signatures, and call finalizeShieldedTx(). + */ +export async function buildShieldedTx(params: ShieldedSendParams): Promise<{ + signing_request: SigningRequest +}> { + if (!isSidecarReady()) { + throw new Error("Sidecar not initialized — call initializeOrchard() first") + } + + return await sendCommand("build_pczt", { + recipient: params.recipient, + amount: params.amount, + account: params.account ?? 0, + memo: params.memo, + }) +} + +/** + * Apply device signatures and produce the final broadcast-ready transaction. + * + * @param signatures - Array of 64-byte RedPallas signatures (hex strings), one per action + */ +export async function finalizeShieldedTx(signatures: string[]): Promise<{ + raw_tx: string + txid: string +}> { + if (!isSidecarReady()) { + throw new Error("Sidecar not initialized") + } + + return await sendCommand("finalize", { signatures }) +} + +/** + * Broadcast a finalized transaction via lightwalletd. + */ +export async function broadcastShieldedTx(rawTxHex: string): Promise<{ + txid: string +}> { + if (!isSidecarReady()) { + throw new Error("Sidecar not initialized") + } + + return await sendCommand("broadcast", { raw_tx: rawTxHex }) +} + +/** + * Full shielded send flow — orchestrates sidecar + device signing. + * + * @param wallet - hdwallet instance with zcashSignPczt method + * @param params - Send parameters + * @returns Transaction ID + */ +let sendInProgress = false + +export async function sendShielded( + wallet: any, + params: ShieldedSendParams, +): Promise<{ txid: string }> { + if (sendInProgress) { + throw new Error("A shielded send is already in progress — wait for it to complete") + } + sendInProgress = true + try { + return await _sendShieldedInner(wallet, params) + } finally { + sendInProgress = false + } +} + +async function _sendShieldedInner( + wallet: any, + params: ShieldedSendParams, +): Promise<{ txid: string }> { + // 0. Ensure sidecar is running and FVK is set from device + if (!isSidecarReady()) { + await startSidecar() + } + // Always refresh FVK from device to ensure ak matches device's ask + console.log("[zcash-shielded] Refreshing FVK from device before build...") + await initializeOrchardFromDevice(wallet, params.account ?? 0) + + // 1. Build PCZT via sidecar + console.log("[zcash-shielded] Building PCZT...") + const { signing_request } = await buildShieldedTx(params) + + console.log(`[zcash-shielded] PCZT built: ${signing_request.n_actions} actions`) + console.log(`[zcash-shielded] Display: ${signing_request.display.amount} to ${signing_request.display.to}`) + + // 2. Send to device for signing via hdwallet + // The device protobuf flow: + // ZcashSignPCZT (digests + metadata) → ZcashPCZTActionAck + // For each action: ZcashPCZTAction (fields) → ZcashPCZTActionAck | ZcashSignedPCZT + console.log("[zcash-shielded] Requesting device signatures...") + const signatures = await deviceSign(wallet, signing_request) + console.log(`[zcash-shielded] Got ${signatures.length} signatures`) + + // 3. Finalize via sidecar (apply sigs + binding sig + serialize) + console.log("[zcash-shielded] Finalizing transaction...") + const { raw_tx, txid } = await finalizeShieldedTx(signatures) + + // 4. Broadcast + console.log("[zcash-shielded] Broadcasting...") + await broadcastShieldedTx(raw_tx) + + console.log(`[zcash-shielded] Transaction sent: ${txid}`) + return { txid } +} + +/** + * Send signing request to device and collect signatures. + * + * Uses the hdwallet zcashSignPczt method which handles the full protobuf + * message flow: ZcashSignPCZT → ZcashPCZTAction(s) → ZcashSignedPCZT. + */ +async function deviceSign(wallet: any, sr: SigningRequest): Promise { + if (typeof wallet.zcashSignPczt !== "function") { + throw new Error( + "hdwallet does not support zcashSignPczt — " + + "ensure keepkey-firmware with Zcash support is flashed" + ) + } + + // The hdwallet zcashSignPczt method takes the signing request directly + // and handles the protobuf streaming internally + const signatures = await wallet.zcashSignPczt(sr, sr.sighash) + + if (!signatures || !Array.isArray(signatures)) { + throw new Error("Device did not return signatures") + } + + if (signatures.length !== sr.n_actions) { + throw new Error( + `Signature count mismatch: got ${signatures.length} signatures for ${sr.n_actions} actions` + ) + } + + return signatures +} diff --git a/projects/keepkey-vault/src/bun/zcash-sidecar.ts b/projects/keepkey-vault/src/bun/zcash-sidecar.ts new file mode 100644 index 0000000..6fa021b --- /dev/null +++ b/projects/keepkey-vault/src/bun/zcash-sidecar.ts @@ -0,0 +1,389 @@ +/** + * Zcash CLI sidecar manager — spawns and communicates with the Rust zcash-cli process. + * + * Uses NDJSON (newline-delimited JSON) over stdin/stdout for IPC. + * The sidecar handles chain scanning, PCZT construction, Halo2 proving, + * and transaction finalization. It NEVER opens the KeepKey device. + */ + +import { Subprocess } from "bun" +import { join, dirname, resolve } from "path" +import { existsSync } from "fs" + +interface IpcResponse { + ok: boolean + _req_id?: number + [key: string]: any +} + +type PendingRequest = { + resolve: (value: IpcResponse) => void + reject: (error: Error) => void + timeout: ReturnType +} + +let sidecarProc: Subprocess<"pipe", "pipe", "pipe"> | null = null +let pendingRequests = new Map() +let nextReqId = 1 +let ready = false +let buffer = "" +let initPromise: Promise | null = null +let scanProgressCallback: ((progress: { percent: number; scannedHeight: number; tipHeight: number; blocksPerSec: number; etaSeconds: number }) => void) | null = null +let lastProgressTime = 0 +let lastProgressHeight = 0 + +/** Cached FVK + address from auto-load or set_fvk */ +let cachedAddress: string | null = null +let cachedFvk: { ak: string; nk: string; rivk: string } | null = null + +/** + * Resolve the path to the zcash-cli binary. + * + * Search order: + * 1. ZCASH_CLI_BIN env var (explicit override) + * 2. Source-tree dev build (zcash-cli/target/release/) + * 3. Source-tree debug build (zcash-cli/target/debug/) + * 4. Bundled alongside the app (production) + * + * Throws if the binary cannot be found anywhere. + */ +function getBinaryPath(): string { + // Allow explicit override + if (process.env.ZCASH_CLI_BIN && existsSync(process.env.ZCASH_CLI_BIN)) { + return process.env.ZCASH_CLI_BIN + } + + const candidates: string[] = [] + + // 1. cwd-relative (works if cwd is the project root) + const cwdRoot = process.cwd() + candidates.push(join(cwdRoot, "zcash-cli", "target", "release", "zcash-cli")) + candidates.push(join(cwdRoot, "zcash-cli", "target", "debug", "zcash-cli")) + + // 2. Walk up from app bundle to source project root. + const fromBundle = resolve(import.meta.dir, "..", "..", "..", "..", "..", "..", "..") + candidates.push(join(fromBundle, "zcash-cli", "target", "release", "zcash-cli")) + candidates.push(join(fromBundle, "zcash-cli", "target", "debug", "zcash-cli")) + + // 3. Relative to import.meta.dir for non-bundled dev (running bun directly from src/bun/) + const srcRelRoot = dirname(dirname(import.meta.dir)) + candidates.push(join(srcRelRoot, "zcash-cli", "target", "release", "zcash-cli")) + + // 4. Production: bundled next to the app binary + const appBundleDir = resolve(import.meta.dir, "..", "..", "..") + candidates.push(join(appBundleDir, "zcash-cli")) + + console.log(`[zcash-sidecar] Searching for binary (cwd=${cwdRoot})`) + + for (const p of candidates) { + if (existsSync(p)) { + console.log(`[zcash-sidecar] Found binary: ${p}`) + return p + } + } + + const searched = candidates.map(p => ` - ${p}`).join("\n") + throw new Error( + `zcash-cli binary not found. Build it with: cd zcash-cli && cargo build --release\n` + + `Searched:\n${searched}` + ) +} + +/** + * Start the Zcash sidecar process. + * Guards against concurrent startup calls. + */ +export async function startSidecar(): Promise { + if (sidecarProc && ready) { + return + } + // Prevent concurrent startSidecar() calls + if (initPromise) { + return initPromise + } + + initPromise = (async () => { + try { + const binPath = getBinaryPath() + console.log(`[zcash-sidecar] Starting: ${binPath}`) + + sidecarProc = Bun.spawn([binPath], { + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + RUST_LOG: "info", + }, + }) + + // Read stderr for logs (Rust logs go to stderr) + readStderr(sidecarProc) + + // Read stdout for NDJSON responses + readStdout(sidecarProc) + + // Wait for ready signal (startup uses req_id 0) + const readyResponse = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pendingRequests.delete(0) + reject(new Error("Sidecar startup timeout")) + }, 10000) + pendingRequests.set(0, { + resolve: (r) => { clearTimeout(timeout); resolve(r) }, + reject: (e) => { clearTimeout(timeout); reject(e) }, + timeout, + }) + }) + + if (!readyResponse.ok || !readyResponse.ready) { + throw new Error("Sidecar failed to start") + } + + ready = true + + // Capture auto-loaded FVK if the sidecar had one persisted + if (readyResponse.fvk_loaded && readyResponse.address) { + cachedAddress = readyResponse.address + cachedFvk = readyResponse.fvk || null + console.log(`[zcash-sidecar] Ready (version ${readyResponse.version}) — FVK auto-loaded, UA: ${cachedAddress?.slice(0, 20)}...`) + } else { + console.log(`[zcash-sidecar] Ready (version ${readyResponse.version}) — no saved FVK`) + } + } finally { + initPromise = null + } + })() + + return initPromise +} + +/** + * Send a command to the sidecar and wait for the response. + * Uses request IDs to correctly match responses to requests. + */ +export async function sendCommand(cmd: string, params: Record = {}, timeoutMs: number = 300000): Promise { + if (!sidecarProc || !ready) { + throw new Error("Sidecar not running — call startSidecar() first") + } + + const reqId = nextReqId++ + const request = JSON.stringify({ cmd, _req_id: reqId, ...params }) + "\n" + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pendingRequests.delete(reqId) + reject(new Error(`Sidecar command '${cmd}' timed out after ${timeoutMs}ms`)) + }, timeoutMs) + + pendingRequests.set(reqId, { + resolve: (response) => { + clearTimeout(timeout) + if (response.ok) { + resolve(response) + } else { + reject(new Error(response.error || "Sidecar command failed")) + } + }, + reject: (e) => { clearTimeout(timeout); reject(e) }, + timeout, + }) + + try { + sidecarProc!.stdin.write(request) + sidecarProc!.stdin.flush() + } catch (e: any) { + clearTimeout(timeout) + pendingRequests.delete(reqId) + reject(new Error(`Failed to write to sidecar stdin: ${e.message}`)) + } + }) +} + +/** + * Stop the sidecar process. + */ +export function stopSidecar(): void { + ready = false + if (sidecarProc) { + try { + // Send quit command gracefully + sidecarProc.stdin.write('{"cmd":"quit"}\n') + sidecarProc.stdin.flush() + } catch { /* already dead */ } + + // Force kill after 2s + setTimeout(() => { + try { sidecarProc?.kill() } catch { /* already dead */ } + sidecarProc = null + }, 2000) + + console.log("[zcash-sidecar] Stopping") + } +} + +/** + * Check if the sidecar is running and ready. + */ +export function isSidecarReady(): boolean { + return ready && sidecarProc !== null +} + +/** + * Check if the sidecar has a FVK loaded (either auto-loaded from DB or set via device). + */ +export function hasFvkLoaded(): boolean { + return cachedAddress !== null +} + +/** + * Get the cached address + FVK (from auto-load or set_fvk). + */ +export function getCachedFvk(): { address: string; fvk: { ak: string; nk: string; rivk: string } } | null { + if (!cachedAddress || !cachedFvk) return null + return { address: cachedAddress, fvk: cachedFvk } +} + +/** + * Update the cached FVK (called after set_fvk succeeds). + */ +export function setCachedFvk(address: string, fvk: { ak: string; nk: string; rivk: string }): void { + cachedAddress = address + cachedFvk = fvk +} + +/** + * Clear the cached FVK (call on device disconnect to prevent stale state). + */ +export function clearCachedFvk(): void { + cachedAddress = null + cachedFvk = null +} + +/** + * Register a callback for scan progress events parsed from sidecar stderr. + */ +export function onScanProgress(cb: typeof scanProgressCallback): void { + scanProgressCallback = cb + lastProgressTime = 0 + lastProgressHeight = 0 +} + +// ── Internal I/O ──────────────────────────────────────────────────────── + +function readStdout(proc: Subprocess<"pipe", "pipe", "pipe">): void { + ;(async () => { + const reader = proc.stdout.getReader() + const decoder = new TextDecoder() + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + + // Parse complete NDJSON lines + let nlIdx: number + while ((nlIdx = buffer.indexOf("\n")) !== -1) { + const line = buffer.slice(0, nlIdx).trim() + buffer = buffer.slice(nlIdx + 1) + + if (!line) continue + + try { + const response: IpcResponse = JSON.parse(line) + // Match by request ID if present, otherwise use FIFO for startup + const reqId = response._req_id + if (reqId !== undefined && pendingRequests.has(reqId)) { + const pending = pendingRequests.get(reqId)! + pendingRequests.delete(reqId) + pending.resolve(response) + } else if (reqId === undefined && pendingRequests.has(0)) { + // Startup ready signal (no req_id) — matched to id 0 + const pending = pendingRequests.get(0)! + pendingRequests.delete(0) + pending.resolve(response) + } else { + console.warn("[zcash-sidecar] Unexpected response (req_id:", reqId, "):", line.slice(0, 200)) + } + } catch (e) { + console.error("[zcash-sidecar] Invalid JSON from sidecar:", line.slice(0, 200)) + } + } + } + } catch (e) { + console.error("[zcash-sidecar] stdout reader error:", e) + } + try { reader.releaseLock() } catch { /* already released */ } + + // Process exited — reject any pending requests + for (const [, pending] of pendingRequests) { + clearTimeout(pending.timeout) + pending.reject(new Error("Sidecar process exited")) + } + pendingRequests.clear() + ready = false + sidecarProc = null + console.log("[zcash-sidecar] Process exited") + })() +} + +// Regex to parse: "Scan progress: 0.6% (1697103/3266985)" +const PROGRESS_RE = /Scan progress:\s+([\d.]+)%\s+\((\d+)\/(\d+)\)/ + +function readStderr(proc: Subprocess<"pipe", "pipe", "pipe">): void { + ;(async () => { + const reader = proc.stderr.getReader() + const decoder = new TextDecoder() + let stderrBuf = "" + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + stderrBuf += decoder.decode(value, { stream: true }) + + // Forward complete lines as log messages + let nlIdx: number + while ((nlIdx = stderrBuf.indexOf("\n")) !== -1) { + const line = stderrBuf.slice(0, nlIdx).trim() + stderrBuf = stderrBuf.slice(nlIdx + 1) + if (line) { + console.log(`[zcash-sidecar] ${line}`) + + // Parse scan progress for UI updates + const match = line.match(PROGRESS_RE) + if (match && scanProgressCallback) { + const percent = parseFloat(match[1]) + const scannedHeight = parseInt(match[2], 10) + const tipHeight = parseInt(match[3], 10) + + const now = Date.now() + let blocksPerSec = 0 + let etaSeconds = 0 + + if (lastProgressTime > 0 && lastProgressHeight > 0) { + const elapsed = (now - lastProgressTime) / 1000 + const blocksDone = scannedHeight - lastProgressHeight + if (elapsed > 0 && blocksDone > 0) { + blocksPerSec = Math.round(blocksDone / elapsed) + const remaining = tipHeight - scannedHeight + etaSeconds = blocksPerSec > 0 ? Math.round(remaining / blocksPerSec) : 0 + } + } + + lastProgressTime = now + lastProgressHeight = scannedHeight + + scanProgressCallback({ percent, scannedHeight, tipHeight, blocksPerSec, etaSeconds }) + } + } + } + + // Prevent unbounded growth + if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-2048) + } + } catch (e) { + console.error("[zcash-sidecar] stderr reader error:", e) + } + try { reader.releaseLock() } catch { /* already released */ } + })() +} diff --git a/projects/keepkey-vault/src/mainview/App.tsx b/projects/keepkey-vault/src/mainview/App.tsx index 516cc90..a756b01 100644 --- a/projects/keepkey-vault/src/mainview/App.tsx +++ b/projects/keepkey-vault/src/mainview/App.tsx @@ -113,16 +113,21 @@ function App() { const handlePassphraseSubmit = useCallback(async (passphrase: string) => { try { await rpcRequest("sendPassphrase", { passphrase }) } catch (e) { console.error("sendPassphrase:", e) } - setPinRequestType(null) // ensure PIN overlay stays cleared - setPassphraseRequested(false) + setPinRequestType(null) + // Don't dismiss overlay here — sendPassphrase returns instantly (omitLock/noWait) + // but the device still needs physical confirmation. The overlay stays visible + // showing "Confirm on your KeepKey" until state transitions to 'ready'. }, []) const handlePassphraseCancel = useCallback(() => { setPinRequestType(null); setPassphraseRequested(false) }, []) - // Auto-show passphrase overlay when device needs passphrase + // Auto-show passphrase overlay when device needs passphrase; + // auto-dismiss when device leaves needs_passphrase (e.g. → ready). useEffect(() => { if (deviceState.state === "needs_passphrase" && !passphraseRequested) { setPassphraseRequested(true) + } else if (deviceState.state !== "needs_passphrase" && passphraseRequested) { + setPassphraseRequested(false) } }, [deviceState.state, passphraseRequested]) @@ -253,9 +258,9 @@ function App() { }, []) // Auto-show PIN for locked device (only once — respect user dismiss) - // M3 fix: skip auto-show during reboot phase — backend promptPin handles it with a delay + // Skip auto-show during any firmware operation phase — backend promptPin handles it with a delay useEffect(() => { - if (deviceState.state === "needs_pin" && !pinRequestType && !pinDismissed && deviceState.updatePhase !== "rebooting") { + if (deviceState.state === "needs_pin" && !pinRequestType && !pinDismissed && (!deviceState.updatePhase || deviceState.updatePhase === "idle")) { setPinRequestType("current") } }, [deviceState.state, deviceState.updatePhase, pinRequestType, pinDismissed]) @@ -404,7 +409,8 @@ function App() { : isClaimed ? "claimed" : ["disconnected", "connected_unpaired", "error"].includes(deviceState.state) ? "splash" : !wizardComplete && ["bootloader", "needs_firmware", "needs_init"].includes(deviceState.state) ? "setup" - : ["ready", "needs_pin", "needs_passphrase"].includes(deviceState.state) ? "ready" + : deviceState.state === "ready" ? "ready" + : ["needs_pin", "needs_passphrase"].includes(deviceState.state) ? "splash" : "splash" // ── Overlays (render above everything, only one at a time) ────── @@ -480,7 +486,8 @@ function App() { connected={false} firmwareVersion={undefined} firmwareVerified={undefined} - onSettingsToggle={() => {}} + onSettingsToggle={() => setSettingsOpen((o) => !o)} + settingsOpen={settingsOpen} activeTab="vault" onTabChange={() => {}} watchOnly @@ -489,6 +496,14 @@ function App() { {}} /> + setSettingsOpen(false)} + deviceState={deviceState} + appVersion={appVersion} + onCheckForUpdate={update.checkForUpdate} + updatePhase={update.phase} + /> ) } @@ -556,7 +571,7 @@ function App() { > {/* pt: 54px TopNav + 50px banner height when visible */} - {activeTab === "vault" && setSettingsOpen(true)} />} + {activeTab === "vault" && setSettingsOpen(true)} firmwareVersion={deviceState.firmwareVersion} />} {activeTab === "apps" && } diff --git a/projects/keepkey-vault/src/mainview/components/AssetPage.tsx b/projects/keepkey-vault/src/mainview/components/AssetPage.tsx index 89cd20a..5ce35cc 100644 --- a/projects/keepkey-vault/src/mainview/components/AssetPage.tsx +++ b/projects/keepkey-vault/src/mainview/components/AssetPage.tsx @@ -4,13 +4,14 @@ import { Box, Flex, Text, Button, Image, VStack, HStack, IconButton } from "@cha import { FaArrowDown, FaArrowUp, FaPlus, FaEye, FaEyeSlash, FaShieldAlt, FaCheck } from "react-icons/fa" import { rpcRequest } from "../lib/rpc" import type { ChainDef } from "../../shared/chains" -import { BTC_SCRIPT_TYPES, btcAccountPath } from "../../shared/chains" +import { CHAINS, BTC_SCRIPT_TYPES, btcAccountPath, isChainSupported } from "../../shared/chains" import type { ChainBalance, TokenBalance, TokenVisibilityStatus } from "../../shared/types" import { getAssetIcon, caipToIcon } from "../../shared/assetLookup" import { AnimatedUsd } from "./AnimatedUsd" import { formatBalance, formatUsd } from "../lib/formatting" import { ReceiveView } from "./ReceiveView" import { SendForm } from "./SendForm" +import { ZcashPrivacyTab } from "./ZcashPrivacyTab" import { BtcXpubSelector } from "./BtcXpubSelector" import { EvmAddressSelector } from "./EvmAddressSelector" import { useBtcAccounts } from "../hooks/useBtcAccounts" @@ -18,15 +19,16 @@ import { useEvmAddresses } from "../hooks/useEvmAddresses" import { AddTokenDialog } from "./AddTokenDialog" import { detectSpamToken, type SpamResult } from "../../shared/spamFilter" -type AssetView = "receive" | "send" +type AssetView = "receive" | "send" | "privacy" interface AssetPageProps { chain: ChainDef balance?: ChainBalance onBack: () => void + firmwareVersion?: string } -export function AssetPage({ chain, balance, onBack }: AssetPageProps) { +export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPageProps) { const { t } = useTranslation("asset") const [view, setView] = useState("receive") const [selectedToken, setSelectedToken] = useState(null) @@ -219,9 +221,14 @@ export function AssetPage({ chain, balance, onBack }: AssetPageProps) { } }, []) + const isZcash = chain.id === 'zcash' + const zcashShieldedDef = CHAINS.find(c => c.id === 'zcash-shielded') + const zcashShieldedSupported = isZcash && zcashShieldedDef && isChainSupported(zcashShieldedDef, firmwareVersion) + const PILLS: { id: AssetView; label: string; icon: typeof FaArrowDown }[] = [ { id: "receive", label: t("receive"), icon: FaArrowDown }, { id: "send", label: t("send"), icon: FaArrowUp }, + ...(zcashShieldedSupported ? [{ id: "privacy" as const, label: t("privacy"), icon: FaShieldAlt }] : []), ] // Shared token row renderer @@ -504,6 +511,9 @@ export function AssetPage({ chain, balance, onBack }: AssetPageProps) { evmAddressIndex={isEvm ? evmAddresses.selectedIndex : undefined} /> )} + {view === "privacy" && isZcash && ( + + )} {/* Tokens Section — with spam filter */} diff --git a/projects/keepkey-vault/src/mainview/components/Dashboard.tsx b/projects/keepkey-vault/src/mainview/components/Dashboard.tsx index 3ad183e..1ccd840 100644 --- a/projects/keepkey-vault/src/mainview/components/Dashboard.tsx +++ b/projects/keepkey-vault/src/mainview/components/Dashboard.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from "react" import { Box, Flex, Text, Spinner, Image, SimpleGrid } from "@chakra-ui/react" import { useTranslation } from "react-i18next" -import { CHAINS, customChainToChainDef, type ChainDef } from "../../shared/chains" +import { CHAINS, customChainToChainDef, isChainSupported, type ChainDef } from "../../shared/chains" import { formatBalance } from "../lib/formatting" import { AnimatedUsd } from "./AnimatedUsd" import { getAssetIcon, registerCustomAsset } from "../../shared/assetLookup" @@ -29,6 +29,7 @@ interface DashboardProps { onLoaded?: () => void watchOnly?: boolean onOpenSettings?: () => void + firmwareVersion?: string } /** Format a timestamp as a relative "time ago" string (i18n-aware) */ @@ -43,7 +44,7 @@ function formatTimeAgo(ts: number, t: (key: string, opts?: Record(null) const [balances, setBalances] = useState>(new Map()) @@ -202,7 +203,9 @@ export function Dashboard({ onLoaded, watchOnly, onOpenSettings }: DashboardProp const hasAnyBalance = chartData.length > 0 - const sortedChains = useMemo(() => [...allChains].sort((a, b) => { + const visibleChains = useMemo(() => allChains.filter(c => !c.hidden && isChainSupported(c, firmwareVersion)), [allChains, firmwareVersion]) + + const sortedChains = useMemo(() => [...visibleChains].sort((a, b) => { const aUsd = cleanBalanceUsd.get(a.id)?.usd || 0 const bUsd = cleanBalanceUsd.get(b.id)?.usd || 0 const aHas = aUsd > 0 || parseFloat(balances.get(a.id)?.balance || '0') > 0 @@ -211,14 +214,14 @@ export function Dashboard({ onLoaded, watchOnly, onOpenSettings }: DashboardProp if (!aHas && bHas) return 1 if (aHas && bHas) return bUsd - aUsd return 0 - }), [allChains, balances, cleanBalanceUsd]) + }), [visibleChains, balances, cleanBalanceUsd]) // Is data stale? (loaded from cache but haven't refreshed yet this session) const isStale = !hasEverRefreshed && !loadingBalances if (selectedChain) { const bal = balances.get(selectedChain.id) - return setSelectedChain(null)} /> + return setSelectedChain(null)} firmwareVersion={firmwareVersion} /> } return ( diff --git a/projects/keepkey-vault/src/mainview/components/TopNav.tsx b/projects/keepkey-vault/src/mainview/components/TopNav.tsx index 3732197..7313db2 100644 --- a/projects/keepkey-vault/src/mainview/components/TopNav.tsx +++ b/projects/keepkey-vault/src/mainview/components/TopNav.tsx @@ -1,6 +1,9 @@ import { Flex, Text, Box, Image, IconButton, HStack } from "@chakra-ui/react" import { useTranslation } from "react-i18next" import { Z } from "../lib/z-index" +import { IS_WINDOWS } from "../lib/platform" +import { useWindowDrag } from "../hooks/useWindowDrag" +import { rpcRequest } from "../lib/rpc" import kkIcon from "../assets/icon.png" export type NavTab = "vault" | "shapeshift" | "apps" @@ -48,6 +51,7 @@ const GridIcon = () => ( /** Minimal nav bar for splash / setup phases. */ export function SplashNav() { + const windowDrag = useWindowDrag() return ( rpcRequest("windowMaximize") : undefined} > rpcRequest("windowMaximize") : undefined} > {/* Left: device icon + label */} @@ -212,13 +223,11 @@ export function TopNav({ label, connected, firmwareVersion, firmwareVerified, ne diff --git a/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx b/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx new file mode 100644 index 0000000..b59674b --- /dev/null +++ b/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx @@ -0,0 +1,531 @@ +import { useState, useEffect, useCallback, useRef } from "react" +import { useTranslation } from "react-i18next" +import { Box, Flex, Text, Button, Input, Spinner } from "@chakra-ui/react" +import { FaShieldAlt, FaCopy, FaCheck } from "react-icons/fa" +import { rpcRequest, onRpcMessage } from "../lib/rpc" +import { generateQRSvg } from "../lib/qr" + +type SidecarStatus = "checking" | "ready" | "not_running" | "initializing" +type ScanState = "idle" | "scanning" | "done" + +interface ScanProgress { + percent: number + scannedHeight: number + tipHeight: number + blocksPerSec: number + etaSeconds: number +} + +function formatEta(seconds: number): string { + if (seconds <= 0) return "calculating..." + if (seconds < 60) return `${seconds}s` + if (seconds < 3600) { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return s > 0 ? `${m}m ${s}s` : `${m}m` + } + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + return m > 0 ? `${h}h ${m}m` : `${h}h` +} + +export function ZcashPrivacyTab() { + const { t } = useTranslation("asset") + + // ── State ────────────────────────────────────────────────────────── + const [status, setStatus] = useState("checking") + const [orchardAddress, setOrchardAddress] = useState(null) + const [balance, setBalance] = useState<{ confirmed: number; pending: number } | null>(null) + const [syncedTo, setSyncedTo] = useState(null) + const [scanState, setScanState] = useState("idle") + const [scanResult, setScanResult] = useState(null) + const [copied, setCopied] = useState(false) + + // Send form state + const [recipient, setRecipient] = useState("") + const [amount, setAmount] = useState("") + const [memo, setMemo] = useState("") + const [sending, setSending] = useState(false) + const [sendResult, setSendResult] = useState(null) + const [sendError, setSendError] = useState(null) + + // ── Fetch balance ───────────────────────────────────────────────── + const refreshBalance = useCallback(async () => { + try { + const bal = await rpcRequest<{ confirmed: number; pending: number }>( + "zcashShieldedBalance", undefined, 10000 + ) + setBalance(bal) + } catch { + // Balance not available yet (needs scan first) + } + }, []) + + // ── Auto-initialize: check status, auto-init from device if needed ── + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const r = await rpcRequest<{ ready: boolean; fvk_loaded: boolean; address: string | null }>( + "zcashShieldedStatus", undefined, 5000 + ) + if (cancelled) return + if (!r.ready) { setStatus("not_running"); return } + + if (r.fvk_loaded && r.address) { + // FVK auto-loaded from DB — no device interaction needed + setOrchardAddress(r.address) + setStatus("ready") + refreshBalance() + return + } + + // Sidecar ready but no FVK — auto-init from device + setStatus("initializing") + const result = await rpcRequest<{ fvk: any; address: string }>( + "zcashShieldedInit", { account: 0 }, 60000 + ) + if (cancelled) return + setOrchardAddress(result.address) + setStatus("ready") + refreshBalance() + } catch (e: any) { + if (cancelled) return + console.error("[ZcashPrivacyTab] Auto-init failed:", e) + setStatus("not_running") + } + })() + return () => { cancelled = true } + }, [refreshBalance]) + + // ── Manual re-init (fallback button, rarely needed) ─────────────── + const handleInit = useCallback(async () => { + setStatus("initializing") + try { + const result = await rpcRequest<{ fvk: any; address: string }>( + "zcashShieldedInit", { account: 0 }, 60000 + ) + setOrchardAddress(result.address) + setStatus("ready") + refreshBalance() + } catch (e: any) { + console.error("[ZcashPrivacyTab] Init failed:", e) + setStatus("not_running") + } + }, [refreshBalance]) + + // ── Scan progress (pushed from bun via RPC message) ────────────── + const [scanProgress, setScanProgress] = useState(null) + const smoothPercent = useRef(0) + const [displayPercent, setDisplayPercent] = useState(0) + + useEffect(() => { + return onRpcMessage("scan-progress", (payload: ScanProgress) => { + setScanProgress(payload) + }) + }, []) + + // Smooth the progress bar animation + useEffect(() => { + if (!scanProgress) return + const target = scanProgress.percent + if (target <= smoothPercent.current) { + // Reset on new scan + smoothPercent.current = 0 + } + const id = setInterval(() => { + const diff = target - smoothPercent.current + if (diff < 0.05) { + smoothPercent.current = target + setDisplayPercent(target) + clearInterval(id) + } else { + smoothPercent.current += diff * 0.3 + setDisplayPercent(smoothPercent.current) + } + }, 50) + return () => clearInterval(id) + }, [scanProgress]) + + // Clear progress when scan finishes + useEffect(() => { + if (scanState !== "scanning") { + setScanProgress(null) + smoothPercent.current = 0 + setDisplayPercent(0) + } + }, [scanState]) + + // ── Scan for payments ───────────────────────────────────────────── + const [scanFromHeight, setScanFromHeight] = useState("") + + const handleScan = useCallback(async (startHeight?: number, fullRescan?: boolean) => { + setScanState("scanning") + setScanResult(null) + try { + const params: { startHeight?: number; fullRescan?: boolean } = {} + if (startHeight != null) params.startHeight = startHeight + if (fullRescan) params.fullRescan = true + const timeout = startHeight != null ? 300000 : 1800000 // 5 min partial, 30 min full + const result = await rpcRequest<{ balance: number; notes_found: number; synced_to: number }>( + "zcashShieldedScan", params, timeout + ) + setSyncedTo(result.synced_to) + setScanResult(t("notesFound", { count: result.notes_found })) + setScanState("done") + refreshBalance() + } catch (e: any) { + setScanResult(e.message || "Scan failed") + setScanState("idle") + } + }, [t, refreshBalance]) + + // ── Send shielded ───────────────────────────────────────────────── + const handleSend = useCallback(async () => { + if (!recipient || !amount) return + setSending(true) + setSendError(null) + setSendResult(null) + try { + // String-based decimal conversion to avoid floating-point precision loss + const parts = amount.split(".") + const whole = BigInt(parts[0] || "0") * 100_000_000n + const fracStr = (parts[1] || "").padEnd(8, "0").slice(0, 8) + const frac = BigInt(fracStr) + const zatoshis = Number(whole + frac) + if (!Number.isFinite(zatoshis) || zatoshis <= 0) throw new Error("Invalid amount") + // Validate memo length (Orchard memo field is 512 bytes max) + if (memo && new TextEncoder().encode(memo).length > 512) { + throw new Error(t("memoTooLong")) + } + const result = await rpcRequest<{ txid: string }>( + "zcashShieldedSend", + { recipient, amount: zatoshis, memo: memo || undefined }, + 120000 + ) + setSendResult(result.txid) + setRecipient("") + setAmount("") + setMemo("") + refreshBalance() + } catch (e: any) { + setSendError(e.message || "Send failed") + } + setSending(false) + }, [recipient, amount, memo, refreshBalance]) + + // ── Copy address ────────────────────────────────────────────────── + const copyAddress = useCallback(() => { + if (!orchardAddress) return + navigator.clipboard.writeText(orchardAddress) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [orchardAddress]) + + // ── Status indicator color ──────────────────────────────────────── + const statusColor = status === "ready" ? "#4ADE80" + : status === "initializing" || status === "checking" ? "#FBBF24" + : "#F87171" + + const statusText = status === "ready" ? t("sidecarReady") + : status === "initializing" || status === "checking" ? t("initializing") + : t("sidecarNotReady") + + // Format balance from zatoshis to ZEC + const formatZec = (zatoshis: number) => (zatoshis / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "") + + return ( + + {/* Section A: Status bar */} + + + + {statusText} + + {status === "ready" && !orchardAddress && ( + + )} + {status === "not_running" && ( + {t("zcashCliRequired")} + )} + {status === "initializing" && } + + + {/* Section B: Shielded balance */} + {orchardAddress && ( + + + {t("shieldedBalance")} + + {balance ? ( + + + + {formatZec(balance.confirmed)} + + ZEC + + {balance.pending > 0 && ( + + {t("pendingBalance")}: {formatZec(balance.pending)} ZEC + + )} + {syncedTo && ( + + {t("lastSynced", { height: syncedTo.toLocaleString() })} + + )} + + ) : ( + {t("initRequired")} + )} + + )} + + {/* Section C: Orchard address (receive) */} + {orchardAddress && ( + + + {t("orchardAddress")} + + + + + + {orchardAddress} + + + + + + )} + + {/* Section D: Scan controls */} + {orchardAddress && ( + + + {t("scanPayments")} + + + {/* Progress bar — visible during scan */} + {scanState === "scanning" && ( + + {/* Bar track */} + + {/* Filled portion */} + + + + {/* Stats row */} + + + {displayPercent.toFixed(1)}% + + {scanProgress ? ( + + + {scanProgress.scannedHeight.toLocaleString()} / {scanProgress.tipHeight.toLocaleString()} + + {scanProgress.blocksPerSec > 0 && ( + + {scanProgress.blocksPerSec.toLocaleString()} blk/s + + )} + + ETA {formatEta(scanProgress.etaSeconds)} + + + ) : ( + + + Connecting... + + )} + + + )} + + {/* Scan buttons — hidden during scan */} + {scanState !== "scanning" && ( + + + + + + )} + + {scanResult && ( + + {scanResult} + + )} + + )} + + {/* Section E: Send shielded */} + {orchardAddress && ( + + + {t("sendPrivately")} + + + + + )} + + ) +} diff --git a/projects/keepkey-vault/src/mainview/components/device/PassphraseEntry.tsx b/projects/keepkey-vault/src/mainview/components/device/PassphraseEntry.tsx index a01a159..b137f69 100644 --- a/projects/keepkey-vault/src/mainview/components/device/PassphraseEntry.tsx +++ b/projects/keepkey-vault/src/mainview/components/device/PassphraseEntry.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback, useRef } from "react" -import { Box, Text, Flex, Button, Input } from "@chakra-ui/react" +import { Box, Text, Flex, Button, Input, Spinner } from "@chakra-ui/react" import { useTranslation } from "react-i18next" interface PassphraseEntryProps { - onSubmit: (passphrase: string) => void + onSubmit: (passphrase: string) => Promise onCancel: () => void } @@ -16,17 +16,26 @@ export function PassphraseEntry({ onSubmit, onCancel }: PassphraseEntryProps) { const { t } = useTranslation("device") const [passphrase, setPassphrase] = useState("") const [showPassphrase, setShowPassphrase] = useState(false) + const [submitting, setSubmitting] = useState(false) const inputRef = useRef(null) // Auto-focus the input on mount useEffect(() => { - setTimeout(() => inputRef.current?.focus(), 100) - }, []) + if (!submitting) setTimeout(() => inputRef.current?.focus(), 100) + }, [submitting]) - const handleSubmit = useCallback(() => { - onSubmit(passphrase) - setPassphrase("") - }, [passphrase, onSubmit]) + const handleSubmit = useCallback(async () => { + if (submitting) return + setSubmitting(true) + try { + await onSubmit(passphrase) + } catch { + // If sendPassphrase fails (e.g. device disconnected), reset so user can retry or cancel + setSubmitting(false) + } + // Stay in "Confirm on device" state — the parent will unmount us + // when the device state transitions away from needs_passphrase. + }, [passphrase, onSubmit, submitting]) // Keyboard: Enter on input submits; Escape anywhere dismisses const handleInputKeyDown = useCallback((e: React.KeyboardEvent) => { @@ -38,11 +47,11 @@ export function PassphraseEntry({ onSubmit, onCancel }: PassphraseEntryProps) { useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") onCancel() + if (e.key === "Escape" && !submitting) onCancel() } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) - }, [onCancel]) + }, [onCancel, submitting]) return ( - - {t("passphrase.title")} - - - {t("passphrase.description")} - - - {/* Passphrase input */} - - + + {t("passphrase.confirmOnDevice")} + + + {t("passphrase.confirmOnDeviceHint")} + + + + ) : ( + /* ── Passphrase input state ── */ + <> + + {t("passphrase.title")} + + + {t("passphrase.description")} + + + {/* Passphrase input */} + + - - {t("passphrase.warning")} - + + {t("passphrase.warning")} + - {/* Action buttons */} - - - - + {/* Action buttons */} + + + + + + )} ) diff --git a/projects/keepkey-vault/src/mainview/i18n/index.ts b/projects/keepkey-vault/src/mainview/i18n/index.ts index 3a77ee0..e754162 100644 --- a/projects/keepkey-vault/src/mainview/i18n/index.ts +++ b/projects/keepkey-vault/src/mainview/i18n/index.ts @@ -32,6 +32,7 @@ i18n .init({ lng: savedLang, fallbackLng: "en", + partialBundledLanguages: true, defaultNS: "common", ns: [ "common", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json index d64cb27..2aa87e0 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json @@ -20,7 +20,9 @@ "description": "Ihr Gerät hat den Passphrase-Schutz aktiviert. Geben Sie Ihre BIP-39-Passphrase ein, um zu entsperren. Lassen Sie das Feld leer für die Standard-Wallet.", "placeholder": "Passphrase (optional)", "warning": "Passphrasen unterscheiden Groß- und Kleinschreibung. Eine andere Passphrase erzeugt eine völlig andere Wallet.", - "unlock": "Entsperren" + "unlock": "Entsperren", + "confirmOnDevice": "Auf deinem KeepKey bestätigen", + "confirmOnDeviceHint": "Überprüfe den Bildschirm deines Geräts und drücke die Taste zum Bestätigen." }, "pairing": { "wantsToConnect": "möchte sich mit Ihrem KeepKey verbinden", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/en/asset.json b/projects/keepkey-vault/src/mainview/i18n/locales/en/asset.json index 410782e..719b9dd 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/en/asset.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/en/asset.json @@ -16,5 +16,32 @@ "hideToken": "Hide token", "revertToAutoDetect": "Revert to auto-detect", "unhide": "Unhide", - "addCustomToken": "Add custom token" + "addCustomToken": "Add custom token", + "privacy": "Privacy", + "shieldedBalance": "Shielded Balance", + "orchardAddress": "Orchard Address", + "scanPayments": "Scan for Payments", + "scanFromHeight": "Scan from Height", + "fullScan": "Full Scan (~30 min)", + "scanFromHeightPlaceholder": "Block height", + "sendPrivately": "Send Privately", + "sidecarReady": "Privacy engine ready", + "sidecarNotReady": "Privacy engine not running", + "initializePrivacy": "Initialize Privacy", + "scanning": "Scanning...", + "lastSynced": "Last synced: block {{height}}", + "confirmedBalance": "Confirmed", + "pendingBalance": "Pending", + "recipientAddress": "Recipient address", + "amountZec": "Amount (ZEC)", + "memo": "Memo (optional)", + "notesFound": "{{count}} payments found", + "initRequired": "Initialize privacy to view shielded balance", + "initializing": "Initializing...", + "sending": "Sending...", + "txSent": "Transaction sent", + "copyAddress": "Copy address", + "copied": "Copied", + "zcashCliRequired": "Build zcash-cli to enable", + "memoTooLong": "Memo exceeds 512 bytes" } diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json index b343540..67c7380 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json @@ -20,7 +20,9 @@ "description": "Your device has passphrase protection enabled. Enter your BIP-39 passphrase to unlock. Leave empty for the default wallet.", "placeholder": "Passphrase (optional)", "warning": "Passphrases are case-sensitive. A different passphrase generates a completely different wallet.", - "unlock": "Unlock" + "unlock": "Unlock", + "confirmOnDevice": "Confirm on Your KeepKey", + "confirmOnDeviceHint": "Check your device screen and press the button to confirm." }, "pairing": { "wantsToConnect": "wants to connect to your KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json index a67b63a..c37228c 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json @@ -20,7 +20,9 @@ "description": "Tu dispositivo tiene protección por contraseña habilitada. Ingresa tu contraseña BIP-39 para desbloquear. Déjalo vacío para la billetera predeterminada.", "placeholder": "Contraseña (opcional)", "warning": "Las contraseñas distinguen entre mayúsculas y minúsculas. Una contraseña diferente genera una billetera completamente diferente.", - "unlock": "Desbloquear" + "unlock": "Desbloquear", + "confirmOnDevice": "Confirmar en tu KeepKey", + "confirmOnDeviceHint": "Revisa la pantalla de tu dispositivo y presiona el botón para confirmar." }, "pairing": { "wantsToConnect": "quiere conectarse a tu KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json index ee15b92..55691c0 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json @@ -20,7 +20,9 @@ "description": "Votre appareil a la protection par phrase secrète activée. Entrez votre phrase secrète BIP-39 pour déverrouiller. Laissez vide pour le portefeuille par défaut.", "placeholder": "Phrase secrète (facultatif)", "warning": "Les phrases secrètes sont sensibles à la casse. Une phrase secrète différente génère un portefeuille complètement différent.", - "unlock": "Déverrouiller" + "unlock": "Déverrouiller", + "confirmOnDevice": "Confirmer sur votre KeepKey", + "confirmOnDeviceHint": "Vérifiez l'écran de votre appareil et appuyez sur le bouton pour confirmer." }, "pairing": { "wantsToConnect": "veut se connecter à votre KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json index fbfee0d..c5bac75 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json @@ -20,7 +20,9 @@ "description": "Il tuo dispositivo ha la protezione passphrase abilitata. Inserisci la tua passphrase BIP-39 per sbloccare. Lascia vuoto per il portafoglio predefinito.", "placeholder": "Passphrase (facoltativa)", "warning": "Le passphrase distinguono maiuscole e minuscole. Una passphrase diversa genera un portafoglio completamente diverso.", - "unlock": "Sblocca" + "unlock": "Sblocca", + "confirmOnDevice": "Conferma sul tuo KeepKey", + "confirmOnDeviceHint": "Controlla lo schermo del dispositivo e premi il pulsante per confermare." }, "pairing": { "wantsToConnect": "vuole connettersi al tuo KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json index 60fc5b2..d196b81 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json @@ -20,7 +20,9 @@ "description": "お使いのデバイスはパスフレーズ保護が有効になっています。ロックを解除するにはBIP-39パスフレーズを入力してください。デフォルトのウォレットの場合は空のままにしてください。", "placeholder": "パスフレーズ(任意)", "warning": "パスフレーズは大文字と小文字を区別します。異なるパスフレーズは完全に異なるウォレットを生成します。", - "unlock": "ロック解除" + "unlock": "ロック解除", + "confirmOnDevice": "KeepKeyで確認", + "confirmOnDeviceHint": "デバイスの画面を確認し、ボタンを押して承認してください。" }, "pairing": { "wantsToConnect": "がKeepKeyに接続しようとしています", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json index 1662838..a8e5e8c 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json @@ -20,7 +20,9 @@ "description": "기기에 비밀번호 구문 보호가 활성화되어 있습니다. 잠금 해제하려면 BIP-39 비밀번호 구문을 입력하세요. 기본 지갑을 사용하려면 비워두세요.", "placeholder": "비밀번호 구문 (선택사항)", "warning": "비밀번호 구문은 대소문자를 구분합니다. 다른 비밀번호 구문은 완전히 다른 지갑을 생성합니다.", - "unlock": "잠금 해제" + "unlock": "잠금 해제", + "confirmOnDevice": "KeepKey에서 확인", + "confirmOnDeviceHint": "기기 화면을 확인하고 버튼을 눌러 승인하세요." }, "pairing": { "wantsToConnect": "이(가) KeepKey에 연결하려고 합니다", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json index bb01c1c..2294536 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json @@ -20,7 +20,9 @@ "description": "Seu dispositivo tem proteção por frase-senha ativada. Insira sua frase-senha BIP-39 para desbloquear. Deixe vazio para a carteira padrão.", "placeholder": "Frase-senha (opcional)", "warning": "Frases-senha diferenciam maiúsculas de minúsculas. Uma frase-senha diferente gera uma carteira completamente diferente.", - "unlock": "Desbloquear" + "unlock": "Desbloquear", + "confirmOnDevice": "Confirmar no seu KeepKey", + "confirmOnDeviceHint": "Verifique a tela do seu dispositivo e pressione o botão para confirmar." }, "pairing": { "wantsToConnect": "quer se conectar ao seu KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json index e320546..9743ada 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json @@ -20,7 +20,9 @@ "description": "На вашем устройстве включена защита кодовой фразой. Введите кодовую фразу BIP-39 для разблокировки. Оставьте пустым для кошелька по умолчанию.", "placeholder": "Кодовая фраза (необязательно)", "warning": "Кодовые фразы чувствительны к регистру. Другая кодовая фраза создаёт совершенно другой кошелёк.", - "unlock": "Разблокировать" + "unlock": "Разблокировать", + "confirmOnDevice": "Подтвердите на KeepKey", + "confirmOnDeviceHint": "Проверьте экран устройства и нажмите кнопку для подтверждения." }, "pairing": { "wantsToConnect": "хочет подключиться к вашему KeepKey", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json index 4113bce..593f648 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json @@ -20,7 +20,9 @@ "description": "您的设备已启用密码短语保护。输入您的 BIP-39 密码短语以解锁。留空则使用默认钱包。", "placeholder": "密码短语(可选)", "warning": "密码短语区分大小写。不同的密码短语会生成完全不同的钱包。", - "unlock": "解锁" + "unlock": "解锁", + "confirmOnDevice": "在 KeepKey 上确认", + "confirmOnDeviceHint": "请检查设备屏幕并按下按钮确认。" }, "pairing": { "wantsToConnect": "想要连接到您的 KeepKey", diff --git a/projects/keepkey-vault/src/shared/chains.ts b/projects/keepkey-vault/src/shared/chains.ts index 9c6c23c..1587363 100644 --- a/projects/keepkey-vault/src/shared/chains.ts +++ b/projects/keepkey-vault/src/shared/chains.ts @@ -1,5 +1,6 @@ import { Chain, ChainToNetworkId, ChainToCaip, BaseDecimal } from '@pioneer-platform/pioneer-caip' import type { BtcScriptType, CustomChain } from './types' +import { versionCompare } from './firmware-versions' export interface ChainDef { id: string @@ -9,7 +10,7 @@ export interface ChainDef { networkId: string // CAIP-2 (derived from pioneer-caip) caip: string // CAIP-19 (derived from pioneer-caip) decimals: number // Base decimals (derived from pioneer-caip) - chainFamily: 'utxo' | 'evm' | 'cosmos' | 'xrp' | 'solana' + chainFamily: 'utxo' | 'evm' | 'cosmos' | 'xrp' | 'solana' | 'zcash-shielded' color: string rpcMethod: string signMethod: string @@ -19,6 +20,8 @@ export interface ChainDef { chainId?: string explorerAddressUrl?: string // e.g. "https://etherscan.io/address/{{address}}" explorerTxUrl?: string // e.g. "https://etherscan.io/tx/{{txid}}" + hidden?: boolean // If true, hide from Dashboard grid (used for internal-only chains) + minFirmware?: string // Minimum firmware version required (e.g. '7.11.0') } // ── Bitcoin multi-account constants ───────────────────────────────────── @@ -155,6 +158,21 @@ const CONFIGS: ChainConfig[] = [ rpcMethod: 'btcGetAddress', signMethod: 'btcSignTx', defaultPath: [0x8000002C, 0x80000005, 0x80000000, 0, 0], scriptType: 'p2pkh', }, + { + id: 'zcash', chain: Chain.Zcash, coin: 'Zcash', symbol: 'ZEC', + chainFamily: 'utxo', color: '#ECB244', + rpcMethod: 'btcGetAddress', signMethod: 'btcSignTx', + defaultPath: [0x8000002C, 0x80000085, 0x80000000, 0, 0], scriptType: 'p2pkh', + minFirmware: '7.11.0', + }, + { + id: 'zcash-shielded', chain: Chain.Zcash, coin: 'Zcash', symbol: 'ZEC', + chainFamily: 'zcash-shielded', color: '#ECB244', + rpcMethod: 'zcashGetOrchardFvk', signMethod: 'zcashSignPczt', + defaultPath: [0x80000020, 0x80000085, 0x80000000], // m/32'/133'/0' (ZIP-32 Orchard) + hidden: true, // Shown via Privacy tab on Zcash AssetPage, not as separate Dashboard card + minFirmware: '7.11.0', + }, { id: 'digibyte', chain: Chain.Digibyte, coin: 'DigiByte', symbol: 'DGB', chainFamily: 'utxo', color: '#315BCA', @@ -189,6 +207,13 @@ export const CHAINS: ChainDef[] = CONFIGS.map(c => ({ decimals: BaseDecimal[c.chain as keyof typeof BaseDecimal] ?? DECIMAL_FALLBACKS[c.chain] ?? 8, })) +/** Check if a chain is supported by the given firmware version. Chains without minFirmware are always supported. */ +export function isChainSupported(chain: ChainDef, firmwareVersion?: string): boolean { + if (!chain.minFirmware) return true + if (!firmwareVersion) return false + return versionCompare(firmwareVersion, chain.minFirmware) >= 0 +} + /** Convert a user-added custom EVM chain into a ChainDef */ export function customChainToChainDef(c: CustomChain): ChainDef { return { diff --git a/projects/keepkey-vault/src/shared/firmware-versions.ts b/projects/keepkey-vault/src/shared/firmware-versions.ts index f63e749..d32e899 100644 --- a/projects/keepkey-vault/src/shared/firmware-versions.ts +++ b/projects/keepkey-vault/src/shared/firmware-versions.ts @@ -146,7 +146,7 @@ export function resolveOndeviceFirmwareVersion(hash: string | undefined): string } /** Compare semver strings: returns -1 (ab) */ -function versionCompare(a: string, b: string): number { +export function versionCompare(a: string, b: string): number { const pa = a.split('.').map(Number) const pb = b.split('.').map(Number) for (let i = 0; i < 3; i++) { diff --git a/projects/keepkey-vault/src/shared/rpc-schema.ts b/projects/keepkey-vault/src/shared/rpc-schema.ts index 8477f71..b9075f9 100644 --- a/projects/keepkey-vault/src/shared/rpc-schema.ts +++ b/projects/keepkey-vault/src/shared/rpc-schema.ts @@ -98,6 +98,13 @@ export type VaultRPCSchema = ElectrobunRPCSchema & { removeTokenVisibility: { params: { caip: string }; response: void } getTokenVisibilityMap: { params: void; response: Record } + // ── Zcash Shielded (Orchard) ────────────────────────────────────── + zcashShieldedStatus: { params: void; response: { ready: boolean; fvk_loaded: boolean; address: string | null; fvk: { ak: string; nk: string; rivk: string } | null } } + zcashShieldedInit: { params: { account?: number }; response: { fvk: { ak: string; nk: string; rivk: string }; address: string } } + zcashShieldedScan: { params: { startHeight?: number; fullRescan?: boolean }; response: { balance: number; notes_found: number; synced_to: number } } + zcashShieldedBalance: { params: void; response: { confirmed: number; pending: number } } + zcashShieldedSend: { params: { recipient: string; amount: number; memo?: string }; response: { txid: string } } + // ── Camera / QR scanning ────────────────────────────────────────── startQrScan: { params: void; response: void } stopQrScan: { params: void; response: void } @@ -171,6 +178,7 @@ export type VaultRPCSchema = ElectrobunRPCSchema & { 'api-log': ApiLogEntry 'report-progress': { id: string; message: string; percent: number } 'walletconnect-uri': string + 'scan-progress': { percent: number; scannedHeight: number; tipHeight: number; blocksPerSec: number; etaSeconds: number } } } webview: { diff --git a/projects/keepkey-vault/zcash-cli/.gitignore b/projects/keepkey-vault/zcash-cli/.gitignore new file mode 100644 index 0000000..f187ec6 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/.gitignore @@ -0,0 +1,2 @@ +/target/ +/examples/ diff --git a/projects/keepkey-vault/zcash-cli/Cargo.lock b/projects/keepkey-vault/zcash-cli/Cargo.lock new file mode 100644 index 0000000..ed28e35 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/Cargo.lock @@ -0,0 +1,3241 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bellman" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afceed28bac7f9f5a508bca8aeeff51cdfa4770c0b967ac55c621e2ddfd6171" +dependencies = [ + "bitvec", + "blake2s_simd", + "byteorder", + "crossbeam-channel", + "ff", + "group", + "lazy_static", + "log", + "num_cpus", + "pairing", + "rand_core", + "rayon", + "subtle", +] + +[[package]] +name = "bip32" +version = "0.6.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143f5327f23168716be068f8e1014ba2ea16a6c91e8777bc8927da7b51e1df1f" +dependencies = [ + "bs58", + "hmac", + "rand_core", + "ripemd 0.2.0-pre.4", + "sha2 0.11.0-pre.4", + "subtle", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core", + "subtle", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.9", + "tinyvec", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.1", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equihash" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4f333d4ccc9d23c06593733673026efa71a332e028b00f12cf427b9677dce9" +dependencies = [ + "blake2b_simd", + "core2", + "document-features", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "f4jumble" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fpe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +dependencies = [ + "cbc", + "cipher", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "memuse", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "halo2_gadgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_gadgets" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_legacy_pdqsort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" + +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + +[[package]] +name = "halo2_proofs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "indexmap 1.9.3", + "maybe-rayon", + "pasta_curves", + "rand_core", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" +dependencies = [ + "digest 0.11.0-pre.9", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "incrementalmerkletree" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216c71634ac6f6ed13c2102d64354c0a04dcbdc30e31692c5972d3974d8b6d97" +dependencies = [ + "either", +] + +[[package]] +name = "incrementalmerkletree" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" +dependencies = [ + "either", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core", + "subtle", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nonempty" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orchard" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f4cf75baf85bbd6f15eb919b7e70afdc4a311eef0a3e8a053e65542fe2b58e" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "core2", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets 0.3.1", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree 0.7.1", + "lazy_static", + "memuse", + "nonempty 0.7.0", + "pasta_curves", + "rand", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec 0.1.2", + "zip32 0.1.3", +] + +[[package]] +name = "orchard" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01cd4ea711aab5f263f2b7aa6966687a2d6c7df4f78eb1b97a66a7a4e78e3b" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "core2", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets 0.4.0", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree 0.8.2", + "lazy_static", + "memuse", + "nonempty 0.11.0", + "pasta_curves", + "rand", + "rand_core", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec 0.2.1", + "zip32 0.2.1", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand", + "static_assertions", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reddsa" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +dependencies = [ + "blake2b_simd", + "byteorder", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a60db2c3bc9c6fd1e8631fee75abc008841d27144be744951d6b9b75f9b569c" +dependencies = [ + "rand_core", + "reddsa", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ripemd" +version = "0.2.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48cf93482ea998ad1302c42739bc73ab3adc574890c373ec89710e219357579" +dependencies = [ + "digest 0.11.0-pre.9", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sapling-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfff8cfce16aeb38da50b8e2ed33c9018f30552beff2210c266662a021b17f38" +dependencies = [ + "aes", + "bellman", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381", + "byteorder", + "document-features", + "ff", + "fpe", + "group", + "hex", + "incrementalmerkletree 0.7.1", + "jubjub", + "lazy_static", + "memuse", + "rand", + "rand_core", + "redjubjub", + "subtle", + "tracing", + "zcash_note_encryption", + "zcash_spec 0.1.2", + "zip32 0.1.3", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-pre.9", +] + +[[package]] +name = "shardtree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359e552886ae54d1642091645980d83f7db465fd9b5b0248e3680713c1773388" +dependencies = [ + "bitflags", + "either", + "incrementalmerkletree 0.8.2", + "tracing", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-native-certs", + "rustls-pemfile", + "socket2 0.5.10", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zcash-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake2b_simd", + "dirs", + "env_logger", + "ff", + "hex", + "incrementalmerkletree 0.8.2", + "log", + "orchard 0.12.0", + "pasta_curves", + "prost", + "rand", + "rusqlite", + "serde", + "serde_json", + "shardtree", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "zcash_address 0.10.1", + "zcash_keys", + "zcash_note_encryption", + "zcash_primitives", + "zcash_protocol 0.7.2", + "zip32 0.2.1", +] + +[[package]] +name = "zcash_address" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32b380113014b136aec579ea1c07fef747a818b9ac97d91daa0ec3b7a642bc0" +dependencies = [ + "bech32", + "bs58", + "core2", + "f4jumble", + "zcash_encoding 0.2.2", + "zcash_protocol 0.4.3", +] + +[[package]] +name = "zcash_address" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71591bb4eb2fd7622e88eed42e7d7d8501cd1e920a0698c7fb08723a8c1d0b4f" +dependencies = [ + "bech32", + "bs58", + "core2", + "f4jumble", + "zcash_encoding 0.3.0", + "zcash_protocol 0.5.4", +] + +[[package]] +name = "zcash_address" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4491dddd232de02df42481757054dc19c8bc51cf709cfec58feebfef7c3c9a" +dependencies = [ + "bech32", + "bs58", + "core2", + "f4jumble", + "zcash_encoding 0.3.0", + "zcash_protocol 0.7.2", +] + +[[package]] +name = "zcash_encoding" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3654116ae23ab67dd1f849b01f8821a8a156f884807ff665eac109bf28306c4d" +dependencies = [ + "core2", + "nonempty 0.7.0", +] + +[[package]] +name = "zcash_encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca38087e6524e5f51a5b0fb3fc18f36d7b84bf67b2056f494ca0c281590953d" +dependencies = [ + "core2", + "nonempty 0.11.0", +] + +[[package]] +name = "zcash_keys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ac4adc9d1e6442d4ef700b983c1f8ba8f701c8c215490562546be475aff240" +dependencies = [ + "bech32", + "blake2b_simd", + "bls12_381", + "bs58", + "core2", + "document-features", + "group", + "memuse", + "nonempty 0.11.0", + "rand_core", + "secrecy", + "subtle", + "tracing", + "zcash_address 0.7.1", + "zcash_encoding 0.3.0", + "zcash_protocol 0.5.4", + "zcash_transparent", + "zip32 0.2.1", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core", + "subtle", +] + +[[package]] +name = "zcash_primitives" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d66e2f114bf81094bc5bf74860048ea1ae3911c424567cb9cb4d0cbb71ec7a" +dependencies = [ + "aes", + "blake2b_simd", + "bs58", + "byteorder", + "document-features", + "equihash", + "ff", + "fpe", + "group", + "hex", + "incrementalmerkletree 0.7.1", + "jubjub", + "memuse", + "nonempty 0.7.0", + "orchard 0.10.2", + "rand", + "rand_core", + "redjubjub", + "sapling-crypto", + "sha2 0.10.9", + "subtle", + "tracing", + "zcash_address 0.6.3", + "zcash_encoding 0.2.2", + "zcash_note_encryption", + "zcash_protocol 0.4.3", + "zcash_spec 0.1.2", + "zip32 0.1.3", +] + +[[package]] +name = "zcash_protocol" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cb36b15b5a1be70b30c32ce40372dead6561df8a467e297f96b892873a63a2" +dependencies = [ + "core2", + "document-features", + "hex", + "memuse", +] + +[[package]] +name = "zcash_protocol" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42344f5735237d6e0eedd3680f1c92f64e9c4144045d7b5c82f4867c2cbc0a02" +dependencies = [ + "core2", + "hex", +] + +[[package]] +name = "zcash_protocol" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b1a337bbc9a7d55ae35d31189f03507dbc7934e9a4bee5c1d5c47464860e48" +dependencies = [ + "core2", + "document-features", + "hex", + "memuse", +] + +[[package]] +name = "zcash_spec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cede95491c2191d3e278cab76e097a44b17fde8d6ca0d4e3a22cf4807b2d857" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_spec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_transparent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd8c2d138ec893d3d384d97304da9ff879424056087c8ac811780a0e8d96a99" +dependencies = [ + "bip32", + "blake2b_simd", + "bs58", + "core2", + "getset", + "hex", + "ripemd 0.1.3", + "sha2 0.10.9", + "subtle", + "zcash_address 0.7.1", + "zcash_encoding 0.3.0", + "zcash_protocol 0.5.4", + "zcash_spec 0.2.1", + "zip32 0.2.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip32" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9943793abf9060b68e1889012dafbd5523ab5b125c0fcc24802d69182f2ac9" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec 0.1.2", +] + +[[package]] +name = "zip32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64bf5186a8916f7a48f2a98ef599bf9c099e2458b36b819e393db1c0e768c4b" +dependencies = [ + "bech32", + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec 0.2.1", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/projects/keepkey-vault/zcash-cli/Cargo.toml b/projects/keepkey-vault/zcash-cli/Cargo.toml new file mode 100644 index 0000000..62f4f77 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "zcash-cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "zcash-cli" +path = "src/main.rs" + +[dependencies] +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Error handling +anyhow = "1.0" + +# Hex encoding/decoding +hex = "0.4" + +# Logging +log = "0.4" +env_logger = "0.11" + +# BLAKE2b for ZIP-244 sighash computation +blake2b_simd = "1.0" + +# Zcash crates +orchard = "0.12" +zcash_address = "0.10" +zcash_protocol = "0.7" +zcash_note_encryption = "0.4" +zcash_primitives = "0.19" +zcash_keys = "0.8" + +# Lightwalletd gRPC client +tonic = { version = "0.12", features = ["tls", "tls-roots"] } +prost = "0.13" +tokio-stream = "0.1" + +# Wallet database (note persistence) +rusqlite = { version = "0.32", features = ["bundled"] } +dirs = "5.0" + +# Orchard spending (PCZT construction + proofs) +rand = "0.8" +pasta_curves = "0.5" +ff = "0.13" +incrementalmerkletree = "0.8" +shardtree = "0.6" + +# ZIP-32 key derivation types +zip32 = "0.2" + +[build-dependencies] +tonic-build = "0.12" diff --git a/projects/keepkey-vault/zcash-cli/build.rs b/projects/keepkey-vault/zcash-cli/build.rs new file mode 100644 index 0000000..a482ce1 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/build.rs @@ -0,0 +1,9 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(false) + .compile_protos( + &["proto/service.proto", "proto/compact_formats.proto"], + &["proto/"], + )?; + Ok(()) +} diff --git a/projects/keepkey-vault/zcash-cli/proto/compact_formats.proto b/projects/keepkey-vault/zcash-cli/proto/compact_formats.proto new file mode 100644 index 0000000..2e39319 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/proto/compact_formats.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; + +message ChainMetadata { + uint32 saplingCommitmentTreeSize = 1; + uint32 orchardCommitmentTreeSize = 2; +} + +message CompactBlock { + uint32 protoVersion = 1; + uint64 height = 2; + bytes hash = 3; + bytes prevHash = 4; + uint32 time = 5; + bytes header = 6; + repeated CompactTx vtx = 7; + ChainMetadata chainMetadata = 8; +} + +message CompactTx { + uint64 index = 1; + bytes txid = 2; + uint32 fee = 3; + repeated CompactSaplingSpend spends = 4; + repeated CompactSaplingOutput outputs = 5; + repeated CompactOrchardAction actions = 6; + repeated CompactTxIn vin = 7; + repeated TxOut vout = 8; +} + +message CompactTxIn { + bytes prevoutTxid = 1; + uint32 prevoutIndex = 2; +} + +message TxOut { + uint64 value = 1; + bytes scriptPubKey = 2; +} + +message CompactSaplingSpend { + bytes nf = 1; +} + +message CompactSaplingOutput { + bytes cmu = 1; + bytes ephemeralKey = 2; + bytes ciphertext = 3; +} + +message CompactOrchardAction { + bytes nullifier = 1; + bytes cmx = 2; + bytes ephemeralKey = 3; + bytes ciphertext = 4; +} diff --git a/projects/keepkey-vault/zcash-cli/proto/service.proto b/projects/keepkey-vault/zcash-cli/proto/service.proto new file mode 100644 index 0000000..8ca2414 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/proto/service.proto @@ -0,0 +1,129 @@ +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +import "compact_formats.proto"; + +message BlockID { + uint64 height = 1; + bytes hash = 2; +} + +message BlockRange { + BlockID start = 1; + BlockID end = 2; +} + +message TxFilter { + BlockID block = 1; + uint64 index = 2; + bytes hash = 3; +} + +message RawTransaction { + bytes data = 1; + uint64 height = 2; +} + +message SendResponse { + int32 errorCode = 1; + string errorMessage = 2; +} + +message ChainSpec {} + +message Empty {} + +message LightdInfo { + string version = 1; + string vendor = 2; + bool taddrSupport = 3; + string chainName = 4; + uint64 saplingActivationHeight = 5; + string consensusBranchId = 6; + uint64 blockHeight = 7; + string gitCommit = 8; + string branch = 9; + string buildDate = 10; + string buildUser = 11; + uint64 estimatedHeight = 12; + string zcashdBuild = 13; + string zcashdSubversion = 14; + string donationAddress = 15; +} + +message TransparentAddressBlockFilter { + string address = 1; + BlockRange range = 2; +} + +message Address { + string address = 1; +} + +message AddressList { + repeated string addresses = 1; +} + +message Balance { + int64 valueZat = 1; +} + +message GetAddressUtxosArg { + repeated string addresses = 1; + uint64 startHeight = 2; + uint32 maxEntries = 3; +} + +message GetAddressUtxosReply { + string address = 6; + bytes txid = 1; + int32 index = 2; + bytes script = 3; + int64 valueZat = 4; + uint64 height = 5; +} + +message GetAddressUtxosReplyList { + repeated GetAddressUtxosReply addressUtxos = 1; +} + +enum ShieldedProtocol { + sapling = 0; + orchard = 1; +} + +message SubtreeRoot { + bytes rootHash = 2; + bytes completingBlockHash = 3; + uint64 completingBlockHeight = 4; +} + +message GetSubtreeRootsArg { + uint32 startIndex = 1; + ShieldedProtocol shieldedProtocol = 2; + uint32 maxEntries = 3; +} + +message TreeState { + string network = 1; + uint64 height = 2; + string hash = 3; + uint32 time = 4; + string saplingTree = 5; + string orchardTree = 6; +} + +service CompactTxStreamer { + rpc GetLatestBlock(ChainSpec) returns (BlockID) {} + rpc GetBlock(BlockID) returns (CompactBlock) {} + rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {} + rpc GetTransaction(TxFilter) returns (RawTransaction) {} + rpc SendTransaction(RawTransaction) returns (SendResponse) {} + rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {} + rpc GetTaddressBalance(AddressList) returns (Balance) {} + rpc GetTaddressBalanceStream(stream Address) returns (Balance) {} + rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {} + rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {} + rpc GetLightdInfo(Empty) returns (LightdInfo) {} + rpc GetSubtreeRoots(GetSubtreeRootsArg) returns (stream SubtreeRoot) {} + rpc GetTreeState(BlockID) returns (TreeState) {} +} diff --git a/projects/keepkey-vault/zcash-cli/src/main.rs b/projects/keepkey-vault/zcash-cli/src/main.rs new file mode 100644 index 0000000..3833685 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/src/main.rs @@ -0,0 +1,548 @@ +//! Zcash CLI sidecar for KeepKey Vault v11. +//! +//! Communicates with Electrobun via NDJSON (newline-delimited JSON) over stdin/stdout. +//! Handles chain scanning, PCZT construction, Halo2 proving, and transaction finalization. +//! NEVER opens the KeepKey device — Electrobun owns USB exclusively. + +mod wallet_db; +mod zip244; +mod scanner; +mod pczt_builder; + +use anyhow::Result; +use log::{info, error}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::io::{self, BufRead, Write}; + +use orchard::keys::FullViewingKey; +use zcash_address::unified::{self, Container, Encoding}; +use zcash_protocol::consensus::NetworkType; + +/// Global state persisted across IPC commands within a single sidecar session. +struct State { + db: Option, + fvk: Option, + /// Pending PCZT state waiting for signatures + pending_pczt: Option, +} + +impl State { + fn new() -> Self { + Self { + db: None, + fvk: None, + pending_pczt: None, + } + } + + fn ensure_db(&mut self) -> Result<&wallet_db::WalletDb> { + if self.db.is_none() { + self.db = Some(wallet_db::WalletDb::open_default()?); + } + Ok(self.db.as_ref().unwrap()) + } + + /// Try to load a previously saved FVK from the database. + fn try_load_fvk(&mut self) -> Result { + let db = self.ensure_db()?; + if let Some(fvk_bytes) = db.load_fvk()? { + match FullViewingKey::from_bytes(&fvk_bytes) { + Some(fvk) => { + let addr = fvk.address_at(0u32, orchard::keys::Scope::External); + let ua = encode_unified_address(&addr)?; + info!("Auto-loaded FVK from database, UA: {}...", &ua[..20]); + self.fvk = Some(fvk); + Ok(true) + } + None => { + error!("Saved FVK is corrupt — ignoring"); + Ok(false) + } + } + } else { + Ok(false) + } + } +} + +// ── IPC Message Types ────────────────────────────────────────────────── + +#[derive(Deserialize)] +struct IpcRequest { + cmd: String, + #[serde(default)] + _req_id: Option, + #[serde(flatten)] + params: Value, +} + +#[derive(Serialize)] +struct IpcResponse { + ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + _req_id: Option, + #[serde(flatten)] + data: Value, +} + +fn ok_response_with_id(data: Value, req_id: Option) -> IpcResponse { + IpcResponse { ok: true, _req_id: req_id, data } +} + +fn err_response_with_id(msg: &str, req_id: Option) -> IpcResponse { + IpcResponse { + ok: false, + _req_id: req_id, + data: serde_json::json!({ "error": msg }), + } +} + +/// Encode an Orchard address as a Zcash Unified Address (Bech32m, `u1...`). +fn encode_unified_address(addr: &orchard::Address) -> Result { + let raw = addr.to_raw_address_bytes(); + let receiver = unified::Receiver::Orchard(raw); + let ua = unified::Address::try_from_items(vec![receiver]) + .map_err(|e| anyhow::anyhow!("Failed to build Unified Address: {:?}", e))?; + Ok(ua.encode(&NetworkType::Main)) +} + +/// Parse a recipient address string into an Orchard Address. +/// +/// Supports: +/// - Unified Address (`u1...`) — extracts the Orchard receiver +/// - Transparent (`t1...`) — returns error (deshielding not yet supported) +/// - Raw hex (86 chars = 43 bytes) — legacy/debug path +fn parse_recipient_address(addr: &str) -> Result { + let trimmed = addr.trim(); + + // Unified Address (u1...) + if trimmed.starts_with("u1") { + let (network, ua) = unified::Address::decode(trimmed) + .map_err(|e| anyhow::anyhow!("Invalid Unified Address: {:?}", e))?; + if network != NetworkType::Main { + return Err(anyhow::anyhow!("Expected mainnet address, got {:?}", network)); + } + // Look for the Orchard receiver + for receiver in ua.items() { + if let unified::Receiver::Orchard(raw) = receiver { + return orchard::Address::from_raw_address_bytes(&raw) + .into_option() + .ok_or_else(|| anyhow::anyhow!("Corrupt Orchard receiver in UA")); + } + } + return Err(anyhow::anyhow!("Unified Address has no Orchard receiver — cannot send from shielded pool")); + } + + // Transparent address (t1... / t3...) + if trimmed.starts_with("t1") || trimmed.starts_with("t3") { + return Err(anyhow::anyhow!( + "Deshielding (Orchard → transparent) is not yet supported. \ + Please send to a Unified Address (u1...) that contains an Orchard receiver." + )); + } + + // Raw hex fallback (43 bytes = 86 hex chars) + let bytes = hex::decode(trimmed) + .map_err(|_| anyhow::anyhow!("Invalid address — expected u1... (Unified) or t1... (transparent)"))?; + let arr: [u8; 43] = bytes.try_into() + .map_err(|_| anyhow::anyhow!("Raw hex address must be 43 bytes (86 hex chars)"))?; + orchard::Address::from_raw_address_bytes(&arr) + .into_option() + .ok_or_else(|| anyhow::anyhow!("Invalid raw Orchard address bytes")) +} + +// ── Command handlers ─────────────────────────────────────────────────── + +async fn handle_derive_fvk(state: &mut State, params: &Value) -> Result { + let seed_hex = params.get("seed_hex") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing seed_hex"))?; + let account = params.get("account") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as u32; + + let seed_bytes = hex::decode(seed_hex)?; + + // Derive Orchard FVK from seed using ZIP-32 + use orchard::keys::SpendingKey; + let account_id = zip32::AccountId::try_from(account) + .map_err(|_| anyhow::anyhow!("Invalid account index: {}", account))?; + let sk = SpendingKey::from_zip32_seed(&seed_bytes, 133, account_id) + .map_err(|_| anyhow::anyhow!("Invalid seed for ZIP-32 derivation"))?; + let fvk = FullViewingKey::from(&sk); + + // Get default address and encode as Unified Address (u1...) + let addr = fvk.address_at(0u32, orchard::keys::Scope::External); + let ua_string = encode_unified_address(&addr)?; + + // Store FVK for later use + persist to DB + state.fvk = Some(fvk.clone()); + let fvk_bytes = fvk.to_bytes(); + let _ = state.ensure_db().and_then(|db| db.save_fvk(&fvk_bytes)); + + let ak = hex::encode(&fvk_bytes[..32]); + let nk = hex::encode(&fvk_bytes[32..64]); + let rivk = hex::encode(&fvk_bytes[64..96]); + + Ok(serde_json::json!({ + "fvk": { "ak": ak, "nk": nk, "rivk": rivk }, + "address": ua_string, + })) +} + +/// Accept a FullViewingKey directly from the device (no seed needed). +/// The device exports {ak, nk, rivk} as hex strings (32 bytes each, 96 total). +async fn handle_set_fvk(state: &mut State, params: &Value) -> Result { + let ak_hex = params.get("ak") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing ak"))?; + let nk_hex = params.get("nk") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing nk"))?; + let rivk_hex = params.get("rivk") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing rivk"))?; + + let ak_bytes = hex::decode(ak_hex)?; + let nk_bytes = hex::decode(nk_hex)?; + let rivk_bytes = hex::decode(rivk_hex)?; + + if ak_bytes.len() != 32 || nk_bytes.len() != 32 || rivk_bytes.len() != 32 { + return Err(anyhow::anyhow!("Each FVK component must be 32 bytes")); + } + + // Reconstruct the 96-byte FVK encoding: ak || nk || rivk + let mut fvk_bytes = [0u8; 96]; + fvk_bytes[..32].copy_from_slice(&ak_bytes); + fvk_bytes[32..64].copy_from_slice(&nk_bytes); + fvk_bytes[64..96].copy_from_slice(&rivk_bytes); + + info!("set_fvk: ak = {}", ak_hex); + info!("set_fvk: nk = {}", nk_hex); + info!("set_fvk: rivk= {}", rivk_hex); + + // Diagnostic: try decompressing ak as a Pallas point + let ak_valid = { + use pasta_curves::group::GroupEncoding; + let ak_arr: [u8; 32] = ak_bytes.clone().try_into().unwrap(); + let ak_point = pasta_curves::pallas::Affine::from_bytes(&ak_arr); + let valid = bool::from(ak_point.is_some()); + info!("set_fvk: ak decompresses as valid Pallas point? {}", valid); + + if !valid { + // Check: is the x-coord on the curve? (x^3 + 5 must be a QR) + let mut x_bytes = ak_arr; + x_bytes[31] &= 0x7f; // clear sign bit + info!("set_fvk: ak x-coord (sign cleared) = {}", hex::encode(&x_bytes)); + + // Also verify SpendAuth basepoint bytes are valid + let spendauth_bytes: [u8; 32] = [ + 0x63, 0xc9, 0x75, 0xb8, 0x84, 0x72, 0x1a, 0x8d, + 0x0c, 0xa1, 0x70, 0x7b, 0xe3, 0x0c, 0x7f, 0x0c, + 0x5f, 0x44, 0x5f, 0x3e, 0x7c, 0x18, 0x8d, 0x3b, + 0x06, 0xd6, 0xf1, 0x28, 0xb3, 0x23, 0x55, 0xb7, + ]; + let sa_point = pasta_curves::pallas::Affine::from_bytes(&spendauth_bytes); + let sa_valid = bool::from(sa_point.is_some()); + info!("set_fvk: SpendAuth basepoint decompresses? {}", sa_valid); + } + valid + }; + + // Diagnostic: validate each FVK component individually + let nk_valid = { + use ff::PrimeField; + let nk_arr: [u8; 32] = nk_bytes.clone().try_into().unwrap(); + let nk_field = pasta_curves::pallas::Base::from_repr(nk_arr); + let valid = bool::from(nk_field.is_some()); + info!("set_fvk: nk valid as Pallas base field element? {}", valid); + valid + }; + let rivk_valid = { + use ff::PrimeField; + let rivk_arr: [u8; 32] = rivk_bytes.clone().try_into().unwrap(); + let rivk_scalar = pasta_curves::pallas::Scalar::from_repr(rivk_arr); + let valid = bool::from(rivk_scalar.is_some()); + info!("set_fvk: rivk valid as Pallas scalar? {}", valid); + valid + }; + + let fvk = FullViewingKey::from_bytes(&fvk_bytes) + .ok_or_else(|| anyhow::anyhow!( + "Invalid FVK bytes — decode failed. ak_valid={}, nk_valid={}, rivk_valid={}. \ + ak={}, nk={}, rivk={}", + ak_valid, nk_valid, rivk_valid, ak_hex, nk_hex, rivk_hex + ))?; + + // Get default address and encode as Unified Address (u1...) + let addr = fvk.address_at(0u32, orchard::keys::Scope::External); + let ua_string = encode_unified_address(&addr)?; + + info!("FVK set from device, UA: {}...", &ua_string[..20]); + state.fvk = Some(fvk); + + // Check if FVK changed (e.g. firmware basepoint fix) and auto-reset if so + if let Ok(db) = state.ensure_db() { + if let Ok(false) = db.fvk_matches(&fvk_bytes) { + info!("FVK ak fingerprint changed (firmware update?) — resetting wallet DB"); + let _ = db.reset(); + } + let _ = db.save_fvk(&fvk_bytes); + } + + Ok(serde_json::json!({ + "fvk": { "ak": ak_hex, "nk": nk_hex, "rivk": rivk_hex }, + "address": ua_string, + })) +} + +async fn handle_scan(state: &mut State, params: &Value) -> Result { + let fvk = state.fvk.as_ref() + .ok_or_else(|| anyhow::anyhow!("No FVK set — call derive_fvk first"))? + .clone(); + + let start_height = params.get("start_height") + .and_then(|v| v.as_u64()); + let full_rescan = params.get("full_rescan") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let db = state.ensure_db()?; + + // Full rescan: clear DB scan progress so scanner starts from Orchard activation + if full_rescan { + info!("Full rescan requested — clearing scan progress"); + db.clear_scan_progress()?; + } + + let mut client = scanner::LightwalletClient::connect(None).await?; + let result = client.scan_with_persistence(&fvk, db, start_height).await?; + + Ok(serde_json::json!({ + "balance": result.total_received, + "notes_found": result.notes_found, + "blocks_scanned": result.blocks_scanned, + "synced_to": result.tip_height, + })) +} + +async fn handle_balance(state: &mut State, _params: &Value) -> Result { + let db = state.ensure_db()?; + let balance = db.get_balance()?; + let (total, unspent) = db.get_note_count()?; + + Ok(serde_json::json!({ + "confirmed": balance, + "pending": 0, + "notes_total": total, + "notes_unspent": unspent, + })) +} + +async fn handle_build_pczt(state: &mut State, params: &Value) -> Result { + let fvk = state.fvk.as_ref() + .ok_or_else(|| anyhow::anyhow!("No FVK set — call derive_fvk first"))? + .clone(); + + let recipient_str = params.get("recipient") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing recipient"))?; + let amount = params.get("amount") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow::anyhow!("Missing amount"))?; + let account = params.get("account") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as u32; + + // Parse optional memo (UTF-8 text, max 512 bytes) + let memo = params.get("memo") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + + // Parse recipient address — supports UA (u1...), transparent (t1...), or raw hex + let recipient = parse_recipient_address(recipient_str)?; + + // Get spendable notes + let db = state.ensure_db()?; + let notes = db.get_spendable_notes()?; + if notes.is_empty() { + return Err(anyhow::anyhow!("No spendable notes — scan first")); + } + + // Query current consensus branch ID from lightwalletd + let mut lwd_client = scanner::LightwalletClient::connect(None).await?; + let branch_id = lwd_client.get_consensus_branch_id().await?; + info!("Using consensus branch ID: 0x{:08x}", branch_id); + + // Build PCZT with real chain tree data + let pczt_state = pczt_builder::build_pczt( + &fvk, notes, recipient, amount, account, branch_id, + &mut lwd_client, db, memo, + ).await?; + + let signing_request = serde_json::to_value(&pczt_state.signing_request)?; + + // Store the PCZT state for finalization + state.pending_pczt = Some(pczt_state); + + Ok(serde_json::json!({ + "signing_request": signing_request, + })) +} + +async fn handle_finalize(state: &mut State, params: &Value) -> Result { + let pczt_state = state.pending_pczt.take() + .ok_or_else(|| anyhow::anyhow!("No pending PCZT — call build_pczt first"))?; + + let sigs_json = params.get("signatures") + .and_then(|v| v.as_array()) + .ok_or_else(|| anyhow::anyhow!("Missing signatures array"))?; + + let mut signatures: Vec> = Vec::new(); + for sig_val in sigs_json { + let sig_hex = sig_val.as_str() + .ok_or_else(|| anyhow::anyhow!("Signature must be hex string"))?; + let sig_bytes = hex::decode(sig_hex)?; + signatures.push(sig_bytes); + } + + let (raw_tx, txid) = pczt_builder::finalize_pczt( + pczt_state.pczt_bundle, + pczt_state.sighash, + pczt_state.branch_id, + &signatures, + )?; + + Ok(serde_json::json!({ + "raw_tx": hex::encode(&raw_tx), + "txid": txid, + })) +} + +async fn handle_broadcast(_state: &mut State, params: &Value) -> Result { + let raw_tx_hex = params.get("raw_tx") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing raw_tx"))?; + + let raw_tx = hex::decode(raw_tx_hex)?; + + let mut client = scanner::LightwalletClient::connect(None).await?; + let txid = client.send_transaction(&raw_tx).await?; + + Ok(serde_json::json!({ + "txid": txid, + })) +} + +// ── Main IPC loop ────────────────────────────────────────────────────── + +#[tokio::main] +async fn main() { + env_logger::Builder::from_env( + env_logger::Env::default().default_filter_or("info") + ) + // Log to stderr so stdout stays clean for NDJSON IPC + .target(env_logger::Target::Stderr) + .init(); + + info!("zcash-cli sidecar starting"); + + let mut state = State::new(); + let stdin = io::stdin(); + let stdout = io::stdout(); + + // Try to auto-load FVK from database + let has_fvk = match state.try_load_fvk() { + Ok(loaded) => loaded, + Err(e) => { error!("Failed to auto-load FVK: {}", e); false } + }; + + // Build ready signal with FVK status + let ready_data = if has_fvk { + let fvk = state.fvk.as_ref().unwrap(); + let addr = fvk.address_at(0u32, orchard::keys::Scope::External); + let ua = encode_unified_address(&addr).unwrap_or_default(); + let fvk_bytes = fvk.to_bytes(); + serde_json::json!({ + "ok": true, "ready": true, "version": "0.1.0", + "fvk_loaded": true, + "address": ua, + "fvk": { + "ak": hex::encode(&fvk_bytes[..32]), + "nk": hex::encode(&fvk_bytes[32..64]), + "rivk": hex::encode(&fvk_bytes[64..96]), + } + }) + } else { + serde_json::json!({"ok": true, "ready": true, "version": "0.1.0", "fvk_loaded": false}) + }; + + // Send ready signal + { + let mut out = stdout.lock(); + serde_json::to_writer(&mut out, &ready_data).ok(); + writeln!(out).ok(); + out.flush().ok(); + } + + for line_result in stdin.lock().lines() { + let line = match line_result { + Ok(l) => l, + Err(e) => { + error!("stdin read error: {}", e); + break; + } + }; + + if line.trim().is_empty() { + continue; + } + + let request: IpcRequest = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + let resp = err_response_with_id(&format!("Invalid JSON: {}", e), None); + let mut out = stdout.lock(); + serde_json::to_writer(&mut out, &resp).ok(); + writeln!(out).ok(); + out.flush().ok(); + continue; + } + }; + + info!("Received command: {}", request.cmd); + + let result = match request.cmd.as_str() { + "derive_fvk" => handle_derive_fvk(&mut state, &request.params).await, + "set_fvk" => handle_set_fvk(&mut state, &request.params).await, + "scan" => handle_scan(&mut state, &request.params).await, + "balance" => handle_balance(&mut state, &request.params).await, + "build_pczt" => handle_build_pczt(&mut state, &request.params).await, + "finalize" => handle_finalize(&mut state, &request.params).await, + "broadcast" => handle_broadcast(&mut state, &request.params).await, + "ping" => Ok(serde_json::json!({"pong": true})), + "quit" => { + info!("Received quit command"); + break; + } + other => Err(anyhow::anyhow!("Unknown command: {}", other)), + }; + + let response = match result { + Ok(data) => ok_response_with_id(data, request._req_id), + Err(e) => { + error!("Command {} failed: {}", request.cmd, e); + err_response_with_id(&e.to_string(), request._req_id) + } + }; + + let mut out = stdout.lock(); + serde_json::to_writer(&mut out, &response).ok(); + writeln!(out).ok(); + out.flush().ok(); + } + + info!("zcash-cli sidecar exiting"); +} diff --git a/projects/keepkey-vault/zcash-cli/src/pczt_builder.rs b/projects/keepkey-vault/zcash-cli/src/pczt_builder.rs new file mode 100644 index 0000000..f6a77f2 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/src/pczt_builder.rs @@ -0,0 +1,720 @@ +//! Orchard PCZT construction and finalization for hardware wallet signing. +//! +//! Adapted from v10 orchard_send.rs — instead of streaming to device directly, +//! this module outputs a JSON signing request that Electrobun forwards to the +//! KeepKey device, then accepts signatures back for finalization. +//! +//! The sidecar NEVER opens the device — it only does crypto/proving. + +use anyhow::{Result, Context}; +use log::{info, debug}; +use rand::rngs::OsRng; +use serde::Serialize; + +use orchard::{ + builder::{Builder, BundleType}, + circuit::ProvingKey, + keys::{FullViewingKey, Scope}, + note::{ExtractedNoteCommitment, RandomSeed, Rho}, + tree::MerkleHashOrchard, + value::NoteValue, + Note, Address, Anchor, +}; +use orchard::primitives::redpallas::{self, SpendAuth}; +use ff::PrimeField; +use incrementalmerkletree::{Marking, Retention}; +use shardtree::{store::memory::MemoryShardStore, ShardTree}; + +use crate::scanner::LightwalletClient; +use crate::wallet_db::{SpendableNote, WalletDb}; +use crate::zip244; + +const DEFAULT_FEE: u64 = 10000; // 0.0001 ZEC + +/// Per-action fields needed by the device for signing + digest verification. +#[derive(Debug, Clone, Serialize)] +#[allow(dead_code)] +pub struct ActionFields { + pub index: u32, + #[serde(with = "hex_bytes")] + pub alpha: Vec, + #[serde(with = "hex_bytes")] + pub cv_net: Vec, + #[serde(with = "hex_bytes")] + pub nullifier: Vec, + #[serde(with = "hex_bytes")] + pub cmx: Vec, + #[serde(with = "hex_bytes")] + pub epk: Vec, + #[serde(with = "hex_bytes")] + pub enc_compact: Vec, + #[serde(with = "hex_bytes")] + pub enc_memo: Vec, + #[serde(with = "hex_bytes")] + pub enc_noncompact: Vec, + #[serde(with = "hex_bytes")] + pub rk: Vec, + #[serde(with = "hex_bytes")] + pub out_ciphertext: Vec, + pub value: u64, + pub is_spend: bool, +} + +/// The signing request sent to Electrobun, which forwards fields to the device. +#[derive(Debug, Serialize)] +pub struct SigningRequest { + pub n_actions: u32, + pub account: u32, + pub branch_id: u32, + #[serde(with = "hex_bytes")] + pub sighash: Vec, + pub digests: DigestFields, + pub bundle_meta: BundleMeta, + pub actions: Vec, + pub display: DisplayInfo, +} + +#[derive(Debug, Serialize)] +pub struct DigestFields { + #[serde(with = "hex_bytes")] + pub header: Vec, + #[serde(with = "hex_bytes")] + pub transparent: Vec, + #[serde(with = "hex_bytes")] + pub sapling: Vec, + #[serde(with = "hex_bytes")] + pub orchard: Vec, +} + +#[derive(Debug, Serialize)] +pub struct BundleMeta { + pub flags: u32, + pub value_balance: i64, + #[serde(with = "hex_bytes")] + pub anchor: Vec, +} + +#[derive(Debug, Serialize)] +pub struct DisplayInfo { + pub amount: String, + pub fee: String, + pub to: String, +} + +/// Hex-encoded bytes serializer for serde +mod hex_bytes { + use serde::Serializer; + pub fn serialize(bytes: &Vec, s: S) -> Result { + s.serialize_str(&hex::encode(bytes)) + } +} + +/// Intermediate state between build_pczt and finalize — holds the PCZT bundle +/// and metadata needed to apply signatures. +pub struct PcztState { + pub pczt_bundle: orchard::pczt::Bundle, + pub sighash: [u8; 32], + pub branch_id: u32, + pub signing_request: SigningRequest, +} + +/// Build a PCZT and extract the signing request. +/// +/// Returns a PcztState that can be finalized with device signatures. +/// Now async — fetches real chain data to build a valid Merkle tree anchor. +pub async fn build_pczt( + fvk: &FullViewingKey, + notes: Vec, + recipient: Address, + amount: u64, + account: u32, + branch_id: u32, + lwd_client: &mut LightwalletClient, + db: &WalletDb, + memo: Option, +) -> Result { + let mut rng = OsRng; + let fee = DEFAULT_FEE; + let total_input: u64 = notes.iter().map(|n| n.value).sum(); + let change = total_input.checked_sub(amount + fee) + .ok_or_else(|| anyhow::anyhow!( + "Insufficient funds: have {} ZAT, need {} ZAT (amount {} + fee {})", + total_input, amount + fee, amount, fee + ))?; + + let fvk_bytes = fvk.to_bytes(); + let ak_bytes = &fvk_bytes[..32]; + debug!("FVK ak (first 4 bytes): {}", hex::encode(&ak_bytes[..4])); + + info!("Building Orchard transaction:"); + info!(" Inputs: {} ZAT from {} notes", total_input, notes.len()); + info!(" Amount: {} ZAT", amount); + info!(" Fee: {} ZAT", fee); + info!(" Change: {} ZAT", change); + + // Step 1: Compute note positions in the global commitment tree + // For each note, we need its absolute position: + // position = tree_size_at(block_height - 1) + actions_in_block_before_note + // + // tree_size_at(h) comes from CompactBlock.chainMetadata.orchardCommitmentTreeSize at height h, + // which gives the CUMULATIVE tree size AFTER that block's actions are added. + // So tree_size_at(h-1) is the size BEFORE block h's actions. + + let mut note_positions: Vec = Vec::new(); + + for (i, spendable) in notes.iter().enumerate() { + if let Some(pos) = spendable.position { + info!("Note {} already has position {} (cached)", i, pos); + note_positions.push(pos); + continue; + } + + // Get tree size at the block BEFORE this note's block + let tree_size_before = if spendable.block_height > 0 { + lwd_client.get_orchard_tree_size_at(spendable.block_height - 1).await? + } else { + 0 + }; + + // Fetch the note's block to count actions before our note + let blocks = lwd_client.fetch_block_actions( + spendable.block_height, + spendable.block_height, + ).await?; + + let mut actions_before = 0u64; + if let Some((_, txs)) = blocks.first() { + for (tx_idx, cmxs) in txs { + if *tx_idx < spendable.tx_index { + actions_before += cmxs.len() as u64; + } else if *tx_idx == spendable.tx_index { + actions_before += spendable.action_index as u64; + break; + } + } + } + + let position = tree_size_before + actions_before; + info!("Note {}: block={}, tx_idx={}, action_idx={}, tree_before={}, actions_before={}, position={}", + i, spendable.block_height, spendable.tx_index, spendable.action_index, + tree_size_before, actions_before, position); + + // Cache the position in the database + db.update_note_position(spendable.id, position)?; + note_positions.push(position); + } + + // Step 2: Determine which shards contain our notes + // Each level-16 shard contains 2^16 = 65536 leaves + const SHARD_SIZE: u64 = 1 << 16; // 65536 + let mut note_shards: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for pos in ¬e_positions { + note_shards.insert((*pos / SHARD_SIZE) as u32); + } + info!("Notes span {} shard(s): {:?}", note_shards.len(), note_shards); + + // Step 3: Fetch all subtree roots + let subtree_roots = lwd_client.get_subtree_roots(0, 0).await?; + let num_shards = subtree_roots.len(); + info!("Chain has {} completed Orchard subtree shards", num_shards); + + if subtree_roots.is_empty() { + return Err(anyhow::anyhow!("No Orchard subtree roots available from lightwalletd")); + } + + // Step 4: Build position-ordered checkpoint map + // ShardTree requires checkpoint IDs to be monotonically increasing with position. + // Sort note positions and assign checkpoint IDs in ascending position order. + let mut sorted_by_pos: Vec<(u64, usize)> = note_positions.iter() + .enumerate() + .map(|(i, &pos)| (pos, i)) + .collect(); + sorted_by_pos.sort_by_key(|(pos, _)| *pos); + + let mut pos_to_checkpoint: std::collections::HashMap = std::collections::HashMap::new(); + for (ckpt_id, (pos, _)) in sorted_by_pos.iter().enumerate() { + pos_to_checkpoint.insert(*pos, ckpt_id as u32); + debug!("Checkpoint {}: position {} (note index {})", ckpt_id, pos, sorted_by_pos[ckpt_id].1); + } + let last_checkpoint_id = (notes.len() - 1) as u32; + + // Step 5: Build ShardTree with real chain data + let mut tree: ShardTree, 32, 16> = + ShardTree::new(MemoryShardStore::empty(), 100); + + // For shards NOT containing our notes, insert pre-computed roots + for (shard_idx, root_hash, completing_height) in &subtree_roots { + if note_shards.contains(shard_idx) { + // We need to fill this shard with individual leaves + continue; + } + + let root = MerkleHashOrchard::from_bytes(&root_hash); + if bool::from(root.is_none()) { + continue; + } + let addr = incrementalmerkletree::Address::above_position( + 16.into(), + incrementalmerkletree::Position::from((*shard_idx as u64) * SHARD_SIZE), + ); + tree.insert(addr, root.unwrap()) + .map_err(|e| anyhow::anyhow!("Failed to insert shard root {}: {:?}", shard_idx, e))?; + debug!("Inserted shard {} root (completing_height={})", shard_idx, completing_height); + } + + // For shards containing our notes, fetch all leaves and append + for shard_idx in ¬e_shards { + let shard_start_pos = (*shard_idx as u64) * SHARD_SIZE; + + // Find the height range for this shard + let shard_start_height = if *shard_idx == 0 { + 1687104 // Orchard activation height + } else { + // The previous shard's completing height + 1 + subtree_roots.iter() + .find(|(idx, _, _)| *idx == shard_idx - 1) + .map(|(_, _, h)| *h + 1) + .unwrap_or(1687104) + }; + + let shard_end_height = subtree_roots.iter() + .find(|(idx, _, _)| idx == shard_idx) + .map(|(_, _, h)| *h) + .unwrap_or_else(|| { + // This shard is not yet complete — use the latest note's block + notes.iter().map(|n| n.block_height).max().unwrap_or(0) + }); + + info!("Fetching leaves for shard {} (heights {} to {})", shard_idx, shard_start_height, shard_end_height); + + let chunk_size = 10000u64; + let mut current_pos = shard_start_pos; + let mut current_height = shard_start_height; + + while current_height <= shard_end_height { + let end = std::cmp::min(current_height + chunk_size - 1, shard_end_height); + let blocks = lwd_client.fetch_block_actions(current_height, end).await?; + + for (block_height, txs) in &blocks { + for (tx_idx, cmxs) in txs { + for (action_idx, cmx_bytes) in cmxs.iter().enumerate() { + let cmx = ExtractedNoteCommitment::from_bytes(cmx_bytes); + if bool::from(cmx.is_none()) { + continue; + } + let leaf = MerkleHashOrchard::from_cmx(&cmx.unwrap()); + + // Use position-ordered checkpoint IDs (monotonically increasing) + let retention = if let Some(&ckpt_id) = pos_to_checkpoint.get(¤t_pos) { + Retention::Checkpoint { + id: ckpt_id, + marking: Marking::Marked, + } + } else { + Retention::Ephemeral + }; + + tree.append(leaf, retention) + .context(format!("Failed to append leaf at position {} (block {} tx {} action {})", + current_pos, block_height, tx_idx, action_idx))?; + + current_pos += 1; + } + } + } + + current_height = end + 1; + } + + info!("Shard {}: inserted {} leaves", shard_idx, current_pos - shard_start_pos); + } + + // Step 6: Reconstruct notes and get anchor + witnesses + let mut orchard_notes: Vec = Vec::new(); + for (i, spendable) in notes.iter().enumerate() { + let recipient_arr: [u8; 43] = spendable.recipient.clone().try_into() + .map_err(|_| anyhow::anyhow!("Invalid recipient bytes for note {}", i))?; + let note_recipient = Address::from_raw_address_bytes(&recipient_arr) + .into_option() + .ok_or_else(|| anyhow::anyhow!("Invalid Orchard address for note {}", i))?; + + let rho = Rho::from_bytes(&spendable.rho) + .into_option() + .ok_or_else(|| anyhow::anyhow!("Invalid rho for note {}", i))?; + + let rseed = RandomSeed::from_bytes(spendable.rseed, &rho) + .into_option() + .ok_or_else(|| anyhow::anyhow!("Invalid rseed for note {}", i))?; + + let note = Note::from_parts( + note_recipient, + NoteValue::from_raw(spendable.value), + rho, + rseed, + ).into_option() + .ok_or_else(|| anyhow::anyhow!("Failed to reconstruct note {}", i))?; + + orchard_notes.push(note); + } + + let root = tree.root_at_checkpoint_id(&last_checkpoint_id) + .context("Failed to get Merkle root")? + .ok_or_else(|| anyhow::anyhow!("Empty Merkle tree — no checkpoint found"))?; + let anchor: Anchor = root.into(); + info!("Computed anchor: {}", hex::encode(&anchor.to_bytes())); + + // Step 7: Build PCZT bundle + // CRITICAL: Add spends in position-sorted order so checkpoint IDs match witnesses. + // ShardTree checkpoint IDs are assigned by ascending position, so witnesses must + // be requested in the same order. + let mut builder = Builder::new(BundleType::DEFAULT, anchor); + + for &(pos, orig_idx) in &sorted_by_pos { + let position = incrementalmerkletree::Position::from(pos); + let merkle_path = tree.witness_at_checkpoint_id(position, &last_checkpoint_id) + .context(format!("Failed to get Merkle witness for note {} at position {}", orig_idx, pos))? + .ok_or_else(|| anyhow::anyhow!("No witness for note {} at position {}", orig_idx, pos))?; + + builder.add_spend(fvk.clone(), orchard_notes[orig_idx].clone(), merkle_path.into()) + .map_err(|e| anyhow::anyhow!("Failed to add spend {}: {:?}", orig_idx, e))?; + } + + // Encode memo: UTF-8 text zero-padded to 512 bytes (Zcash memo field spec) + let memo_bytes: [u8; 512] = { + let mut buf = [0u8; 512]; + if let Some(ref text) = memo { + let bytes = text.as_bytes(); + let len = std::cmp::min(bytes.len(), 512); + buf[..len].copy_from_slice(&bytes[..len]); + } + buf + }; + + let ovk = fvk.to_ovk(Scope::External); + builder.add_output(Some(ovk.clone()), recipient, NoteValue::from_raw(amount), memo_bytes) + .map_err(|e| anyhow::anyhow!("Failed to add output: {:?}", e))?; + + if change > 0 { + let change_addr = fvk.address_at(0u32, Scope::Internal); + let internal_ovk = fvk.to_ovk(Scope::Internal); + builder.add_output(Some(internal_ovk), change_addr, NoteValue::from_raw(change), [0u8; 512]) + .map_err(|e| anyhow::anyhow!("Failed to add change output: {:?}", e))?; + } + + let (mut pczt_bundle, _) = builder.build_for_pczt(&mut rng) + .map_err(|e| anyhow::anyhow!("Failed to build PCZT: {:?}", e))?; + + // Step 3: Compute ZIP-244 digests + let effects_bundle = pczt_bundle.extract_effects::() + .map_err(|e| anyhow::anyhow!("Failed to extract effects: {:?}", e))? + .ok_or_else(|| anyhow::anyhow!("Empty effects bundle"))?; + + let digests = zip244::compute_zip244_digests_effects(&effects_bundle, branch_id, 0, 0); + let sighash = zip244::compute_sighash(&digests, branch_id); + + // ── DEBUG: Log all digest components ── + debug!("DEBUG sighash: {}", hex::encode(&sighash)); + debug!("DEBUG header: {}", hex::encode(&digests.header_digest)); + debug!("DEBUG transparent: {}", hex::encode(&digests.transparent_digest)); + debug!("DEBUG sapling: {}", hex::encode(&digests.sapling_digest)); + debug!("DEBUG orchard: {}", hex::encode(&digests.orchard_digest)); + + // Log effects rk before randomization + for (i, action) in effects_bundle.actions().iter().enumerate() { + let rk_bytes: [u8; 32] = action.rk().into(); + debug!("DEBUG effects_rk[{}]: {}", i, hex::encode(&rk_bytes)); + } + + // Step 4: Finalize IO + pczt_bundle.finalize_io(sighash, &mut rng) + .map_err(|e| anyhow::anyhow!("IO finalization failed: {:?}", e))?; + + // Log PCZT rk after randomization + alpha + for (i, action) in pczt_bundle.actions().iter().enumerate() { + let rk = action.spend().rk(); + let rk_arr: [u8; 32] = rk.clone().into(); + debug!("DEBUG pczt_rk[{}]: {}", i, hex::encode(&rk_arr)); + if let Some(alpha) = action.spend().alpha() { + debug!("DEBUG alpha[{}]: {}", i, hex::encode(&alpha.to_repr())); + } else { + debug!("DEBUG alpha[{}]: NONE (dummy action)", i); + } + } + + // Step 5: Generate Halo2 proof + info!("Generating Halo2 proof (this may take a while on first run)..."); + let pk = ProvingKey::build(); + pczt_bundle.create_proof(&pk, &mut rng) + .map_err(|e| anyhow::anyhow!("Proof generation failed: {:?}", e))?; + info!("Proof generated successfully"); + + // Step 6: Extract signing fields + let n_actions = pczt_bundle.actions().len(); + let mut action_fields: Vec = Vec::new(); + + for i in 0..n_actions { + let alpha_bytes = pczt_bundle.actions()[i].spend().alpha() + .map(|a| a.to_repr().to_vec()) + .unwrap_or_else(|| vec![0u8; 32]); + + let cv_net_bytes = pczt_bundle.actions()[i].cv_net().to_bytes().to_vec(); + // After finalize_io(), dummy spends already have spend_auth_sig set + // (signed by finalize_io with their dummy_sk). Real spends have + // spend_auth_sig=None, waiting for the device signature. + // alpha().is_some() is NOT reliable — builder sets alpha for ALL actions. + let is_spend = pczt_bundle.actions()[i].spend().spend_auth_sig().is_none(); + let value = pczt_bundle.actions()[i].spend().value() + .map(|v| v.inner()) + .unwrap_or(0); + + let effects_action = &effects_bundle.actions()[i]; + let nullifier_bytes = effects_action.nullifier().to_bytes().to_vec(); + let cmx_bytes = effects_action.cmx().to_bytes().to_vec(); + let epk_bytes = effects_action.encrypted_note().epk_bytes.as_ref().to_vec(); + let enc = &effects_action.encrypted_note().enc_ciphertext; + if enc.len() != 580 { + return Err(anyhow::anyhow!( + "Invalid enc_ciphertext length for action {}: expected 580, got {}", + i, enc.len() + )); + } + let enc_compact = enc[..52].to_vec(); + let enc_memo = enc[52..564].to_vec(); + let enc_noncompact = enc[564..].to_vec(); + let rk_bytes: [u8; 32] = effects_action.rk().into(); + let out_ciphertext = effects_action.encrypted_note().out_ciphertext.to_vec(); + + action_fields.push(ActionFields { + index: i as u32, + alpha: alpha_bytes, + cv_net: cv_net_bytes, + nullifier: nullifier_bytes, + cmx: cmx_bytes, + epk: epk_bytes, + enc_compact, + enc_memo, + enc_noncompact, + rk: rk_bytes.to_vec(), + out_ciphertext, + value, + is_spend, + }); + } + + let orchard_flags = effects_bundle.flags().to_byte() as u32; + let orchard_value_balance: i64 = *effects_bundle.value_balance(); + let orchard_anchor_bytes = effects_bundle.anchor().to_bytes(); + + let signing_request = SigningRequest { + n_actions: n_actions as u32, + account, + branch_id, + sighash: sighash.to_vec(), + digests: DigestFields { + header: digests.header_digest.to_vec(), + transparent: digests.transparent_digest.to_vec(), + sapling: digests.sapling_digest.to_vec(), + orchard: digests.orchard_digest.to_vec(), + }, + bundle_meta: BundleMeta { + flags: orchard_flags, + value_balance: orchard_value_balance, + anchor: orchard_anchor_bytes.to_vec(), + }, + actions: action_fields, + display: DisplayInfo { + amount: format!("{:.8} ZEC", amount as f64 / 1e8), + fee: format!("{:.8} ZEC", fee as f64 / 1e8), + to: format!("(account {})", account), + }, + }; + + Ok(PcztState { + pczt_bundle, + sighash, + branch_id, + signing_request, + }) +} + +/// Apply device signatures to the PCZT and produce the final v5 transaction bytes. +pub fn finalize_pczt( + mut pczt_bundle: orchard::pczt::Bundle, + sighash: [u8; 32], + branch_id: u32, + signatures: &[Vec], +) -> Result<(Vec, String)> { + let mut rng = OsRng; + let n_actions = pczt_bundle.actions().len(); + + if signatures.len() != n_actions { + return Err(anyhow::anyhow!( + "Signature count mismatch: got {} signatures for {} actions", + signatures.len(), n_actions + )); + } + + info!("Applying {} signatures...", signatures.len()); + debug!("finalize sighash: {}", hex::encode(&sighash)); + + for (i, sig_bytes) in signatures.iter().enumerate() { + // Check if this action is a real spend or a dummy (output-only). + // Dummy spends were already signed by finalize_io — skip them. + // After finalize_io(), dummies have spend_auth_sig=Some, real spends have None. + let is_spend = pczt_bundle.actions()[i].spend().spend_auth_sig().is_none(); + if !is_spend { + info!("Action {}: dummy spend (already signed by finalize_io) — skipping device sig", i); + continue; + } + + if sig_bytes.len() != 64 { + return Err(anyhow::anyhow!( + "Invalid signature length for action {}: expected 64, got {}", + i, sig_bytes.len() + )); + } + + info!("Action {}: real spend — applying device signature", i); + + let rk = pczt_bundle.actions()[i].spend().rk(); + let rk_arr: [u8; 32] = rk.clone().into(); + debug!(" rk: {}", hex::encode(&rk_arr)); + debug!(" sighash: {}", hex::encode(&sighash)); + debug!(" sig_R: {}", hex::encode(&sig_bytes[..32])); + debug!(" sig_S: {}", hex::encode(&sig_bytes[32..])); + if let Some(alpha) = pczt_bundle.actions()[i].spend().alpha() { + debug!(" alpha: {}", hex::encode(&alpha.to_repr())); + } + + // Manual reddsa verify before apply_signature + let mut sig_arr = [0u8; 64]; + sig_arr.copy_from_slice(sig_bytes); + let signature: redpallas::Signature = sig_arr.into(); + + let verify_result = rk.verify(&sighash, &signature); + if verify_result.is_err() { + return Err(anyhow::anyhow!( + "Signature verification failed for action {}: {:?}", i, verify_result + )); + } + + pczt_bundle.actions_mut()[i] + .apply_signature(sighash, signature) + .map_err(|e| anyhow::anyhow!("Failed to apply signature for action {}: {}", i, e))?; + } + + // Extract final bundle + let unbound_bundle = pczt_bundle.extract::() + .map_err(|e| anyhow::anyhow!("Failed to extract bundle: {}", e))? + .ok_or_else(|| anyhow::anyhow!("Empty bundle after extraction"))?; + + // Apply binding signature + let authorized_bundle = unbound_bundle.apply_binding_signature(sighash, &mut rng) + .ok_or_else(|| anyhow::anyhow!("Binding signature verification failed"))?; + + // Serialize as v5 transaction + let tx_bytes = serialize_v5_shielded_tx(&authorized_bundle, branch_id)?; + + // Compute txid + use blake2b_simd::Params; + let mut txid_personal = [0u8; 16]; + txid_personal[..12].copy_from_slice(b"ZcashTxHash_"); + txid_personal[12..16].copy_from_slice(&branch_id.to_le_bytes()); + let txid_hash = Params::new() + .hash_length(32) + .personal(&txid_personal) + .hash(&tx_bytes); + let txid = hex::encode(txid_hash.as_bytes()); + + info!("Transaction built: {} bytes, txid: {}", tx_bytes.len(), txid); + Ok((tx_bytes, txid)) +} + +/// Serialize an authorized Orchard bundle as a v5 Zcash transaction. +fn serialize_v5_shielded_tx( + bundle: &orchard::Bundle, + branch_id: u32, +) -> Result> { + let mut tx = Vec::new(); + + // Header (v5) + let version: u32 = 5 | (1 << 31); + tx.extend_from_slice(&version.to_le_bytes()); + + // version_group_id for v5 + tx.extend_from_slice(&0x26A7270Au32.to_le_bytes()); + + // consensus_branch_id + tx.extend_from_slice(&branch_id.to_le_bytes()); + + // lock_time + tx.extend_from_slice(&0u32.to_le_bytes()); + + // expiry_height + tx.extend_from_slice(&0u32.to_le_bytes()); + + // Transparent inputs (varint 0) + tx.push(0x00); + // Transparent outputs (varint 0) + tx.push(0x00); + + // Sapling spends (varint 0) + tx.push(0x00); + // Sapling outputs (varint 0) + tx.push(0x00); + + // Orchard bundle + let n_actions = bundle.actions().len(); + write_compact_size(&mut tx, n_actions as u64); + + for action in bundle.actions() { + tx.extend_from_slice(&action.cv_net().to_bytes()); + tx.extend_from_slice(&action.nullifier().to_bytes()); + tx.extend_from_slice(&<[u8; 32]>::from(action.rk())); + tx.extend_from_slice(&action.cmx().to_bytes()); + tx.extend_from_slice(action.encrypted_note().epk_bytes.as_ref()); + tx.extend_from_slice(&action.encrypted_note().enc_ciphertext); + tx.extend_from_slice(&action.encrypted_note().out_ciphertext); + } + + // Orchard flags + tx.push(bundle.flags().to_byte()); + + // valueBalanceOrchard (i64, 8 bytes LE) + tx.extend_from_slice(&bundle.value_balance().to_le_bytes()); + + // anchor (32 bytes) + tx.extend_from_slice(&bundle.anchor().to_bytes()); + + // proof length + proof bytes + let proof_bytes = bundle.authorization().proof().as_ref(); + write_compact_size(&mut tx, proof_bytes.len() as u64); + tx.extend_from_slice(proof_bytes); + + // spend_auth_sig for each action + for action in bundle.actions() { + let sig_bytes: [u8; 64] = action.authorization().into(); + tx.extend_from_slice(&sig_bytes); + } + + // binding_sig + let binding_sig_bytes: [u8; 64] = bundle.authorization().binding_signature().into(); + tx.extend_from_slice(&binding_sig_bytes); + + Ok(tx) +} + +fn write_compact_size(buf: &mut Vec, n: u64) { + if n < 253 { + buf.push(n as u8); + } else if n <= 0xFFFF { + buf.push(253); + buf.extend_from_slice(&(n as u16).to_le_bytes()); + } else if n <= 0xFFFFFFFF { + buf.push(254); + buf.extend_from_slice(&(n as u32).to_le_bytes()); + } else { + buf.push(255); + buf.extend_from_slice(&n.to_le_bytes()); + } +} diff --git a/projects/keepkey-vault/zcash-cli/src/scanner.rs b/projects/keepkey-vault/zcash-cli/src/scanner.rs new file mode 100644 index 0000000..8467fa9 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/src/scanner.rs @@ -0,0 +1,418 @@ +//! Lightwalletd gRPC client for Zcash chain scanning. +//! +//! Connects to a public lightwalletd server to scan compact blocks, +//! trial-decrypt Orchard notes, persist to wallet DB, track nullifiers, +//! and broadcast transactions. + +use anyhow::{Result, Context}; +use log::{info, debug}; +use tonic::transport::{Channel, ClientTlsConfig}; +use tokio_stream::StreamExt; + +use orchard::keys::{FullViewingKey, PreparedIncomingViewingKey, Scope}; +use orchard::note::ExtractedNoteCommitment; +use orchard::note::Nullifier; +use orchard::note_encryption::{CompactAction, OrchardDomain}; +use zcash_note_encryption::{try_compact_note_decryption, EphemeralKeyBytes}; + +use crate::wallet_db::{WalletDb, ScannedNote}; + +// Generated from proto files +pub mod proto { + tonic::include_proto!("cash.z.wallet.sdk.rpc"); +} + +use proto::compact_tx_streamer_client::CompactTxStreamerClient; + +const DEFAULT_LWD_SERVER: &str = "https://na.zec.rocks:443"; +// Orchard activated at NU5 height +const ORCHARD_ACTIVATION_HEIGHT: u64 = 1687104; + +pub struct LightwalletClient { + client: CompactTxStreamerClient, +} + +impl LightwalletClient { + /// Connect to a lightwalletd gRPC server. + pub async fn connect(server_url: Option<&str>) -> Result { + let url = server_url.unwrap_or(DEFAULT_LWD_SERVER); + info!("Connecting to lightwalletd: {}", url); + + let tls = ClientTlsConfig::new().with_native_roots(); + let channel = Channel::from_shared(url.to_string()) + .context("Invalid server URL")? + .tls_config(tls) + .context("TLS config failed")? + .connect() + .await + .context("Failed to connect to lightwalletd")?; + + let client = CompactTxStreamerClient::new(channel); + info!("Connected to lightwalletd"); + Ok(Self { client }) + } + + /// Get the current consensus branch ID from lightwalletd. + pub async fn get_consensus_branch_id(&mut self) -> Result { + let response = self.client + .get_lightd_info(proto::Empty {}) + .await + .context("GetLightdInfo failed")?; + + let info = response.into_inner(); + let branch_str = info.consensus_branch_id.trim_start_matches("0x"); + let branch_id = u32::from_str_radix(branch_str, 16) + .context(format!("Invalid consensus branch ID string: '{}'", info.consensus_branch_id))?; + + info!("Current consensus branch ID: 0x{:08x}", branch_id); + Ok(branch_id) + } + + /// Get the current chain tip height. + pub async fn get_latest_block_height(&mut self) -> Result { + let response = self.client + .get_latest_block(proto::ChainSpec {}) + .await + .context("GetLatestBlock failed")?; + Ok(response.into_inner().height) + } + + /// Broadcast a raw transaction via lightwalletd. + pub async fn send_transaction(&mut self, raw_tx: &[u8]) -> Result { + let response = self.client + .send_transaction(proto::RawTransaction { + data: raw_tx.to_vec(), + height: 0, + }) + .await + .context("SendTransaction failed")?; + + let send_resp = response.into_inner(); + if send_resp.error_code != 0 { + return Err(anyhow::anyhow!( + "Transaction broadcast failed (code {}): {}", + send_resp.error_code, + send_resp.error_message + )); + } + + info!("Transaction broadcast successfully"); + Ok(send_resp.error_message) + } + + /// Fetch Orchard subtree roots from lightwalletd. + /// Returns Vec of (shard_index, root_hash, completing_height). + pub async fn get_subtree_roots( + &mut self, + start_index: u32, + max_entries: u32, + ) -> Result> { + let request = proto::GetSubtreeRootsArg { + start_index, + shielded_protocol: proto::ShieldedProtocol::Orchard as i32, + max_entries, + }; + + let mut stream = self.client + .get_subtree_roots(request) + .await + .context("GetSubtreeRoots failed")? + .into_inner(); + + let mut roots = Vec::new(); + let mut index = start_index; + while let Some(root_result) = stream.next().await { + let root = root_result.context("Error reading subtree root")?; + if root.root_hash.len() == 32 { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&root.root_hash); + roots.push((index, hash, root.completing_block_height)); + index += 1; + } + } + + info!("Fetched {} Orchard subtree roots (start_index={})", roots.len(), start_index); + Ok(roots) + } + + /// Get the Orchard tree state (frontier) at a specific block height. + /// Returns the orchardCommitmentTreeSize from ChainMetadata at that height. + #[allow(dead_code)] + pub async fn get_tree_state(&mut self, height: u64) -> Result<(u64, String)> { + let request = proto::BlockId { + height, + hash: vec![], + }; + + let response = self.client + .get_tree_state(request) + .await + .context("GetTreeState failed")?; + + let state = response.into_inner(); + info!("Tree state at height {}: orchard_tree len={}", height, state.orchard_tree.len()); + Ok((state.height, state.orchard_tree)) + } + + /// Fetch compact blocks in a range and extract all Orchard action cmx values. + /// Returns Vec of (block_height, Vec of (tx_index, Vec of cmx_bytes)). + pub async fn fetch_block_actions( + &mut self, + start_height: u64, + end_height: u64, + ) -> Result)>)>> { + let request = proto::BlockRange { + start: Some(proto::BlockId { height: start_height, hash: vec![] }), + end: Some(proto::BlockId { height: end_height, hash: vec![] }), + }; + + let mut stream = self.client + .get_block_range(request) + .await + .context("GetBlockRange for actions failed")? + .into_inner(); + + let mut blocks = Vec::new(); + while let Some(block_result) = stream.next().await { + let block = block_result.context("Error reading compact block")?; + let mut txs = Vec::new(); + for tx in &block.vtx { + let mut cmxs = Vec::new(); + for action in &tx.actions { + if action.cmx.len() == 32 { + let mut cmx = [0u8; 32]; + cmx.copy_from_slice(&action.cmx); + cmxs.push(cmx); + } + } + if !cmxs.is_empty() { + txs.push((tx.index as u32, cmxs)); + } + } + blocks.push((block.height, txs)); + } + + let total_actions: usize = blocks.iter() + .flat_map(|(_, txs)| txs.iter().map(|(_, cmxs)| cmxs.len())) + .sum(); + info!("Fetched {} blocks with {} total Orchard actions ({} to {})", + blocks.len(), total_actions, start_height, end_height); + Ok(blocks) + } + + /// Get the Orchard commitment tree size at a given block height by fetching + /// the compact block's ChainMetadata. + pub async fn get_orchard_tree_size_at(&mut self, height: u64) -> Result { + let request = proto::BlockId { + height, + hash: vec![], + }; + + let response = self.client + .get_block(request) + .await + .context("GetBlock failed")?; + + let block = response.into_inner(); + let size = block.chain_metadata + .map(|m| m.orchard_commitment_tree_size as u64) + .unwrap_or(0); + + debug!("Orchard tree size at height {}: {}", height, size); + Ok(size) + } + + /// Scan compact blocks with persistence — saves notes to wallet DB, + /// tracks nullifiers for spend detection, supports incremental scanning. + pub async fn scan_with_persistence( + &mut self, + fvk: &FullViewingKey, + db: &WalletDb, + force_start: Option, + ) -> Result { + let ivk = fvk.to_ivk(Scope::External); + let prepared_ivk = PreparedIncomingViewingKey::new(&ivk); + + let tip = self.get_latest_block_height().await?; + + let start = match force_start { + Some(h) => h, + None => db.last_scanned_height()? + .map(|h| h + 1) + .unwrap_or_else(|| { + // First scan: start from Orchard activation height to + // catch all possible notes. This is slow (~2M blocks) + // but necessary to avoid silently missing funds. + info!("First scan — starting from Orchard activation height {} (tip={})", + ORCHARD_ACTIVATION_HEIGHT, tip); + ORCHARD_ACTIVATION_HEIGHT + }), + }; + + if start > tip { + info!("Already up to date (scanned to {}, tip is {})", start - 1, tip); + let balance = db.get_balance()?; + let (_total, unspent) = db.get_note_count()?; + return Ok(OrchardScanResult { + total_received: balance, + notes_found: unspent as u32, + blocks_scanned: 0, + tip_height: tip, + }); + } + + info!("Scanning blocks {} to {} for Orchard notes...", start, tip); + + let mut new_notes: u32 = 0; + let mut spent_notes: u32 = 0; + let mut blocks_scanned: u64 = 0; + + let chunk_size: u64 = 10000; + let mut current = start; + + while current <= tip { + let end = std::cmp::min(current + chunk_size - 1, tip); + + let request = proto::BlockRange { + start: Some(proto::BlockId { height: current, hash: vec![] }), + end: Some(proto::BlockId { height: end, hash: vec![] }), + }; + + let mut stream = self.client + .get_block_range(request) + .await + .context("GetBlockRange failed")? + .into_inner(); + + while let Some(block_result) = stream.next().await { + let block = block_result.context("Error reading compact block")?; + blocks_scanned += 1; + + for tx in &block.vtx { + for (action_idx, action) in tx.actions.iter().enumerate() { + // Check nullifier — does this action spend one of our notes? + if action.nullifier.len() == 32 { + let mut nf_bytes = [0u8; 32]; + nf_bytes.copy_from_slice(&action.nullifier); + if db.mark_note_spent(&nf_bytes)? { + spent_notes += 1; + } + } + + // Try to decrypt — is this action a note to us? + if let Some((note, addr)) = try_decrypt_action(action, &prepared_ivk) { + let value = note.value().inner(); + let recipient_bytes = addr.to_raw_address_bytes().to_vec(); + + let nf = note.nullifier(fvk); + let mut nf_bytes = [0u8; 32]; + nf_bytes.copy_from_slice(&nf.to_bytes()); + + let mut rho_bytes = [0u8; 32]; + rho_bytes.copy_from_slice(¬e.rho().to_bytes()); + + let mut rseed_bytes = [0u8; 32]; + rseed_bytes.copy_from_slice(note.rseed().as_bytes()); + + let mut cmx_bytes = [0u8; 32]; + cmx_bytes.copy_from_slice(&action.cmx); + + let scanned = ScannedNote { + value, + recipient: recipient_bytes, + rho: rho_bytes, + rseed: rseed_bytes, + cmx: cmx_bytes, + nullifier: nf_bytes, + block_height: block.height, + tx_index: tx.index as u32, + action_index: action_idx as u32, + }; + + if db.insert_note(&scanned)? { + new_notes += 1; + info!( + "Found note: {} ZAT ({:.8} ZEC) in block {}", + value, + value as f64 / 1e8, + block.height, + ); + } + } + } + } + } + + db.set_last_scanned_height(end)?; + current = end + 1; + + // Log progress every chunk (parsed by sidecar manager for UI) + let progress = ((end - start + 1) as f64 / (tip - start + 1) as f64) * 100.0; + info!("Scan progress: {:.1}% ({}/{})", progress, end, tip); + } + + let balance = db.get_balance()?; + let (_total, unspent) = db.get_note_count()?; + + info!( + "Scan complete: {} blocks, {} new notes, {} spent, balance: {:.8} ZEC", + blocks_scanned, new_notes, spent_notes, balance as f64 / 1e8 + ); + + Ok(OrchardScanResult { + total_received: balance, + notes_found: unspent as u32, + blocks_scanned, + tip_height: tip, + }) + } +} + +/// Try to trial-decrypt a compact Orchard action. +fn try_decrypt_action( + action: &proto::CompactOrchardAction, + prepared_ivk: &PreparedIncomingViewingKey, +) -> Option<(orchard::Note, orchard::Address)> { + if action.nullifier.len() != 32 + || action.cmx.len() != 32 + || action.ephemeral_key.len() != 32 + || action.ciphertext.len() != 52 + { + return None; + } + + let nf_arr: [u8; 32] = action.nullifier.clone().try_into().ok()?; + let nullifier = Nullifier::from_bytes(&nf_arr); + if nullifier.is_none().into() { + return None; + } + + let cmx_arr: [u8; 32] = action.cmx.clone().try_into().ok()?; + let cmx = ExtractedNoteCommitment::from_bytes(&cmx_arr); + if cmx.is_none().into() { + return None; + } + + let ek_arr: [u8; 32] = action.ephemeral_key.clone().try_into().ok()?; + let ephemeral_key = EphemeralKeyBytes(ek_arr); + + let enc_ciphertext: [u8; 52] = action.ciphertext.clone().try_into().ok()?; + + let compact = CompactAction::from_parts( + nullifier.unwrap(), + cmx.unwrap(), + ephemeral_key, + enc_ciphertext, + ); + + let domain = OrchardDomain::for_compact_action(&compact); + + try_compact_note_decryption(&domain, prepared_ivk, &compact) +} + +pub struct OrchardScanResult { + pub total_received: u64, + pub notes_found: u32, + pub blocks_scanned: u64, + pub tip_height: u64, +} diff --git a/projects/keepkey-vault/zcash-cli/src/wallet_db.rs b/projects/keepkey-vault/zcash-cli/src/wallet_db.rs new file mode 100644 index 0000000..d7f4508 --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/src/wallet_db.rs @@ -0,0 +1,345 @@ +//! SQLite-backed wallet database for persisting scanned Orchard notes. +//! +//! Stores decrypted notes, scan progress, and nullifier tracking +//! to enable incremental scanning and spending. + +use anyhow::{Result, Context}; +use rusqlite::{Connection, params}; +use std::path::PathBuf; +use log::{info, debug}; + +/// A scanned Orchard note with all fields needed to reconstruct it for spending. +#[derive(Debug, Clone)] +pub struct ScannedNote { + pub value: u64, + pub recipient: Vec, // 43-byte Orchard address + pub rho: [u8; 32], + pub rseed: [u8; 32], + pub cmx: [u8; 32], + pub nullifier: [u8; 32], + pub block_height: u64, + pub tx_index: u32, + pub action_index: u32, +} + +/// A spendable (unspent) note with its database ID. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct SpendableNote { + pub id: i64, + pub value: u64, + pub recipient: Vec, + pub rho: [u8; 32], + pub rseed: [u8; 32], + pub cmx: [u8; 32], + pub nullifier: [u8; 32], + pub block_height: u64, + pub tx_index: u32, + pub action_index: u32, + pub position: Option, +} + +pub struct WalletDb { + conn: Connection, +} + +impl WalletDb { + /// Open the wallet database at the default location (~/.keepkey/zcash_wallet.db). + pub fn open_default() -> Result { + let db_dir = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))? + .join(".keepkey"); + + std::fs::create_dir_all(&db_dir) + .context("Failed to create ~/.keepkey directory")?; + + let db_path = db_dir.join("zcash_wallet.db"); + Self::open(&db_path) + } + + /// Open (or create) the wallet database at a specific path. + pub fn open(path: &PathBuf) -> Result { + debug!("Opening wallet database: {}", path.display()); + let conn = Connection::open(path) + .context("Failed to open wallet database")?; + + let db = Self { conn }; + db.initialize_schema()?; + Ok(db) + } + + fn initialize_schema(&self) -> Result<()> { + self.conn.execute_batch( + "CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY, + value INTEGER NOT NULL, + recipient BLOB NOT NULL, + rho BLOB NOT NULL, + rseed BLOB NOT NULL, + cmx BLOB NOT NULL, + nullifier BLOB NOT NULL UNIQUE, + block_height INTEGER NOT NULL, + tx_index INTEGER NOT NULL, + action_index INTEGER NOT NULL, + is_spent INTEGER NOT NULL DEFAULT 0, + position INTEGER + ); + + CREATE TABLE IF NOT EXISTS scan_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS tree_state ( + id INTEGER PRIMARY KEY, + data BLOB NOT NULL + ); + + CREATE TABLE IF NOT EXISTS fvk_store ( + id INTEGER PRIMARY KEY CHECK (id = 1), + ak BLOB NOT NULL, + nk BLOB NOT NULL, + rivk BLOB NOT NULL, + ak_hash TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_notes_nullifier ON notes(nullifier); + CREATE INDEX IF NOT EXISTS idx_notes_unspent ON notes(is_spent) WHERE is_spent = 0; + " + ).context("Failed to initialize database schema")?; + + // Migration: add position column if missing (for existing databases) + let has_position = self.conn.query_row( + "SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='position'", + [], + |row| row.get::<_, i64>(0), + ).unwrap_or(0); + if has_position == 0 { + self.conn.execute("ALTER TABLE notes ADD COLUMN position INTEGER", []) + .context("Failed to add position column")?; + info!("Migrated notes table: added position column"); + } + + debug!("Database schema initialized"); + Ok(()) + } + + /// Get the last scanned block height, or None if never scanned. + pub fn last_scanned_height(&self) -> Result> { + let result: Option = self.conn.query_row( + "SELECT value FROM scan_state WHERE key = 'last_scanned_height'", + [], + |row| row.get(0), + ).ok(); + + match result { + Some(s) => Ok(Some(s.parse().context("Invalid last_scanned_height")?)), + None => Ok(None), + } + } + + /// Update the last scanned block height. + pub fn set_last_scanned_height(&self, height: u64) -> Result<()> { + self.conn.execute( + "INSERT OR REPLACE INTO scan_state (key, value) VALUES ('last_scanned_height', ?1)", + params![height.to_string()], + ).context("Failed to update scan height")?; + Ok(()) + } + + /// Insert a newly discovered note. + /// Returns true if the note was inserted, false if it already exists (duplicate nullifier). + pub fn insert_note(&self, note: &ScannedNote) -> Result { + let result = self.conn.execute( + "INSERT OR IGNORE INTO notes (value, recipient, rho, rseed, cmx, nullifier, block_height, tx_index, action_index) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + note.value as i64, + note.recipient, + note.rho.as_slice(), + note.rseed.as_slice(), + note.cmx.as_slice(), + note.nullifier.as_slice(), + note.block_height as i64, + note.tx_index as i64, + note.action_index as i64, + ], + ).context("Failed to insert note")?; + + Ok(result > 0) + } + + /// Mark a note as spent by its nullifier. + pub fn mark_note_spent(&self, nullifier: &[u8; 32]) -> Result { + let updated = self.conn.execute( + "UPDATE notes SET is_spent = 1 WHERE nullifier = ?1 AND is_spent = 0", + params![nullifier.as_slice()], + ).context("Failed to mark note spent")?; + + if updated > 0 { + debug!("Marked note as spent: {}", hex::encode(nullifier)); + } + Ok(updated > 0) + } + + /// Get all unspent notes that can be used for spending. + pub fn get_spendable_notes(&self) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT id, value, recipient, rho, rseed, cmx, nullifier, block_height, tx_index, action_index, position + FROM notes WHERE is_spent = 0 ORDER BY value DESC" + )?; + + let notes = stmt.query_map([], |row| { + let rho_blob: Vec = row.get(3)?; + let rseed_blob: Vec = row.get(4)?; + let cmx_blob: Vec = row.get(5)?; + let nf_blob: Vec = row.get(6)?; + + let mut rho = [0u8; 32]; + let mut rseed = [0u8; 32]; + let mut cmx = [0u8; 32]; + let mut nullifier = [0u8; 32]; + rho.copy_from_slice(&rho_blob); + rseed.copy_from_slice(&rseed_blob); + cmx.copy_from_slice(&cmx_blob); + nullifier.copy_from_slice(&nf_blob); + + Ok(SpendableNote { + id: row.get(0)?, + value: row.get::<_, i64>(1)? as u64, + recipient: row.get(2)?, + rho, + rseed, + cmx, + nullifier, + block_height: row.get::<_, i64>(7)? as u64, + tx_index: row.get::<_, i64>(8)? as u32, + action_index: row.get::<_, i64>(9)? as u32, + position: row.get::<_, Option>(10)?.map(|p| p as u64), + }) + })?.collect::, _>>() + .context("Failed to read spendable notes")?; + + Ok(notes) + } + + /// Update the tree position for a note after computing it from the chain. + pub fn update_note_position(&self, note_id: i64, position: u64) -> Result<()> { + self.conn.execute( + "UPDATE notes SET position = ?1 WHERE id = ?2", + params![position as i64, note_id], + ).context("Failed to update note position")?; + debug!("Updated note {} position to {}", note_id, position); + Ok(()) + } + + /// Get the total balance of unspent notes (in zatoshis). + pub fn get_balance(&self) -> Result { + let balance: i64 = self.conn.query_row( + "SELECT COALESCE(SUM(value), 0) FROM notes WHERE is_spent = 0", + [], + |row| row.get(0), + )?; + Ok(balance as u64) + } + + /// Get total count of notes (spent + unspent). + pub fn get_note_count(&self) -> Result<(u64, u64)> { + let total: i64 = self.conn.query_row( + "SELECT COUNT(*) FROM notes", [], |row| row.get(0), + )?; + let unspent: i64 = self.conn.query_row( + "SELECT COUNT(*) FROM notes WHERE is_spent = 0", [], |row| row.get(0), + )?; + Ok((total as u64, unspent as u64)) + } + + /// Save the FVK to the database for auto-loading on restart. + /// Stores a full ak hash to detect firmware/basepoint changes. + pub fn save_fvk(&self, fvk_bytes: &[u8; 96]) -> Result<()> { + let ak_hash = hex::encode(&fvk_bytes[..32]); // Full 32-byte ak as fingerprint + self.conn.execute( + "INSERT OR REPLACE INTO fvk_store (id, ak, nk, rivk, ak_hash) VALUES (1, ?1, ?2, ?3, ?4)", + params![ + &fvk_bytes[..32], + &fvk_bytes[32..64], + &fvk_bytes[64..96], + &ak_hash, + ], + ).context("Failed to save FVK")?; + info!("FVK saved to database (ak_hash={})", ak_hash); + Ok(()) + } + + /// Load a previously saved FVK, or None if no FVK has been stored. + pub fn load_fvk(&self) -> Result> { + let result = self.conn.query_row( + "SELECT ak, nk, rivk FROM fvk_store WHERE id = 1", + [], + |row| { + let ak: Vec = row.get(0)?; + let nk: Vec = row.get(1)?; + let rivk: Vec = row.get(2)?; + Ok((ak, nk, rivk)) + }, + ); + + match result { + Ok((ak, nk, rivk)) => { + if ak.len() != 32 || nk.len() != 32 || rivk.len() != 32 { + return Err(anyhow::anyhow!("Corrupt FVK in database")); + } + let mut fvk_bytes = [0u8; 96]; + fvk_bytes[..32].copy_from_slice(&ak); + fvk_bytes[32..64].copy_from_slice(&nk); + fvk_bytes[64..96].copy_from_slice(&rivk); + Ok(Some(fvk_bytes)) + } + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(anyhow::anyhow!("Failed to load FVK: {}", e)), + } + } + + /// Clear scan progress so the next scan starts from Orchard activation. + /// Keeps the FVK and existing notes (duplicate nullifiers are ignored on re-insert). + pub fn clear_scan_progress(&self) -> Result<()> { + self.conn.execute( + "DELETE FROM scan_state WHERE key = 'last_scanned_height'", + [], + ).context("Failed to clear scan progress")?; + // Also clear notes since a full rescan will re-find them, + // and stale spent-tracking could be inconsistent + self.conn.execute("DELETE FROM notes", []) + .context("Failed to clear notes for rescan")?; + info!("Scan progress cleared for full rescan (FVK preserved)"); + Ok(()) + } + + /// Delete all data for a full rescan, including stored FVK. + pub fn reset(&self) -> Result<()> { + self.conn.execute_batch( + "DELETE FROM notes; + DELETE FROM scan_state; + DELETE FROM tree_state; + DELETE FROM fvk_store;" + ).context("Failed to reset wallet database")?; + info!("Wallet database reset for rescan (FVK cleared)"); + Ok(()) + } + + /// Check if a new FVK matches the stored one (by full ak comparison). + /// Returns true if they match or no FVK is stored yet. + pub fn fvk_matches(&self, new_fvk_bytes: &[u8; 96]) -> Result { + let new_hash = hex::encode(&new_fvk_bytes[..32]); + let result = self.conn.query_row( + "SELECT ak_hash FROM fvk_store WHERE id = 1", + [], + |row| row.get::<_, Option>(0), + ); + match result { + Ok(Some(stored_hash)) => Ok(stored_hash == new_hash), + Ok(None) | Err(rusqlite::Error::QueryReturnedNoRows) => Ok(true), + Err(e) => Err(anyhow::anyhow!("Failed to check FVK: {}", e)), + } + } +} diff --git a/projects/keepkey-vault/zcash-cli/src/zip244.rs b/projects/keepkey-vault/zcash-cli/src/zip244.rs new file mode 100644 index 0000000..2cd17ac --- /dev/null +++ b/projects/keepkey-vault/zcash-cli/src/zip244.rs @@ -0,0 +1,285 @@ +//! ZIP-244 Transaction Identifier and Signature Digest computation. +//! +//! Implements the full BLAKE2b-256 digest tree for Zcash v5 transactions +//! as specified in ZIP-244. Used to compute sub-digests that the device +//! combines into the final sighash for on-device spend authorization. + +use blake2b_simd::Params; + +/// NU5 consensus branch ID (used in tests) +#[allow(dead_code)] +pub const NU5_BRANCH_ID: u32 = 0x37519621; + +/// v5 transaction version (with overwintered flag) +const TX_VERSION: u32 = 5 | (1 << 31); + +/// v5 version group ID +const VERSION_GROUP_ID: u32 = 0x26A7270A; + +/// Precomputed BLAKE2b-256("ZTxIdTranspaHash", "") for shielded-only txs +pub const EMPTY_TRANSPARENT_DIGEST: [u8; 32] = [ + 0xc3, 0x3f, 0x2e, 0x95, 0x70, 0x5f, 0xaa, 0xb3, + 0x5f, 0x8d, 0x53, 0x3f, 0xa6, 0x1e, 0x95, 0xc3, + 0xb7, 0xaa, 0xba, 0x07, 0x76, 0xb8, 0x74, 0xa9, + 0xf7, 0x4f, 0xc1, 0x27, 0x84, 0x37, 0x6a, 0x59, +]; + +/// Precomputed BLAKE2b-256("ZTxIdSaplingHash", "") for Orchard-only txs +pub const EMPTY_SAPLING_DIGEST: [u8; 32] = [ + 0x6f, 0x2f, 0xc8, 0xf9, 0x8f, 0xea, 0xfd, 0x94, + 0xe7, 0x4a, 0x0d, 0xf4, 0xbe, 0xd7, 0x43, 0x91, + 0xee, 0x0b, 0x5a, 0x69, 0x94, 0x5e, 0x4c, 0xed, + 0x8c, 0xa8, 0xa0, 0x95, 0x20, 0x6f, 0x00, 0xae, +]; + +/// All ZIP-244 sub-digests needed for sighash computation. +#[derive(Debug, Clone)] +pub struct Zip244Digests { + pub header_digest: [u8; 32], + pub transparent_digest: [u8; 32], + pub sapling_digest: [u8; 32], + pub orchard_digest: [u8; 32], +} + +/// BLAKE2b-256 with a 16-byte personalization string. +fn blake2b_256(personal: &[u8; 16], data: &[u8]) -> [u8; 32] { + let hash = Params::new() + .hash_length(32) + .personal(personal) + .hash(data); + let mut out = [0u8; 32]; + out.copy_from_slice(hash.as_bytes()); + out +} + +/// Compute the ZIP-244 header digest. +pub fn digest_header( + branch_id: u32, + lock_time: u32, + expiry_height: u32, +) -> [u8; 32] { + let mut data = Vec::with_capacity(20); + data.extend_from_slice(&TX_VERSION.to_le_bytes()); + data.extend_from_slice(&VERSION_GROUP_ID.to_le_bytes()); + data.extend_from_slice(&branch_id.to_le_bytes()); + data.extend_from_slice(&lock_time.to_le_bytes()); + data.extend_from_slice(&expiry_height.to_le_bytes()); + blake2b_256(b"ZTxIdHeadersHash", &data) +} + +/// Compute the ZIP-244 orchard digest from an authorized Orchard bundle. +#[allow(dead_code)] +pub fn digest_orchard(bundle: &orchard::Bundle) -> [u8; 32] { + let (compact_hash, memos_hash, noncompact_hash) = + compute_orchard_action_hashes(bundle); + + let mut data = Vec::with_capacity(32 * 3 + 1 + 8 + 32); + data.extend_from_slice(&compact_hash); + data.extend_from_slice(&memos_hash); + data.extend_from_slice(&noncompact_hash); + data.push(bundle.flags().to_byte()); + data.extend_from_slice(&bundle.value_balance().to_le_bytes()); + data.extend_from_slice(&bundle.anchor().to_bytes()); + + blake2b_256(b"ZTxIdOrchardHash", &data) +} + +/// Compute the three orchard action sub-hashes (compact, memos, noncompact). +#[allow(dead_code)] +fn compute_orchard_action_hashes( + bundle: &orchard::Bundle, +) -> ([u8; 32], [u8; 32], [u8; 32]) { + let mut compact_data = Vec::new(); + let mut memos_data = Vec::new(); + let mut noncompact_data = Vec::new(); + + for action in bundle.actions() { + // Compact: nf(32) || cmx(32) || epk(32) || enc[0..52] + compact_data.extend_from_slice(&action.nullifier().to_bytes()); + compact_data.extend_from_slice(&action.cmx().to_bytes()); + compact_data.extend_from_slice(action.encrypted_note().epk_bytes.as_ref()); + compact_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[..52]); + + // Memos: enc[52..564] + memos_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[52..564]); + + // Noncompact: cv_net(32) || rk(32) || enc[564..](16) || out_ciphertext(80) + noncompact_data.extend_from_slice(&action.cv_net().to_bytes()); + noncompact_data.extend_from_slice(&<[u8; 32]>::from(action.rk())); + noncompact_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[564..]); + noncompact_data.extend_from_slice(&action.encrypted_note().out_ciphertext); + } + + let compact_hash = blake2b_256(b"ZTxIdOrcActCHash", &compact_data); + let memos_hash = blake2b_256(b"ZTxIdOrcActMHash", &memos_data); + let noncompact_hash = blake2b_256(b"ZTxIdOrcActNHash", &noncompact_data); + + (compact_hash, memos_hash, noncompact_hash) +} + +/// Compute the orchard digest from an EffectsOnly bundle (before finalization). +pub fn digest_orchard_effects( + bundle: &orchard::Bundle, +) -> [u8; 32] +where + V: Copy + Into, +{ + let mut compact_data = Vec::new(); + let mut memos_data = Vec::new(); + let mut noncompact_data = Vec::new(); + + for action in bundle.actions() { + compact_data.extend_from_slice(&action.nullifier().to_bytes()); + compact_data.extend_from_slice(&action.cmx().to_bytes()); + compact_data.extend_from_slice(action.encrypted_note().epk_bytes.as_ref()); + compact_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[..52]); + + memos_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[52..564]); + + noncompact_data.extend_from_slice(&action.cv_net().to_bytes()); + noncompact_data.extend_from_slice(&<[u8; 32]>::from(action.rk())); + noncompact_data.extend_from_slice(&action.encrypted_note().enc_ciphertext[564..]); + noncompact_data.extend_from_slice(&action.encrypted_note().out_ciphertext); + } + + let compact_hash = blake2b_256(b"ZTxIdOrcActCHash", &compact_data); + let memos_hash = blake2b_256(b"ZTxIdOrcActMHash", &memos_data); + let noncompact_hash = blake2b_256(b"ZTxIdOrcActNHash", &noncompact_data); + + let mut orchard_data = Vec::with_capacity(32 * 3 + 1 + 8 + 32); + orchard_data.extend_from_slice(&compact_hash); + orchard_data.extend_from_slice(&memos_hash); + orchard_data.extend_from_slice(&noncompact_hash); + orchard_data.push(bundle.flags().to_byte()); + orchard_data.extend_from_slice(&(*bundle.value_balance()).into().to_le_bytes()); + orchard_data.extend_from_slice(&bundle.anchor().to_bytes()); + + blake2b_256(b"ZTxIdOrchardHash", &orchard_data) +} + +/// Compute ZIP-244 digests from an EffectsOnly bundle (before finalization). +pub fn compute_zip244_digests_effects( + bundle: &orchard::Bundle, + branch_id: u32, + lock_time: u32, + expiry_height: u32, +) -> Zip244Digests +where + V: Copy + Into, +{ + Zip244Digests { + header_digest: digest_header(branch_id, lock_time, expiry_height), + transparent_digest: EMPTY_TRANSPARENT_DIGEST, + sapling_digest: EMPTY_SAPLING_DIGEST, + orchard_digest: digest_orchard_effects(bundle), + } +} + +/// Compute the final sighash from sub-digests. +pub fn compute_sighash(digests: &Zip244Digests, branch_id: u32) -> [u8; 32] { + let mut personal = [0u8; 16]; + personal[..12].copy_from_slice(b"ZcashTxHash_"); + personal[12..16].copy_from_slice(&branch_id.to_le_bytes()); + + let mut data = Vec::with_capacity(128); + data.extend_from_slice(&digests.header_digest); + data.extend_from_slice(&digests.transparent_digest); + data.extend_from_slice(&digests.sapling_digest); + data.extend_from_slice(&digests.orchard_digest); + + blake2b_256(&personal, &data) +} + +/// Compute orchard digest from raw action data (hex strings). +/// Used by the IPC layer to compute digests from JSON fields. +#[allow(dead_code)] +pub fn digest_orchard_from_raw( + actions: &[crate::pczt_builder::ActionFields], + flags: u8, + value_balance: i64, + anchor: &[u8; 32], +) -> [u8; 32] { + let mut compact_data = Vec::new(); + let mut memos_data = Vec::new(); + let mut noncompact_data = Vec::new(); + + for action in actions { + compact_data.extend_from_slice(&action.nullifier); + compact_data.extend_from_slice(&action.cmx); + compact_data.extend_from_slice(&action.epk); + compact_data.extend_from_slice(&action.enc_compact); + + memos_data.extend_from_slice(&action.enc_memo); + + noncompact_data.extend_from_slice(&action.cv_net); + noncompact_data.extend_from_slice(&action.rk); + noncompact_data.extend_from_slice(&action.enc_noncompact); + noncompact_data.extend_from_slice(&action.out_ciphertext); + } + + let compact_hash = blake2b_256(b"ZTxIdOrcActCHash", &compact_data); + let memos_hash = blake2b_256(b"ZTxIdOrcActMHash", &memos_data); + let noncompact_hash = blake2b_256(b"ZTxIdOrcActNHash", &noncompact_data); + + let mut orchard_data = Vec::with_capacity(32 * 3 + 1 + 8 + 32); + orchard_data.extend_from_slice(&compact_hash); + orchard_data.extend_from_slice(&memos_hash); + orchard_data.extend_from_slice(&noncompact_hash); + orchard_data.push(flags); + orchard_data.extend_from_slice(&value_balance.to_le_bytes()); + orchard_data.extend_from_slice(anchor); + + blake2b_256(b"ZTxIdOrchardHash", &orchard_data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_transparent_digest() { + let computed = blake2b_256(b"ZTxIdTranspaHash", &[]); + assert_eq!(computed, EMPTY_TRANSPARENT_DIGEST); + } + + #[test] + fn test_empty_sapling_digest() { + let computed = blake2b_256(b"ZTxIdSaplingHash", &[]); + assert_eq!(computed, EMPTY_SAPLING_DIGEST); + assert_eq!( + hex::encode(computed), + "6f2fc8f98feafd94e74a0df4bed74391ee0b5a69945e4ced8ca8a095206f00ae" + ); + } + + #[test] + fn test_header_digest_keystone3_vector1() { + let header = digest_header(NU5_BRANCH_ID, 0, 0); + assert_eq!( + hex::encode(header), + "3f85a5b3ff138bde71704243213f0cdd8d7483832dc4c2007c0f15fc2e3d17eb" + ); + } + + #[test] + fn test_sighash_from_keystone3_digests() { + let digests = Zip244Digests { + header_digest: hex_to_array("3f85a5b3ff138bde71704243213f0cdd8d7483832dc4c2007c0f15fc2e3d17eb"), + transparent_digest: EMPTY_TRANSPARENT_DIGEST, + sapling_digest: hex_to_array("6f2fc8f98feafd94e74a0df4bed74391ee0b5a69945e4ced8ca8a095206f00ae"), + orchard_digest: hex_to_array("0ee1912a92e13f43e2511d9c0a12ab26c165391eefc7311e382d752806e6cb8a"), + }; + let sighash = compute_sighash(&digests, NU5_BRANCH_ID); + assert_eq!( + hex::encode(sighash), + "bd0488e0117fe59e2b58fe9897ce803200ad72f74a9a94594217a6a79050f66f" + ); + } + + fn hex_to_array(s: &str) -> [u8; 32] { + let bytes = hex::decode(s).unwrap(); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + arr + } +} diff --git a/scripts/build-windows-production.ps1 b/scripts/build-windows-production.ps1 index a328dbc..4b1dc65 100644 --- a/scripts/build-windows-production.ps1 +++ b/scripts/build-windows-production.ps1 @@ -80,7 +80,7 @@ if ($PSCommandPath) { } $RepoRoot = Split-Path -Path $ScriptDir -Parent $ProjectDir = Join-Path $RepoRoot "projects\keepkey-vault" -$BuildDir = Join-Path $ProjectDir "build\dev-win-x64\keepkey-vault-dev" +$BuildDir = Join-Path $ProjectDir "_build\dev-win-x64\keepkey-vault-dev" $ArtifactsDir = Join-Path $RepoRoot $OutputDir # Read version from package.json