From b2c5162bc47e98e040ba444592b5814d5247564d Mon Sep 17 00:00:00 2001 From: Mati OS Date: Mon, 14 Sep 2026 20:11:37 -0300 Subject: [PATCH 1/3] feat(i18n): add zcash block page strings and tooltips --- src/locales/en/tooltips.json | 21 ++++++++++++ src/locales/en/zcash.json | 58 +++++++++++++++++++++++++++++++++ src/locales/es/tooltips.json | 21 ++++++++++++ src/locales/es/zcash.json | 58 +++++++++++++++++++++++++++++++++ src/locales/ja/tooltips.json | 21 ++++++++++++ src/locales/ja/zcash.json | 58 +++++++++++++++++++++++++++++++++ src/locales/pt-BR/tooltips.json | 21 ++++++++++++ src/locales/pt-BR/zcash.json | 58 +++++++++++++++++++++++++++++++++ src/locales/zh/tooltips.json | 21 ++++++++++++ src/locales/zh/zcash.json | 58 +++++++++++++++++++++++++++++++++ 10 files changed, 395 insertions(+) diff --git a/src/locales/en/tooltips.json b/src/locales/en/tooltips.json index 55f6273f..ad226cba 100644 --- a/src/locales/en/tooltips.json +++ b/src/locales/en/tooltips.json @@ -161,6 +161,27 @@ "orchard": "Shielded pool introduced with NU5 in 2022, using Halo 2 proofs that need no trusted setup.", "lockbox": "Deferred development fund: part of every block reward since NU6, held by the protocol until its use is decided.", "ironwood": "Shielded pool introduced with the NU6.3 (Ironwood) network upgrade." + }, + "block": { + "hash": "The unique identifier of this block, derived from its header.", + "confirmations": "Blocks mined on top of this one. More confirmations means the block is harder to reverse.", + "reward": "Total value of the coinbase outputs: the miner's subsidy plus fees, and funding streams paid to transparent addresses.", + "coinbaseOutputs": "Transparent outputs created by the coinbase transaction, such as the miner reward and development fund payments.", + "transactions": "Transactions in this block. Chips count the transactions that use each shielded pool.", + "transparentOutput": "Sum of all transparent outputs in this block. Shielded amounts are not publicly visible.", + "difficulty": "How hard it was to find a valid Equihash solution for this block.", + "size": "Size of the serialized block, including shielded proofs.", + "previousBlock": "The block this one builds on.", + "nextBlock": "The block built on top of this one, if any.", + "merkleRoot": "Root of the Merkle tree of all transaction IDs in this block.", + "blockCommitments": "Commitment to the chain history and authorizing data, required since NU5.", + "finalSaplingRoot": "Root of the Sapling note commitment tree after this block.", + "finalOrchardRoot": "Root of the Orchard note commitment tree after this block. Present on blocks after NU5.", + "version": "Block format version.", + "bits": "Compact encoding of the proof-of-work target.", + "nonce": "32-byte value miners vary while searching for a valid Equihash solution.", + "solution": "The Equihash proof-of-work solution for this block.", + "trees": "Number of notes committed to each shielded pool's note commitment tree as of this block." } }, "network": { diff --git a/src/locales/en/zcash.json b/src/locales/en/zcash.json index 78e67405..dbb3394b 100644 --- a/src/locales/en/zcash.json +++ b/src/locales/en/zcash.json @@ -41,6 +41,64 @@ "mixed": "Mixed" } }, + "breadcrumb": { + "home": "Home", + "blocks": "Blocks", + "block": "Block #{{height}}" + }, + "blocksPage": { + "title": "{{network}} Blocks", + "showingLatest": "Showing the {{count}} most recent blocks", + "showingRange": "Showing blocks {{from}} – {{to}}", + "height": "Height", + "hash": "Hash", + "time": "Time", + "txns": "Txns", + "size": "Size", + "latest": "Latest", + "latestTitle": "Go to the latest blocks", + "newer": "← Newer", + "newerTitle": "View newer blocks", + "older": "Older →", + "olderTitle": "View older blocks", + "loadError": "Couldn't load blocks: {{message}}" + }, + "block": { + "title": "Block", + "loading": "Loading block data...", + "loadError": "Couldn't load this block: {{message}}", + "notFound": "Block not found", + "previousBlock": "Previous block", + "nextBlock": "Next block", + "confirmations": "{{count}} Confirmations", + "hash": "Block Hash:", + "reward": "Block Reward:", + "coinbaseOutputs": "Coinbase Outputs:", + "noAddress": "Non-standard script", + "transactions": "Transactions:", + "txCount": "{{count}} transactions", + "poolTxCount": "{{count}} {{pool}}", + "transparentOutput": "Transparent Output:", + "difficulty": "Difficulty:", + "size": "Size:", + "previousBlockLabel": "Previous Block:", + "nextBlockLabel": "Next Block:", + "showDetails": "+ Show More Details", + "hideDetails": "− Hide More Details", + "merkleRoot": "Merkle Root:", + "blockCommitments": "Block Commitments:", + "finalSaplingRoot": "Final Sapling Root:", + "finalOrchardRoot": "Final Orchard Root:", + "version": "Version:", + "bits": "Bits:", + "nonce": "Nonce:", + "solution": "Equihash Solution:", + "trees": "Note Commitment Trees:", + "treeSize": "{{pool}} {{size}}", + "valuePoolsTitle": "Value Pools After This Block", + "showTransactions": "+ Show Transactions ({{count}})", + "hideTransactions": "− Hide Transactions ({{count}})" + }, "common": { "secondsAgo": "{{count}}s ago", "minutesAgo": "{{count}}m ago", diff --git a/src/locales/es/tooltips.json b/src/locales/es/tooltips.json index 081bfe0b..4097de05 100644 --- a/src/locales/es/tooltips.json +++ b/src/locales/es/tooltips.json @@ -161,6 +161,27 @@ "orchard": "Pool blindado introducido con NU5 en 2022, con pruebas Halo 2 que no requieren configuración de confianza.", "lockbox": "Fondo de desarrollo diferido: parte de cada recompensa de bloque desde NU6, retenida por el protocolo hasta que se decida su uso.", "ironwood": "Pool blindado introducido con la actualización de red NU6.3 (Ironwood)." + }, + "block": { + "hash": "Identificador único de este bloque, derivado de su encabezado.", + "confirmations": "Bloques minados sobre este. Más confirmaciones significa que el bloque es más difícil de revertir.", + "reward": "Valor total de las salidas coinbase: el subsidio del minero más las comisiones y los flujos de financiamiento pagados a direcciones transparentes.", + "coinbaseOutputs": "Salidas transparentes creadas por la transacción coinbase, como la recompensa del minero y los pagos al fondo de desarrollo.", + "transactions": "Transacciones en este bloque. Las etiquetas cuentan las transacciones que usan cada pool blindado.", + "transparentOutput": "Suma de todas las salidas transparentes de este bloque. Los montos blindados no son públicos.", + "difficulty": "Qué tan difícil fue encontrar una solución Equihash válida para este bloque.", + "size": "Tamaño del bloque serializado, incluidas las pruebas blindadas.", + "previousBlock": "El bloque sobre el que se construye este.", + "nextBlock": "El bloque construido sobre este, si existe.", + "merkleRoot": "Raíz del árbol Merkle de todos los IDs de transacción de este bloque.", + "blockCommitments": "Compromiso con el historial de la cadena y los datos de autorización, requerido desde NU5.", + "finalSaplingRoot": "Raíz del árbol de compromisos de notas de Sapling después de este bloque.", + "finalOrchardRoot": "Raíz del árbol de compromisos de notas de Orchard después de este bloque. Presente en bloques posteriores a NU5.", + "version": "Versión del formato del bloque.", + "bits": "Codificación compacta del objetivo de prueba de trabajo.", + "nonce": "Valor de 32 bytes que los mineros varían al buscar una solución Equihash válida.", + "solution": "La solución de prueba de trabajo Equihash de este bloque.", + "trees": "Cantidad de notas comprometidas en el árbol de compromisos de cada pool blindado hasta este bloque." } }, "network": { diff --git a/src/locales/es/zcash.json b/src/locales/es/zcash.json index 6f1311b1..99edaa69 100644 --- a/src/locales/es/zcash.json +++ b/src/locales/es/zcash.json @@ -41,6 +41,64 @@ "mixed": "Mixta" } }, + "breadcrumb": { + "home": "Inicio", + "blocks": "Bloques", + "block": "Bloque #{{height}}" + }, + "blocksPage": { + "title": "Bloques de {{network}}", + "showingLatest": "Mostrando los {{count}} bloques más recientes", + "showingRange": "Mostrando bloques {{from}} – {{to}}", + "height": "Altura", + "hash": "Hash", + "time": "Tiempo", + "txns": "Txs", + "size": "Tamaño", + "latest": "Últimos", + "latestTitle": "Ir a los últimos bloques", + "newer": "← Más recientes", + "newerTitle": "Ver bloques más recientes", + "older": "Más antiguos →", + "olderTitle": "Ver bloques más antiguos", + "loadError": "No se pudieron cargar los bloques: {{message}}" + }, + "block": { + "title": "Bloque", + "loading": "Cargando datos del bloque...", + "loadError": "No se pudo cargar este bloque: {{message}}", + "notFound": "Bloque no encontrado", + "previousBlock": "Bloque anterior", + "nextBlock": "Bloque siguiente", + "confirmations": "{{count}} confirmaciones", + "hash": "Hash del bloque:", + "reward": "Recompensa del bloque:", + "coinbaseOutputs": "Salidas coinbase:", + "noAddress": "Script no estándar", + "transactions": "Transacciones:", + "txCount": "{{count}} transacciones", + "poolTxCount": "{{count}} {{pool}}", + "transparentOutput": "Salida transparente:", + "difficulty": "Dificultad:", + "size": "Tamaño:", + "previousBlockLabel": "Bloque anterior:", + "nextBlockLabel": "Bloque siguiente:", + "showDetails": "+ Mostrar más detalles", + "hideDetails": "− Ocultar detalles", + "merkleRoot": "Raíz Merkle:", + "blockCommitments": "Compromisos del bloque:", + "finalSaplingRoot": "Raíz final de Sapling:", + "finalOrchardRoot": "Raíz final de Orchard:", + "version": "Versión:", + "bits": "Bits:", + "nonce": "Nonce:", + "solution": "Solución Equihash:", + "trees": "Árboles de compromisos de notas:", + "treeSize": "{{pool}} {{size}}", + "valuePoolsTitle": "Pools de valor después de este bloque", + "showTransactions": "+ Mostrar transacciones ({{count}})", + "hideTransactions": "− Ocultar transacciones ({{count}})" + }, "common": { "secondsAgo": "hace {{count}} s", "minutesAgo": "hace {{count}} min", diff --git a/src/locales/ja/tooltips.json b/src/locales/ja/tooltips.json index 37f51cd2..5875807d 100644 --- a/src/locales/ja/tooltips.json +++ b/src/locales/ja/tooltips.json @@ -161,6 +161,27 @@ "orchard": "2022年のNU5で導入されたシールドプール。信頼されたセットアップが不要なHalo 2証明を使用します。", "lockbox": "繰り延べ開発資金:NU6以降、各ブロック報酬の一部が使途決定までプロトコルによって保管されます。", "ironwood": "NU6.3(Ironwood)ネットワークアップグレードで導入されたシールドプール。" + }, + "block": { + "hash": "ヘッダーから導出される、このブロックの一意の識別子。", + "confirmations": "このブロックの上にマイニングされたブロック数。承認数が多いほど、ブロックは覆りにくくなります。", + "reward": "コインベース出力の合計額:マイナーの補助金と手数料、およびトランスペアレントアドレスに支払われる資金ストリーム。", + "coinbaseOutputs": "コインベーストランザクションが作成したトランスペアレント出力(マイナー報酬や開発資金への支払いなど)。", + "transactions": "このブロック内のトランザクション。チップは各シールドプールを使用するトランザクション数を示します。", + "transparentOutput": "このブロック内のすべてのトランスペアレント出力の合計。シールドされた金額は公開されません。", + "difficulty": "このブロックの有効なEquihashソリューションを見つける難しさ。", + "size": "シールド証明を含む、シリアライズされたブロックのサイズ。", + "previousBlock": "このブロックが基づいているブロック。", + "nextBlock": "このブロックの上に構築されたブロック(存在する場合)。", + "merkleRoot": "このブロック内のすべてのトランザクションIDのマークルツリーのルート。", + "blockCommitments": "NU5以降に必要な、チェーン履歴と承認データへのコミットメント。", + "finalSaplingRoot": "このブロック後のSaplingノートコミットメントツリーのルート。", + "finalOrchardRoot": "このブロック後のOrchardノートコミットメントツリーのルート。NU5以降のブロックに存在します。", + "version": "ブロックフォーマットのバージョン。", + "bits": "プルーフ・オブ・ワークのターゲットのコンパクトな表現。", + "nonce": "マイナーが有効なEquihashソリューションを探す際に変化させる32バイトの値。", + "solution": "このブロックのEquihashプルーフ・オブ・ワークのソリューション。", + "trees": "このブロック時点で各シールドプールのノートコミットメントツリーにコミットされたノート数。" } }, "network": { diff --git a/src/locales/ja/zcash.json b/src/locales/ja/zcash.json index 08b2d900..7fb8fa8d 100644 --- a/src/locales/ja/zcash.json +++ b/src/locales/ja/zcash.json @@ -41,6 +41,64 @@ "mixed": "混合" } }, + "breadcrumb": { + "home": "ホーム", + "blocks": "ブロック", + "block": "ブロック #{{height}}" + }, + "blocksPage": { + "title": "{{network}} のブロック", + "showingLatest": "最新 {{count}} 件のブロックを表示", + "showingRange": "ブロック {{from}} – {{to}} を表示", + "height": "高さ", + "hash": "ハッシュ", + "time": "時間", + "txns": "Tx数", + "size": "サイズ", + "latest": "最新", + "latestTitle": "最新のブロックへ移動", + "newer": "← 新しい", + "newerTitle": "新しいブロックを表示", + "older": "古い →", + "olderTitle": "古いブロックを表示", + "loadError": "ブロックを読み込めませんでした: {{message}}" + }, + "block": { + "title": "ブロック", + "loading": "ブロックデータを読み込み中...", + "loadError": "このブロックを読み込めませんでした: {{message}}", + "notFound": "ブロックが見つかりません", + "previousBlock": "前のブロック", + "nextBlock": "次のブロック", + "confirmations": "{{count}} 承認", + "hash": "ブロックハッシュ:", + "reward": "ブロック報酬:", + "coinbaseOutputs": "コインベース出力:", + "noAddress": "非標準スクリプト", + "transactions": "トランザクション:", + "txCount": "{{count}} 件のトランザクション", + "poolTxCount": "{{pool}} {{count}} 件", + "transparentOutput": "トランスペアレント出力:", + "difficulty": "難易度:", + "size": "サイズ:", + "previousBlockLabel": "前のブロック:", + "nextBlockLabel": "次のブロック:", + "showDetails": "+ 詳細を表示", + "hideDetails": "− 詳細を隠す", + "merkleRoot": "マークルルート:", + "blockCommitments": "ブロックコミットメント:", + "finalSaplingRoot": "最終 Sapling ルート:", + "finalOrchardRoot": "最終 Orchard ルート:", + "version": "バージョン:", + "bits": "Bits:", + "nonce": "ノンス:", + "solution": "Equihash ソリューション:", + "trees": "ノートコミットメントツリー:", + "treeSize": "{{pool}} {{size}}", + "valuePoolsTitle": "このブロック時点のバリュープール", + "showTransactions": "+ トランザクションを表示 ({{count}})", + "hideTransactions": "− トランザクションを隠す ({{count}})" + }, "common": { "secondsAgo": "{{count}}秒前", "minutesAgo": "{{count}}分前", diff --git a/src/locales/pt-BR/tooltips.json b/src/locales/pt-BR/tooltips.json index 68242f78..5fbfa73e 100644 --- a/src/locales/pt-BR/tooltips.json +++ b/src/locales/pt-BR/tooltips.json @@ -161,6 +161,27 @@ "orchard": "Pool blindado introduzido com o NU5 em 2022, usando provas Halo 2 que dispensam configuração confiável.", "lockbox": "Fundo de desenvolvimento diferido: parte de cada recompensa de bloco desde o NU6, retida pelo protocolo até que seu uso seja decidido.", "ironwood": "Pool blindado introduzido com a atualização de rede NU6.3 (Ironwood)." + }, + "block": { + "hash": "Identificador único deste bloco, derivado do seu cabeçalho.", + "confirmations": "Blocos minerados sobre este. Mais confirmações significa que o bloco é mais difícil de reverter.", + "reward": "Valor total das saídas coinbase: o subsídio do minerador mais as taxas e os fluxos de financiamento pagos a endereços transparentes.", + "coinbaseOutputs": "Saídas transparentes criadas pela transação coinbase, como a recompensa do minerador e os pagamentos ao fundo de desenvolvimento.", + "transactions": "Transações neste bloco. As etiquetas contam as transações que usam cada pool blindado.", + "transparentOutput": "Soma de todas as saídas transparentes deste bloco. Valores blindados não são públicos.", + "difficulty": "Quão difícil foi encontrar uma solução Equihash válida para este bloco.", + "size": "Tamanho do bloco serializado, incluindo as provas blindadas.", + "previousBlock": "O bloco sobre o qual este é construído.", + "nextBlock": "O bloco construído sobre este, se houver.", + "merkleRoot": "Raiz da árvore Merkle de todos os IDs de transação deste bloco.", + "blockCommitments": "Compromisso com o histórico da cadeia e os dados de autorização, exigido desde o NU5.", + "finalSaplingRoot": "Raiz da árvore de compromissos de notas do Sapling após este bloco.", + "finalOrchardRoot": "Raiz da árvore de compromissos de notas do Orchard após este bloco. Presente em blocos posteriores ao NU5.", + "version": "Versão do formato do bloco.", + "bits": "Codificação compacta do alvo da prova de trabalho.", + "nonce": "Valor de 32 bytes que os mineradores variam ao buscar uma solução Equihash válida.", + "solution": "A solução de prova de trabalho Equihash deste bloco.", + "trees": "Número de notas comprometidas na árvore de compromissos de cada pool blindado até este bloco." } }, "network": { diff --git a/src/locales/pt-BR/zcash.json b/src/locales/pt-BR/zcash.json index e0505505..11470feb 100644 --- a/src/locales/pt-BR/zcash.json +++ b/src/locales/pt-BR/zcash.json @@ -41,6 +41,64 @@ "mixed": "Mista" } }, + "breadcrumb": { + "home": "Início", + "blocks": "Blocos", + "block": "Bloco #{{height}}" + }, + "blocksPage": { + "title": "Blocos de {{network}}", + "showingLatest": "Mostrando os {{count}} blocos mais recentes", + "showingRange": "Mostrando blocos {{from}} – {{to}}", + "height": "Altura", + "hash": "Hash", + "time": "Tempo", + "txns": "Txs", + "size": "Tamanho", + "latest": "Mais recentes", + "latestTitle": "Ir para os blocos mais recentes", + "newer": "← Mais novos", + "newerTitle": "Ver blocos mais novos", + "older": "Mais antigos →", + "olderTitle": "Ver blocos mais antigos", + "loadError": "Não foi possível carregar os blocos: {{message}}" + }, + "block": { + "title": "Bloco", + "loading": "Carregando dados do bloco...", + "loadError": "Não foi possível carregar este bloco: {{message}}", + "notFound": "Bloco não encontrado", + "previousBlock": "Bloco anterior", + "nextBlock": "Próximo bloco", + "confirmations": "{{count}} confirmações", + "hash": "Hash do bloco:", + "reward": "Recompensa do bloco:", + "coinbaseOutputs": "Saídas coinbase:", + "noAddress": "Script não padrão", + "transactions": "Transações:", + "txCount": "{{count}} transações", + "poolTxCount": "{{count}} {{pool}}", + "transparentOutput": "Saída transparente:", + "difficulty": "Dificuldade:", + "size": "Tamanho:", + "previousBlockLabel": "Bloco anterior:", + "nextBlockLabel": "Próximo bloco:", + "showDetails": "+ Mostrar mais detalhes", + "hideDetails": "− Ocultar detalhes", + "merkleRoot": "Raiz Merkle:", + "blockCommitments": "Compromissos do bloco:", + "finalSaplingRoot": "Raiz final do Sapling:", + "finalOrchardRoot": "Raiz final do Orchard:", + "version": "Versão:", + "bits": "Bits:", + "nonce": "Nonce:", + "solution": "Solução Equihash:", + "trees": "Árvores de compromissos de notas:", + "treeSize": "{{pool}} {{size}}", + "valuePoolsTitle": "Pools de valor após este bloco", + "showTransactions": "+ Mostrar transações ({{count}})", + "hideTransactions": "− Ocultar transações ({{count}})" + }, "common": { "secondsAgo": "há {{count}} s", "minutesAgo": "há {{count}} min", diff --git a/src/locales/zh/tooltips.json b/src/locales/zh/tooltips.json index 239836c8..5c62967c 100644 --- a/src/locales/zh/tooltips.json +++ b/src/locales/zh/tooltips.json @@ -161,6 +161,27 @@ "orchard": "随 2022 年 NU5 升级引入的屏蔽池,使用无需可信设置的 Halo 2 证明。", "lockbox": "递延开发基金:自 NU6 起每个区块奖励的一部分,由协议保管,直至决定其用途。", "ironwood": "随 NU6.3(Ironwood)网络升级引入的屏蔽池。" + }, + "block": { + "hash": "由区块头派生的该区块唯一标识符。", + "confirmations": "在此区块之上挖出的区块数。确认越多,区块越难被撤销。", + "reward": "Coinbase 输出总额:矿工补贴加手续费,以及支付给透明地址的资金流。", + "coinbaseOutputs": "Coinbase 交易创建的透明输出,例如矿工奖励和开发基金付款。", + "transactions": "此区块中的交易。标签统计使用各屏蔽池的交易数量。", + "transparentOutput": "此区块中所有透明输出的总和。屏蔽金额不公开。", + "difficulty": "为此区块找到有效 Equihash 解的难度。", + "size": "序列化区块的大小,包括屏蔽证明。", + "previousBlock": "此区块所基于的区块。", + "nextBlock": "构建在此区块之上的区块(如有)。", + "merkleRoot": "此区块中所有交易 ID 的默克尔树根。", + "blockCommitments": "对链历史和授权数据的承诺,自 NU5 起必需。", + "finalSaplingRoot": "此区块之后 Sapling 票据承诺树的根。", + "finalOrchardRoot": "此区块之后 Orchard 票据承诺树的根。仅存在于 NU5 之后的区块。", + "version": "区块格式版本。", + "bits": "工作量证明目标的紧凑编码。", + "nonce": "矿工在寻找有效 Equihash 解时改变的 32 字节值。", + "solution": "此区块的 Equihash 工作量证明解。", + "trees": "截至此区块,各屏蔽池票据承诺树中已提交的票据数量。" } }, "network": { diff --git a/src/locales/zh/zcash.json b/src/locales/zh/zcash.json index 3b64c42c..690e6e35 100644 --- a/src/locales/zh/zcash.json +++ b/src/locales/zh/zcash.json @@ -41,6 +41,64 @@ "mixed": "混合" } }, + "breadcrumb": { + "home": "首页", + "blocks": "区块", + "block": "区块 #{{height}}" + }, + "blocksPage": { + "title": "{{network}} 区块", + "showingLatest": "显示最近 {{count}} 个区块", + "showingRange": "显示区块 {{from}} – {{to}}", + "height": "高度", + "hash": "哈希", + "time": "时间", + "txns": "交易数", + "size": "大小", + "latest": "最新", + "latestTitle": "前往最新区块", + "newer": "← 较新", + "newerTitle": "查看较新的区块", + "older": "较旧 →", + "olderTitle": "查看较旧的区块", + "loadError": "无法加载区块: {{message}}" + }, + "block": { + "title": "区块", + "loading": "正在加载区块数据...", + "loadError": "无法加载此区块: {{message}}", + "notFound": "未找到区块", + "previousBlock": "上一个区块", + "nextBlock": "下一个区块", + "confirmations": "{{count}} 个确认", + "hash": "区块哈希:", + "reward": "区块奖励:", + "coinbaseOutputs": "Coinbase 输出:", + "noAddress": "非标准脚本", + "transactions": "交易:", + "txCount": "{{count}} 笔交易", + "poolTxCount": "{{pool}} {{count}} 笔", + "transparentOutput": "透明输出:", + "difficulty": "难度:", + "size": "大小:", + "previousBlockLabel": "上一个区块:", + "nextBlockLabel": "下一个区块:", + "showDetails": "+ 显示更多详情", + "hideDetails": "− 隐藏详情", + "merkleRoot": "默克尔根:", + "blockCommitments": "区块承诺:", + "finalSaplingRoot": "最终 Sapling 根:", + "finalOrchardRoot": "最终 Orchard 根:", + "version": "版本:", + "bits": "Bits:", + "nonce": "Nonce:", + "solution": "Equihash 解:", + "trees": "票据承诺树:", + "treeSize": "{{pool}} {{size}}", + "valuePoolsTitle": "此区块后的价值池", + "showTransactions": "+ 显示交易 ({{count}})", + "hideTransactions": "− 隐藏交易 ({{count}})" + }, "common": { "secondsAgo": "{{count}}秒前", "minutesAgo": "{{count}}分钟前", From ca3db6ac67c1b64047d585b32de423ea484ed6a6 Mon Sep 17 00:00:00 2001 From: Mati OS Date: Mon, 14 Sep 2026 20:11:38 -0300 Subject: [PATCH 2/3] feat(zcash): add blocks list and block detail pages Block details come from a single getblock call and surface Zcash header fields (hex nonce, Equihash solution, commitment roots, note commitment tree sizes), coinbase outputs, per-pool shielded transaction counts and value pools after the block. --- src/App.tsx | 6 + src/components/LazyComponents.tsx | 6 + .../pages/zcash/ZcashBlockDisplay.tsx | 399 ++++++++++++++++++ src/components/pages/zcash/ZcashBlockPage.tsx | 125 ++++++ .../pages/zcash/ZcashBlocksPage.tsx | 227 ++++++++++ .../pages/zcash/ZcashValuePools.tsx | 25 +- src/components/pages/zcash/index.tsx | 6 +- src/config/zcashConstants.ts | 3 + src/styles/components.css | 59 +++ 9 files changed, 845 insertions(+), 11 deletions(-) create mode 100644 src/components/pages/zcash/ZcashBlockDisplay.tsx create mode 100644 src/components/pages/zcash/ZcashBlockPage.tsx create mode 100644 src/components/pages/zcash/ZcashBlocksPage.tsx diff --git a/src/App.tsx b/src/App.tsx index b6294f31..018d08e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,6 +56,8 @@ import { LazyTokenDetails, LazyTx, LazyTxs, + LazyZcashBlock, + LazyZcashBlocks, LazyZcashNetwork, preloadAllRoutes, } from "./components/LazyComponents"; @@ -162,8 +164,12 @@ function AppContent() { } /> {/* Zcash Mainnet routes (must come before :networkId catch-all) */} } /> + } /> + } /> {/* Zcash Testnet routes */} } /> + } /> + } /> {/* Solana Mainnet routes (must come before :networkId catch-all) */} } /> } /> diff --git a/src/components/LazyComponents.tsx b/src/components/LazyComponents.tsx index 80b70255..0a5bf25a 100644 --- a/src/components/LazyComponents.tsx +++ b/src/components/LazyComponents.tsx @@ -22,6 +22,8 @@ const BitcoinMempoolPage = lazy(() => import("./pages/bitcoin/BitcoinMempoolPage // Lazy load page components - Zcash const ZcashNetwork = lazy(() => import("./pages/zcash")); +const ZcashBlocksPage = lazy(() => import("./pages/zcash/ZcashBlocksPage")); +const ZcashBlockPage = lazy(() => import("./pages/zcash/ZcashBlockPage")); // Lazy load page components - Solana const SolanaNetwork = lazy(() => import("./pages/solana")); @@ -68,6 +70,8 @@ export const LazyBitcoinTxs = withSuspense(BitcoinTransactionsPage); export const LazyBitcoinAddress = withSuspense(BitcoinAddressPage); export const LazyBitcoinMempool = withSuspense(BitcoinMempoolPage); export const LazyZcashNetwork = withSuspense(ZcashNetwork); +export const LazyZcashBlocks = withSuspense(ZcashBlocksPage); +export const LazyZcashBlock = withSuspense(ZcashBlockPage); export const LazySolanaNetwork = withSuspense(SolanaNetwork); export const LazySolanaSlots = withSuspense(SolanaSlotsPage); export const LazySolanaSlot = withSuspense(SolanaSlotPage); @@ -115,6 +119,8 @@ export function preloadAllRoutes() { import("./pages/bitcoin/BitcoinMempoolPage"); // Zcash pages import("./pages/zcash"); + import("./pages/zcash/ZcashBlocksPage"); + import("./pages/zcash/ZcashBlockPage"); // Solana pages import("./pages/solana"); import("./pages/solana/SolanaSlotsPage"); diff --git a/src/components/pages/zcash/ZcashBlockDisplay.tsx b/src/components/pages/zcash/ZcashBlockDisplay.tsx new file mode 100644 index 00000000..a68bd7e7 --- /dev/null +++ b/src/components/pages/zcash/ZcashBlockDisplay.tsx @@ -0,0 +1,399 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { useSettings } from "../../../context/SettingsContext"; +import { useZcashTimeAgo } from "../../../hooks/useZcashTimeAgo"; +import type { ZcashBlock, ZcashShieldedPool } from "../../../types"; +import { + formatDifficulty, + formatNumber, + formatSize, + formatTimestamp, + formatZEC, + truncateBlockHash, +} from "../../../utils/zcashFormatters"; +import CopyButton from "../../common/CopyButton"; +import FieldLabel from "../../common/FieldLabel"; +import HelperTooltip from "../../common/HelperTooltip"; +import ZcashValuePools from "./ZcashValuePools"; + +const SHIELDED_POOLS: ZcashShieldedPool[] = ["sprout", "sapling", "orchard", "ironwood"]; +const TREE_POOLS = ["sapling", "orchard", "ironwood"] as const; + +const POOL_NAME_KEYS = { + sprout: "valuePools.pools.sprout", + sapling: "valuePools.pools.sapling", + orchard: "valuePools.pools.orchard", + ironwood: "valuePools.pools.ironwood", +} as const satisfies Record; + +interface DetailRowProps { + label: string; + tooltip?: string; + mono?: boolean; + children: React.ReactNode; +} + +const DetailRow: React.FC = ({ label, tooltip, mono, children }) => ( +
+ + {label} + {tooltip && } + + {children} +
+); + +interface ZcashBlockDisplayProps { + block: ZcashBlock; + networkId: string; + currency: string; +} + +const ZcashBlockDisplay: React.FC = React.memo( + ({ block, networkId, currency }) => { + const { t } = useTranslation("zcash"); + const { t: tTooltips } = useTranslation("tooltips"); + const { settings } = useSettings(); + const timeAgo = useZcashTimeAgo(); + const [showMoreDetails, setShowMoreDetails] = useState(false); + const [showTransactions, setShowTransactions] = useState(false); + const showTooltips = settings.showHelperTooltips !== false; + + const coinbaseOutputs = block.coinbaseOutputs ?? []; + const rewardZat = coinbaseOutputs.reduce((sum, output) => sum + output.valueZat, 0); + const txids = block.txids ?? []; + const poolCounts = SHIELDED_POOLS.flatMap((pool) => { + const count = block.shieldedTxCounts?.[pool]; + return count ? [{ pool, count }] : []; + }); + const treeSizes = TREE_POOLS.flatMap((pool) => { + const size = block.trees?.[pool]; + return size === undefined + ? [] + : [t("block.treeSize", { pool: t(POOL_NAME_KEYS[pool]), size: formatNumber(size) })]; + }); + + return ( +
+
+
+ {block.height > 0 && ( + + ← + + )} +
+ {t("block.title")} + #{block.height.toLocaleString()} +
+ {block.nextBlockHash && ( + + → + + )} + + + {timeAgo(block.time)} + ({formatTimestamp(block.time)}) + +
+ {block.confirmations !== undefined && ( + + {t("block.confirmations", { count: block.confirmations })} + {showTooltips && ( + + )} + + )} +
+ +
+
+ + + {block.hash} + + +
+ +
+
+ {coinbaseOutputs.length > 0 && ( +
+ + + {formatZEC(rewardZat, currency)} + +
+ )} + +
+ + + {t("block.txCount", { count: block.nTx })} + {poolCounts.length > 0 && ( + + {poolCounts.map(({ pool, count }) => ( + + {t("block.poolTxCount", { count, pool: t(POOL_NAME_KEYS[pool]) })} + + ))} + + )} + +
+ + {block.transparentOutputZat !== undefined && ( +
+ + + {formatZEC(block.transparentOutputZat, currency)} + +
+ )} +
+ +
+
+ + {formatDifficulty(block.difficulty)} +
+ +
+ + {formatSize(block.size)} +
+ + {block.previousBlockHash && ( +
+ + + + {truncateBlockHash(block.previousBlockHash)} + + +
+ )} + + {block.nextBlockHash && ( +
+ + + + {truncateBlockHash(block.nextBlockHash)} + + +
+ )} +
+
+ + {coinbaseOutputs.length > 0 && ( +
+ +
    + {coinbaseOutputs.map((output, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: outputs keep their on-chain order +
  • + {output.address ? ( + + {output.address} + + ) : ( + {t("block.noAddress")} + )} + + {formatZEC(output.valueZat, currency)} + +
  • + ))} +
+
+ )} + +
+ + + {showMoreDetails && ( +
+ + {block.merkleRoot} + + {block.blockCommitments && ( + + {block.blockCommitments} + + )} + {block.finalSaplingRoot && ( + + {block.finalSaplingRoot} + + )} + {block.finalOrchardRoot && ( + + {block.finalOrchardRoot} + + )} + + {block.version} + + + {block.bits} + + + {block.nonce} + + {block.solution && ( + + + {block.solution.length > 64 + ? `${block.solution.slice(0, 32)}…${block.solution.slice(-32)}` + : block.solution} + + + + )} + {treeSizes.length > 0 && ( + + {treeSizes.join(" · ")} + + )} +
+ )} +
+ + {txids.length > 0 && ( +
+ + + {showTransactions && ( +
+
+ {txids.map((txid, index) => ( +
+ {index} + + + {txid} + + +
+ ))} +
+
+ )} +
+ )} +
+ + {block.valuePools && block.valuePools.length > 0 && ( + + )} +
+ ); + }, +); + +ZcashBlockDisplay.displayName = "ZcashBlockDisplay"; +export default ZcashBlockDisplay; diff --git a/src/components/pages/zcash/ZcashBlockPage.tsx b/src/components/pages/zcash/ZcashBlockPage.tsx new file mode 100644 index 00000000..e0021f4c --- /dev/null +++ b/src/components/pages/zcash/ZcashBlockPage.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation, useParams } from "react-router-dom"; +import { getNetworkBySlug } from "../../../config/networks"; +import { useDataService } from "../../../hooks/useDataService"; +import { usePersistentCache } from "../../../hooks/usePersistentCache"; +import type { ZcashBlock } from "../../../types"; +import Breadcrumb from "../../common/Breadcrumb"; +import LoaderWithTimeout from "../../common/LoaderWithTimeout"; +import ZcashBlockDisplay from "./ZcashBlockDisplay"; + +export default function ZcashBlockPage() { + const { t } = useTranslation("zcash"); + const { filter } = useParams<{ filter?: string }>(); + const location = useLocation(); + + // Extract network slug from path (e.g., "/tzec/block/123" → "tzec") + const networkSlug = location.pathname.split("/")[1] || "zec"; + const dataService = useDataService(networkSlug); + const network = getNetworkBySlug(networkSlug); + const networkLabel = network?.shortName || networkSlug.toUpperCase(); + const cacheNetworkId = network?.networkId ?? networkSlug; + const { getCached, setCached } = usePersistentCache(); + + const [block, setBlock] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!dataService || !dataService.isZcash() || !filter) return; + + let cancelled = false; + const adapter = dataService.getZcashAdapter(); + + const fetchBlock = async () => { + setLoading(true); + setError(null); + + try { + let blockId: string | number = filter; + if (filter === "latest") { + blockId = await adapter.getLatestBlockNumber(); + } else if (/^\d+$/.test(filter)) { + const cached = getCached(cacheNetworkId, "block", filter); + if (cached) { + if (!cancelled) setBlock(cached); + return; + } + } + + // A single getblock call returns the header, transactions and value pools + const { data } = await adapter.getBlock(blockId); + if (cancelled) return; + setBlock(data.block); + setCached(cacheNetworkId, "block", String(data.block.height), data.block); + } catch (err) { + if (!cancelled) setError(err instanceof Error ? err.message : String(err)); + } finally { + if (!cancelled) setLoading(false); + } + }; + + fetchBlock(); + return () => { + cancelled = true; + }; + }, [dataService, filter, getCached, setCached, cacheNetworkId]); + + if (loading) { + return ( +
+
+
+ {t("block.title")} + {filter} +
+
+ window.location.reload()} /> +
+
+
+ ); + } + + if (error) { + return ( +
+
+
+ {t("block.title")} +
+
+

{t("block.loadError", { message: error })}

+
+
+
+ ); + } + + return ( +
+ + {block ? ( + + ) : ( +
+
+

{t("block.notFound")}

+
+
+ )} +
+ ); +} diff --git a/src/components/pages/zcash/ZcashBlocksPage.tsx b/src/components/pages/zcash/ZcashBlocksPage.tsx new file mode 100644 index 00000000..c47ad496 --- /dev/null +++ b/src/components/pages/zcash/ZcashBlocksPage.tsx @@ -0,0 +1,227 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom"; +import { getNetworkBySlug } from "../../../config/networks"; +import { BLOCKS_PER_PAGE } from "../../../config/zcashConstants"; +import { useDataService } from "../../../hooks/useDataService"; +import { useZcashTimeAgo } from "../../../hooks/useZcashTimeAgo"; +import type { ZcashBlock } from "../../../types"; +import { logger } from "../../../utils/logger"; +import { + formatNumber, + formatSize, + formatTimestamp, + truncateBlockHash, +} from "../../../utils/zcashFormatters"; +import Breadcrumb from "../../common/Breadcrumb"; + +const skeletonCell = (width: string) => ( + +); + +export default function ZcashBlocksPage() { + const { t } = useTranslation("zcash"); + const timeAgo = useZcashTimeAgo(); + const location = useLocation(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + // Extract network slug from path (e.g., "/tzec/blocks" → "tzec") + const networkSlug = location.pathname.split("/")[1] || "zec"; + const dataService = useDataService(networkSlug); + const network = getNetworkBySlug(networkSlug); + const networkName = network?.name ?? networkSlug; + const networkLabel = network?.shortName || networkSlug.toUpperCase(); + + const fromBlockParam = searchParams.get("fromBlock"); + const fromBlock = + fromBlockParam !== null && /^\d+$/.test(fromBlockParam) ? Number(fromBlockParam) : null; + + const [blocks, setBlocks] = useState([]); + const [latestHeight, setLatestHeight] = useState(null); + const [fetching, setFetching] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!dataService || !dataService.isZcash()) return; + + let cancelled = false; + const adapter = dataService.getZcashAdapter(); + + const fetchBlocks = async () => { + setFetching(true); + setError(null); + setBlocks([]); + + try { + const tipHeight = await adapter.getLatestBlockNumber(); + if (cancelled) return; + setLatestHeight(tipHeight); + + const start = Math.min(fromBlock ?? tipHeight, tipHeight); + for (let height = start; height > start - BLOCKS_PER_PAGE && height >= 0; height--) { + // One block per call, so rows appear as they arrive on rate-limited endpoints + const [block] = await adapter.getBlockSummaries([height]); + if (cancelled) return; + if (block) setBlocks((prev) => [...prev, block]); + } + } catch (err) { + logger.error("Error fetching Zcash blocks:", err); + if (!cancelled) setError(err instanceof Error ? err.message : String(err)); + } finally { + if (!cancelled) setFetching(false); + } + }; + + fetchBlocks(); + return () => { + cancelled = true; + }; + }, [dataService, fromBlock]); + + const startHeight = + latestHeight === null ? null : Math.min(fromBlock ?? latestHeight, latestHeight); + const oldestHeight = startHeight === null ? null : Math.max(startHeight - BLOCKS_PER_PAGE + 1, 0); + const isAtLatest = fromBlock === null || (latestHeight !== null && fromBlock >= latestHeight); + const canGoNewer = !isAtLatest && startHeight !== null; + const canGoOlder = startHeight !== null && startHeight - BLOCKS_PER_PAGE >= 0; + + const expectedRows = + startHeight === null ? BLOCKS_PER_PAGE : Math.min(BLOCKS_PER_PAGE, startHeight + 1); + const skeletonRows = fetching ? Math.max(expectedRows - blocks.length, 0) : 0; + + const goToLatest = () => navigate(`/${networkSlug}/blocks`); + + const goToNewer = () => { + if (startHeight === null || latestHeight === null) return; + const next = startHeight + BLOCKS_PER_PAGE; + navigate( + next >= latestHeight ? `/${networkSlug}/blocks` : `/${networkSlug}/blocks?fromBlock=${next}`, + ); + }; + + const goToOlder = () => { + if (startHeight === null) return; + navigate(`/${networkSlug}/blocks?fromBlock=${startHeight - BLOCKS_PER_PAGE}`); + }; + + return ( +
+ +
+
+
+ {t("blocksPage.title", { network: networkName })} + {startHeight !== null && oldestHeight !== null && ( + <> + + + {isAtLatest + ? t("blocksPage.showingLatest", { count: BLOCKS_PER_PAGE }) + : t("blocksPage.showingRange", { + from: formatNumber(oldestHeight), + to: formatNumber(startHeight), + })} + + + )} +
+
+ + {error ? ( +
+

{t("blocksPage.loadError", { message: error })}

+
+ ) : ( +
+ + + + + + + + + + + + {blocks.map((block) => ( + + + + + + + + ))} + {Array.from({ length: skeletonRows }).map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: skeleton placeholder + + + + + + + + ))} + +
{t("blocksPage.height")}{t("blocksPage.hash")}{t("blocksPage.time")}{t("blocksPage.txns")}{t("blocksPage.size")}
+ + {block.height.toLocaleString()} + + + + {truncateBlockHash(block.hash)} + + + {timeAgo(block.time)} + {block.nTx.toLocaleString()}{formatSize(block.size)}
{skeletonCell("70px")}{skeletonCell("120px")}{skeletonCell("60px")}{skeletonCell("40px")}{skeletonCell("80px")}
+
+ )} + +
+ + + +
+
+
+ ); +} diff --git a/src/components/pages/zcash/ZcashValuePools.tsx b/src/components/pages/zcash/ZcashValuePools.tsx index 55b7cab7..9cc63b2d 100644 --- a/src/components/pages/zcash/ZcashValuePools.tsx +++ b/src/components/pages/zcash/ZcashValuePools.tsx @@ -1,7 +1,7 @@ import type React from "react"; import { useTranslation } from "react-i18next"; import { useSettings } from "../../../context/SettingsContext"; -import type { ZcashNetworkStats, ZcashValuePoolId } from "../../../types"; +import type { ZcashValuePool, ZcashValuePoolId } from "../../../types"; import { formatZECCompact } from "../../../utils/zcashFormatters"; import HelperTooltip from "../../common/HelperTooltip"; @@ -24,32 +24,37 @@ const POOL_TOOLTIP_KEYS = { } as const satisfies Record; interface ZcashValuePoolsProps { - stats: ZcashNetworkStats | null; + valuePools: ZcashValuePool[]; + chainSupplyZat?: number; currency: string; + title?: string; } -const ZcashValuePools: React.FC = ({ stats, currency }) => { +const ZcashValuePools: React.FC = ({ + valuePools, + chainSupplyZat, + currency, + title, +}) => { const { t } = useTranslation("zcash"); const { t: tTooltips } = useTranslation("tooltips"); const { settings } = useSettings(); const showTooltips = settings.showHelperTooltips !== false; - const pools = stats?.valuePools ?? []; - const totalZat = - stats?.chainSupplyZat || pools.reduce((sum, pool) => sum + pool.valueZat, 0) || 0; - if (pools.length === 0 || totalZat <= 0) return null; + const totalZat = chainSupplyZat || valuePools.reduce((sum, pool) => sum + pool.valueZat, 0); + if (valuePools.length === 0 || totalZat <= 0) return null; const sharePercent = (valueZat: number) => (valueZat / totalZat) * 100; return (
-

{t("valuePools.title")}

+

{title ?? t("valuePools.title")}

{showTooltips && }
- {pools.map((pool) => ( + {valuePools.map((pool) => ( = ({ stats, currency }) =>
    - {pools.map((pool) => ( + {valuePools.map((pool) => (
  • {t(POOL_LABEL_KEYS[pool.id])} diff --git a/src/components/pages/zcash/index.tsx b/src/components/pages/zcash/index.tsx index 263420f3..a4f7e5fb 100644 --- a/src/components/pages/zcash/index.tsx +++ b/src/components/pages/zcash/index.tsx @@ -63,7 +63,11 @@ export default function ZcashNetwork() { loading={dashboard.loading} /> - +
    Date: Mon, 14 Sep 2026 20:11:39 -0300 Subject: [PATCH 3/3] test(e2e): add hermetic Zcash blocks spec --- e2e/tests/shared/mocked/zcash-blocks.spec.ts | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 e2e/tests/shared/mocked/zcash-blocks.spec.ts diff --git a/e2e/tests/shared/mocked/zcash-blocks.spec.ts b/e2e/tests/shared/mocked/zcash-blocks.spec.ts new file mode 100644 index 00000000..41962735 --- /dev/null +++ b/e2e/tests/shared/mocked/zcash-blocks.spec.ts @@ -0,0 +1,56 @@ +import { BLOCK_3483400 } from "../../../../src/services/adapters/ZcashAdapter/fixtures"; +import { expect, test } from "../../../fixtures/test"; +import { mockZcashRpc, ZCASH } from "../../../fixtures/zcash"; + +/** + * Hermetic Zcash blocks list and block detail pages, served from mainnet block 3,483,400. + */ +test.describe("Zcash blocks", () => { + test("lists the latest blocks one call per block and pages to older blocks", async ({ + page, + }) => { + const calls = await mockZcashRpc(page); + + await page.goto(`/#/${ZCASH.networkSlug}/blocks`); + + const table = page.locator("table.blocks-table"); + await expect(table).toContainText("3,483,400"); + await expect(table).toContainText("3,483,391"); + await expect(table.locator("tbody tr")).toHaveCount(10); + + const summaryHeights = calls + .filter((call) => call.method === "getblock" && call.params[1] === 1) + .map((call) => call.params[0]); + expect(summaryHeights).toContain("3483391"); + expect(calls.map((call) => call.method)).not.toContain("getblockhash"); + + await page.getByRole("button", { name: "Older →" }).click(); + await expect(page).toHaveURL(/fromBlock=3483390/); + await expect(table).toContainText("3,483,381"); + }); + + test("shows Zcash-specific block details", async ({ page }) => { + const calls = await mockZcashRpc(page); + + await page.goto(`/#/${ZCASH.networkSlug}/block/${ZCASH.tipHeight}`); + + await expect(page.locator(".block-number")).toHaveText("#3,483,400"); + await expect(page.getByText(BLOCK_3483400.hash)).toBeVisible(); + await expect(page.getByText("t1SqwRAAdSig6dE4EBPLonAait219VmkUjP")).toBeVisible(); + await expect(page.getByText("2 Sapling")).toBeVisible(); + await expect(page.getByText("2 Ironwood")).toBeVisible(); + await expect(page.getByTestId("zcash-value-pools")).toContainText("Value Pools After This Block"); + + await page.getByRole("button", { name: "+ Show More Details" }).click(); + await expect(page.getByText(BLOCK_3483400.nonce)).toBeVisible(); + await expect(page.getByText("Equihash Solution:")).toBeVisible(); + await expect(page.getByText("Ironwood 318,976")).toBeVisible(); + + // Only the requested block is fetched with full transactions, by height string. The dev + // server runs React StrictMode, which can issue the same request twice. + const verboseBlockCalls = calls.filter( + (call) => call.method === "getblock" && call.params[1] === 2, + ); + expect(new Set(verboseBlockCalls.map((call) => call.params[0]))).toEqual(new Set(["3483400"])); + }); +});