Recentemente eu escrevi um tutorial ensinando como fazer login em dapps web3 usando a carteira de criptomoedas MetaMask. Se você não leu este tutorial, ele está disponível neste link.
No entanto, muitas pessoas me sugeriram que eu fizesse uma segunda parte, já que nem todos estão trabalhando com dapps “puros” (com backend somente na blockchain) e gostariam de ter esta funcionalidade de login integrada a um backend já existente, fazendo uma aplicação híbrida com recursos web2 e web3.
Então nesta segunda parte vamos continuar o projeto de dapp da parte 1, adicionando um backend fake com o qual iremos interagir em paralelo com a blockchain. Caso prefira, você pode assistir ao vídeo ao invés de ler o post.
Backend Fake
Ok, fizemos o login do nosso dapp. No entanto, pode ser que você queria ainda ter um backend “tradicional” adicional já que podem ter informações que você queira armazenar que não sejam transações na blockchain (até por causa dos custos) ou mesmo regras de negócio que você não quer que estejam públicas em smart contracts. Estas aplicações web que misturam blockchain e backend tradicional eu costumo chamar de dapp híbrido ou web 2.5. 😀
Independente do motivo nós vamos ter um backend fake neste projeto, pra não termos que entrar em aspectos de desenvolvimento que fogem do escopo do frontend.
Imagine que eu tenho uma API REST com endpoints para cadastro, login, logout e para pegar informações de um usuário autenticado. Vamos simular a existência deste backend criando um AppService.js dentro da pasta src do nosso projeto com o seguinte conteúdo, que explicarei logo a seguir.
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 |
export function doSignUpBackend(credentials){ return new Promise((resolve, reject) => { return resolve({ token: '<seu token>' }); }) } export function doSignInBackend(credentials) { return new Promise((resolve, reject) => { if (credentials.user === '<sua carteira>' &&credentials.secret === '<seu segredo>' ) return resolve({ token: '<seu token>' }); return reject(`401 Unauthorized`); }) } export function doSignOutBackend(token) { return new Promise((resolve, reject) => { if (token === '<seu token>') return resolve({ token: null }); return reject(`401 Unauthorized`); }) } export function getProfileBackend(token) { return new Promise((resolve, reject) => { if (token === '<seu token>') return resolve({ name: '<seu nome>' }); return reject(`401 Unauthorized`); }) } |
A função doSignUpBackend está simulando o cadastro de um novo usuário. Ela vai enviar usuário e senha dele para o servidor, onde você armazenaria estas informações em um banco de dados e devolveria um token JWT pro frontend. Com este token, o frontend vai poder realizar novas requisições ao backend sem exigir um novo login. Se você quer aprender mais sobre tokens JWT, confira este tutorial aqui. No nosso backend fake, coloque o que quiser no token.
A função doSignInBackend é para usuários que já possuem cadastro em nossa plataforma e querem apenas se autenticar novamente. Ela espera um par de credenciais (user e secret) e envia para o backend, que estamos simulando com um IF na promise mesmo. Coloque o endereço da sua carteira no local apropriado e o segredo eu vou falar depois. O resultado se o usuário tiver acertado user e secret é o token JWT, caso contrário um erro 401. Falo mais de erros HTTP neste post aqui.
A próxima função é a doSignOutBackend. Isso porque quando você faz logout de uma aplicação web você limpa os dados no frontend mas deve invalidar o token JWT no backend. Então é isso que simulamos aqui, enviamos o token pro backend conferir e invalidar.
E por último, estava pensando em quê funcionalidade nosso backend poderia entregar aos usuários autenticados. Então pensei que usuários poderiam ter informações de cadastro como seu nome. Então essa função getProfileBackend espera o token de um usuário já logado e retorna as informações pessoais dele. Obviamente se o token estiver válido.
Usamos promises em todas funções para simular as chamadas assíncronas ao backend, já que em um AppService.js real você usaria o axios ou fetch para fazer estas chamadas e eles retornam promises também.
Agora volte ao nosso App.js e importe estas funções no topo do arquivo como abaixo.
1 2 3 |
import { doSignInBackend, doSignOutBackend, doSignUpBackend, getProfileBackend } from './AppService'; |
Pronto, agora nosso dapp híbrido tem um backend fake para se integrar, podendo mesclar funções da blockchain (dapp raiz) com funções REST comuns, fazendo nosso “dapp Nutella”. XD
DApp Nutella (Híbrido)
Vamos começar fazendo algumas refatorações na nossa aplicação React, para prepará-la para uso do nosso backend fake. Primeiro, vamos entender o que muda em nosso fluxo.
Até então, nosso front interagia somente com a carteira MetaMask, era um dapp puro. Agora ele terá de interagir também com nosso backend fake. Quando fazemos o signup (cadastro), além de pedir a permissão da carteira, precisamos cadastrar esta carteira no backend, junto de um segredo que vai funcionar como se fosse a senha do usuário mas que ele nunca vai ter de digitar.
Mas como gerar uma senha para o usuário que seja segura e que não precise ser armazenada no frontend, já que não queremos que o usuário digite nada?
Uma alternativa seria usarmos a chave privada da carteira dele como senha, já que ela é única, é de posse do usuário e dá legitimidade de que ele é o dono da carteira. No entanto, JAMAIS compartilhe ou peça para compartilharem a chave privada de carteiras de criptomoedas, isso deveria ser crime, haha.
Vamos fazer algo muito melhor e mais seguro que isso. Existe um procedimento chamado assinatura de mensagens, onde o usuário assina com a sua chave privada a informação que ele deseja dar legitimidade. Essa assinatura é um hash único daquela mensagem assinada por aquele usuário e podemos usá-lo como senha, pois não é adivinhável e não precisa ser armazenado no frontend, apenas no backend.
Assim, cada usuário que se cadastrar vai assinar com sua chave privada uma mensagem fixa que nós vamos definir e com isso terá a sua “senha do backend”.
Para fazer isso, primeiro vamos primeiro criar um arquivo .env (sem nome, apenas a extensão) na raiz da pasta do projeto contendo uma única variável que vai ser a mensagem a ser assinada pelos usuários. Coloque a mensagem que quiser, mas o nome da variável recomendo manter este e não esqueça que sempre que criamos ou alteramos um arquivo .env precisamos reiniciar nossa aplicação para que ele seja (re)carregado.
1 2 3 |
REACT_APP_CHALLENGE=HelloWorld |
Agora vamos refatorar algumas coisas. Praticamente todo o conteúdo do nosso doSignUp vai ir para uma função connect, como abaixo. Tem algumas novidades no final, logo depois da setWallet, que eu já vou explicar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
async function connect() { setError(''); if (!window.ethereum) return setError(`No MetaMask found!`); const provider = new ethers.BrowserProvider(window.ethereum); const accounts = await provider.send("eth_requestAccounts", []); if (!accounts || !accounts.length) return setError('Wallet not found/allowed!'); localStorage.setItem('wallet', accounts[0]); setWallet(accounts[0]); const signer = await provider.getSigner(); const secret = await signer.signMessage(process.env.REACT_APP_CHALLENGE); return { user: accounts[0], secret }; } |
Agora, logo depois de obtermos permissão de conexão na carteira nós vamos usar a função getSigner que serve para carregarmos o objeto de assinatura (signer) da carteira que está conectada e em seguida usamos o signMessage passando a mensagem que deve ser assinada. Esta chamada ao signMessage faz com que a MetaMask avise ao usuário que a aplicação deseja que ele assine algo.
Esta assinatura não incorre em custos (já que não acontece na blockchain, apenas em memória) e não expõe a chave privada do usuário, apesar de que a MetaMask usará ela internamente durante o processo de assinatura. O resultado do procedimento é um hash que usaremos como secret da credencial do usuário, que será devolvida em um objeto.
Agora vamos usar esta função connect na antiga função doSignUp, que está inteiramente nova.
1 2 3 4 5 6 7 8 |
function doSignUp() { connect() .then(credentials => doSignUpBackend(credentials)) .then(result => localStorage.setItem('token', result.token)) .catch(err => setError(err.message)) } |
Agora, nós fazemos a conexão e ela nos retorna as credenciais do usuário. Com elas, nós chamamos a função do backend fake para fazermos o cadastro lá e como resultado ele nos manda um token JWT, que nós armazenamos no nosso localStorage, para que requisições subsequentes o usuário não precise assinar a mensagem de novo.
Agora vamos algo semelhante com a função doSignIn, que até então não tinha utilidade.
1 2 3 4 5 6 7 8 9 10 11 |
function doSignIn() { connect() .then(credentials => doSignInBackend(credentials)) .then(result => { localStorage.setItem('token', result.token); loadProfile(result.token); }) .catch(err => setError(err.message)) } |
Aqui, nós fazemos a conexão normalmente e enviamos as credenciais para o backend, mas usando a função de signin (login), que não faz um novo cadastro, apenas verifica se as credenciais são válidas para emitir um token JWT. Com este token armazenado no localStorage (para uso subsequente), nós chamamos uma segunda função chamada loadProfile, que mostro a seguir.
1 2 3 4 5 6 7 8 |
const [profile, setProfile] = useState({}); async function loadProfile(token) { const profile = await getProfileBackend(token) setProfile(profile); } |
Claro que você poderia já retornar os dados do usuário quando ele fizer login, mas aqui a ideia é demonstrar como que uma chamada autenticada acontece depois que o usuário já está logado. A loadProfile faz esta chamada e armazena em um novo state que você deve criar.
É interessante que você modifique o HTML para que ele inclua na área logada as informações do perfil do usuário, como abaixo.
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 |
return ( <div className="App"> <header className="App-header"> <h1>Login</h1> <div> { !wallet ? ( <> <button onClick={doSignIn}> Sign In with MetaMask </button> <button onClick={doSignUp}> Sign Up with MetaMask </button> </> ) : ( <> <p> Wallet: {wallet} </p> <p> Name: {profile.name} </p> <p> <button onClick={getBalance}> See Balance </button> {balance} </p> <button onClick={doLogout}> Logout </button> </> ) } { error ? <p>{error}</p> : <></> } </div> </header> </div> ); |
Repare também que coloquei um botão para ver o saldo do usuário. Esta é uma funcionalidade que serve para mostrar como fazer as chamadas à blockchain a partir da nossa aplicação. Eu falo dessa função em mais detalhes em outro tutorial aqui do blog, mas veja abaixo o código necessário para ver o saldo.
1 2 3 4 5 6 7 8 9 10 |
const [balance, setBalance] = useState(''); function getBalance() { const provider = new ethers.BrowserProvider(window.ethereum); provider.getBalance(wallet) .then(balance => setBalance(ethers.formatEther(balance.toString()))) .catch(err => setError(err.message)) } |
Esta função getBalance presume que o usuário já esteja autenticado, por isso que mostramos o botão somente na área logada. Ela se conecta na carteira do navegador e chama a função getBalance passando o endereço da carteira a ser consultada. O resultado é armazenado em um state que renderizamos no HTML anterior.
Agora para finalizar, só precisamos refatorar o nosso doLogout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function doLogout() { setError(''); const token = localStorage.getItem('token'); doSignOutBackend(token) .then(response => { localStorage.removeItem('wallet'); localStorage.removeItem('token'); setWallet(''); setBalance(''); }) .catch(err => setError(err.message)); } |
Agora nosso doLogout pega o token que foi armazenado no localStorage após a autenticação e usa ele para se comunicar com o backend, visando fazer o logout por lá. No front, o logout consiste apenas na limpeza do localStorage e states.
E com isso, agora você tem uma aplicação web híbrida, uma mistura de dapp/web3 com web2. Esses exemplos de código lhe permitem entender tanto como se comunicar com a blockchain através da MetaMask, como se comunicar com um backend comum usando a autenticação da carteira também.
Espero ter ajudado!
Quer aprender a fazer transferências e pagamentos de cripto usando JavaScript e MetaMask? Confere então este outro tutorial aqui.
Quer aprender a fazer deploy de dapps web3? Confere no vídeo abaixo.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.