From f7a346d39bea67a1413bec34166434bbfd062d86 Mon Sep 17 00:00:00 2001 From: MarkFrFn <39285682+MarkFrFn@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:06:18 -0500 Subject: [PATCH 1/3] feat: add practica-01 ETH Document Registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dApp completa para almacenar y verificar autenticidad de documentos en blockchain Ethereum mediante firmas digitales ECDSA. Stack: Solidity 0.8.18 · Foundry · OpenZeppelin ECDSA · Next.js 14 · TypeScript · ethers.js v6 · Tailwind CSS Incluye: - Smart contract optimizado (sin bool exists, modifiers, optimizer) - 11/11 tests pasando con script de visualizacion detallada - Frontend con dark mode, drag & drop, CSV export, busqueda - Script start.sh para arrancar todo el stack en un comando - Documentacion completa: README, lecciones aprendidas, guion video - Submodulos: OpenZeppelin contracts + forge-std Co-Authored-By: Claude Sonnet 4.6 --- .gitmodules | 6 + README.md | 30 +- practica-01-eth-document-registry/.gitignore | 43 + practica-01-eth-document-registry/.gitmodules | 3 + practica-01-eth-document-registry/README.md | 189 + .../contracts/.gitignore | 14 + .../contracts/README.md | 66 + .../contracts/foundry.toml | 8 + .../contracts/lib/forge-std | 1 + .../contracts/lib/openzeppelin-contracts | 1 + .../contracts/run_tests.sh | 118 + .../contracts/script/Counter.s.sol | 19 + .../contracts/script/Deploy.s.sol | 16 + .../contracts/src/Counter.sol | 14 + .../contracts/src/DocumentRegistry.sol | 96 + .../src/interfaces/IDocumentRegistry.sol | 13 + .../contracts/test/Counter.t.sol | 24 + .../contracts/test/DocumentRegistry.t.sol | 166 + .../dapp/.env.local | 38 + .../dapp/.eslintrc.json | 3 + .../dapp/.gitignore | 20 + .../dapp/README.md | 16 + .../dapp/app/globals.css | 48 + .../dapp/app/layout.tsx | 32 + .../dapp/app/page.tsx | 131 + .../dapp/app/providers.tsx | 7 + .../dapp/components/DocumentHistory.tsx | 214 + .../dapp/components/DocumentSigner.tsx | 184 + .../dapp/components/DocumentVerifier.tsx | 171 + .../dapp/components/FileUploader.tsx | 150 + .../dapp/components/WalletSelector.tsx | 142 + .../dapp/contexts/MetaMaskContext.tsx | 199 + .../dapp/hooks/useContract.ts | 185 + .../dapp/hooks/useFileHash.ts | 27 + .../dapp/hooks/useMetaMask.ts | 1 + .../dapp/hooks/useTheme.ts | 22 + .../dapp/next-env.d.ts | 5 + .../dapp/next.config.js | 16 + .../dapp/package-lock.json | 6409 +++++++++++++++++ .../dapp/package.json | 35 + .../dapp/postcss.config.js | 6 + .../dapp/tailwind.config.ts | 19 + .../dapp/tsconfig.json | 48 + .../dapp/types/ethereum.d.ts | 30 + .../dapp/utils/ethers.ts | 79 + .../dapp/utils/hash.ts | 67 + .../docs/DEPLOYMENT_GUIDE.md | 136 + .../docs/GUIA_DE_USO.md | 384 + .../docs/GUION_VIDEO.md | 208 + .../docs/LECCIONES_APRENDIDAS.md | 299 + .../docs/QUICK_START.md | 337 + .../docs/TAREA PARA ESTUDIANTE.md | 664 ++ .../foundry.toml | 8 + practica-01-eth-document-registry/start.sh | 188 + 54 files changed, 11353 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 100644 practica-01-eth-document-registry/.gitignore create mode 100644 practica-01-eth-document-registry/.gitmodules create mode 100644 practica-01-eth-document-registry/README.md create mode 100644 practica-01-eth-document-registry/contracts/.gitignore create mode 100644 practica-01-eth-document-registry/contracts/README.md create mode 100644 practica-01-eth-document-registry/contracts/foundry.toml create mode 160000 practica-01-eth-document-registry/contracts/lib/forge-std create mode 160000 practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts create mode 100644 practica-01-eth-document-registry/contracts/run_tests.sh create mode 100644 practica-01-eth-document-registry/contracts/script/Counter.s.sol create mode 100644 practica-01-eth-document-registry/contracts/script/Deploy.s.sol create mode 100644 practica-01-eth-document-registry/contracts/src/Counter.sol create mode 100644 practica-01-eth-document-registry/contracts/src/DocumentRegistry.sol create mode 100644 practica-01-eth-document-registry/contracts/src/interfaces/IDocumentRegistry.sol create mode 100644 practica-01-eth-document-registry/contracts/test/Counter.t.sol create mode 100644 practica-01-eth-document-registry/contracts/test/DocumentRegistry.t.sol create mode 100644 practica-01-eth-document-registry/dapp/.env.local create mode 100644 practica-01-eth-document-registry/dapp/.eslintrc.json create mode 100644 practica-01-eth-document-registry/dapp/.gitignore create mode 100644 practica-01-eth-document-registry/dapp/README.md create mode 100644 practica-01-eth-document-registry/dapp/app/globals.css create mode 100644 practica-01-eth-document-registry/dapp/app/layout.tsx create mode 100644 practica-01-eth-document-registry/dapp/app/page.tsx create mode 100644 practica-01-eth-document-registry/dapp/app/providers.tsx create mode 100644 practica-01-eth-document-registry/dapp/components/DocumentHistory.tsx create mode 100644 practica-01-eth-document-registry/dapp/components/DocumentSigner.tsx create mode 100644 practica-01-eth-document-registry/dapp/components/DocumentVerifier.tsx create mode 100644 practica-01-eth-document-registry/dapp/components/FileUploader.tsx create mode 100644 practica-01-eth-document-registry/dapp/components/WalletSelector.tsx create mode 100644 practica-01-eth-document-registry/dapp/contexts/MetaMaskContext.tsx create mode 100644 practica-01-eth-document-registry/dapp/hooks/useContract.ts create mode 100644 practica-01-eth-document-registry/dapp/hooks/useFileHash.ts create mode 100644 practica-01-eth-document-registry/dapp/hooks/useMetaMask.ts create mode 100644 practica-01-eth-document-registry/dapp/hooks/useTheme.ts create mode 100644 practica-01-eth-document-registry/dapp/next-env.d.ts create mode 100644 practica-01-eth-document-registry/dapp/next.config.js create mode 100644 practica-01-eth-document-registry/dapp/package-lock.json create mode 100644 practica-01-eth-document-registry/dapp/package.json create mode 100644 practica-01-eth-document-registry/dapp/postcss.config.js create mode 100644 practica-01-eth-document-registry/dapp/tailwind.config.ts create mode 100644 practica-01-eth-document-registry/dapp/tsconfig.json create mode 100644 practica-01-eth-document-registry/dapp/types/ethereum.d.ts create mode 100644 practica-01-eth-document-registry/dapp/utils/ethers.ts create mode 100644 practica-01-eth-document-registry/dapp/utils/hash.ts create mode 100644 practica-01-eth-document-registry/docs/DEPLOYMENT_GUIDE.md create mode 100644 practica-01-eth-document-registry/docs/GUIA_DE_USO.md create mode 100644 practica-01-eth-document-registry/docs/GUION_VIDEO.md create mode 100644 practica-01-eth-document-registry/docs/LECCIONES_APRENDIDAS.md create mode 100644 practica-01-eth-document-registry/docs/QUICK_START.md create mode 100644 practica-01-eth-document-registry/docs/TAREA PARA ESTUDIANTE.md create mode 100644 practica-01-eth-document-registry/foundry.toml create mode 100644 practica-01-eth-document-registry/start.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4f2960c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts"] + path = practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "practica-01-eth-document-registry/contracts/lib/forge-std"] + path = practica-01-eth-document-registry/contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 79d1d79..bf7ad16 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ -# MarkFrFn -###### Codecrypto Academy Student Repo \ No newline at end of file +# MarkFrFn — Codecrypto Academy + +Repositorio de prácticas del curso **Desarrollo de dApps con Ethereum** — CODECRYPTO. + +## Prácticas + +| # | Proyecto | Descripción | Stack | +|---|----------|-------------|-------| +| 01 | [ETH Document Registry](./practica-01-eth-document-registry/) | dApp para almacenar y verificar autenticidad de documentos en blockchain mediante firmas ECDSA | Solidity · Foundry · Next.js · ethers.js v6 | + +## Cómo usar cada práctica + +Cada carpeta contiene su propio `README.md` con instrucciones detalladas. La forma más rápida de arrancar cualquier práctica es: + +```bash +cd practica-XX-nombre +bash start.sh +``` + +## Requisitos comunes + +- Node.js v18+ +- [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `anvil`) +- Git + +--- + +**Curso**: Desarrollo de dApps con Ethereum — CODECRYPTO diff --git a/practica-01-eth-document-registry/.gitignore b/practica-01-eth-document-registry/.gitignore new file mode 100644 index 0000000..3004c6a --- /dev/null +++ b/practica-01-eth-document-registry/.gitignore @@ -0,0 +1,43 @@ +# ─── Foundry (raiz) ─────────────────────────────────────── +cache/ +out/ + +# Broadcasts de red local (31337 = Anvil); conservar testnet/mainnet +broadcast/*/31337/ +broadcast/**/dry-run/ + +# ─── Contratos ──────────────────────────────────────────── +contracts/cache/ +contracts/out/ + +# ─── Frontend ───────────────────────────────────────────── +dapp/node_modules/ +dapp/.next/ +dapp/out/ +dapp/dist/ +dapp/build/ + +# ─── Logs y temporales ──────────────────────────────────── +*.log +test_results.log + +# ─── Entorno ────────────────────────────────────────────── +.env +.env.local.backup +*.env + +# Mantener .env.local del dapp (solo contiene datos publicos de Anvil) +!dapp/.env.local + +# ─── Sistema operativo ──────────────────────────────────── +.DS_Store +Thumbs.db + +# ─── Editor ─────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo + +# ─── Claude Code ────────────────────────────────────────── +.claude/ diff --git a/practica-01-eth-document-registry/.gitmodules b/practica-01-eth-document-registry/.gitmodules new file mode 100644 index 0000000..62f0dfc --- /dev/null +++ b/practica-01-eth-document-registry/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/practica-01-eth-document-registry/README.md b/practica-01-eth-document-registry/README.md new file mode 100644 index 0000000..d991342 --- /dev/null +++ b/practica-01-eth-document-registry/README.md @@ -0,0 +1,189 @@ +# ETH Document Registry + +dApp para almacenar y verificar la autenticidad de documentos en blockchain Ethereum mediante firmas digitales ECDSA. + +## Stack + +| Capa | Tecnología | +|------|-----------| +| Smart Contract | Solidity 0.8.18, Foundry, OpenZeppelin ECDSA | +| Frontend | Next.js 14, TypeScript, ethers.js v6, Tailwind CSS | +| Red local | Anvil (Foundry) | + +## Estructura del proyecto + +``` +eth-database-document/ +├── start.sh # Script para arrancar todo el stack de desarrollo +├── contracts/ # Smart contracts (Foundry) +│ ├── src/ +│ │ └── DocumentRegistry.sol +│ ├── test/ +│ │ └── DocumentRegistry.t.sol +│ ├── script/ +│ │ └── Deploy.s.sol +│ ├── foundry.toml +│ └── run_tests.sh +├── dapp/ # Frontend (Next.js) +│ ├── app/ +│ │ ├── page.tsx # Página principal con tabs +│ │ ├── layout.tsx +│ │ └── providers.tsx +│ ├── components/ +│ │ ├── FileUploader.tsx # Carga, hash y drag & drop +│ │ ├── DocumentSigner.tsx # Firma y almacenamiento +│ │ ├── DocumentVerifier.tsx +│ │ ├── DocumentHistory.tsx # Historial on-chain con búsqueda y exportar CSV +│ │ └── WalletSelector.tsx +│ ├── contexts/ +│ │ └── MetaMaskContext.tsx # Wallets Anvil via JsonRpcProvider +│ ├── hooks/ +│ │ ├── useContract.ts +│ │ ├── useFileHash.ts +│ │ └── useTheme.ts # Dark mode con persistencia en localStorage +│ └── utils/ +│ ├── ethers.ts +│ └── hash.ts +└── docs/ # Documentación adicional + ├── DEPLOYMENT_GUIDE.md + ├── GUIA_DE_USO.md + ├── QUICK_START.md + └── LECCIONES_APRENDIDAS.md +``` + +## Requisitos + +- Node.js v18+ +- [Foundry](https://book.getfoundry.sh/getting-started/installation) +- Git + +```bash +# Verificar +node --version # v18+ +forge --version # forge 0.2+ +``` + +## Instalación + +```bash +# 1. Clonar con submodulos (OpenZeppelin) +git clone --recurse-submodules +cd eth-database-document + +# 2. Instalar dependencias del frontend +cd dapp && npm install && cd .. +``` + +## Uso + +### Opción A — Script todo-en-uno (recomendado) + +Un único comando arranca Anvil, despliega el contrato y lanza el frontend: + +```bash +bash start.sh +``` + +El script: +1. Verifica que `forge`, `anvil` y `node`/`npm` estén disponibles +2. Inicia Anvil en `http://localhost:8545` +3. Despliega `DocumentRegistry` y actualiza `dapp/.env.local` con la dirección +4. Arranca el frontend en `http://localhost:3000` + +Presiona `Ctrl+C` para detener todos los servicios. + +### Opción B — Pasos manuales + +```bash +# Terminal 1 — red local +anvil + +# Terminal 2 — desplegar contrato +cd contracts +forge script script/Deploy.s.sol \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +# Copiar la dirección del contrato y actualizar dapp/.env.local: +# NEXT_PUBLIC_CONTRACT_ADDRESS=0x + +# Terminal 3 — frontend +cd dapp && npm run dev +``` + +Abrir [http://localhost:3000](http://localhost:3000) + +## Flujo de uso + +1. **Conectar wallet** — seleccionar una de las 10 wallets de Anvil +2. **Upload & Sign** — subir un archivo (click o drag & drop), firmar su hash keccak256 +3. **Store** — almacenar hash + firma + timestamp en blockchain +4. **Verify** — subir el mismo archivo, ingresar la dirección del firmante; el frontend consulta `isDocumentStored()` + `getDocumentInfo()` y compara el firmante almacenado +5. **History** — consulta `getDocumentCount()` + `getDocumentHashByIndex(i)` + `getDocumentInfo(hash)` para listar todos los documentos directamente desde la blockchain (hash, firmante, timestamp, firma truncada), ordenados del más reciente al más antiguo + +## Funcionalidades de UI + +| Feature | Descripción | +|---------|-------------| +| Drag & Drop | Arrastrar archivos directamente al área de carga | +| Dark mode | Toggle sol/luna en el header; persiste en `localStorage`; anti-FOUC | +| Animaciones | Fade + slide al cambiar de tab y al mostrar resultados | +| Exportar CSV | Botón en History descarga todos los documentos (RFC 4180) | +| Búsqueda | Filtro en tiempo real por hash o dirección del firmante | +| Skeleton loader | Filas animadas mientras History carga desde blockchain | + +## Tests del contrato + +```bash +cd contracts + +# Ejecutar los 11 tests con reporte detallado +bash run_tests.sh + +# Comandos directos de forge +forge test -vv # tests con logs +forge coverage # cobertura de codigo +forge build # compilar +``` + +### Suite de tests (11/11) + +| # | Test | Caso | +|---|------|------| +| 1 | `testStoreAndVerify` | Happy path completo | +| 2 | `testCannotStoreTwice` | Duplicado rechazado | +| 3 | `testVerifyWrongSigner` | Firmante incorrecto → false | +| 4 | `testDocumentCount_StartsAtZero` | Contador inicial en 0 | +| 5 | `testDocumentCount_AfterStore` | Contador incrementa | +| 6 | `testGetDocumentHashByIndex` | Iteración por índice | +| 7 | `testGetDocumentHashByIndex_OutOfBounds` | Índice inválido revierte | +| 8 | `testGetDocumentInfo_Reverts_IfNotStored` | Info doc inexistente revierte | +| 9 | `testIsDocumentStored_ReturnsFalse` | Doc inexistente → false | +| 10 | `testVerifyDocument_ReturnsFalse_IfNotStored` | Verificar inexistente → false | +| 11 | `testStoreMultipleDocuments` | Multi-wallet, conteo e iteración | + +## Variables de entorno + +`dapp/.env.local`: + +```env +NEXT_PUBLIC_CONTRACT_ADDRESS=0x... # Actualizado automáticamente por start.sh +NEXT_PUBLIC_RPC_URL=http://localhost:8545 +NEXT_PUBLIC_CHAIN_ID=31337 +NEXT_PUBLIC_MNEMONIC="test test test test test test test test test test test junk" +``` + +> Las wallets de Anvil usan el mnemonic público estándar. No usar en mainnet. + +## Optimizaciones del contrato + +- Sin campo `bool exists` redundante — existencia verificada via `signer != address(0)` +- Sin mapping `hashExists` separado +- Modifiers `documentNotExists` / `documentExists` para guards reutilizables +- Optimizer habilitado (`optimizer_runs = 200`) +- Ahorro estimado: ~39% en gas de almacenamiento respecto al diseño naive + +--- + +**Curso**: Desarrollo de dApps con Ethereum — CODECRYPTO diff --git a/practica-01-eth-document-registry/contracts/.gitignore b/practica-01-eth-document-registry/contracts/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/practica-01-eth-document-registry/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/practica-01-eth-document-registry/contracts/README.md b/practica-01-eth-document-registry/contracts/README.md new file mode 100644 index 0000000..8817d6a --- /dev/null +++ b/practica-01-eth-document-registry/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/practica-01-eth-document-registry/contracts/foundry.toml b/practica-01-eth-document-registry/contracts/foundry.toml new file mode 100644 index 0000000..32ffcb9 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 200 + +# Ver todas las opciones: https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/practica-01-eth-document-registry/contracts/lib/forge-std b/practica-01-eth-document-registry/contracts/lib/forge-std new file mode 160000 index 0000000..0844d7e --- /dev/null +++ b/practica-01-eth-document-registry/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 0844d7e1fc5e60d77b68e469bff60265f236c398 diff --git a/practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts b/practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000..8ff78ff --- /dev/null +++ b/practica-01-eth-document-registry/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 8ff78ffb6e78463f070eab59487b4ba30481b53c diff --git a/practica-01-eth-document-registry/contracts/run_tests.sh b/practica-01-eth-document-registry/contracts/run_tests.sh new file mode 100644 index 0000000..e7f21b1 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/run_tests.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +# Ir al directorio del script sin importar desde donde se ejecute +cd "$(dirname "$0")" + +FORGE="$HOME/.foundry/bin/forge" +BOLD="\033[1m" +DIM="\033[2m" +GREEN="\033[0;32m" +RED="\033[0;31m" +CYAN="\033[0;36m" +WHITE="\033[1;37m" +RESET="\033[0m" + +declare -A TEST_DESC +TEST_DESC["testStoreAndVerify"]="[1/11] Almacenar y verificar documento correctamente" +TEST_DESC["testCannotStoreTwice"]="[2/11] Rechazar documento duplicado" +TEST_DESC["testVerifyWrongSigner"]="[3/11] Verificar con firmante incorrecto devuelve false" +TEST_DESC["testDocumentCount_StartsAtZero"]="[4/11] Contador empieza en cero" +TEST_DESC["testDocumentCount_AfterStore"]="[5/11] Contador incrementa al almacenar" +TEST_DESC["testGetDocumentHashByIndex"]="[6/11] Obtener hash por indice correctamente" +TEST_DESC["testGetDocumentHashByIndex_OutOfBounds"]="[7/11] Indice fuera de rango revierte" +TEST_DESC["testGetDocumentInfo_Reverts_IfNotStored"]="[8/11] getDocumentInfo revierte si no existe" +TEST_DESC["testIsDocumentStored_ReturnsFalse"]="[9/11] isDocumentStored devuelve false si no existe" +TEST_DESC["testVerifyDocument_ReturnsFalse_IfNotStored"]="[10/11] verifyDocument devuelve false si no existe" +TEST_DESC["testStoreMultipleDocuments"]="[11/11] Multiples documentos con distintas wallets" + +echo "" +echo -e "${BOLD}${CYAN}+======================================================+" +echo -e "| DocumentRegistry . Test Runner |" +echo -e "| Optimizer: ON . optimizer_runs: 200 |" +echo -e "+======================================================+${RESET}" +echo "" + +echo -e "${DIM} Compilando...${RESET}" +BUILD_OUT=$("$FORGE" build 2>&1) +BUILD_CODE=$? +if [ $BUILD_CODE -ne 0 ]; then + echo -e "${RED} ERROR DE COMPILACION:${RESET}" + echo "$BUILD_OUT" | grep -E "^Error|^error" | sed 's/^/ /' + exit 1 +fi +echo -e "${GREEN} Compilacion OK${RESET}" +echo "" + +OUTPUT=$("$FORGE" test -vvv --match-contract DocumentRegistryTest 2>&1) +EXIT_CODE=$? + +echo -e "${BOLD}${WHITE} Resultados:${RESET}" +echo -e " ${DIM}------------------------------------------------------${RESET}" + +PASS_COUNT=0 +FAIL_COUNT=0 +declare -A GAS_MAP + +while IFS= read -r line; do + if [[ "$line" =~ \[(PASS|FAIL.*)\]\ (test[A-Za-z_0-9]+)\(\)\ \(gas:\ ([0-9]+)\) ]]; then + STATUS="${BASH_REMATCH[1]}" + FUNC="${BASH_REMATCH[2]}" + GAS="${BASH_REMATCH[3]}" + GAS_MAP["$FUNC"]=$GAS + DESC="${TEST_DESC[$FUNC]:-$FUNC}" + if [[ "$STATUS" == "PASS" ]]; then + echo -e " ${GREEN}PASS${RESET} $DESC" + echo -e " ${DIM}gas usado: $GAS${RESET}" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo -e " ${RED}FAIL${RESET} ${RED}$DESC${RESET}" + echo -e " ${DIM}gas usado: $GAS${RESET}" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi + fi +done <<< "$OUTPUT" + +if [ $FAIL_COUNT -gt 0 ]; then + echo "" + echo -e " ${DIM}------------------------------------------------------${RESET}" + echo -e " ${BOLD}${RED}Detalle de fallos:${RESET}" + echo "$OUTPUT" | grep -A 10 "\[FAIL" | grep -v "^--$" | grep -v "Warning\|note\[" | sed 's/^/ /' +fi + +TOTAL=$((PASS_COUNT + FAIL_COUNT)) + +if [ ${#GAS_MAP[@]} -gt 0 ]; then + echo "" + echo -e " ${DIM}------------------------------------------------------${RESET}" + echo -e " ${BOLD}${WHITE}Consumo de gas:${RESET}" + TOTAL_GAS=0 + MAX_GAS=0 + MAX_FUNC="" + for func in "${!GAS_MAP[@]}"; do + g=${GAS_MAP[$func]} + TOTAL_GAS=$((TOTAL_GAS + g)) + if [ "$g" -gt "$MAX_GAS" ]; then + MAX_GAS=$g + MAX_FUNC=$func + fi + printf " ${DIM}%-52s %6d gas${RESET}\n" "$func" "$g" + done + if [ $TOTAL -gt 0 ]; then + AVG_GAS=$((TOTAL_GAS / TOTAL)) + echo "" + printf " ${DIM}Promedio: %d gas | Max: %d gas${RESET}\n" "$AVG_GAS" "$MAX_GAS" + echo -e " ${DIM}Test mas costoso: $MAX_FUNC${RESET}" + fi +fi + +echo "" +echo -e "${BOLD}${CYAN}+======================================================+${RESET}" +if [ "$EXIT_CODE" -eq 0 ]; then + echo -e "${BOLD}${CYAN}| ${GREEN}ALL TESTS PASSED${CYAN} . ${WHITE}${PASS_COUNT}/${TOTAL} tests OK${CYAN} |${RESET}" +else + echo -e "${BOLD}${CYAN}| ${RED}FALLOS: ${FAIL_COUNT}${CYAN} . ${GREEN}OK: ${PASS_COUNT}/${TOTAL}${CYAN} |${RESET}" +fi +echo -e "${BOLD}${CYAN}+======================================================+${RESET}" +echo "" + +exit $EXIT_CODE diff --git a/practica-01-eth-document-registry/contracts/script/Counter.s.sol b/practica-01-eth-document-registry/contracts/script/Counter.s.sol new file mode 100644 index 0000000..f01d69c --- /dev/null +++ b/practica-01-eth-document-registry/contracts/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/practica-01-eth-document-registry/contracts/script/Deploy.s.sol b/practica-01-eth-document-registry/contracts/script/Deploy.s.sol new file mode 100644 index 0000000..3f42502 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/script/Deploy.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "forge-std/Script.sol"; +import "forge-std/console.sol"; +import "../src/DocumentRegistry.sol"; + +contract DeployDocumentRegistry is Script { + function run() external returns (address) { + vm.startBroadcast(); + DocumentRegistry registry = new DocumentRegistry(); + vm.stopBroadcast(); + console.log("Deployed DocumentRegistry at:", address(registry)); + return address(registry); + } +} diff --git a/practica-01-eth-document-registry/contracts/src/Counter.sol b/practica-01-eth-document-registry/contracts/src/Counter.sol new file mode 100644 index 0000000..aded799 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/practica-01-eth-document-registry/contracts/src/DocumentRegistry.sol b/practica-01-eth-document-registry/contracts/src/DocumentRegistry.sol new file mode 100644 index 0000000..62df70c --- /dev/null +++ b/practica-01-eth-document-registry/contracts/src/DocumentRegistry.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; + +/// @title Document Registry +/// @notice Registro de hashes de documentos con firmas ECDSA +/// @dev Optimizado: sin campo `exists` redundante, se usa signer != address(0) para verificar existencia +contract DocumentRegistry { + using ECDSA for bytes32; + + struct Document { + bytes32 hash; + uint256 timestamp; + address signer; + bytes signature; + } + + mapping(bytes32 => Document) private documents; + bytes32[] private documentHashes; + + event DocumentStored(bytes32 indexed hash, address indexed signer, uint256 timestamp, bytes signature); + event DocumentVerified(bytes32 indexed hash, address indexed signer, bool isValid); + + modifier documentNotExists(bytes32 _hash) { + require(documents[_hash].signer == address(0), "Document already exists"); + _; + } + + modifier documentExists(bytes32 _hash) { + require(documents[_hash].signer != address(0), "Document does not exist"); + _; + } + + /// @notice Almacena el hash de un documento junto con timestamp y firma + /// @dev La firma debe ser del mensaje `hash` firmado con `eth_sign` prefijo + function storeDocumentHash(bytes32 hash, uint256 timestamp, bytes calldata signature) external documentNotExists(hash) { + require(hash != bytes32(0), "Invalid hash"); + + bytes32 ethSigned = ECDSA.toEthSignedMessageHash(abi.encodePacked(hash)); + address recovered = ethSigned.recover(signature); + require(recovered != address(0), "Invalid signature"); + + documents[hash] = Document({ + hash: hash, + timestamp: timestamp, + signer: recovered, + signature: signature + }); + documentHashes.push(hash); + + emit DocumentStored(hash, recovered, timestamp, signature); + } + + /// @notice Verifica que una firma corresponde al `signer` para un `hash` + /// @return isValid True si la firma es válida y coincide con `signer` + function verifyDocument(bytes32 hash, address signer, bytes calldata signature) external returns (bool isValid) { + if (documents[hash].signer == address(0)) { + emit DocumentVerified(hash, signer, false); + return false; + } + + bytes32 ethSigned = ECDSA.toEthSignedMessageHash(abi.encodePacked(hash)); + address recovered = ethSigned.recover(signature); + isValid = (recovered == signer); + + emit DocumentVerified(hash, signer, isValid); + return isValid; + } + + /// @notice Obtiene la información completa de un documento almacenado + function getDocumentInfo(bytes32 hash) external view documentExists(hash) returns (Document memory) { + return documents[hash]; + } + + /// @notice Comprueba si un documento está almacenado + function isDocumentStored(bytes32 hash) external view returns (bool) { + return documents[hash].signer != address(0); + } + + /// @notice Devuelve el número total de documentos almacenados + function getDocumentCount() external view returns (uint256) { + return documentHashes.length; + } + + /// @notice Devuelve el hash de un documento por su índice + function getDocumentHashByIndex(uint256 index) external view returns (bytes32) { + require(index < documentHashes.length, "Index out of bounds"); + return documentHashes[index]; + } + + /// @notice Devuelve la firma almacenada para un documento + function getDocumentSignature(bytes32 hash) external view documentExists(hash) returns (bytes memory) { + return documents[hash].signature; + } +} diff --git a/practica-01-eth-document-registry/contracts/src/interfaces/IDocumentRegistry.sol b/practica-01-eth-document-registry/contracts/src/interfaces/IDocumentRegistry.sol new file mode 100644 index 0000000..978fde9 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/src/interfaces/IDocumentRegistry.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IDocumentRegistry { + event DocumentStored(bytes32 indexed hash, address indexed signer, uint256 timestamp, bytes signature); + event DocumentVerified(bytes32 indexed hash, address indexed signer, bool isValid); + + function storeDocumentHash(bytes32 hash, uint256 timestamp, bytes calldata signature) external; + function verifyDocument(bytes32 hash, address signer, bytes calldata signature) external returns (bool); + function getDocumentInfo(bytes32 hash) external view returns (bytes32, uint256, address, bytes memory, bool); + function isDocumentStored(bytes32 hash) external view returns (bool); + function getDocumentSignature(bytes32 hash) external view returns (bytes memory); +} diff --git a/practica-01-eth-document-registry/contracts/test/Counter.t.sol b/practica-01-eth-document-registry/contracts/test/Counter.t.sol new file mode 100644 index 0000000..4831910 --- /dev/null +++ b/practica-01-eth-document-registry/contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/practica-01-eth-document-registry/contracts/test/DocumentRegistry.t.sol b/practica-01-eth-document-registry/contracts/test/DocumentRegistry.t.sol new file mode 100644 index 0000000..e34032b --- /dev/null +++ b/practica-01-eth-document-registry/contracts/test/DocumentRegistry.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; +import "../src/DocumentRegistry.sol"; + +contract DocumentRegistryTest is Test { + using ECDSA for bytes32; + + DocumentRegistry registry; + + // Dos claves privadas de Anvil para tests multi-wallet + uint256 privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + uint256 privateKey2 = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + + function setUp() public { + registry = new DocumentRegistry(); + } + + // ───────────────────────────────────────────────────────── + // Helper: firma un hash con una clave privada dada + // ───────────────────────────────────────────────────────── + function _sign(bytes32 docHash, uint256 key) internal returns (bytes memory) { + bytes32 ethSigned = ECDSA.toEthSignedMessageHash(abi.encodePacked(docHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, ethSigned); + return abi.encodePacked(r, s, v); + } + + // ───────────────────────────────────────────────────────── + // TEST 1: Almacenar y verificar documento correctamente + // ───────────────────────────────────────────────────────── + function testStoreAndVerify() public { + bytes32 docHash = keccak256(abi.encodePacked("documento de prueba")); + uint256 ts = block.timestamp; + bytes memory sig = _sign(docHash, privateKey); + + registry.storeDocumentHash(docHash, ts, sig); + + assertTrue(registry.isDocumentStored(docHash)); + + DocumentRegistry.Document memory doc = registry.getDocumentInfo(docHash); + assertEq(doc.hash, docHash); + assertEq(doc.timestamp, ts); + assertEq(doc.signer, vm.addr(privateKey)); + assertEq(keccak256(doc.signature), keccak256(sig)); + assertTrue(registry.verifyDocument(docHash, doc.signer, sig)); + } + + // ───────────────────────────────────────────────────────── + // TEST 2: Rechazar documento duplicado + // ───────────────────────────────────────────────────────── + function testCannotStoreTwice() public { + bytes32 docHash = keccak256(abi.encodePacked("doc duplicado")); + bytes memory sig = _sign(docHash, privateKey); + + registry.storeDocumentHash(docHash, block.timestamp, sig); + + vm.expectRevert(bytes("Document already exists")); + registry.storeDocumentHash(docHash, block.timestamp, sig); + } + + // ───────────────────────────────────────────────────────── + // TEST 3: Verificar con firmante incorrecto devuelve false + // ───────────────────────────────────────────────────────── + function testVerifyWrongSigner() public { + bytes32 docHash = keccak256(abi.encodePacked("doc firmante incorrecto")); + bytes memory sig = _sign(docHash, privateKey); + + registry.storeDocumentHash(docHash, block.timestamp, sig); + + address wrongAddr = address(0xdeadbeef); + assertFalse(registry.verifyDocument(docHash, wrongAddr, sig)); + } + + // ───────────────────────────────────────────────────────── + // TEST 4: Contador empieza en cero + // ───────────────────────────────────────────────────────── + function testDocumentCount_StartsAtZero() public view { + assertEq(registry.getDocumentCount(), 0); + } + + // ───────────────────────────────────────────────────────── + // TEST 5: Contador incrementa al almacenar + // ───────────────────────────────────────────────────────── + function testDocumentCount_AfterStore() public { + bytes32 docHash = keccak256(abi.encodePacked("doc para contar")); + registry.storeDocumentHash(docHash, block.timestamp, _sign(docHash, privateKey)); + + assertEq(registry.getDocumentCount(), 1); + } + + // ───────────────────────────────────────────────────────── + // TEST 6: Obtener hash por índice correctamente + // ───────────────────────────────────────────────────────── + function testGetDocumentHashByIndex() public { + bytes32 docHash = keccak256(abi.encodePacked("doc por indice")); + registry.storeDocumentHash(docHash, block.timestamp, _sign(docHash, privateKey)); + + assertEq(registry.getDocumentHashByIndex(0), docHash); + } + + // ───────────────────────────────────────────────────────── + // TEST 7: Índice fuera de rango revierte + // ───────────────────────────────────────────────────────── + function testGetDocumentHashByIndex_OutOfBounds() public { + vm.expectRevert(bytes("Index out of bounds")); + registry.getDocumentHashByIndex(0); + } + + // ───────────────────────────────────────────────────────── + // TEST 8: getDocumentInfo revierte si no existe + // ───────────────────────────────────────────────────────── + function testGetDocumentInfo_Reverts_IfNotStored() public { + bytes32 fakeHash = keccak256(abi.encodePacked("no existe")); + vm.expectRevert(bytes("Document does not exist")); + registry.getDocumentInfo(fakeHash); + } + + // ───────────────────────────────────────────────────────── + // TEST 9: isDocumentStored devuelve false para doc inexistente + // ───────────────────────────────────────────────────────── + function testIsDocumentStored_ReturnsFalse() public view { + bytes32 fakeHash = keccak256(abi.encodePacked("jamas almacenado")); + assertFalse(registry.isDocumentStored(fakeHash)); + } + + // ───────────────────────────────────────────────────────── + // TEST 10: verifyDocument devuelve false si el doc no existe + // ───────────────────────────────────────────────────────── + function testVerifyDocument_ReturnsFalse_IfNotStored() public { + bytes32 fakeHash = keccak256(abi.encodePacked("doc fantasma")); + bytes memory anySig = _sign(fakeHash, privateKey); + + assertFalse(registry.verifyDocument(fakeHash, vm.addr(privateKey), anySig)); + } + + // ───────────────────────────────────────────────────────── + // TEST 11: Múltiples documentos con distintas wallets + // ───────────────────────────────────────────────────────── + function testStoreMultipleDocuments() public { + bytes32 hash1 = keccak256(abi.encodePacked("doc uno")); + bytes32 hash2 = keccak256(abi.encodePacked("doc dos")); + bytes32 hash3 = keccak256(abi.encodePacked("doc tres")); + + registry.storeDocumentHash(hash1, block.timestamp, _sign(hash1, privateKey)); + registry.storeDocumentHash(hash2, block.timestamp + 1, _sign(hash2, privateKey2)); + registry.storeDocumentHash(hash3, block.timestamp + 2, _sign(hash3, privateKey)); + + assertEq(registry.getDocumentCount(), 3); + assertEq(registry.getDocumentHashByIndex(0), hash1); + assertEq(registry.getDocumentHashByIndex(1), hash2); + assertEq(registry.getDocumentHashByIndex(2), hash3); + + // Verificar firmantes correctos + DocumentRegistry.Document memory doc1 = registry.getDocumentInfo(hash1); + DocumentRegistry.Document memory doc2 = registry.getDocumentInfo(hash2); + assertEq(doc1.signer, vm.addr(privateKey)); + assertEq(doc2.signer, vm.addr(privateKey2)); + + // Firma de key1 presentada como si fuera de key1 -> valido + assertTrue(registry.verifyDocument(hash1, vm.addr(privateKey), _sign(hash1, privateKey))); + // Firma de key2 presentada como si fuera de key1 -> invalido (recovered != signer param) + assertFalse(registry.verifyDocument(hash1, vm.addr(privateKey), _sign(hash1, privateKey2))); + } +} diff --git a/practica-01-eth-document-registry/dapp/.env.local b/practica-01-eth-document-registry/dapp/.env.local new file mode 100644 index 0000000..ab1a4e5 --- /dev/null +++ b/practica-01-eth-document-registry/dapp/.env.local @@ -0,0 +1,38 @@ +# Ethereum RPC Configuration +NEXT_PUBLIC_RPC_URL=http://localhost:8545 +NEXT_PUBLIC_CHAIN_ID=31337 + +# Contrato desplegado (Anvil - actualizar si se redeploya) +NEXT_PUBLIC_CONTRACT_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 + +# Anvil Test Wallets (10 wallets de prueba con claves privadas) +# NOTA: Solo para desarrollo local, NUNCA usar en producción +NEXT_PUBLIC_WALLET_1_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +NEXT_PUBLIC_WALLET_1_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +NEXT_PUBLIC_WALLET_2_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +NEXT_PUBLIC_WALLET_2_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + +NEXT_PUBLIC_WALLET_3_ADDRESS=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +NEXT_PUBLIC_WALLET_3_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + +NEXT_PUBLIC_WALLET_4_ADDRESS=0x90F79bf6EB2c4f870365E785982E1f101E93b906 +NEXT_PUBLIC_WALLET_4_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 + +NEXT_PUBLIC_WALLET_5_ADDRESS=0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 +NEXT_PUBLIC_WALLET_5_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a + +NEXT_PUBLIC_WALLET_6_ADDRESS=0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc +NEXT_PUBLIC_WALLET_6_PRIVATE_KEY=0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba + +NEXT_PUBLIC_WALLET_7_ADDRESS=0x976EA74026E726554dB657fA54763abd0C3a0aa9 +NEXT_PUBLIC_WALLET_7_PRIVATE_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e + +NEXT_PUBLIC_WALLET_8_ADDRESS=0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 +NEXT_PUBLIC_WALLET_8_PRIVATE_KEY=0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 + +NEXT_PUBLIC_WALLET_9_ADDRESS=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f +NEXT_PUBLIC_WALLET_9_PRIVATE_KEY=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 + +NEXT_PUBLIC_WALLET_10_ADDRESS=0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 +NEXT_PUBLIC_WALLET_10_PRIVATE_KEY=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 diff --git a/practica-01-eth-document-registry/dapp/.eslintrc.json b/practica-01-eth-document-registry/dapp/.eslintrc.json new file mode 100644 index 0000000..da50efd --- /dev/null +++ b/practica-01-eth-document-registry/dapp/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/typescript" +} diff --git a/practica-01-eth-document-registry/dapp/.gitignore b/practica-01-eth-document-registry/dapp/.gitignore new file mode 100644 index 0000000..b51a2ac --- /dev/null +++ b/practica-01-eth-document-registry/dapp/.gitignore @@ -0,0 +1,20 @@ +node_modules/ +.next/ +out/ +dist/ +build/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.env.local +.env.local.backup +.vercel +.idea/ +.vscode/ +*.swp +*.swo +*.env +!.env.local +.cache/ diff --git a/practica-01-eth-document-registry/dapp/README.md b/practica-01-eth-document-registry/dapp/README.md new file mode 100644 index 0000000..df328b7 --- /dev/null +++ b/practica-01-eth-document-registry/dapp/README.md @@ -0,0 +1,16 @@ +# ETH Database Document - dApp + +Node.js application folder for the Next.js frontend. + +## Quick Start + +```bash +npm install +npm run dev +``` + +Visit http://localhost:3000 + +## Environment Variables + +See `.env.local` for Anvil wallet configuration. diff --git a/practica-01-eth-document-registry/dapp/app/globals.css b/practica-01-eth-document-registry/dapp/app/globals.css new file mode 100644 index 0000000..dc8abfb --- /dev/null +++ b/practica-01-eth-document-registry/dapp/app/globals.css @@ -0,0 +1,48 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Tab / result fade-slide animation */ +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +.animate-tab-in { + animation: fadeSlideIn 0.2s ease-out both; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/practica-01-eth-document-registry/dapp/app/layout.tsx b/practica-01-eth-document-registry/dapp/app/layout.tsx new file mode 100644 index 0000000..b7b778e --- /dev/null +++ b/practica-01-eth-document-registry/dapp/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from 'next'; +import './globals.css'; +import Providers from './providers'; + +export const metadata: Metadata = { + title: 'ETH Database Document - dApp', + description: 'Decentralized document verification using Ethereum blockchain', +}; + +// Inline script ejecutado antes de la hidratación para evitar flash de tema incorrecto +const themeScript = ` +(function() { + try { + const t = localStorage.getItem('theme'); + const d = t ? t === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches; + if (d) document.documentElement.classList.add('dark'); + } catch(e) {} +})(); +`; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +