Introducción
Qué es una DApp
Una DApp es un tipo de aplicación de Internet cuyo backend se ejecuta en una red peer-to-peer y, además, es open source. Ningún nodo de la red tiene control completo sobre el funcionamiento de la aplicación.
Dependiendo de la funcionalidad de la DApp, se pueden usar múltiples estructuras para almacenar los datos. Bitcoin es una DApp que usa una Blockchain para almacenar los datos.
Los nodos de la red pueden ser cualquier sistema conectado a Internet, por lo que detectar y prevenir quien distribuye información inválida es una tarea complicada.
Protocolos de concenso
Como no tenemos un nodo central que pueda decidir cuáles son los datos válidos o inválidos, necesitaremos un protocolo que permita a la red decidir. Actualmente existen una serie de protocolos de conceso que intentan solucionar este problema. Uno de los protocolos de conceso más populares es el Proof of Work.
Clientes de la DApp
Para usar una DApp, necesitamos un nodo conectado a la red descentralizada para poder intercambiar información con los otros nodos. Este nodo puede actuar como pasarla para interactuar con la red y proporcionar una API que otras aplicaciones consuman.
Los clientes de las DApps deben ser open source y su código debe ser público, sino toda la idea de la descentralización se vendría abajo. |
Como desplegar un nodo puede ser una tarea complicada y no apta para usuarios no técnicos, podemos encontrar casos en los que se despliega un nodo que da servicio a múltiple clientes para facilitarles el acceso a la DApp.
Aplicación descentralizada != Aplicación distribuida |
Ventajas y desventajas de una DApp
Ventajas
-
Son tolerantes a fallos ya que no existe un punto único de fallo porque las aplicaciones descentralizadas son aplicaciones distribuidas.
-
Evitan la censura, ya que no existe una autoridad central a la que un gobierno pueda hacer presión para que la ejerza.
-
No pueden ser bloqueadas, ya que no dependen de una única dirección IP. La única opción sería intentar localizar todos los nodos de la red y desconectarlos uno por uno, algo bastante complicado si la red es lo suficientemente grande y se encuentra lo suficientemente distribuida.
-
Ya que no existe una autoridad que controle la red, es más fácil depositar la confianza en ella.
Inconvenientes
-
La corrección de bugs o la actualización son complicadas ya que todos los nodos de la red deben actualizar su software.
-
Algunas aplicaciones requieren que el usuario verifique su identidad y no existe una autoridad central que pueda realizar la verificación.
-
Son más complicadas de desarrollar debido a los protocolos de consenso y a que deben estar muy bien diseñadas para que escalen desde el primer momento.
-
No pueden depender de una aplicación de terceros si dicha aplicación es centralizada, ya que se perdería la idea de la descentralización. Por ejemplo, no pueden depende de una API. Aún así, pueden depende de otras DApps.
Autenticación en las DApps
A veces es necesario la funcionalidad de las cuentas de usuarios en una aplicación. Los datos asociados a un usuario sólamente debería poder ser modificados por éste. Las DApps no pueden funcionar mediante un sistema de usuario y contraseña tradicional como hacen las aplicaciones centralizadas.
Existen diferentes formas de implementar cuentas de usuarios en DApps, aunque la más utilizada es usando un sistema de clave pública-privada. Un hash de la clave pública podría ser el ID de la cuenta de usuario. Para realizar un cambio en unos datos asociados a una cuenta de usuario, la instrucción de cambiar los datos debe firmarse utilizando la clave privada del usuario.
Si un usuario pierde su clave privada, pierde para siempre el acceso a su cuenta. |
Oráculos
Una DApp no debería depender de una aplicación centralizada ya que supone un punto único de fallo, pero en algunos casos no queda otra opción si queremos obtener datos como precios, información sobre el tiempo o números aleatorios.
Para acceder a estos datos necesitaríamos lo que se conoce como un oráculo, que es el encargado de proveer estos datos. El oráculo actúa como un intermediario de confianza. Un oráculo puede obtener los datos desde varias fuentes y realizar comprobaciones para validar el dato.
Divisa interna
Para que una aplicación sea sostenible debe ser viable económicamente para que el propietario pueda seguir desarrollándola. Las DApps no tienen un propietario, sin embargo, los nodos que corren el software necesitan hardware y otros recursos para poder funcionar, esto quiere decir que estos nodos deben obtener algún beneficio por el hecho de estar ejecutando el software. Aquí es donde entra en juego las divisas internas.
La mayoría de las DApps cuentan con una divisa interna y es el protocolo de consenso quien decide cuánto se lleva cada nodo. Dependiendo del protocolo de concenso puede ser que sólo un tipo determinado de nodo consiga esta divisa.
Lo siguiente que podríamos preguntarnos es: ¿Para qué nos puede servir esta divisa? Según nos dice la economía, cualquier cosa que tenga una demanda tiene un valor asociado. Si los usuarios deben pagar por usar la DApp, la demanda de la divisa crecerá y el valor de ésta también. Si la cantidad de divisa que se genera está limitada entonces su valor aumentará por la falta de ésta.
La única desventaja de este sistema es que las DApps no son gratuitas. Aquí las aplicaciones centralizadas tienen la ventaja de poder ofrecer un servicio gratis a cambio de publicidad y servicios premium. |
DApps con autorización
Hasta hora hemos estado hablando de DApps que son abiertas, es decir, que pueden ser usadas por cualquiera. Por otra parte, existen DApps que necesitan autorización para ser usadas, son conocidas como Permissioned DApps.
Estas DApps tienen todas las características de las DApps abiertas, pero para entrar en la red se requiere autorización. Las DApps con autorización no usan los mismos protocolos de concenso que las DApps abiertas y no tienen una divisa interna.
Algunas aplicaciones descentralizadas
Blockchain
Una Blockchain es una base de datos distribuida, formada por cadenas de bloques diseñadas para evitar su modificación una vez que un dato ha sido publicado. Se usa un sellado de tiempo confiable y cada bloque que se añade se enlaza al bloque anterior.
Es adecuada para almacenar de forma creciente datos ordenados en el tiempo y sin posibilidad de modificación ni revisión.
Un bloque contiene información referente a los datos que se han almacenado durante un periodo y un número aleatorio llamado nonce. El bloque es minado y añadido a la cadena cuando se encuentra un hash del mismo que cumple unas condiciones impuestas.
El proceso de minado se basa en variar el valor del nonce hasta que el hash del bloque cumpla la condición impuesta. Un minero que encuentra el hash de un bloque es recompensado por la red por hacer el trabajo.
Cada bloque que se añade lleva el hash del bloque anterior. Este sistema provoca que un mínimo cambio que se produzca en un bloque provocaría que ese bloque y todos los que vienen a continuación queden inmediatamente invalidados, pues los hashes de los bloques siguientes serían incorrectos. Esta comprobación es muy sencilla de efectuar para un ordenador, por lo que es muy fácil verificar la integridad de la cadena de bloques.
Los nodos de una DApp pueden usar una cadena de bloques para almacenar los datos y compartirlo con el resto de los nodos. Cada nodo debe verificar que la cadena de bloques recibida sea correcta para aceptarla.
La clave de este sistema es que es muy difícil generar una cadena de bloques y muy fácil comprobar que es correcta. Esto hace que cualquier nodo pueda verificar fácilmente la integridad y que un atacante lo tenga muy complicado para generar una cadena de bloques falsa. |
Ethereum
Ethereum es una plataforma descentralizada (al igual que Bitcoin), pero su objetivo en lugar de crear un libro de cuentas es servir como Backend para otras DApps. La plataforma permite la ejecución de programas conocidos como Smart Contracts que se programan usando el lenguaje de programación Solidity.
Ethereum usa una Blockchain para almacenar los datos de los Smart Contracts y utiliza proof-of-work como protocolo de concenso.
Los Smart Contracts tienen las siguientes propiedades:
-
Se encuentran en funcionamiento prácticamente el 100% del tiempo, ya que para que dejasen de funcionar debería pararse completamente toda la red de Ethereum.
-
No hay censura posible, ya que no pueden ser detenidos.
-
Su comportamiento está definido en código, por lo que un contrato no puede incumplirse (lo que no quiere decir que no puedan tener bugs).
Solidity es el lenguaje de programación oficial, pero existen otros lenguajes. |
Ethereum cuenta con una divisa interna llamada Ether que permite la ejecución de estos Smart Contracts. La ejecución se ha de pagar con Ethers como si éstos fuesen la gasolina que alimenta a los Smart Contracts. La cantidad de Ether que se usa para la ejecución de los Smart Contracts se conoce como gas. Este gas va destinado a los mineros que ejecutan el código.
Al igual que en POO creamos instancias de una clase, en Ethereum creamos instancias de un Smart Contract. El proceso de crear una instancia y almacenarla en la Blockchain se conoce como desplegar un Smart Contract y también cuesta gas, puesto que estamos almacenando código en la Blockchain. Los Smart Contracts tienen métodos al igual que las clases y podemos llamarlos para ejecutar su código.
No todas las llamadas a métodos de los Smart Contracts requieren gas. Por ejemplo, cualquier operación de sólo lectura no necesita modificar ningún valor en la Blockhain, por lo que es gratis e instantáneo. |
Cuando se despliega un Smart Contract, a éste se le asigna una dirección al igual que una instancia de una clase tiene una dirección en memoria RAM. Para interactuar con un contrato desplegado necesitamos conocer su dirección y su ABI.
La ABI de un Smart Contract es la información de cómo se debe llamar a un método, esto es la ID del método, los parámetros de entrada y el valor de retorno. En Ethereum es posible enviar datos (Payload) en una transacción, lo que permite (conociendo la ABI del Smart Contract), llamar a sus métodos.
Además de las direcciones de los Smart Contracts existen las direcciones de usuarios. La dirección de un usuario no contiene código que pueda ser ejecutado. Tanto una dirección de un usuario como de un Smart Contract tiene un balance de Ethers que pueden enviarse y recibirse.
No confundir el Ether enviado en una transacción con el gas que cuesta realizar la transacción. Por ejemplo, para llamar a un método de un contrato no hace falta enviar ethers (el valor de la transacción es 0), pero habría que pagar el gas correspondiente a su ejecución. |
Cuentas de usuario
A diferencia de los Smart Contracts, las direcciones de las cuentas de usuario tienen asociada una clave pública y una clave privada. Un usuario que quiera realizar una transacción generará una clave privada y a partir de ésta obtendrá su clave pública correspondiente. La dirección de la cuenta de usuario se obtiene a partir de un hash de la clave pública.
Gas
El gas es la unidad de medida de la computación requerida para cada instrucción en la EVM. Al realizar una transacción es necesario especificar la cantidad máxima de gas que estamos dispuestos a pagar. Si la cantidad de gas consumida es menor o igual que este límite, la transacción será minada correctamente, en caso contrario sería abortada y sus cambios revertidos.
Aunque una transacción sea abortada por haber llegado al límite de gas especificado, el minero se llevará su parte ya que la computación se ha llevado a cabo y han consumido recursos. |
Otro valor a especificar en una transacción es la cantidad de ethers que queremos pagar por cada unidad de gas. Cuánto más alto sea el precio, más probable será que un minero acepte la transacción y sea minada antes. Transacciones con precios bajos tardarán más en ser minadas o incluso pueden no ser minadas.
Transacciones
Una transacción es un dato que una dirección envía a otra y ha sido firmado por la fuente. En Ethereum el algoritmo de firmado que se usa es el ECDSA (Elliptic Curve Digital Signature Algorithm) que es un algoritmo basado en curvas elípticas. Una trasacción contiene los siguientes datos:
-
Destinatario: Dirección del destinatario de la transacción. Puede ser una cuenta de usuario o un Smart Contract.
-
Valor: Cantidad de ethers que se envían en la transacción. Puede ser 0.
-
Límite de gas: Si la transacción require una catidad de gas superior al límite indicado sería abortada. Cuánto más compleja sea la ejecución de un método, más gas consumirá su llamada.
-
Precio del gas: Cantidad de ethers a pagar por cada unidad de gas. Las transacciones mejor pagadas serán minadas antes, ya que los mineros ganarían más con ellas.
-
Nonce: Contador que se incrementa para evitar posibles ataques de replay.
-
Payload: Datos que podemos enviar en la transacción. También influyen en el gas total a pagar.
-
v
,r
ys
: Parámetros que definen la firma digital. A partir de ellos se puede obtener la clave pública y por lo tanto la dirección de la cuenta (por eso la dirección de origen no aparece en la transacción).
Ethereum Virtual Machine (EVM)
EVM es el entorno donde se ejecuta el byte-code de Ethereum. Cada nodo de la red ejecuta una instancia de la EVM. Todos los nodos ejecutan todas las transacciones que se realizan a un Smart Contract utilizando la EVM, por lo que todos los nodos ejecutan exactamente los mismos cálculos y almacenan los mismos valores.
Las transacciones que no ejecutan código de un Smart Contract también requiren ser procesadas para comprobar que el balance de la cuenta ordenante es suficiente. |
Esta redundancia es útil ya que permite a cualquier nodo almacenar los resultados de las ejecuciones de los Smart Contracts para así tener el valor y almacenarlo.
Cada transacción requiere capacidad de computación y almacenamiento por lo que estas transacciones deben de tener un coste, de lo contrario cualquiera podría inundar la red. Además, si las transacciones fuesen gratis, los mineros no tendrían incentivo para incluir las transacciones en los bloques y minarían bloques vacíos. Como cada transacción require una cantidad diferente de computación también consumirar una cantidad diferente de gas.
Forks
Los forks son situaciones que ocurren en una blockchain cuando se produce un conflicto entre diferentes nodos sobre cuál es la blockchain legítima. Cuando se produce un fork parte de los mineros minarán una blockchain y otra parte minará otra. Existen tres tipos de forks:
-
Forks ordinarios: Ocurren de forma natural el la blockchain cuando dos mineros minan un bloque casi al mismo tiempo. En el caso de Ethereum existe un sistema para tratar estos bloques conocidos como uncles.
-
Soft forks: Ocurre cuando se producen cambios en el código fuente que requiere que más del 50% de la capacidad computacional de la red los apoyen. Por ejemplo, un soft fork se produce cuando se modifica el código para que anule una serie de bloques o transacciones. Basta con que la mayoría de los mineros apliquen el cambio para que la otra parte los acepte.
-
Hard fork: Ocurre cuando se producen cambios en el código fuente que requiere que toda la red los acepten. Un hard fork se produce cuando se cambia la estructura del bloque o se cambia la recompensa que se ofrece a los mineros.
Smart Contracts
Geth
Antes de comenzar el desarrollo debemos ejecutar un nodo de Ethereum que nos permita la comunicación con la red y desplegar contratos. Actualmente existen varios clientes de Ethereum que se pueden utilizar:
-
geth: Es el cliente oficial desarrollado por la fundación Ethereum. Está desarrollado con Go. Es el que usaremos en este workshop.
-
parity: Es el segundo cliente más usado. Desarrollado con Rust por Gavin Wood, uno de los fundadores de Ethereum. Es quizá el cliente más completo, ya que es también un navegador de DApps.
-
cpp-ethereum: La tercera implementación más usada, escrita en C++.
Existen otras implementaciones menos usadas en multitud de lenguajes de programación como JavaScript, Java, Python, Haskell, etc.
Instalación
Tenemos varias opciones para instalar Geth. Puesto que está escrito en Go basta con descargarnos el binario adecuado para nuestra plataforma. También podemos usar Docker para ejecutarlo. Otra opción sería compilarlo desde las fuentes.
Podemos ejecutar Geth usando Docker con el siguiente comando:
docker run -it -p 30303:30303 ethereum/client-go --syncmode=fast
Geth usará los llamados bootnodes
para descubrir nodos en la red. Estos nodos
son especiales ya que sólo se encargan de compartir otros nodos y no de
almacenar la cadena. Al poco de iniciar comenzará a descargar la blockchain de
Ethereum al completo, lo cual es un proceso largo y requiere varios gigabytes
disponibles para almacenar toda la cadena. Además, desplegar un contrato e
interactuar con ellos requiere disponer con una cuenta con ethers para poder
pagar a los mineros.
Usando el parámetro --syncmode=fast no se computan los estados de la
blockchain sino que ya se descargan computados, lo que hace que sea más rápida
la sincronización.
|
Afortunadamente existen varias redes de Ethereum actualmente además de la red principal (main net). Estas redes alternativas conocidas como test nets nos permiten desarrollar sin tener que pagar dinero real. Las tres test nets más conocidas son:
-
Ropsten
-
Kovan
-
Rinkeby
Para conectarnos a una red testing tenemos que indicarle a geth otros
bootnodes
manualmente:
docker run --rm -it ethereum/client-go --testnet --syncmode=fast --bootnodes "enode://20c9ad97c081d63397d7b685a412227a40e23c8bdc6688c6f37e97cfbc22d2b4d1db1510d8f61e6a8866ad7f0e17c02b14182d37ea7c3c8b9c2683aeb6b733a1@52.169.14.227:30303,enode://6ce05930c72abc632c58e2e4324f7c7ea478cec0ed4fa2528982cf34483094e9cbc9216e7aa349691242576d552a2a56aaeae426c5303ded677ce455ba1acd9d@13.84.180.240:30303"
Puppeth
También existe la posibilidad de crear nuestra propia red privada de Ethereum. Puppeth es una utilidad que nos ayuda a crear nuestras propias redes privadas.
Lo primero es generar el bloque génesis de la red, que es
donde se define un estado inicial de la red, por ejemplo, pueden existir
cuentas con ethers desde el primer momento. Puppeth es un CLI que podemos
encontrar en las imágenes de docker etiquetadas como alltools
.
Para crear una red privada puede no ser conveniente usar proof-of-work como protocolo de concenso ya que consume muchos recursos. Como alternativa tenemos el proof-of-authority que se basa en que sólo unas direcciones especificadas en el bloque génesis tienen permisos para crear bloques. |
El primer paso es generar una dirección de la siguiente forma:
mkdir workshop; cd workshop
docker run --rm -it -v $(pwd):/data ethereum/client-go:alltools-latest geth --datadir /data account new
Una vez introducida la contraseña, nos generará la dirección que se mostrará por pantalla. Ahora ejecutaremos puppeth:
Como se puede ver, hemos creado la cuenta antes de inicializar la blockchain. Esto es porque una cuenta no es más que un par de claves pública y privada, por lo que a una dirección le corresponde una clave privada independientemente de la blockchain. No es recomendable usar en la blockchain principal direcciones que ya hayamos usado en redes privadas o de testing. |
docker run --rm -it -v $(pwd):/data ethereum/client-go:alltools-latest puppeth
Puppeth nos pedirá un nombre para nuestra red privada, podemos poner cualquier
cosa, por ejemplo "workshop". A continuación mostrará un menú donde tenemos
que elegir la opción 2. Configure new genesis
. Seguiremos los
siguientes pasos:
-
Which consensus engine to use?:
2. Clique - proof-of-authority
-
How many seconds should blocks take?:
2
-
Which accounts are allowed to seal?: Aquí introducimos la dirección que generamos antes.
-
Which accounts should be pre-funded?: Aquí también introducimos la dirección que generamos antes.
-
Specify your chain/network ID if you want an explicit one:
random
-
Anything fun to embed into the genesis block?: ``
Una vez generado, volveremos al menú y seleccionaremos la opción
2. Manage existing genesis
. Y luego 2. Export genesis configuration
. En
el nombre del fichero introduciremos: /data/workshop.json
y ya tendremos
nuestro bloque génesis generado.
docker run --rm -it -v $(pwd):/data ethereum/client-go:alltools-latest geth --datadir /data init /data/workshop.json
Ahora podemos iniciar un nodo en nuestra red privada:
docker run --rm -it -v $(pwd):/data -p 8545:8545 ethereum/client-go:alltools-latest geth --networkid 39475 --rpc --rpcaddr 0.0.0.0 --datadir /data console
El networkid si lo generamos aleatoriamente podemos encontrarlo en el
fichero workshop.json .
|
Una vez veamos la consola, procedemos a desbloquear la cuenta autorizada y comenzar a minar:
personal.unlockAccount("0x39091b5f009f21b0dead7320d9487842b0655e12", "password", 0)
miner.start()
Ganache
Crear una red privada es un proceso relativamente complejo, por ello existe una utilidad que nos permite simular una red privada sólo para tests y depuración de contratos: Ganache.
Esta herramienta se comporta como un cliente de Ethereum (como geth
) ofreciendo
la misma API, pero funciona de forma más rápida ya que no hay que esperar los
15 segundos, los bloques se minan instantáneamente y, por supuesto, sin
ningún coste. Su funcinamiento es local y no depende de una conexión a internet.
Anteriormente, ganache era conocida como testrpc .
|
Para instalar ganache
ejecutamos el siguiente comando:
npm install -g ganache-cli
Para iniciar la simulación de una red ejecutamos el siguiente comando:
ganache-cli
También podemos pasarle una clave privada y una cantidad de ethers para que
ganache
genere la cuenta automáticamente:
ganache-cli --account="0x54f773ff404a530d7426daf071c098617eb34d54dbe1a198354049125243948a,10000000000000000000"
|
Infura
Además de desplegar nuestro propio nodo, existen nodos públicos a los que podemos conectarnos. Los nodos públicos están fuera de nuestro control, aunque muchas veces son convenientes porque nos ahorra tener un equipo en constante sincronización.
Infura provee de una serie de nodos públicos a los que podemos conectarnos. Cuando usamos Metamask, realmente estamos usando un nodo de Infura, aunque podemos configurar Metamask para que use el nodo que queramos.
Cuando usamos un nodo, podemos tener nuestras cuentas (claves privadas) almacenadas en el propio nodo. En el caso de los nodos públicos, esta funcionalidad está desactivada, puesto que no tendría sentido que los usuarios de los nodos públicos almacenasen su clave privada en ellos.
A la hora de firmar transacciones debemos hacerlo de forma local y, una vez firmadas, podemos usar el nodo público para enviarla a la red.
Hay que tener muchísimo cuidado si tenemos un nodo funcionando, puesto que este nodo no debería exponer nunca el puerto donde se encuentra escuchando la API. Si algún atacante tiene acceso a nuestro nodo puede intentar robarnos ether en el momento de desbloquear la cuenta. |
Truffle
Configuración
Truffle es un framework que nos permite automatizar ciertas tareas de desarrollo. Para crear un nuevo proyecto con Truffle ejecutamos lo siguiente:
mkdir -p workshop/truffle
cd workshop/truffle
truffle init
Tendremos la siguiente estructura dentro de nuestro directorio workshop
:
-
build: Contiene la ABI de los contratos. La ABI se almacena en un fichero con formato JSON. Usaremos la ABI para interactuar con nuestro contrato.
-
contracts: Contiene el código fuente de los contratos que vamos a desarrollar.
-
migrations: Contiene las migraciones. Las migraciones son scripts que se ejecutan cuando actualizamos nuestro contratos.
-
test: Contiene los tests para nuestros contratos.
-
truffle-config.js
/truffle.js
: Contiene la configuración para el despligue de los contratos. Podemos borrartruffle-config.js
.
Creamos un fichero llamado package.json
que contenga lo siguiente:
package.json
{
"name": "erc20-example",
"version": "1.0.0",
"description": "Basic ERC20 Token",
"main": "truffle.js",
"scripts": {
"test": "truffle test",
"compile": "truffle compile",
"deploy:dev": "truffle migrate --network development",
"deploy:test": "truffle migrate --network test"
},
"dependencies": {
"ethereumjs-wallet": "^0.6.0",
"truffle-wallet-provider": "0.0.5",
"web3": "^1.0.0-beta.30"
},
"devDependencies": {
"babel-eslint": "^8.2.2",
"eslint": "^4.18.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-import": "^2.9.0"
}
}
Por último, el fichero truffle.js
debe quedar de las siguiente forma:
truffle.js
const Wallet = require('ethereumjs-wallet');
const WalletProvider = require('truffle-wallet-provider');
const Web3 = require('web3');
const PRIVATE_KEY = '';
const NODE_URL = '';
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 8545,
network_id: '*'
},
test: {
provider: new WalletProvider(
Wallet.fromPrivateKey(Buffer.from(PRIVATE_KEY, 'hex')),
NODE_URL
),
gas: 4600000,
gasPrice: Web3.utils.toWei('20', 'gwei'),
network_id: '3'
}
}
};
Para instalar las dependencias ejecutamos:
npm install
Deberíamos tener la siguiente estructura de directorios:
├── build
│ └── contracts
│ └── Migrations.json
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── node_modules
├── test
├── package.json
└── truffle.js
Compilar contratos
Vamos a crear un nuevo contrato en la carpeta contracts
. Lo llamaremos
HelloWorld.sol
y tendrá el siguiente contenido:
pragma solidity ^0.4.11;
contract HelloWorld {
function greet() public pure returns (string) {
return "Mi primer contrato en Solidity";
}
}
Para compilar un contrato (generar su ABI) ejecutamos:
npm run compile
Tras compilar un contrato, encontraremos los ficheros compilados dentro del
directorio build/contracts
.
Crear migraciones
Las migraciones nos ayudan con la actualización de contratos. Al crear un
proyecto con Truffle, automáticamente se genera un Smart Contract llamado
Migrations.sol
que lleva la cuenta de los despliegues que hacemos, de forma
que no desplegará dos veces el mismo contrato. También se incluye una migración
llamada 1_initial_migration.js
que se encarga de desplegar el contrato
Migrations.sol
.
Cada vez que modificamos un contrato y queremos desplegar una nueva versión,
debemos crear una nueva migración en el directorio migrations
.
Para desplegar nuestro contrato HelloWorld.sol
debemos crear una nueva
migración en el directorio migrations
. Debe contener lo siguiente:
2_hello_world_deploy.js
var HelloWorld = artifacts.require('./HelloWorld.sol');
module.exports = function(deployer) {
deployer.deploy(HelloWorld);
};
Desplegar contratos
Desplegar un Smart Contract en nuestra red de desarrollo:
npm run deploy:dev
Para que funcione el despliegue de un Smart Contract debemos tener
ganache-cli funcionando mientras realizamos el despliegue.
|
Para desplegar en una testnet, necesitamos un nodo que esté conectado a la ella y esté sincronizado. Una forma de evitar disponer de un nodo sincronizado (que puede tardar horas) es usar Infura. Con Infura disponemos nodos públicos que nos dan acceso a diferentes redes de Ethereum, como la red principal o las testnets.
Para usar Infura con truffle, editamos el fichero truffle.js
y modificamos la
variable NODE_URL
:
truffle.js
const NODE_URL = 'https://ropsten.infura.io';
De esta forma usaremos un nodo de Infura para desplegar contratos en la red Ropsten. Para iniciar el despliegue ejecutamos:
npm run deploy:test
Tokens ERC20
ERC20 es un estándar que permite desarrollar tokens de Ethereum con una interfaz común. Esto permite que sean compatibles con multitud de aplicaciones. La interfaz se define de la siguiente forma:
erc20.sol
contract ERC20Basic {
event Transfer(address indexed from, address indexed to, uint256 value);
function totalSupply() public view returns (uint256);
function balanceOf(address who) public view returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
}
Una posible implementación de un Token ERC20 sería la siguiente:
pragma solidity ^0.4.20;
import "./ERC20Basic.sol";
contract BasicToken is ERC20Basic {
// Evento que se ejecuta cuando se realiza una compra. Se emite en el evento
// la dirección de quien ha comprado tokens y la cantidad.
event Buy(address buyer, uint256 amount);
// Nombre del token
string public name = "Colmena Token";
// Símbolo mostrado en algunas aplicaciones
string public symbol = "CTKN";
// Número de decimales que soporta el token
uint8 public decimals = 18;
// Precio en wei de cada token
uint256 public price;
// Se almacena el saldo de cada cuenta (address) de Ethereum
mapping(address => uint256) private balances;
// Tokens que existen actualmente
uint256 private totalSupply_;
/**
* Constructor del token. Se ejecuta sólamente en el momento que se
* depliega en la red.
*
* @param _price uint256 Precio del token
*/
function BasicToken(uint256 _price) public {
// Existen 0 tokens en el momento de la creación
totalSupply_ = 0;
price = _price;
}
/**
* Muestra la cantidad de tokens que existen actualmente.
*
* @return uint256 La cantidad existente de tokens
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* Muestra el saldo de una cuenta
*
* @param _owner address Cuenta de la que se comprueba su saldo
* @return uint256 Balance de la cuenta en wei
*/
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
/**
* Método para comprar tokens. Se calcula la cantidad de tokens en función
* de los ethers enviados al método.
*
* @return bool True si se ha producido correctamente la compra
*/
function buy() public payable returns (bool) {
require(msg.value > 0);
// Importante comprobar que el precio no sea cero porque provocaría
// una división por cero.
assert(price != 0);
uint256 amount = msg.value / price;
balances[msg.sender] = balances[msg.sender] + amount;
totalSupply_ = totalSupply_ + amount;
emit Buy(msg.sender, amount);
return true;
}
/**
* Método para transferir tokens a otra dirección de Ethereum. No se puede
* enviar tokens a la dirección cero.
*
* @param _to address Dirección a la que se envían token
* @param _value uint256 Cantidad de tokens a enviar
* @return bool True si se ejecuta correctamente
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender] - _value;
balances[_to] = balances[_to] + _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
Frontend
Una vez tenemos un contrato desplegado en una red, el siguiente paso es realizar una interfaz gráfica para el mismo. Se puede hacer todo tipo de interfaces, sin embargo, en esta guía veremos como hacer una interfaz web.
web3.js
web3.js se trata de una librería para JavaScript que nos permite interactuar con los contratos desplegados. Con esta librería podemos conectarnos a un nodo de Ethereum y usar la JSON-RPC.
Existen dos posibilidades para conectarnos a un nodo:
-
Conexión directa: Se realiza una conexión a un nodo del cual conocemos su dirección IP. Es recomendable permitir la elección del nodo al cual conectarse, ya que esta decisión corresponde al usuario de la aplicación y no al desarrollador.
-
Navegador compatible con web3: Podemos usar el nodo configurado en el propio navegador por el usuario. Para ello, el navegador debe ser compatible con web3, por ejemplo Mist es un navegador compatible. También podemos usar Metamask si usamos Firefox o Chrome.
Una DApp puede elegir usar uno de los dos métodos o los dos. La forma recomendada es mediante un navegador compatible con web3, ya que permite al elegir el nodo de forma global para todas las DApps.
Existen dos versiones de web3.js. La versión actual y la beta de la versión 1.0. En esta guía se usará la versión 1.0 beta. |
Creación de proyecto
Usaremos Vue.js como framework para desarrollar nuestra interfaz web. Para
crear nuestro proyecto, usaremos vue-cli
.
En esta guía no veremos cómo funciona Vue.js, aunque lo usaremos durante toda la sección de frontend. Puesto que el proyecto de ejemplo es bastante simple no es necesario tener mucho conocimiento sobre el framework. Se ha elegido Vue.js por su simplicidad y por ser fácil de aprender. |
Primero debemos instalar vue-cli
:
vue-cli
npm install -g vue-cli
Una vez instalado, entramos en la carpeta creada anteriormente e iniciamos el proyecto:
vue init webpack erc20-webui
El comando anterior pedirá una serie de parámetros que podemos rellenar de la siguiente forma:
. ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── index.html ├── package.json ├── package-lock.json ├── README.md ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ └── main.js └── static
A continuación eliminamos el fichero src/components/HelloWorld.vue
y editamos
el fichero src/App.vue
para que quede de la siguiente forma:
src/App.vue
<template>
<div id="app">
Mi primera aplicación con Vue.js
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
Para ejecutar un servidor de pruebas en local, ejecutamos el siguiente comando:
npm run dev