IMPLEMENTACIÓ D'APLICACIONS DESCENTRALITZADES (DApps)¶
Resultat d'Aprenentatge: Implementa aplicacions descentralitzades (DApps)
Criteris d'avaluació:
- a) Ha desenvolupat una DApp que interactua amb smart contracts, integrant correctament el frontend i el backend
- b) Utilitza eines com Web3.js o Ethers.js per a la interacció amb la blockchain
ÍNDEX DE CONTINGUTS¶
- Introducció a les DApps
- Arquitectura d'una DApp
- Ethers.js: La Biblioteca Essencial
- Connexió amb Wallets (MetaMask, WalletConnect)
- Interacció amb Smart Contracts
- Gestió de Transaccions
- Escolta d'Events en Temps Real
- Frontend amb React + Ethers.js
- Backend per a DApps
- Seguretat en DApps
- Exercicis Guiats
- Col·lecció d'Exercicis Proposats
1. INTRODUCCIÓ A LES DApps¶
1.1 Què és una DApp?¶
Una Aplicació Descentralitzada (DApp) és una aplicació que s'executa en una xarxa blockchain o P2P, en lloc d'un sol ordinador. A diferència de les aplicacions tradicionals:
| Característica | App Tradicional | DApp |
|---|---|---|
| Backend | Servidors centralitzats | Smart contracts a blockchain |
| Dades | Base de dades centralitzada | Blockchain descentralitzada |
| Control | Empresa/organització | Comunitat/usuaris |
| Disponibilitat | Pot caure si el servidor falla | Sempre disponible (mentre la blockchain ho estigui) |
| Censura | Pot ser censurada | Resistència a la censura |
| Transparència | Codi tancat generalment | Codi obert i verificable |
1.2 Components d'una DApp¶
flowchart TB
subgraph CAPA_D_USUARI["CAPA D'USUARI"]
direction TB
Frontend["Frontend<br/>React.js"]
Wallet["Wallet<br/>MetaMask"]
Browser["Navegador<br/>Chrome"]
end
Ethers["Ethers.js<br/>(Bridge)"]
subgraph CAPA_DESCENTRALITZADA["CAPA DESCENTRALITZADA"]
direction TB
Smart["Smart Contracts<br/>(Lògica on-chain)"]
Network["Blockchain Network<br/>(Ethereum, Polygon, BSC...)"]
end
subgraph CAPA_OPCIONAL["CAPA OPCIONAL"]
direction TB
Backend["Backend<br/>(API)"]
Oracle["Oracle<br/>(Chainlink)"]
Storage["Storage<br/>(IPFS)"]
end
%% Connexions
Frontend --> Ethers
Wallet --> Ethers
Browser --> Ethers
Ethers --> Smart
Smart --> Network
Network --> Backend
Network --> Oracle
Network --> Storage
1.3 Casos d'Ús de DApps¶
-
Finances Descentralitzades (DeFi) - Exchanges descentralitzats (Uniswap, SushiSwap) - Préstecs (Aave, Compound) - Staking i yield farming
-
NFTs i Mercats Digitals - Mercats de NFTs (OpenSea, Rarible) - Jocs play-to-earn (Axie Infinity) - Col·leccionables digitals
-
Governança DAO - Votació descentralitzada - Gestió de tresoreries - Propostes comunitàries
-
Supply Chain - Traçabilitat de productes - Verificació d'autenticitat - Gestió logística
-
Identitat Digital - Credencials verificables - Identitat auto-soberana - Reputació descentralitzada
2. ARQUITECTURA D'UNA DApp¶
2.1 Patrons d'Arquitectura¶
Patró 1: DApp Completa On-Chain¶
Frontend → Smart Contracts → Blockchain
Patró 2: DApp Híbrida¶
Frontend → Smart Contracts → Blockchain
↓
Backend API → Base de Dades Tradicional
↓
IPFS/Arweave → Emmagatzematge descentralitzat
Patró 3: DApp amb Oracle¶
Frontend → Smart Contracts → Blockchain
↓
Chainlink Oracle
↓
Dades Externes (APIs, preus, etc.)
A què ens referim amb Chainlink Oracle?
-
Un oracle és un servei que permet als smart contracts accedir a dades externes a la blockchain, com preus de mercat, resultats esportius o informació meteorològica.
-
Chainlink és el proveïdor d'oracles més popular i fiable, que connecta els smart contracts amb fonts de dades del món real de manera segura i descentralitzada.
Per què és tan important Chainlink / Oracle?
Molts casos d'ús de DApps depenen de dades externes per funcionar correctament. Sense oracles, els smart contracts serien limitats a operar només amb dades ja presents a la blockchain, cosa que restringiria enormement les seves aplicacions pràctiques. Chainlink resol aquest problema proporcionant una infraestructura robusta i descentralitzada per a l'accés a dades externes, garantint la seguretat i la fiabilitat de les dades que arriben als smart contracts.
En l'àmbit de la blockchain, quan parlam de Chainlink (o del concepte d'Oracle en general), ens referim a la tecnologia que actua com un pont o traductor entre el món real (fora de la blockchain / off-chain) i els Smart Contracts (dins la blockchain / on-chain).
Per entendre exactament què és i per què és tan crucial, hem de desglossar dos conceptes: el problema de l'oracle i com el resol Chainlink.
1. El Problema de l'Oracle (The Oracle Problem)¶
Per disseny, les blockchains com Ethereum són completament deterministes i aïllades. Això vol dir que un Smart Contract no pot fer una consulta a una API d'Internet per saber el temps que fa a Barcelona, el preu de l'Euro o qui ha guanyat un partit de futbol.
Si el contracte pogués fer consultes externes directament, cada node de la xarxa obtindria respostes diferents segons el mil·lisegon en què fes la consulta, trencant el consens i la seguretat de la blockchain. Per tant:
- Un Smart Contract és cec al món exterior.
- Un Oracle és qualsevol servei que busca informació de l'exterior i la injecta dins la blockchain mitjançant una transacció.
2. Què fa exactament Chainlink?¶
Si utilitzéssim un sol oracle centralitzat (per exemple, una sola API de l'oratge), estaríem creant un punt únic de fallada (Single Point of Failure). Si aquesta API fos hackejada o fallés, tot el nostre contracte intel·ligent descentralizat prendria decisions basades en dades falses.
Chainlink és una Xarxa d'Oracles Descentralitzats (DON - Decentralized Oracle Network). En lloc de confiar en una sola font, Chainlink utilitza múltiples nodes independents que recopilen la mateixa informació de múltiples fonts diferents. Després, aquests nodes validen, contrasten i fan la mitjana d'aquestes dades (consens) abans d'enviar-les de forma segura al Smart Contract.
3. Els serveis principals que ofereix Chainlink¶
Quan un desenvolupador integra Chainlink a la seva DApp, sol referir-se a algun d'aquests serveis concrets:
- Data Feeds (Alimentació de dades): És el servei més utilitzat (sobretot en DeFi). Són canals constants de dades financeres actualitzades (per exemple, saber exactament el preu de \(1 \text{ ETH}\) en dòlars en temps real).
- VRF (Verifiable Random Function): La blockchain no pot generar números aleatoris segurs per si mateixa (perquè tot és determinista). Chainlink VRF proveeix un número aleatori generat fora de la xarxa, juntament com una prova criptogràfica demostrant que ningú (ni els miners ni els creadors) ha pogut manipular el resultat. És vital per a jocs Web3 i sortejos.
- Automation (Keepers): Els Smart Contracts són passius; no s'executen sols tret que un usuari engegui una transacció. Chainlink té nodes escoltant constantment si es compleix una condició (per exemple: "si ha passat un mes, executa el pagament d'aquest sou").
- CCIP (Cross-Chain Interoperability Protocol): Permet que dades i tokens viatgin de forma segura entre diferents blockchains (per exemple, enviar un missatge des d'Ethereum cap a Polygon).
Un exemple pràctic: L'assegurança agrícola¶
Imagina un Smart Contract d'assegurances per a un pagès: "Si plou menys de 10 l/m² aquest mes a Sevilla, el contracte paga automàticament 1.000€ al pagès".
Chainlink és l'encarregat de consultar de manera descentralitzada a les agències meteorològiques del món real, validar que les dades de pluja són correctes, i cridar la funció del contracte de la blockchain per fer el pagament automàtic sense que hi hagi d'intervenir cap humà ni cap banc intermediari.
2.2 Flux de Treball Típic¶
1. USUARI
↓
2. CONNECTA WALLET (MetaMask)
↓
3. FRONTEND LLEGEIX DADES DE BLOCKCHAIN
↓
4. USUARI INICIA ACCIÓ (click button)
↓
5. FRONTEND PREPARA TRANSACCIÓ
↓
6. WALLET DEMANA CONFIRMACIÓ
↓
7. USUARI SIGNA TRANSACCIÓ
↓
8. TRANSACCIÓ ENVIADA A BLOCKCHAIN
↓
9. ESPERA CONFIRMACIÓ (miners/validators)
↓
10. FRONTEND ACTUALITZA UI
↓
11. ESCOLTA EVENTS PER ACTUALITZACIONS
3. ETHERS.JS: LA BIBLIOTECA ESSENCIAL¶
3.1 Introducció a Ethers.js¶
Ethers.js és una biblioteca completa i compacta per interactuar amb Ethereum i altres blockchains EVM-compatible. És més moderna i segura que Web3.js. [[1]][[2]][[3]]
Instal·lació:
# Amb npm
npm install ethers
# Amb yarn
yarn add ethers
# Versió actual: ethers v6.x
Importació:
// ES6 Modules (React, modern)
import { ethers } from 'ethers';
// CommonJS (Node.js)
const { ethers } = require('ethers');
// Browser (CDN)
<script src="https://cdn.ethers.io/lib/ethers-6.0.umd.min.js"></script>
3.2 Conceptes Clau d'Ethers.js¶
Provider¶
El provider és la connexió a la blockchain. Permet llegir dades però no enviar transaccions.
import { ethers } from 'ethers';
// Provider públic (només lectura)
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
// Provider de testnet
const sepoliaProvider = new ethers.JsonRpcProvider('https://sepolia.infura.io/v3/YOUR_KEY');
// Provider des de MetaMask (browser)
const provider = new ethers.BrowserProvider(window.ethereum);
// Llegir informació de la xarxa
const network = await provider.getNetwork();
console.log('Chain ID:', network.chainId);
console.log('Nom:', network.name);
// Llegir saldo d'una adreça
const balance = await provider.getBalance('0x71C7656EC7ab88b098defB751B7401B5f6d8976F');
console.log('Saldo:', ethers.formatEther(balance), 'ETH');
// Llegir número de bloc actual
const blockNumber = await provider.getBlockNumber();
console.log('Bloc actual:', blockNumber);
// Escolta de nous blocs
provider.on('block', (blockNumber) => {
console.log('Nou bloc:', blockNumber);
});
Signer¶
El signer permet signar transaccions. Normalment ve del wallet de l'usuari (MetaMask).
// Obtenir signer des de MetaMask
async function getSigner() {
// Demanar connexió a MetaMask
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
console.log('Adreça:', await signer.getAddress());
console.log('Xarxa:', await signer.provider.getNetwork());
return signer;
}
// Signar un missatge
async function signMessage(signer) {
const message = 'Hola Blockchain!';
const signature = await signer.signMessage(message);
console.log('Signatura:', signature);
// Verificar signatura
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log('Adreça recuperada:', recoveredAddress);
}
// Signar transacció
async function sendTransaction(signer, to, amount) {
const tx = await signer.sendTransaction({
to: to,
value: ethers.parseEther(amount)
});
console.log('TX Hash:', tx.hash);
// Esperar confirmació
const receipt = await tx.wait();
console.log('Confirmat al bloc:', receipt.blockNumber);
}
Contract¶
El contracte permet interactuar amb smart contracts desplegats.
// ABI del contracte (generat per Hardhat/Truffle)
const contractABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
// Adreça del contracte desplegat
const contractAddress = '0xYourContractAddress';
// Contracte de només lectura (amb provider)
const provider = new ethers.JsonRpcProvider('https://sepolia.infura.io/v3/YOUR_KEY');
const readOnlyContract = new ethers.Contract(contractAddress, contractABI, provider);
// Contracte amb capacitat d'escriptura (amb signer)
async function getWriteContract() {
const signer = await getSigner();
const contract = new ethers.Contract(contractAddress, contractABI, signer);
return contract;
}
// Cridar funcions view (gratis)
async function readData() {
const name = await readOnlyContract.name();
const symbol = await readOnlyContract.symbol();
const totalSupply = await readOnlyContract.totalSupply();
console.log('Token:', name, `(${symbol})`);
console.log('Total Supply:', ethers.formatEther(totalSupply));
}
// Cridar funcions que modifiquen estat (paga gas)
async function writeData() {
const contract = await getWriteContract();
const tx = await contract.transfer('0xRecipientAddress', ethers.parseEther('100'));
console.log('TX enviada:', tx.hash);
const receipt = await tx.wait();
console.log('TX confirmada:', receipt.status);
}
3.3 Conversió de Tipus¶
Ethers.js v6 utilitza BigInt per a números grans. Cal convertir correctament:
// De string/number a wei (BigInt)
const amount1 = ethers.parseEther('1.5'); // 1.5 ETH en wei
const amount2 = ethers.parseUnits('100', 18); // 100 tokens amb 18 decimals
const amount3 = ethers.parseUnits('50.5', 6); // 50.5 tokens amb 6 decimals
// De wei a string/number llegible
const ethAmount = ethers.formatEther(amount1); // "1.5"
const tokenAmount = ethers.formatUnits(amount2, 18); // "100.0"
const usdcAmount = ethers.formatUnits(amount3, 6); // "50.5"
// Conversió manual
const bigNumber = 1000000000000000000n; // BigInt literal
const formatted = ethers.formatEther(bigNumber); // "1.0"
// Adreces
const checksumAddress = ethers.getAddress('0x71c7656ec7ab88b098defb751b7401b5f6d8976f');
console.log(checksumAddress); // "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
// Verificar adreça
const isValid = ethers.isAddress('0x71C7656EC7ab88b098defB751B7401B5f6d8976F');
console.log('És vàlida:', isValid);
// Hash
const hash = ethers.keccak256(ethers.toUtf8Bytes('Hola'));
console.log('Hash:', hash);
4. CONNEXIÓ AMB WALLETS¶
4.1 MetaMask Integration¶
MetaMask és el wallet més utilitzat per a DApps. [[4]][[5]][[6]]
Detectar MetaMask¶
// Detectar si MetaMask està instal·lat
function isMetaMaskInstalled() {
if (typeof window !== 'undefined' && window.ethereum) {
return true;
}
return false;
}
// Detectar provider actual
function getCurrentProvider() {
if (window.ethereum?.isMetaMask) {
return 'MetaMask';
} else if (window.ethereum?.isCoinbaseWallet) {
return 'Coinbase Wallet';
} else if (window.ethereum?.isTrust) {
return 'Trust Wallet';
}
return 'Unknown';
}
Connectar Wallet¶
import { ethers } from 'ethers';
class WalletConnector {
constructor() {
this.provider = null;
this.signer = null;
this.account = null;
this.chainId = null;
}
// Connectar a MetaMask
async connect() {
try {
// Verificar MetaMask
if (!window.ethereum) {
throw new Error('MetaMask no està instal·lat');
}
// Demanar accés als comptes
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
// Inicialitzar provider i signer
this.provider = new ethers.BrowserProvider(window.ethereum);
this.signer = await this.provider.getSigner();
this.account = accounts[0];
// Obtenir chain ID
const network = await this.provider.getNetwork();
this.chainId = Number(network.chainId);
console.log('Connectat:', this.account);
console.log('Chain ID:', this.chainId);
// Configurar listeners per canvis
this.setupListeners();
return {
account: this.account,
chainId: this.chainId,
provider: this.provider,
signer: this.signer
};
} catch (error) {
console.error('Error connectant wallet:', error);
throw error;
}
}
// Desconnectar
disconnect() {
this.provider = null;
this.signer = null;
this.account = null;
this.chainId = null;
console.log('Desconnectat');
}
// Configurar listeners per canvis de compte/xarxa
setupListeners() {
// Canvi de compte
window.ethereum.on('accountsChanged', (accounts) => {
console.log('Compte canviat:', accounts);
if (accounts.length === 0) {
this.disconnect();
} else {
this.account = accounts[0];
// Actualitzar UI
this.onAccountChanged(accounts[0]);
}
});
// Canvi de xarxa
window.ethereum.on('chainChanged', (chainId) => {
console.log('Xarxa canviada:', chainId);
this.chainId = Number(chainId);
// Recarregar pàgina per seguretat
window.location.reload();
});
}
// Callback per canvis de compte (sobreescriure)
onAccountChanged(newAccount) {
console.log('Actualitzar UI per:', newAccount);
}
// Obtenir saldo
async getBalance(address = null) {
const targetAddress = address || this.account;
const balance = await this.provider.getBalance(targetAddress);
return ethers.formatEther(balance);
}
}
// Ús
const wallet = new WalletConnector();
// Botó de connexió
document.getElementById('connectBtn').addEventListener('click', async () => {
try {
const connection = await wallet.connect();
console.log('Connectat:', connection.account);
} catch (error) {
console.error('Error:', error.message);
}
});
4.2 Canviar de Xarxa¶
async function switchNetwork(chainId) {
const chainIdHex = '0x' + chainId.toString(16);
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }],
});
} catch (switchError) {
// Xarxa no afegida a MetaMask
if (switchError.code === 4902) {
await addNetwork(chainId);
} else {
throw switchError;
}
}
}
async function addNetwork(chainId) {
const networks = {
1: {
chainId: '0x1',
chainName: 'Ethereum Mainnet',
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://mainnet.infura.io/v3/'],
blockExplorerUrls: ['https://etherscan.io']
},
11155111: {
chainId: '0xaa36a7',
chainName: 'Sepolia Testnet',
nativeCurrency: { name: 'Sepolia ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://sepolia.infura.io/v3/'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
},
137: {
chainId: '0x89',
chainName: 'Polygon Mainnet',
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
rpcUrls: ['https://polygon-rpc.com'],
blockExplorerUrls: ['https://polygonscan.com']
}
};
const network = networks[chainId];
if (!network) throw new Error('Xarxa no suportada');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [network],
});
}
// Ús
// Canviar a Sepolia
switchNetwork(11155111);
4.3 WalletConnect (Mobile Wallets)¶
WalletConnect permet connectar wallets mòbils a DApps web. [[7]][[8]]
npm install @walletconnect/web3-provider
import WalletConnectProvider from "@walletconnect/web3-provider";
async function connectWalletConnect() {
const provider = new WalletConnectProvider({
rpc: {
1: "https://mainnet.infura.io/v3/YOUR_KEY",
11155111: "https://sepolia.infura.io/v3/YOUR_KEY"
},
chainId: 11155111,
});
// Enable session (triggers QR Code modal)
await provider.enable();
const ethersProvider = new ethers.BrowserProvider(provider);
const signer = await ethersProvider.getSigner();
console.log('Connectat via WalletConnect');
return { provider, signer };
}
5. INTERACCIÓ AMB SMART CONTRACTS¶
5.1 Carregar Contracte¶
import { ethers } from 'ethers';
class ContractManager {
constructor(provider, signer) {
this.provider = provider;
this.signer = signer;
this.contracts = {};
}
// Carregar contracte amb ABI i adreça
loadContract(name, address, abi) {
// Contracte de lectura
const readOnlyContract = new ethers.Contract(address, abi, this.provider);
// Contracte d'escriptura (si hi ha signer)
const writeContract = this.signer
? new ethers.Contract(address, abi, this.signer)
: null;
this.contracts[name] = {
read: readOnlyContract,
write: writeContract,
address: address,
abi: abi
};
return this.contracts[name];
}
// Carregar des d'artefactes de Hardhat
async loadFromArtifacts(name, artifacts, networkName = 'sepolia') {
const deployment = artifacts.deployments?.[networkName]?.[name];
if (!deployment) {
throw new Error(`Contracte ${name} no trobat a ${networkName}`);
}
return this.loadContract(name, deployment.address, deployment.abi);
}
// Obtenir contracte
getContract(name, write = false) {
const contract = this.contracts[name];
if (!contract) throw new Error(`Contracte ${name} no carregat`);
return write ? contract.write : contract.read;
}
}
// Exemple d'ús
const abi = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const contractManager = new ContractManager(provider, signer);
const token = contractManager.loadContract('Token', '0xContractAddress', abi);
5.2 Crides de Lectura (View/Pure)¶
Les crides de lectura no consumeixen gas i són immediates.
async function readTokenData(contract) {
try {
// Funcions simples
const name = await contract.read.name();
const symbol = await contract.read.symbol();
const decimals = await contract.read.decimals();
const totalSupply = await contract.read.totalSupply();
console.log('=== Informació del Token ===');
console.log('Nom:', name);
console.log('Símbol:', symbol);
console.log('Decimals:', decimals);
console.log('Total Supply:', ethers.formatUnits(totalSupply, decimals));
// Saldo d'un compte
const account = '0xUserAddress';
const balance = await contract.read.balanceOf(account);
console.log('Saldo:', ethers.formatUnits(balance, decimals));
// Múltiples crides en paral·lel (més eficient)
const [name2, symbol2, supply2] = await Promise.all([
contract.read.name(),
contract.read.symbol(),
contract.read.totalSupply()
]);
return {
name,
symbol,
decimals,
totalSupply: ethers.formatUnits(totalSupply, decimals),
balance: ethers.formatUnits(balance, decimals)
};
} catch (error) {
console.error('Error llegint dades:', error);
throw error;
}
}
5.3 Crides d'Escriptura (Transaccions)¶
Les crides d'escriptura requereixen signatura i pagament de gas.
async function transferTokens(contract, to, amount) {
try {
// Preparar transacció
const amountWei = ethers.parseUnits(amount.toString(), 18);
// Estimar gas (opcional però recomanat)
const gasEstimate = await contract.write.transfer.estimateGas(to, amountWei);
console.log('Gas estimat:', gasEstimate.toString());
// Enviar transacció
const tx = await contract.write.transfer(to, amountWei, {
gasLimit: gasEstimate * 120n / 100n // 20% buffer
});
console.log('Transacció enviada:', tx.hash);
console.log('Esperant confirmació...');
// Esperar confirmació
const receipt = await tx.wait();
console.log('Transacció confirmada!');
console.log('Bloc:', receipt.blockNumber);
console.log('Gas utilitzat:', receipt.gasUsed.toString());
return {
hash: tx.hash,
blockNumber: receipt.blockNumber,
status: receipt.status,
gasUsed: receipt.gasUsed.toString()
};
} catch (error) {
console.error('Error en transferència:', error);
// Analitzar error
if (error.reason) {
console.error('Motiu:', error.reason);
}
if (error.code) {
console.error('Codi:', error.code);
}
throw error;
}
}
// Transacció amb valor en ETH
async function depositETH(contract, amount) {
const tx = await contract.write.deposit({
value: ethers.parseEther(amount.toString())
});
await tx.wait();
return tx.hash;
}
5.4 Batch Calls (Multicall)¶
Per optimitzar, agrupar múltiples crides de lectura:
// Contracte Multicall (disponible a la majoria de xarxes)
const MULTICALL_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
async function multicall(contracts, calls) {
// calls = [
// { contract: tokenContract, function: 'balanceOf', args: [address1] },
// { contract: tokenContract, function: 'balanceOf', args: [address2] },
// ]
const callData = calls.map(call => ({
target: call.contract.address,
callData: call.contract.interface.encodeFunctionData(call.function, call.args)
}));
const multicallContract = new ethers.Contract(
MULTICALL_ADDRESS,
['function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)'],
provider
);
const [, returnData] = await multicallContract.aggregate(callData);
return calls.map((call, i) => {
return call.contract.interface.decodeFunctionResult(call.function, returnData[i]);
});
}
6. GESTIÓ DE TRANSACCIONS¶
6.1 Estat de Transaccions¶
class TransactionManager {
constructor(provider) {
this.provider = provider;
this.pendingTxs = new Map();
}
// Enviar transacció amb tracking
async sendTransaction(txRequest, callbacks = {}) {
const { onPending, onConfirming, onConfirmed, onError } = callbacks;
try {
// Enviar
const tx = await txRequest();
const txHash = tx.hash;
console.log('TX enviada:', txHash);
this.pendingTxs.set(txHash, { status: 'pending', timestamp: Date.now() });
if (onPending) onPending(txHash);
// Esperar primera confirmació
onConfirming && onConfirming(txHash, 0);
const receipt = await tx.wait();
// Actualitzar estat
this.pendingTxs.set(txHash, {
status: 'confirmed',
receipt,
confirmations: receipt.confirmations
});
if (onConfirmed) onConfirmed(receipt);
return receipt;
} catch (error) {
console.error('Error TX:', error);
this.pendingTxs.delete(txHash);
if (onError) onError(error);
throw error;
}
}
// Esperar múltiples confirmacions
async waitForConfirmations(txHash, confirmations = 3) {
const tx = await this.provider.getTransaction(txHash);
if (!tx) throw new Error('Transacció no trobada');
let currentConfirmations = 0;
while (currentConfirmations < confirmations) {
const receipt = await this.provider.getTransactionReceipt(txHash);
if (!receipt) {
await this.sleep(1000);
continue;
}
const currentBlock = await this.provider.getBlockNumber();
currentConfirmations = currentBlock - receipt.blockNumber + 1;
console.log(`Confirmacions: ${currentConfirmations}/${confirmations}`);
if (currentConfirmations < confirmations) {
await this.sleep(3000); // Esperar 3 segons
}
}
return await this.provider.getTransactionReceipt(txHash);
}
// Obtenir estat de TX
async getTransactionStatus(txHash) {
const tx = await this.provider.getTransaction(txHash);
if (!tx) return { status: 'not_found' };
const receipt = await this.provider.getTransactionReceipt(txHash);
if (!receipt) return { status: 'pending', tx };
const currentBlock = await this.provider.getBlockNumber();
const confirmations = currentBlock - receipt.blockNumber + 1;
return {
status: receipt.status === 1 ? 'confirmed' : 'failed',
confirmations,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString(),
tx
};
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Ús
const txManager = new TransactionManager(provider);
await txManager.sendTransaction(
() => contract.write.transfer(to, amount),
{
onPending: (hash) => console.log('Pendent:', hash),
onConfirming: (hash, conf) => console.log('Confirmant:', conf),
onConfirmed: (receipt) => console.log('Confirmat!', receipt),
onError: (error) => console.error('Error:', error)
}
);
6.2 Gas Estimation i Optimització¶
async function estimateAndSend(contract, functionName, args, options = {}) {
const { gasMultiplier = 1.2, maxFeePerGas, maxPriorityFeePerGas } = options;
// Estimar gas
let gasLimit;
try {
gasLimit = await contract.write[functionName].estimateGas(...args);
gasLimit = (gasLimit * BigInt(Math.floor(gasMultiplier * 100))) / 100n;
} catch (error) {
console.error('Error estimant gas:', error);
throw new Error('No es pot estimar el gas');
}
// Obtenir preu de gas actual
const feeData = await provider.getFeeData();
// Preparar opció de gas
const txOptions = {
gasLimit,
maxFeePerGas: maxFeePerGas || feeData.maxFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas || feeData.maxPriorityFeePerGas
};
// Enviar
const tx = await contract.write[functionName](...args, txOptions);
return await tx.wait();
}
// Tipus de transaccions
async function sendLegacyTx(signer, to, value) {
// Transacció legacy (tipus 0)
const tx = await signer.sendTransaction({
to,
value,
gasPrice: await provider.getFeeData().then(f => f.gasPrice)
});
return tx.wait();
}
async function sendEIP1559Tx(signer, to, value) {
// Transacció EIP-1559 (tipus 2) - Ethereum post-London
const feeData = await provider.getFeeData();
const tx = await signer.sendTransaction({
to,
value,
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
});
return tx.wait();
}
6.3 Maneig d'Errors de Transaccions¶
function parseTransactionError(error) {
const errorInfo = {
code: error.code,
reason: error.reason,
message: error.message,
userFriendly: 'Error desconegut'
};
// Errors comuns
switch (error.code) {
case 'ACTION_REJECTED':
case 4001:
errorInfo.userFriendly = 'Transacció rebutjada per l\'usuari';
break;
case 'INSUFFICIENT_FUNDS':
case -32000:
errorInfo.userFriendly = 'Saldo insuficient per pagar el gas';
break;
case 'NONCE_TOO_LOW':
errorInfo.userFriendly = 'Nonce massa baix. Intenta-ho de nou.';
break;
case 'REPLACEMENT_UNDERPRICED':
errorInfo.userFriendly = 'Preu de gas massa baix per reemplaçar TX';
break;
case 'UNPREDICTABLE_GAS_LIMIT':
errorInfo.userFriendly = 'Error en l\'execució. Revisa els paràmetres.';
break;
default:
// Analitzar revert reason
if (error.reason) {
errorInfo.userFriendly = `Error: ${error.reason}`;
} else if (error.data?.message) {
errorInfo.userFriendly = error.data.message;
}
}
console.error('Error detallat:', errorInfo);
return errorInfo;
}
// Ús amb try-catch
async function safeTransfer(contract, to, amount) {
try {
const tx = await contract.write.transfer(to, ethers.parseEther(amount.toString()));
return await tx.wait();
} catch (error) {
const parsedError = parseTransactionError(error);
// Mostrar error a l'usuari
alert(parsedError.userFriendly);
// Log per debugging
console.error('Error complet:', error);
throw error;
}
}
7. ESCOLTA D'EVENTS EN TEMPS REAL¶
7.1 Subscripció a Events¶
class EventListener {
constructor(contract) {
this.contract = contract;
this.listeners = new Map();
}
// Escoltar event específic
onEvent(eventName, callback) {
const listener = this.contract.on(eventName, (...args) => {
const event = args[args.length - 1];
callback({
eventName,
data: args.slice(0, -1),
event: event,
blockNumber: event.blockNumber,
transactionHash: event.log.transactionHash
});
});
this.listeners.set(eventName, listener);
return listener;
}
// Escoltar Transfer events (ERC20)
onTransfer(callback) {
return this.onEvent('Transfer', (data) => {
console.log('Transferència detectada:', {
from: data.data[0],
to: data.data[1],
value: ethers.formatEther(data.data[2]),
block: data.blockNumber
});
callback(data);
});
}
// Escolta temporal (un sol cop)
onceEvent(eventName, callback) {
return this.contract.once(eventName, (...args) => {
const event = args[args.length - 1];
callback({
eventName,
data: args.slice(0, -1),
event
});
// Auto-remove listener
this.listeners.delete(eventName);
});
}
// Remoure listener
offEvent(eventName) {
if (this.listeners.has(eventName)) {
this.contract.removeListener(eventName, this.listeners.get(eventName));
this.listeners.delete(eventName);
}
}
// Remoure tots els listeners
removeAllListeners() {
this.listeners.forEach((listener, eventName) => {
this.contract.removeListener(eventName, listener);
});
this.listeners.clear();
}
// Obtenir events passats
async getPastEvents(eventName, fromBlock, toBlock = 'latest') {
const filter = {
address: await this.contract.getAddress(),
topics: [
this.contract.interface.getEvent(eventName).topicHash
]
};
const logs = await this.provider.getLogs({
...filter,
fromBlock,
toBlock
});
return logs.map(log => this.contract.interface.parseLog(log));
}
}
// Ús
const eventListener = new EventListener(contract, provider);
// Escoltar transferències en temps real
eventListener.onTransfer((data) => {
console.log('Nova transferència!');
console.log('De:', data.data[0]);
console.log('A:', data.data[1]);
console.log('Quantitat:', ethers.formatEther(data.data[2]));
// Actualitzar UI
updateBalanceUI(data.data[1]);
});
// Després de 5 minuts, parar d'escoltar
setTimeout(() => {
eventListener.offEvent('Transfer');
}, 300000);
7.2 Filtratge d'Events¶
// Filtrar events per adreça específica
async function getMyTransfers(contract, myAddress, fromBlock = 0) {
const filter = contract.filters.Transfer(null, myAddress);
const events = await contract.queryFilter(filter, fromBlock, 'latest');
return events.map(event => ({
from: event.args.from,
to: event.args.to,
value: ethers.formatEther(event.args.value),
block: event.blockNumber,
txHash: event.transactionHash
}));
}
// Filtrar per rang de valors
async function getLargeTransfers(contract, minValue, fromBlock) {
const events = await contract.queryFilter('Transfer', fromBlock, 'latest');
return events.filter(event => {
const value = event.args.value;
return value >= ethers.parseEther(minValue.toString());
}).map(event => ({
from: event.args.from,
to: event.args.to,
value: ethers.formatEther(event.args.value),
block: event.blockNumber
}));
}
// Escoltar múltiples events
async function listenToMultipleEvents(contract) {
contract.on('Transfer', (from, to, value, event) => {
console.log('Transfer:', ethers.formatEther(value), 'de', from, 'a', to);
});
contract.on('Approval', (owner, spender, value, event) => {
console.log('Approval:', ethers.formatEther(value), 'de', owner, 'per', spender);
});
}
8. FRONTEND AMB REACT + ETHERS.JS¶
8.1 Configuració del Projecte¶
# Crear projecte React
npx create-react-app my-dapp
cd my-dapp
# Instal·lar dependències
npm install ethers@6
npm install @rainbow-me/rainbowkit # Opcional: millor UX de wallet
npm install wagmi # Opcional: hooks per React
# O instal·lació mínima
npm install ethers@6
8.2 Hook Personalitzat per Web3¶
// hooks/useWeb3.js
import { useState, useEffect, useCallback } from 'react';
import { ethers } from 'ethers';
export function useWeb3() {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [chainId, setChainId] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
// Connectar wallet
const connect = useCallback(async () => {
setIsConnecting(true);
setError(null);
try {
if (!window.ethereum) {
throw new Error('MetaMask no està instal·lat');
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const browserSigner = await browserProvider.getSigner();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(browserSigner);
setAccount(accounts[0]);
setChainId(Number(network.chainId));
// Configurar listeners
setupListeners(browserProvider);
} catch (err) {
setError(err.message);
console.error('Error connectant:', err);
} finally {
setIsConnecting(false);
}
}, []);
// Desconnectar
const disconnect = useCallback(() => {
setAccount(null);
setProvider(null);
setSigner(null);
setChainId(null);
setError(null);
}, []);
// Configurar listeners per canvis
const setupListeners = useCallback((browserProvider) => {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnect();
} else {
setAccount(accounts[0]);
}
});
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
}, [disconnect]);
// Netejar listeners al desmuntar
useEffect(() => {
return () => {
if (window.ethereum) {
window.ethereum.removeAllListeners();
}
};
}, []);
// Comprovar connexió existent
useEffect(() => {
const checkConnection = async () => {
if (window.ethereum) {
try {
const accounts = await window.ethereum.request({
method: 'eth_accounts'
});
if (accounts.length > 0) {
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const browserSigner = await browserProvider.getSigner();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(browserSigner);
setAccount(accounts[0]);
setChainId(Number(network.chainId));
setupListeners(browserProvider);
}
} catch (err) {
console.error('Error checkant connexió:', err);
}
}
};
checkConnection();
}, [setupListeners]);
return {
account,
provider,
signer,
chainId,
isConnecting,
error,
connect,
disconnect,
isConnected: !!account
};
}
export default useWeb3;
8.3 Component de Connexió Wallet¶
// components/WalletConnect.jsx
import React from 'react';
import useWeb3 from '../hooks/useWeb3';
import './WalletConnect.css';
function WalletConnect() {
const { account, chainId, isConnecting, error, connect, disconnect, isConnected } = useWeb3();
const formatAddress = (addr) => {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
const getNetworkName = (id) => {
const networks = {
1: 'Ethereum Mainnet',
11155111: 'Sepolia Testnet',
137: 'Polygon',
56: 'BSC'
};
return networks[id] || `Chain ${id}`;
};
if (isConnected) {
return (
<div className="wallet-connected">
<div className="wallet-info">
<span className="network-badge">
{getNetworkName(chainId)}
</span>
<span className="account-address">
{formatAddress(account)}
</span>
</div>
<button onClick={disconnect} className="disconnect-btn">
Desconnectar
</button>
</div>
);
}
return (
<div className="wallet-connect">
<button
onClick={connect}
disabled={isConnecting}
className="connect-btn"
>
{isConnecting ? 'Connectant...' : 'Connectar Wallet'}
</button>
{error && <div className="error-message">{error}</div>}
</div>
);
}
export default WalletConnect;
8.4 Component d'Interacció amb Contracte¶
// components/TokenTransfer.jsx
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import useWeb3 from '../hooks/useWeb3';
// ABI del token
const TOKEN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const TOKEN_ADDRESS = '0xYourTokenAddress';
function TokenTransfer() {
const { signer, provider, account, isConnected } = useWeb3();
const [tokenContract, setTokenContract] = useState(null);
const [tokenInfo, setTokenInfo] = useState(null);
const [balance, setBalance] = useState('0');
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [txHash, setTxHash] = useState(null);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
// Carregar informació del token
useEffect(() => {
if (provider) {
const contract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, provider);
setTokenContract(contract);
loadTokenInfo(contract);
}
}, [provider]);
// Actualitzar saldo quan canvia el compte
useEffect(() => {
if (tokenContract && account) {
loadBalance();
}
}, [tokenContract, account]);
const loadTokenInfo = async (contract) => {
try {
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals()
]);
setTokenInfo({ name, symbol, decimals });
} catch (err) {
console.error('Error carregant token:', err);
}
};
const loadBalance = async () => {
if (!tokenContract || !account) return;
try {
const balance = await tokenContract.balanceOf(account);
setBalance(ethers.formatUnits(balance, tokenInfo.decimals));
} catch (err) {
console.error('Error carregant saldo:', err);
}
};
const handleTransfer = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);
setTxHash(null);
try {
// Validar
if (!ethers.isAddress(recipient)) {
throw new Error('Adreça invàlida');
}
if (parseFloat(amount) <= 0) {
throw new Error('Quantitat ha de ser major que 0');
}
if (parseFloat(amount) > parseFloat(balance)) {
throw new Error('Saldo insuficient');
}
// Crear contracte amb signer
const writeContract = tokenContract.connect(signer);
// Enviar transacció
const amountWei = ethers.parseUnits(amount, tokenInfo.decimals);
const tx = await writeContract.transfer(recipient, amountWei);
setTxHash(tx.hash);
console.log('TX enviada:', tx.hash);
// Esperar confirmació
await tx.wait();
setSuccess(true);
loadBalance(); // Actualitzar saldo
} catch (err) {
console.error('Error transferència:', err);
setError(err.reason || err.message || 'Error desconegut');
} finally {
setLoading(false);
}
};
if (!isConnected) {
return <div className="not-connected">Connecta la teva wallet primer</div>;
}
return (
<div className="token-transfer">
<h2>{tokenInfo?.name} ({tokenInfo?.symbol})</h2>
<div className="balance-card">
<span className="balance-label">El teu saldo:</span>
<span className="balance-amount">{balance} {tokenInfo?.symbol}</span>
</div>
<form onSubmit={handleTransfer} className="transfer-form">
<div className="form-group">
<label>Adreça destinatari:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
required
/>
</div>
<div className="form-group">
<label>Quantitat:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
min="0"
required
/>
</div>
<button
type="submit"
disabled={loading || !recipient || !amount}
className="submit-btn"
>
{loading ? 'Processant...' : 'Transferir'}
</button>
</form>
{txHash && (
<div className="tx-info">
<p>TX Hash: <a
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash.slice(0, 10)}...{txHash.slice(-8)}
</a></p>
</div>
)}
{success && (
<div className="success-message">
✓ Transferència completada amb èxit!
</div>
)}
{error && (
<div className="error-message">
✗ {error}
</div>
)}
</div>
);
}
export default TokenTransfer;
8.5 App Principal¶
// App.jsx
import React from 'react';
import WalletConnect from './components/WalletConnect';
import TokenTransfer from './components/TokenTransfer';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>La Meva DApp</h1>
<WalletConnect />
</header>
<main className="App-main">
<TokenTransfer />
</main>
</div>
);
}
export default App;
9. BACKEND PER A DApps¶
9.1 Quan Cal un Backend?¶
Tot i que les DApps són descentralitzades, sovint necessiten backend per:
| Cas d'Ús | Solució On-Chain | Solució Off-Chain |
|---|---|---|
| Emmagatzematge gran | ❌ Molt car | ✅ IPFS/Arweave |
| Dades privades | ❌ Tot és públic | ✅ Base de dades tradicional |
| Indexació ràpida | ❌ Lent | ✅ The Graph / Backend propi |
| Oracles | ✅ Chainlink | ✅ API pròpia + oracle |
| Autenticació | ✅ Wallet connect | ✅ JWT + wallet signature |
| Notificacions | ✅ Events | ✅ WebSockets / Push |
9.2 Backend amb Node.js + Express¶
// server/index.js
const express = require('express');
const cors = require('cors');
const { ethers } = require('ethers');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
// Provider per llegir dades
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
// Contracte
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
const CONTRACT_ABI = require('./abi/Token.json');
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
// Endpoint: Obtenir informació del token
app.get('/api/token/info', async (req, res) => {
try {
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
contract.totalSupply()
]);
res.json({
name,
symbol,
decimals: Number(decimals),
totalSupply: ethers.formatUnits(totalSupply, decimals)
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Endpoint: Obtenir saldo
app.get('/api/token/balance/:address', async (req, res) => {
try {
const { address } = req.params;
if (!ethers.isAddress(address)) {
return res.status(400).json({ error: 'Adreça invàlida' });
}
const balance = await contract.balanceOf(address);
const decimals = await contract.decimals();
res.json({
address,
balance: ethers.formatUnits(balance, decimals),
balanceWei: balance.toString()
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Endpoint: Historial de transaccions
app.get('/api/token/transfers/:address', async (req, res) => {
try {
const { address } = req.params;
const { fromBlock = 0, limit = 50 } = req.query;
// Filtrar events
const filter = contract.filters.Transfer(null, address);
const events = await contract.queryFilter(filter, fromBlock, 'latest');
const transfers = events.slice(-limit).map(event => ({
from: event.args.from,
to: event.args.to,
value: ethers.formatEther(event.args.value),
blockNumber: event.blockNumber,
txHash: event.transactionHash,
timestamp: event.block.timestamp
}));
res.json({ transfers, total: events.length });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Endpoint: Verificar signatura (autenticació)
app.post('/api/auth/verify', async (req, res) => {
try {
const { address, signature, message } = req.body;
// Verificar signatura
const recoveredAddress = ethers.verifyMessage(message, signature);
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
return res.status(401).json({ error: 'Signatura invàlida' });
}
// Generar token JWT (exemple)
const token = generateJWT(address);
res.json({ success: true, token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
9.3 Indexació amb The Graph¶
The Graph permet indexar dades de blockchain per consultes ràpides. [[9]][[10]]
Subgraph Manifest (subgraph.yaml):
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: Token
network: sepolia
source:
address: '0xYourContractAddress'
abi: Token
startBlock: 0
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Transfer
- Approval
abis:
- name: Token
file: ./abis/Token.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
- event: Approval(indexed address,indexed address,uint256)
handler: handleApproval
file: ./src/mapping.ts
Schema GraphQL (schema.graphql):
type Transfer @entity {
id: ID!
from: Bytes!
to: Bytes!
value: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
transaction: Transaction!
}
type Transaction @entity {
id: ID!
blockNumber: BigInt!
timestamp: BigInt!
transfers: [Transfer!]! @derivedFrom(field: "transaction")
}
type Account @entity {
id: ID!
balance: BigInt!
transfersFrom: [Transfer!]! @derivedFrom(field: "from")
transfersTo: [Transfer!]! @derivedFrom(field: "to")
}
Mapping (src/mapping.ts):
import { Transfer } from '../generated/Token/Token'
import { Transfer as TransferEntity, Account, Transaction } from '../generated/schema'
export function handleTransfer(event: Transfer): void {
let transfer = new TransferEntity(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
transfer.from = event.params.from
transfer.to = event.params.to
transfer.value = event.params.value
transfer.blockNumber = event.block.number
transfer.timestamp = event.block.timestamp
let transaction = new Transaction(event.transaction.hash.toHex())
transaction.blockNumber = event.block.number
transaction.timestamp = event.block.timestamp
transaction.save()
transfer.transaction = transaction.id
transfer.save()
// Actualitzar balances
updateAccountBalance(event.params.from, event.params.value.neg())
updateAccountBalance(event.params.to, event.params.value)
}
function updateAccountBalance(address: Bytes, delta: BigInt): void {
let account = Account.load(address.toHex())
if (!account) {
account = new Account(address.toHex())
account.balance = BigInt.fromI32(0)
}
account.balance = account.balance.plus(delta)
account.save()
}
Consulta GraphQL:
query {
transfers(first: 10, orderBy: timestamp, orderDirection: desc) {
id
from
to
value
blockNumber
timestamp
}
account(id: "0xUserAddress") {
id
balance
transfersFrom(first: 5) {
to
value
}
}
}
10. SEGURETAT EN DApps¶
10.1 Millors Pràctiques de Seguretat¶
Frontend Security [[11]][[12]]¶
// ❌ INSEGURO: Validació només al frontend
function transfer(amount) {
if (amount > 0) { // Fàcil de bypass
contract.transfer(amount);
}
}
// ✅ SEGURO: Validació també al smart contract
// El frontend només és UX, la seguretat real és on-chain
// ❌ INSEGURO: Guardar private keys al frontend
const privateKey = process.env.PRIVATE_KEY; // MAI al frontend!
// ✅ SEGURO: Utilitzar wallet de l'usuari
const signer = await provider.getSigner(); // L'usuari controla les claus
// ❌ INSEGURO: Confiar en dades del frontend per lògica crítica
const price = await fetchPriceFromFrontend(); // Manipulable!
// ✅ SEGURO: Utilitzar oracles o llegir de blockchain
const price = await priceOracle.getPrice(); // Verificable on-chain
Phishing Protection¶
// Verificar domini abans de signar
function verifyDomain() {
const allowedDomains = ['https://mydapp.com', 'https://www.mydapp.com'];
if (!allowedDomains.includes(window.location.origin)) {
throw new Error('Domini no autoritzat');
}
}
// Mostrar clarament què es signa
async function signMessageWithConfirmation(message, expectedDomain) {
const confirmed = window.confirm(
`Estàs a punt de signar un missatge per a ${expectedDomain}.\n\n` +
`Domini actual: ${window.location.origin}\n\n` +
`Missatge: ${message}\n\n` +
`Confirms?`
);
if (!confirmed) throw new Error('Signatura cancel·lada');
return await signer.signMessage(message);
}
Rate Limiting i DDoS Protection¶
// Backend rate limiting
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minuts
max: 100, // màxim 100 peticions
message: 'Massa peticions, intenta-ho més tard'
});
app.use('/api/', apiLimiter);
// Frontend: debounce per prevenir clics múltiples
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const handleTransferDebounced = debounce(handleTransfer, 1000);
10.2 Validació d'Entrades¶
// Validacions completes abans d'enviar TX
async function validateAndTransfer(to, amount) {
// Validar adreça
if (!ethers.isAddress(to)) {
throw new Error('Adreça invàlida');
}
// Validar quantitat
const amountNum = parseFloat(amount);
if (isNaN(amountNum) || amountNum <= 0) {
throw new Error('Quantitat invàlida');
}
// Validar decimals
const decimals = await contract.decimals();
const amountWei = ethers.parseUnits(amount, decimals);
// Validar saldo
const balance = await contract.balanceOf(account);
if (balance < amountWei) {
throw new Error('Saldo insuficient');
}
// Validar límits
const maxTransfer = ethers.parseUnits('10000', decimals);
if (amountWei > maxTransfer) {
throw new Error('Quantitat excedeix el límit');
}
// Validar contracte no maliciós
const code = await provider.getCode(to);
if (code !== '0x') {
// És un contracte, verificar si és segur
const isVerified = await verifyContractSafety(to);
if (!isVerified) {
const confirmed = window.confirm(
'Estàs enviant tokens a un contracte. Confirms?'
);
if (!confirmed) throw new Error('Cancel·lat per usuari');
}
}
// Tot validat, procedir
return await contract.transfer(to, amountWei);
}
10.3 Error Handling Seguro¶
class SecureTransactionHandler {
async execute(transactionFn, options = {}) {
const {
maxRetries = 3,
timeout = 30000,
onError,
onSuccess
} = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Timeout wrapper
const result = await Promise.race([
transactionFn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
onSuccess && onSuccess(result);
return result;
} catch (error) {
lastError = error;
console.error(`Intent ${attempt} fallit:`, error.message);
// No reintentar errors d'usuari
if (error.code === 'ACTION_REJECTED' || error.code === 4001) {
break;
}
// Esperar abans de reintentar
if (attempt < maxRetries) {
await this.sleep(1000 * attempt);
}
}
}
onError && onError(lastError);
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Ús
const txHandler = new SecureTransactionHandler();
await txHandler.execute(
() => contract.transfer(to, amount),
{
maxRetries: 3,
onError: (error) => {
// Mostrar error amigable a l'usuari
showUserError(parseTransactionError(error));
},
onSuccess: (receipt) => {
showSuccessMessage('Transacció completada!');
}
}
);
11. EXERCICIS GUIATS¶
EXERCICI GUIAT 1: DApp de Token ERC20 Completa¶
Objectiu: Crear una DApp completa per interactuar amb un token ERC20
Requisits: - Connectar MetaMask - Mostrar saldo del token - Transferir tokens - Veure historial de transaccions - Escoltar events en temps real
Pas 1: Configuració del Projecte
# Crear projecte
npx create-react-app token-dapp
cd token-dapp
# Instal·lar dependències
npm install ethers@6
# Estructura
mkdir -p src/hooks src/components src/contracts src/utils
Pas 2: Contracte de Token (ja desplegat a UD2)
// Utilitzem el TokenERC20 de la UD2
// Contracte desplegat a Sepolia: 0xYourDeployedAddress
Pas 3: Hook useWeb3
// src/hooks/useWeb3.js
import { useState, useEffect, useCallback } from 'react';
import { ethers } from 'ethers';
export function useWeb3() {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [chainId, setChainId] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
const connect = useCallback(async () => {
setIsConnecting(true);
setError(null);
try {
if (!window.ethereum) {
throw new Error('MetaMask no està instal·lat. Instal·la\'l a metamask.io');
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const browserSigner = await browserProvider.getSigner();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(browserSigner);
setAccount(accounts[0]);
setChainId(Number(network.chainId));
// Verificar xarxa
if (Number(network.chainId) !== 11155111) {
alert('Si us plau, canvia a Sepolia Testnet');
}
setupListeners();
} catch (err) {
setError(err.message);
} finally {
setIsConnecting(false);
}
}, []);
const disconnect = useCallback(() => {
setAccount(null);
setProvider(null);
setSigner(null);
setChainId(null);
}, []);
const setupListeners = useCallback(() => {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnect();
} else {
setAccount(accounts[0]);
}
});
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
}, [disconnect]);
useEffect(() => {
return () => {
if (window.ethereum) {
window.ethereum.removeAllListeners();
}
};
}, []);
useEffect(() => {
const checkConnection = async () => {
if (window.ethereum) {
try {
const accounts = await window.ethereum.request({
method: 'eth_accounts'
});
if (accounts.length > 0) {
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const browserSigner = await browserProvider.getSigner();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(browserSigner);
setAccount(accounts[0]);
setChainId(Number(network.chainId));
}
} catch (err) {
console.error(err);
}
}
};
checkConnection();
}, []);
return {
account,
provider,
signer,
chainId,
isConnecting,
error,
connect,
disconnect,
isConnected: !!account
};
}
Pas 4: Component Principal
// src/App.jsx
import React from 'react';
import WalletConnect from './components/WalletConnect';
import TokenInfo from './components/TokenInfo';
import TokenTransfer from './components/TokenTransfer';
import TransactionHistory from './components/TransactionHistory';
import './App.css';
function App() {
return (
<div className="App">
<header>
<h1>🪙 Token DApp</h1>
<WalletConnect />
</header>
<main>
<TokenInfo />
<TokenTransfer />
<TransactionHistory />
</main>
</div>
);
}
export default App;
Pas 5: Component TokenInfo
// src/components/TokenInfo.jsx
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import useWeb3 from '../hooks/useWeb3';
const TOKEN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)"
];
const TOKEN_ADDRESS = '0xYourDeployedTokenAddress';
function TokenInfo() {
const { provider, account } = useWeb3();
const [tokenData, setTokenData] = useState(null);
const [balance, setBalance] = useState('0');
const [loading, setLoading] = useState(true);
useEffect(() => {
if (provider) {
loadTokenData();
}
}, [provider]);
useEffect(() => {
if (provider && account && tokenData) {
loadBalance();
}
}, [provider, account, tokenData]);
const loadTokenData = async () => {
try {
const contract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, provider);
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
contract.totalSupply()
]);
setTokenData({
name,
symbol,
decimals: Number(decimals),
totalSupply: ethers.formatUnits(totalSupply, decimals)
});
} catch (error) {
console.error('Error carregant token:', error);
} finally {
setLoading(false);
}
};
const loadBalance = async () => {
try {
const contract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, provider);
const balance = await contract.balanceOf(account);
setBalance(ethers.formatUnits(balance, tokenData.decimals));
} catch (error) {
console.error('Error carregant saldo:', error);
}
};
if (loading) return <div>Carregant...</div>;
if (!tokenData) return <div>Error carregant token</div>;
return (
<div className="token-info-card">
<h2>{tokenData.name} ({tokenData.symbol})</h2>
<div className="info-grid">
<div className="info-item">
<span className="label">Total Supply:</span>
<span className="value">{tokenData.totalSupply}</span>
</div>
<div className="info-item">
<span className="label">Decimals:</span>
<span className="value">{tokenData.decimals}</span>
</div>
{account && (
<div className="info-item highlight">
<span className="label">El teu saldo:</span>
<span className="value">{balance} {tokenData.symbol}</span>
</div>
)}
</div>
</div>
);
}
export default TokenInfo;
Pas 6: Component TokenTransfer
// src/components/TokenTransfer.jsx
import React, { useState } from 'react';
import { ethers } from 'ethers';
import useWeb3 from '../hooks/useWeb3';
const TOKEN_ABI = [
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
];
const TOKEN_ADDRESS = '0xYourDeployedTokenAddress';
function TokenTransfer() {
const { signer, account, isConnected } = useWeb3();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [txHash, setTxHash] = useState(null);
const [status, setStatus] = useState(null);
const handleTransfer = async (e) => {
e.preventDefault();
setLoading(true);
setStatus(null);
setTxHash(null);
try {
// Validacions
if (!ethers.isAddress(recipient)) {
throw new Error('Adreça de destinatari invàlida');
}
const amountNum = parseFloat(amount);
if (isNaN(amountNum) || amountNum <= 0) {
throw new Error('Quantitat invàlida');
}
// Contracte amb signer
const contract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, signer);
const decimals = await contract.decimals();
const amountWei = ethers.parseUnits(amount, decimals);
// Enviar transacció
const tx = await contract.transfer(recipient, amountWei);
setTxHash(tx.hash);
setStatus('pending');
// Esperar confirmació
await tx.wait();
setStatus('success');
setAmount('');
setRecipient('');
} catch (error) {
console.error('Error:', error);
setStatus('error');
alert(error.reason || error.message);
} finally {
setLoading(false);
}
};
if (!isConnected) {
return <div className="not-connected">Connecta la teva wallet</div>;
}
return (
<div className="transfer-card">
<h3>Transferir Tokens</h3>
<form onSubmit={handleTransfer}>
<div className="form-group">
<label>Destinatari:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
required
/>
</div>
<div className="form-group">
<label>Quantitat:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
min="0"
required
/>
</div>
<button
type="submit"
disabled={loading || !recipient || !amount}
className="transfer-btn"
>
{loading ? 'Processant...' : 'Transferir'}
</button>
</form>
{txHash && (
<div className="tx-result">
<p>TX: <a
href={`https://sepolia.etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash.slice(0, 10)}...{txHash.slice(-8)}
</a></p>
{status === 'pending' && <span className="pending">⏳ Pendent...</span>}
{status === 'success' && <span className="success">✓ Completat!</span>}
{status === 'error' && <span className="error">✗ Error</span>}
</div>
)}
</div>
);
}
export default TokenTransfer;
Pas 7: CSS Bàsic
/* src/App.css */
.App {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 2px solid #eee;
}
.token-info-card, .transfer-card {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
padding: 10px;
background: white;
border-radius: 5px;
}
.info-item.highlight {
background: #e3f2fd;
border: 2px solid #2196f3;
}
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.transfer-btn {
width: 100%;
padding: 12px;
background: #2196f3;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
}
.transfer-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.connect-btn {
padding: 10px 20px;
background: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.tx-result {
margin-top: 15px;
padding: 10px;
background: white;
border-radius: 5px;
}
.success { color: green; }
.error { color: red; }
.pending { color: orange; }
EXERCICI GUIAT 2: DApp de Votació Descentralitzada¶
Objectiu: Crear una interfície per al sistema de votació de la UD2
Requisits: - Veure candidats i vots - Votar (si no ha votat) - Veure resultats en temps real - Escoltar events de vots
Pas 1: Contracte (de la UD2)
// Utilitzem SistemaVotacio de la UD2
// Funcions principals:
// - afegirCandidat(string nom)
// - iniciarVotacio(uint256 duradaDies)
// - votar(uint256 candidatId)
// - obtenirCandidat(uint256 id)
// - obtenirGuanyador()
// - obtenirEstadistiques()
Pas 2: Component Principal de Votació
// src/components/Voting/VotingApp.jsx
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import useWeb3 from '../../hooks/useWeb3';
import CandidateList from './CandidateList';
import VoteForm from './VoteForm';
import Results from './Results';
import EventListener from './EventListener';
const VOTING_ABI = [
"function totalCandidats() view returns (uint256)",
"function totalVots() view returns (uint256)",
"function votacioIniciada() view returns (bool)",
"function votacioFinalitzada() view returns (bool)",
"function dataFi() view returns (uint256)",
"function obtenirCandidat(uint256) view returns (uint256, string, uint256, bool)",
"function votar(uint256 candidatId)",
"function haVotat(address) view returns (bool)",
"event VotEmes(address indexed votant, uint256 indexed candidatId)"
];
const VOTING_ADDRESS = '0xYourVotingContractAddress';
function VotingApp() {
const { provider, signer, account, isConnected } = useWeb3();
const [contract, setContract] = useState(null);
const [candidates, setCandidates] = useState([]);
const [stats, setStats] = useState(null);
const [hasVoted, setHasVoted] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (provider) {
const votingContract = new ethers.Contract(VOTING_ADDRESS, VOTING_ABI, provider);
setContract(votingContract);
loadData(votingContract);
}
}, [provider]);
useEffect(() => {
if (contract && account) {
checkIfVoted();
}
}, [contract, account]);
const loadData = async (votingContract) => {
try {
const totalCandidats = await votingContract.totalCandidats();
const totalVots = await votingContract.totalVots();
const votacioIniciada = await votingContract.votacioIniciada();
const votacioFinalitzada = await votingContract.votacioFinalitzada();
const dataFi = await votingContract.dataFi();
// Carregar candidats
const candidatesData = [];
for (let i = 1; i <= totalCandidats; i++) {
const [id, nom, vots, actiu] = await votingContract.obtenirCandidat(i);
candidatesData.push({
id: Number(id),
nom,
vots: Number(vots),
actiu
});
}
setCandidates(candidatesData);
setStats({
totalCandidats: Number(totalCandidats),
totalVots: Number(totalVots),
votacioIniciada,
votacioFinalitzada,
dataFi: Number(dataFi) * 1000 // Convertir a ms
});
} catch (error) {
console.error('Error carregant dades:', error);
} finally {
setLoading(false);
}
};
const checkIfVoted = async () => {
try {
const voted = await contract.haVotat(account);
setHasVoted(voted);
} catch (error) {
console.error('Error verificant vot:', error);
}
};
const handleVote = async (candidateId) => {
try {
const writeContract = contract.connect(signer);
const tx = await writeContract.votar(candidateId);
await tx.wait();
// Actualitzar dades
await loadData(contract);
await checkIfVoted();
alert('Vot registrat correctament!');
} catch (error) {
console.error('Error votant:', error);
alert(error.reason || error.message);
}
};
if (loading) return <div>Carregant...</div>;
if (!isConnected) return <div>Connecta la teva wallet</div>;
return (
<div className="voting-app">
<h1>🗳️ Sistema de Votació</h1>
<div className="stats-bar">
<span>Candidats: {stats.totalCandidats}</span>
<span>Vots totals: {stats.totalVots}</span>
<span>Estat: {stats.votacioFinalitzada ? 'Finalitzada' : stats.votacioIniciada ? 'Activa' : 'No iniciada'}</span>
</div>
{stats.votacioIniciada && !stats.votacioFinalitzada && (
<>
{!hasVoted ? (
<VoteForm
candidates={candidates}
onVote={handleVote}
/>
) : (
<div className="already-voted">
✓ Ja has votat en aquesta elecció
</div>
)}
</>
)}
<CandidateList candidates={candidates} />
{stats.votacioFinalitzada && (
<Results candidates={candidates} />
)}
<EventListener contract={contract} onUpdate={loadData} />
</div>
);
}
export default VotingApp;
Pas 3: Component VoteForm
// src/components/Voting/VoteForm.jsx
import React, { useState } from 'react';
function VoteForm({ candidates, onVote }) {
const [selected, setSelected] = useState(null);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!selected) return;
setSubmitting(true);
try {
await onVote(selected);
} finally {
setSubmitting(false);
}
};
return (
<div className="vote-form">
<h3>Emet el teu vot</h3>
<form onSubmit={handleSubmit}>
<div className="candidates-options">
{candidates.filter(c => c.actiu).map(candidate => (
<label key={candidate.id} className="candidate-option">
<input
type="radio"
name="candidate"
value={candidate.id}
checked={selected === candidate.id}
onChange={() => setSelected(candidate.id)}
/>
<span>{candidate.nom}</span>
<span className="current-votes">({candidate.vots} vots)</span>
</label>
))}
</div>
<button
type="submit"
disabled={!selected || submitting}
>
{submitting ? 'Enviament vot...' : 'Confirmar Vot'}
</button>
</form>
</div>
);
}
export default VoteForm;
Pas 4: Component EventListener
// src/components/Voting/EventListener.jsx
import React, { useEffect } from 'react';
function EventListener({ contract, onUpdate }) {
useEffect(() => {
if (!contract) return;
// Escoltar nous vots
const listener = contract.on('VotEmes', (votant, candidatId, event) => {
console.log('Nou vot detectat:', {
votant,
candidatId: Number(candidatId),
block: event.blockNumber
});
// Actualitzar dades
onUpdate();
});
return () => {
// Netejar listener
contract.removeListener('VotEmes', listener);
};
}, [contract, onUpdate]);
return (
<div className="event-listener-indicator">
<span className="live-indicator">🔴 En directe</span>
<span>Escoltant nous vots...</span>
</div>
);
}
export default EventListener;
EXERCICI GUIAT 3: DApp de NFT Marketplace¶
Objectiu: Crear un marketplace bàsic per comprar/vendre NFTs
Requisits: - Mostrar NFTs disponibles - Listar NFT per vendre - Comprar NFT - Veure els meus NFTs
Pas 1: Contractes
// NFT Contract (ERC721)
// Marketplace Contract (de la UD2)
Pas 2: Component Marketplace
// src/components/Marketplace/Marketplace.jsx
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import useWeb3 from '../../hooks/useWeb3';
import NFTCard from './NFTCard';
import ListNFTForm from './ListNFTForm';
const MARKETPLACE_ABI = [
"function nextListingId() view returns (uint256)",
"function getListing(uint256) view returns (uint256, address, uint256, bool)",
"function buyNFT(uint256 listingId) payable",
"function listNFT(uint256 tokenId, uint256 price)",
"function cancelListing(uint256 listingId)",
"event NFTListed(uint256 indexed listingId, uint256 tokenId, uint256 price)",
"event NFTSold(uint256 indexed listingId, uint256 tokenId, address buyer)"
];
const NFT_ABI = [
"function ownerOf(uint256 tokenId) view returns (address)",
"function tokenURI(uint256 tokenId) view returns (string)",
"function approve(address to, uint256 tokenId)",
"function balanceOf(address owner) view returns (uint256)",
"function tokenByIndex(uint256 index) view returns (uint256)",
"function totalSupply() view returns (uint256)"
];
const MARKETPLACE_ADDRESS = '0xMarketplaceAddress';
const NFT_ADDRESS = '0xNFTAddress';
function Marketplace() {
const { provider, signer, account, isConnected } = useWeb3();
const [listings, setListings] = useState([]);
const [myNFTs, setMyNFTs] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (provider) {
loadListings();
}
}, [provider]);
useEffect(() => {
if (account) {
loadMyNFTs();
}
}, [account]);
const loadListings = async () => {
try {
const marketplace = new ethers.Contract(MARKETPLACE_ADDRESS, MARKETPLACE_ABI, provider);
const nextListingId = await marketplace.nextListingId();
const listingsData = [];
for (let i = 1; i < nextListingId; i++) {
const [tokenId, seller, price, active] = await marketplace.getListing(i);
if (active) {
listingsData.push({
listingId: i,
tokenId: Number(tokenId),
seller,
price: ethers.formatEther(price),
active
});
}
}
setListings(listingsData);
} catch (error) {
console.error('Error carregant listings:', error);
} finally {
setLoading(false);
}
};
const loadMyNFTs = async () => {
try {
const nft = new ethers.Contract(NFT_ADDRESS, NFT_ABI, provider);
const balance = await nft.balanceOf(account);
const myNFTsData = [];
for (let i = 0; i < balance; i++) {
const tokenId = await nft.tokenByIndex(i);
const tokenURI = await nft.tokenURI(tokenId);
myNFTsData.push({
tokenId: Number(tokenId),
uri: tokenURI
});
}
setMyNFTs(myNFTsData);
} catch (error) {
console.error('Error carregant NFTs:', error);
}
};
const handleBuy = async (listingId, price) => {
try {
const marketplace = new ethers.Contract(MARKETPLACE_ADDRESS, MARKETPLACE_ABI, signer);
const tx = await marketplace.buyNFT(listingId, {
value: ethers.parseEther(price)
});
await tx.wait();
await loadListings();
await loadMyNFTs();
alert('NFT comprat correctament!');
} catch (error) {
console.error('Error comprant:', error);
alert(error.reason || error.message);
}
};
const handleList = async (tokenId, price) => {
try {
const nft = new ethers.Contract(NFT_ADDRESS, NFT_ABI, signer);
const marketplace = new ethers.Contract(MARKETPLACE_ADDRESS, MARKETPLACE_ABI, signer);
// Aprovar marketplace
const approveTx = await nft.approve(MARKETPLACE_ADDRESS, tokenId);
await approveTx.wait();
// Listar
const listTx = await marketplace.listNFT(tokenId, ethers.parseEther(price));
await listTx.wait();
await loadListings();
await loadMyNFTs();
alert('NFT listat correctament!');
} catch (error) {
console.error('Error listant:', error);
alert(error.reason || error.message);
}
};
if (loading) return <div>Carregant...</div>;
if (!isConnected) return <div>Connecta la teva wallet</div>;
return (
<div className="marketplace">
<h1>🎨 NFT Marketplace</h1>
<section>
<h2>NFTs Disponibles</h2>
<div className="nft-grid">
{listings.map(listing => (
<NFTCard
key={listing.listingId}
listing={listing}
onBuy={handleBuy}
/>
))}
</div>
</section>
<section>
<h2>Els Meus NFTs</h2>
<div className="nft-grid">
{myNFTs.map(nft => (
<ListNFTForm
key={nft.tokenId}
nft={nft}
onList={handleList}
/>
))}
</div>
</section>
</div>
);
}
export default Marketplace;
12. COL·LECCIÓ D'EXERCICIS PROPOSATS¶
Nivell Bàsic¶
Exercici 1: Explorador de Blocs Simple - Crear una DApp que mostri informació dels últims blocs - Funcionalitats: - Mostrar últims 10 blocs - Detalls de cada bloc (número, timestamp, transaccions) - Cercador per hash de transacció - Actualització automàtica cada 30 segons - Punts: 15
Exercici 2: Verificador de Signatures - DApp per verificar signatures de missatges - Funcionalitats: - Introduir missatge original - Introduir signatura - Mostrar adreça que va signar - Verificar si coincideix amb adreça esperada - Punts: 15
Exercici 3: Faucet de Test Tokens - Interfície per a un faucet de tokens - Funcionalitats: - Connectar wallet - Demanar tokens (màxim 100 per dia) - Mostrar saldo actual - Historial de reclamacions - Punts: 20
Nivell Intermedi¶
Exercici 4: DApp de Staking - Interfície per al token amb staking de la UD2 - Funcionalitats: - Dipositar tokens per staking - Retirar tokens - Veure recompenses acumulades - Claim de recompenses - Temps en staking - Punts: 30
Exercici 5: Multi-Sig Wallet UI - Interfície per al wallet multi-signature - Funcionalitats: - Veure propietaris - Crear nova transacció - Confirmar transaccions pendents - Executar transaccions confirmades - Historial de transaccions - Punts: 35
Exercici 6: DAO Dashboard - Dashboard per a una DAO - Funcionalitats: - Veure propostes actives - Votar en propostes - Crear noves propostes - Veure resultats de votacions - Executar propostes aprovades - Punts: 35
Nivell Avançat¶
Exercici 7: DEX Interface (Uniswap-like) - Interfície per a exchange descentralitzat - Funcionalitats: - Swap de tokens - Seleccionar parells - Veure preus en temps real - Afegir liquiditat - Retirar liquiditat - Veure les meves posicions LP - Punts: 50
Exercici 8: NFT Marketplace Complet - Marketplace amb totes les funcionalitats - Funcionalitats: - Mint de NFTs - Listar per venda fixa - Subhastes - Fer ofertes - Acceptar ofertes - Historial de vendes - Royalties automàtics - Filtratge i cerca - Punts: 55
Exercici 9: Lending Platform UI - Interfície per a protocol de préstecs - Funcionalitats: - Dipositar col·lateral - Sol·licitar préstec - Veure health factor - Repagar préstec - Liquidar posicions - Dashboard de posicions - Punts: 50
Exercicis de Integració¶
Exercici 10: DApp Completa amb Backend - DApp amb backend propi - Funcionalitats: - Frontend React - Backend Node.js/Express - Base de dades per indexació - Autenticació amb wallet signature - API REST per consultes - WebSockets per actualitzacions - Punts: 60
Exercici 11: DApp Cross-Chain - DApp que funciona en múltiples xarxes - Funcionalitats: - Suport per Ethereum, Polygon, BSC - Canviar de xarxa fàcilment - Bridge de tokens simulat - Mostrar saldos a totes les xarxes - Punts: 55
Exercici 12: DApp amb The Graph - DApp utilitzant The Graph per indexació - Funcionalitats: - Configurar subgraph - Consultes GraphQL - Mostrar dades indexades - Actualitzacions en temps real - Punts: 50
Projecte Final Integrador¶
Exercici 13: Plataforma DeFi Completa - Combinar múltiples funcionalitats DeFi - Requisits: - Token ERC20 propi - Staking amb recompenses - Lending/borrowing - Dashboard complet - Tests exhaustius - Desplegament a Sepolia - Documentació completa - Presentació del projecte - Punts: 100
Rúbrica del Projecte Final:
| Criteri | Ponderació | Descripció |
|---|---|---|
| Funcionalitat | 30% | Totes les funcionalitats implementades i funcionant |
| Codi Net | 20% | Codi ben estructurat, comentat, seguint millors pràctiques |
| Seguretat | 20% | Sense vulnerabilitats, validacions completes |
| UX/UI | 15% | Interfície intuïtiva, responsive, accessible |
| Tests | 10% | Cobertura de tests >80% |
| Documentació | 5% | README complet, instruccions clares |
RECURSOS ADDICIONALS¶
Eines i Biblioteques¶
-
Desenvolupament: - Ethers.js: https://docs.ethers.org - Web3.js: https://web3js.readthedocs.io - Wagmi: https://wagmi.sh (React hooks) - RainbowKit: https://www.rainbowkit.com (Wallet UI)
-
Wallets: - MetaMask: https://metamask.io - WalletConnect: https://walletconnect.com - Coinbase Wallet: https://www.coinbase.com/wallet
-
Testnets: - Sepolia Faucet: https://sepoliafaucet.com - Alchemy Faucet: https://www.alchemy.com/faucets - Chainlist: https://chainlist.org (afegir xarxes)
-
Indexació: - The Graph: https://thegraph.com - Moralis: https://moralis.io - Alchemy API: https://www.alchemy.com
-
Oracles: - Chainlink: https://chain.link - API3: https://api3.org - Band Protocol: https://bandprotocol.com
Bones Pràctiques Finals¶
Sempre abans de producció:
- ✅ Tests exhaustius (unitaris, integració, E2E)
- ✅ Auditoria de seguretat
- ✅ Desplegament a testnet amb testing complet
- ✅ Monitorització i alertes
- ✅ Pla de resposta a incidents
- ✅ Documentació per a usuaris i desenvolupadors
- ✅ UX optimitzat per a usuaris no tècnics
- ✅ Accessibilitat (WCAG)
