Hasta ahora hemos visto cómo montar un entorno de trabajo, cuáles serían los entornos de implementación e incluso como crear un primer Smart Contract e integrarlo en una red local, en nuestra propia máquina.
Ahora vamos a salir un poco del mundo Solidity y vamos a ver cómo podríamos explotar el Smart Contract desde nuestro core en Node.js. ¿Y por qué desde Node.js?
- Porque casi todos los ejemplos están basados en hacerlo desde la web en formato stateless y usando metamask. Y nosotros intentamos hacer las cosas siempre al revés 😉
- Porque las apps web con Metamask no son user friendly y creemos que una aplicación tradicional consumiendo desde el backend el Smart Contract amplia las posibilidades.
- Puedes ver más opciones y posibles uniones de arquitecturas en este artículo de Santiago Palladino, de Zeppelin, como no!
El objetivo de este post no es explicar grandes estructuras de Node.js, así que haremos un sistema básico, montando todo desde el index, que nos permita consumir la clase que será la que tendrá toda la lógica de conexión. Como siempre, el código lo tenéis en Github
Lo primero, montar el entorno básico:
1 2 3 4 5 6 7 8 9 |
npm init npm install debug npm install bip39 ethereumjs-wallet web3-provider-engine web3 truffle-hdwallet-provider truffle-contract npm install --save-dev nodemon ( export PKG=eslint-config-airbnb; npm info "[email protected]" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "[email protected]" ) |
Y dejaríamos el package.json de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
{ "name": "solidity-nodejs-connector", "version": "1.0.0", "description": "And example of how to connect a NodeJS project with a Smart Contract", "main": "src/index.js", "scripts": { "start": "node src", "start:dev": "export DEBUG=kubide* && nodemon", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/Kubide/solidity-nodejs-connector.git" }, "keywords": [ "solidity", "node", "smartcontract" ], "author": "Kubide https://kubide.es/", "license": "MIT", "bugs": { "url": "https://github.com/Kubide/solidity-nodejs-connector/issues" }, "homepage": "https://github.com/Kubide/solidity-nodejs-connector#readme", "devDependencies": { "eslint": "^4.16.0", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.8.0", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-react": "^7.6.1", "nodemon": "^1.14.11" }, "dependencies": { "bip39": "^2.5.0", "debug": "^3.1.0", "ethereumjs-wallet": "^0.6.0", "truffle-contract": "^3.0.2", "truffle-hdwallet-provider": "0.0.3", "web3": "^1.0.0-beta.29", "web3-provider-engine": "^13.6.0" } } |
Notas:
- Iremos viendo la utilidad de todos los paquetes que hemos instalado más adelante.
- Instalamos nodemon para hacer las pruebas más fáciles.
- Al igual que en el proyecto de Solidity, incorporamos el linter. No hay mejor forma de hacer bien el trabajo que si nos fuerzan.
- Por último, recordar que este proyecto será muy básico y sólo usaremos una clase para ver los procesos, no valdría para producción.
Ahora, lo primero que haremos es traernos el documento ABI que ha generado Truffle por nosotros cuando hicimos el compile/migrate. Este documento tiene toda la información relevante del Smart Contract, y lo necesitaremos para, entre otras cosas, saber dónde está almacenado (cuál es su address). Los copiaremos todos al directorio /contracts/
OJO esto debéis usar vuestros propios contratos ABI ya que cada uno genera una huella distinta. Recordar también que, cada vez que cerréis vuestra blockchain local tendréis que volver a compilar y migrar los contratos y pasarlos de nuevo a este directorio.
Después de esto, toca crear una clase que nos permita encapsular todas las llamadas con el entorno de Ethereum. Llamaremos a esta clase ethConnector.js (original, verdad!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
const bip39 = require('bip39'); const hdkey = require('ethereumjs-wallet/hdkey'); const ProviderEngine = require('web3-provider-engine'); const WalletSubprovider = require('web3-provider-engine/subproviders/wallet.js'); const Web3Subprovider = require('web3-provider-engine/subproviders/web3.js'); const Web3 = require('web3'); const FilterSubprovider = require('web3-provider-engine/subproviders/filters.js'); const HDWalletProvider = require('truffle-hdwallet-provider'); const truffleContract = require('truffle-contract'); const debug = require('debug')('kubide:component:blockchain'); class EthConnector { constructor(providerURL, smartContractAbi) { this.providerUrl = providerURL; this.smartContractAbi = smartContractAbi; } async setWeb3() { // set the provider you want from Web3.providers const web3 = new Web3(new Web3.providers.HttpProvider(this.providerUrl)); return web3; } async generateMnemonic() { return bip39.generateMnemonic(); } async createWallet(mnemonic) { return hdkey.fromMasterSeed(bip39.mnemonicToSeed(mnemonic)); } async getAccountAddress(hdwallet, accountNumber = 0) { // Get the first account using the standard hd path. const walletHdpath = "m/44'/60'/0'/0/"; const wallet = hdwallet.derivePath(`${walletHdpath}${accountNumber}`).getWallet(); const address = `0x${wallet.getAddress().toString('hex')}`; return address; } async loggedAddress(mnemonic, address = 0) { const provider = new HDWalletProvider(mnemonic, this.providerUrl, address); return provider; } async truffleContract(owner) { const MyContract = truffleContract(this.smartContractAbi); MyContract.setProvider(owner); const instance = await MyContract.deployed(); return instance; } } module.exports = EthConnector; |
Notas:
- Seteamos las variables a la hora de hacer una nueva instancia del contrato, lo que nos permite ir cambiando a cualquier entorno de trabajo sin problemas.
- setWeb3() nos permitirá conectarnos a la red blockchain que le hemos indicado mediante el API de Web3.js
- generateMnemonic() te permite crear una nueva serie de nombres de forma aleatoria. En nuestro caso lo usamos, pero por simplificar lo cambiamos después por las mnemotécnicas de ganache, para facilitar el proceso.
- createWallet() cada mnemotécnico genera infinitas address dentro de un wallet. Con este método podremos crear el wallet con todas las direcciones.
- getAccountAddress() nos dará un address en particular del wallet que hemos generado, podríamos pedir el address 1.234.432 sin ningún problema.
- loggedAddress() con este método podremos validar para la red blockchain que has indicado y para la address que indiques. A partir de aquí podremos validar las TX con este address.
- truffleContract() este método nos permitirá trabajar con el Smart Contract pero firmando todas las peticiones con el loggedAddress
Y por último el index.js que usaremos para realizar las pruebas (a modo de test)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
const debug = require('debug')('kubide:index'); const EthConnector = require('./ethConnector.js'); const smartContractAbi = require('../contracts/OurToken.json'); // const infuraApikey = 'your-key'; // const providerUrl = `https://ropsten.infura.io/${infuraApikey}`; const providerUrl = 'http://localhost:7545'; const ganacheMnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; const a = async () => { const ethConnector = new EthConnector(providerUrl, smartContractAbi); /** First: Set environment */ const web3 = await ethConnector.setWeb3(); // debug('web3', web3); let mnemonic = await ethConnector.generateMnemonic(); // We use the Ganache mnemonic mnemonic = ganacheMnemonic; debug('The mnemonic', mnemonic); const wallet = await ethConnector.createWallet(mnemonic); debug('Created wallet', wallet); const ownerAddress = await ethConnector.getAccountAddress(wallet); debug('Get Owner account address', ownerAddress); const ourTokenOwner = await ethConnector.loggedAddress(mnemonic, 0); debug('The owner of the Smart Contract', ourTokenOwner); const ourTokenDeployed = await ethConnector.truffleContract(ourTokenOwner); // debug('the the Smart Contract', ourTokenDeployed); let totalBalance = await ourTokenDeployed.totalSupply.call(); debug('The total balance is', totalBalance); /** First: Set environment */ /** Second: tests */ // Set address const alice = await ethConnector.getAccountAddress(wallet, 1); debug('Get Alice account address ', alice); const bob = await ethConnector.getAccountAddress(wallet, 2); debug('Get Alice account address ', bob); const carol = await ethConnector.getAccountAddress(wallet, 3); debug('Get Alice account address ', carol); // Transfer from the Owner to Alice let cash = 1000; let balance = await ourTokenDeployed.balanceOf.call(ownerAddress); debug('The current owner\'s balance', balance); let tx = await ourTokenDeployed.transfer(alice, cash, { from: ownerAddress }); // debug('The tx', tx); balance = await ourTokenDeployed.balanceOf.call(ownerAddress); debug('The new owner\'s balance', balance); balance = await ourTokenDeployed.balanceOf.call(alice); debug('The current Alice\'s balance', balance); // Transfer from the Alice to Bob // First, log in Alice const aliceLogged = await ethConnector.loggedAddress(mnemonic, 1); const aliceTokenAccess = await ethConnector.truffleContract(aliceLogged); cash = 50; tx = await aliceTokenAccess.transfer(bob, cash, { from: alice }); // debug('The tx', tx); balance = await ourTokenDeployed.balanceOf.call(alice); debug('The new Alice\'s balance', balance); balance = await ourTokenDeployed.balanceOf.call(bob); debug('The current Bob\'s balance', balance); // Mint to Carol const quantityToAdd = 12002; tx = await ourTokenDeployed.mint(carol, quantityToAdd, { from: ownerAddress }); // debug('The tx', tx); totalBalance = await ourTokenDeployed.totalSupply.call(); debug('The total balance is', totalBalance); balance = await ourTokenDeployed.balanceOf.call(carol); debug('The current Carol\'s balance', balance); /** Second: tests */ return true; }; a(); |
Notas:
- En la primera parte, entre bloques “First: Set environment” seteamos los parámetros y comprobamos que todo funciona correctamente.
- En el siguiente bloque “Second: tests” hemos repetido los test que hicimos en el post anterior pero directamente desde node.
- Hay que tener en cuenta los cambios de contexto para indicar que las transferencias se hacen desde los “usuarios logeados”
- Como supongo que haréis varias pruebas, hemos decidido no comprobar los valores finales, simplemente dejar los debugs como guía
Ahora podréis probarlo ejecutando
1 |
npm run start:dev |
Pero OJO! arrancar primero Ganache, mover vuestros propios ABI para que pueda funcionar y hacer las llamadas oportunas!
Más al respecto:
- Trabajando con Smart Contracts en Ethereum – Parte I – Intro – Entornos de Implementación
- Trabajando con Smart Contracts en Ethereum – Parte II – Crear el entorno de trabajo local
- Trabajando con Smart Contracts en Ethereum – Parte III – Crear un Smart Contract, hacer tests y migrarlo a una red local
- Trabajando con Smart Contracts en Ethereum – Parte IV – Cómo integrar un Smart Contract con Node.js
- Si queréis estar informados y preguntar sobre desarrollo en blockchain, podéis visitar el canal de Telegram https://t.me/blockchainDevs
Foto cortesía de La Huella artworks.
© Todos los derechos reservados.
2 comments