Um faucet é um site através do qual uma empresa pode distribuir criptomoedas a qualquer um interessado em tê-las. Embora no passado existissem faucets de moedas em produção, incluindo o próprio Bitcoin, hoje em dia faucet se tornou sinônimo de site para obter moedas de teste, para fins de desenvolvimento de smart contracts e uso das testnets.
Recentemente eu coloquei no ar um novo projeto que é um faucet de criptomoedas para ajudar meus alunos a conseguirem fundos de teste para seus desenvolvimentos. E aí muitos de vocês começaram a me perguntar como que eu fiz. Bem, neste tutorial eu vou mostrar como você pode criar o seu próprio faucet também, seja ele em teste ou produção, não importa.
Vamos lá!
#1 – Arquitetura de Faucet
Existem três tipos de arquiteturas comuns para faucets: baseada em smart contract, em backend ou híbrida.
Um faucet baseado em smart contract possuirá um contrato com o saldo a ser doado e uma função pública que permite saque de moedas. Essa função deve incluir quaisquer validações que você julgue pertinentes e que evitem que um único usuário consiga exaurir os recursos do mesmo. As vantagens deste modelo é que as taxas de transação relativas à transferência ficam por conta do usuário, mas a desvantagem é que é muito mais difícil controlar abusos, já que o único identificador que temos é o endereço da carteira, o que é muito fácil de obter várias.
Um faucet baseado em backend é uma aplicação web onde o saldo estará guardado em uma carteira e as chaves desta carteira estarão de controle do backend, que fará as transferências conforme a necessidade. Esse mesmo backend é quem estará interagindo com o front usado pelos usuários e tem como principal vantagem poder fazer inúmeras validações que se adequem às características do seu cenário, ou seja, a vantagem é segurança maior. No meu caso, por exemplo, usei um faucet de backend pois eu queria retringir o uso do meu faucet somente aos alunos com emails devidamente cadastrados, além de fazer controle por IP. A desvantagem desse modelo é que como é você quem transfere os fundos, as taxas de transferência ficam por sua conta também.
E a última abordagem, híbrida, é quando você mistura smart contracts e backend na sua solução. Neste caso você usará seu backend como oráculo de segurança, ou seja, o faucet receberá a transação do usuário vinda do frontend, vai sinalizar que o backend deve consultar essa requisição de fundos (usando eventos monitorados), o backend faz todas as validações e escreve a autorização de volta pro frontend que também deve estar monitorando eventos, fazer uma segunda requisição e providenciar o saque de fato. Como pode notar, esta abordagem tem como grande desvantagem a alta complexidade, mas garante os benefícios parciais a arquitetura de smart contract (porque você ainda vai pagar alguma taxa, embora menor que a de transferência) e totalmente os benefícios da arquitetura de backend (segurança).
Para este tutorial, vou exemplificar a criação de um faucet baseado em backend.
#2 – Setup do Projeto
Vou utilizar aqui o Node.js como tecnologia de backend. Para isso é importante que você tenha o mesmo instalado na sua máquina, como ensino no vídeo abaixo.
Depois, você vai criar uma nova pasta chamada faucet e dentro dela vai rodar o comando de inicialização de projeto Node.js.
1 2 3 |
npm init -y |
Com o projeto inicializado, rode o comando para instalar as dependências que vamos utilizar. A saber:
- DotEnv: para configuração das variáveis de ambiente;
- Ethers: para comunicação com a blockchain;
- Express: para construção do backend;
- Morgan: para logging das requisições no terminal;
1 2 3 |
npm install dotenv ethers express morgan |
Com as dependências instaladas, crie agora um arquivo .env na raiz do projeto e coloque nele as seguintes variáveis de ambiente.
1 2 3 4 5 6 7 |
PORT= WALLET= PRIVATE_KEY= RPC_NODE= TOKEN_AMOUNT= |
Cada variável deve ser preenchida adequadamente, como explicado abaixo.
- PORT: a porta onde seu backend irá rodar. Ex: 3000
- WALLET: o endereço público da sua carteira cripto que vai dar os fundos;
- PRIVATE_KEY: a chave privada da sua carteira cripto que vai dar os fundos;
- RPC_NODE: o endereço do nó de blockchain que vai atender suas requisições. Se não possui um, pode criar gratuitamente na Infura;
- TOKEN_AMOUNT: a quantidade de tokens que serão dados a cada chamada, na escala de ether. Ex: 0.01;
- INTERVAL: a quantidade mínima de milisegundos entre cada saque (para evitar abusos). Ex: 86400000 (1 dia);
Agora crie uma pasta src e dentro dela um arquivo index.js, para o nosso backend. Dentro dele coloque o seguinte código para testarmos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
require("dotenv").config(); const express = require('express'); const morgan = require('morgan'); const app = express(); app.use(morgan("tiny")); app.get("/", (req, res) => { res.json({ mesage: "Hello World" }); }) const PORT = Number(process.env.PORT) || 3000; app.listen(PORT, () => console.log("Server is listening at " + PORT)); |
Para realizar o teste, vá no seu package.json e altere a seção scripts para incluir scripts de inicialização.
1 2 3 4 5 6 |
"scripts": { "dev": "npx nodemon index", "start": "node index" }, |
E rode com qualquer um dos dois para ver no terminal a mensagem Hello World quando acessar http://localhost:3000 no navegador.
Isso indica que seu backend está pronto para ser programado como um faucet de criptomoedas, o que faremos a seguir.
#3 – Faucet via Backend
Agora que temos a estrutura inicial do projeto pronta, vamos programar nosso backend para que ele funcione como um faucet seguro. O nível de segurança necessário vai depender dos requisitos do seu projeto, então aqui vou dar exemplo baseado em tempo entre saques e controle por IP, mas pode adicionar outros como API Key, assinatura de mensagens, tokens de autenticação, etc.
Primeiro vamos criar um arquivo SecurityProvider.js dentro de src e nele vamos colocar o seguinte código, que explico a seguir.
1 2 3 4 5 6 7 8 9 10 11 12 |
const ips = {};//ip para wallet function ipCheck(clientIP, wallet) { const walletByIp = ips[clientIP]; if (walletByIp && walletByIp !== wallet) throw new Error(`IP do usuário e carteira não conferem.`); ips[clientIP] = wallet; return true; } |
Aqui temos um controle de que, dado um IP, somente permitiremos saques vindos de uma carteira específica. Isso vai evitar que usuários de um mesmo IP fiquem tentando fazer múltiplos saques usando carteiras diferentes. Depois vou te mostrar como obter o IP da request.
Agora no mesmo arquivo, vamos adicionar outro controle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const nextTry = {};//wallet para timestamp function tryCheck(wallet) { const nextTimestamp = nextTry[wallet]; if (nextTimestamp > Date.now()) throw new Error(`Tente novamente após ${new Date(nextTimestamp)}`); nextTry[wallet] = nextTimestamp + parseInt(process.env.INTERVAL); return true; } module.exports = { ipCheck, tryCheck } |
Aqui temos um controle baseado em intervalo entre saques, usando a carteira como chave. No exemplo, estamos permitindo um saque a cada 24h para um mesma carteira. Ao final do arquivo exportamos as duas funções a fim de que possam ser usadas em outro módulo mais tarde. Como mencionado anteriormente, adiciona quantas validações aqui quanto julgar necessário.
Agora vamos criar o módulo de saque, que fará a comunicação com nossa carteira. Crie um arquivo Web3Provider.js na pasta src e codifique da seguinte forma.
1 2 3 4 5 6 |
const { ethers } = require("ethers"); const provider = new ethers.JsonRpcProvider(process.env.RPC_NODE); const signer = new ethers.Wallet(`${process.env.PRIVATE_KEY}`, provider); |
Primeiramente eu carrego a biblioteca EthersJS e uso ela para inicializar a comunicação com o nó de blockchain (JsonRpcProvider) e com ele, a comunicação direta com a carteira cripto (Wallet), inicializada a partir da nossa chave privada. A seguir, vamos codificar a única função desse arquivo, a de saque.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
async function withdraw(clientWallet) { const value = ethers.parseEther(`${process.env.TOKEN_AMOUNT}`); const balance = await provider.getBalance(clientWallet); if (balance >= (value / 2n)) throw new Error(`You already have enough tokens.`); const tx = await signer.sendTransaction({ from: process.env.WALLET, to: clientWallet, value }); return tx.hash; } module.exports = { withdraw } |
Aqui eu tenho uma função de saque que espera a carteira do cliente. Nós pegamos o valor da transferência na escala correta e fazemos uma rápida verificação de saldo na carteira do cliente para ervitar abusos, já que se ele possui saldo suficiente, para que vamos dar mais à ele, não é mesmo? Tudo estando certo nós chamamos a sendTransaction passando os parâmetros adequados pra transferir fundos (value) da nossa carteira (from) para a do usuário (to). O hash da transação é retornado em caso de sucesso e ao final do arquivo nós exportamos a função para que ela possa ser chamada no index.js.
Falando no index.js, agora vamos voltar à ele para codificar a rota de saque.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const { withdraw } = require('./Web3Provider'); const { ipCheck, tryCheck } = require("./SecurityProvider"); app.post("/withdraw/:wallet", async (req, res) => { try { ipCheck(`${req.headers['x-forwarded-for'] || req.socket.remoteAddress}`, req.params.wallet); tryCheck(req.params.wallet); const tx = await withdraw(req.params.wallet); res.json({ tx, amount: process.env.TOKEN_AMOUNT }); } catch (err) { console.error(err); res.status(400).json({ message: err.message }); } }) |
Aqui nós temos uma rota POST onde o usuário informa sua carteira na URL e automaticamente pegamos o IP dele no cabeçalho da requisição. Com essas duas informações nós passamos nos checks de IP e de intervalo que programamos no SecurityProvider.js e, estando tudo certo (adicione mais validações conforme necessidade), nós chamamos a função withdraw do Web3Provider.js para realizar a transferência.
Em caso de erro, um try/catch garante que tenhamos informações adequadas e devolve um status code de Bad Request para o requisitante.
Uma vez que você coloque fundos na carteira configurada no .env você pode testar os aque utilizando ferramentas de teste HTTP como o Postman. Caso não saiba usá-lo, eu mostro no vídeo abaixo.
Claro que você irá querer ter um frontend para seu backend, certo? Neste caso recomendo aprender como criar um frontend que se comunique com backend neste tutorial abaixo.
Importante frisar que não existe nenhuma forma do faucet “criar moedas”, ele apenas fornece as moedas. Você vai ter de alimentá-lo com fundos para que ele tenha utilizadade para seus usuários. No caso de moeda própria isso é fácil de fazer já que você pode mintá-las como quiser, mas no caso de moedas de terceiros, você terá de obter esses recursos com os mantenedores da própria rede. Por exemplo na Polygon Amoy, você pode apresentar seu projeto e ganhar 100 POL a cada 90 dias para uso nele, o que inclui criar faucets. Mais informações na página do faucet da Polygon.
Espero que tenha gostado do tutorial e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.