Componentes de servidor e agentes para aplicativos web3
Introdução
Na maioria dos casos, um aplicativo descentralizado usa um servidor para distribuir o software, mas toda a interação real acontece entre o cliente (normalmente, o navegador da web) e a cadeia de blocos.
No entanto, há alguns casos em que uma aplicação se beneficiaria de ter um componente de servidor que é executado de forma independente. Tal servidor seria capaz de responder a eventos e a solicitações que vêm de outras fontes, como uma API, emitindo transações.
Existem várias tarefas possíveis que tal servidor poderia cumprir.
-
Detentor de estado secreto. Em jogos, muitas vezes é útil não ter todas as informações que o jogo conhece disponíveis para os jogadores. No entanto, não há segredos na cadeia de blocos, qualquer informação que esteja na cadeia de blocos é fácil para qualquer um descobrir. Portanto, se parte do estado do jogo deve ser mantida em segredo, ela tem que ser armazenada em outro lugar (e possivelmente ter os efeitos desse estado verificados usando provas de conhecimento zero).
-
Oráculo centralizado. Se os valores em jogo forem suficientemente baixos, um servidor externo que lê algumas informações online e depois as publica na cadeia pode ser bom o suficiente para usar como um oráculo.
-
Agente. Nada acontece na cadeia de blocos sem uma transação para ativá-la. Um servidor pode agir em nome de um usuário para executar ações como arbitragem quando a oportunidade se apresenta.
Programa de exemplo
Você pode ver um servidor de exemplo no github (opens in a new tab). Este servidor escuta eventos vindos deste contrato (opens in a new tab), uma versão modificada do Greeter da Hardhat. Quando a saudação é alterada, ele a altera de volta.
Para executá-lo:
-
Clone o repositório.
1git clone https://github.com/qbzzt/20240715-server-component.git2cd 20240715-server-component -
Instale os pacotes necessários. Se você ainda não o tiver, instale o Node primeiro (opens in a new tab).
1npm install -
Edite o
.envpara especificar a chave privada de uma conta que tenha ETH na rede de teste Holesky. Se você não tiver ETH na Holesky, pode usar esta faucet (opens in a new tab).1PRIVATE_KEY=0x <a chave privada vai aqui> -
Inicie o servidor.
1npm start -
Vá para um explorador de blocos (opens in a new tab) e, usando um endereço diferente daquele que tem a chave privada, modifique a saudação. Veja que a saudação é modificada de volta automaticamente.
Como isso funciona?
A maneira mais fácil de entender como escrever um componente de servidor é analisar o exemplo linha por linha.
src/app.ts
A grande maioria do programa está contida em src/app.ts (opens in a new tab).
Criando os objetos de pré-requisito
1import {2 createPublicClient,3 createWalletClient,4 getContract,5 http,6 Address,7} from "viem"Estas são as entidades do Viem (opens in a new tab) que precisamos, funções e o tipo Address (opens in a new tab). Este servidor foi escrito em TypeScript (opens in a new tab), que é uma extensão do JavaScript que o torna fortemente tipado (opens in a new tab).
1import { privateKeyToAccount } from "viem/accounts"Esta função (opens in a new tab) nos permite gerar as informações da carteira, incluindo o endereço, correspondente a uma chave privada.
1import { holesky } from "viem/chains"Para usar uma cadeia de blocos no Viem, você precisa importar sua definição. Neste caso, queremos nos conectar à cadeia de blocos de teste Holesky (opens in a new tab).
1// É assim que adicionamos as definições em .env a process.env.2import * as dotenv from "dotenv"3dotenv.config()É assim que lemos o .env para o ambiente. Precisamos dele para a chave privada (veja mais adiante).
1const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"2const greeterABI = [3 {4 "inputs": [5 {6 "internalType": "string",7 "name": "_greeting",8 "type": "string"9 }10 ],11 "stateMutability": "nonpayable",12 "type": "constructor"13 },14 .15 .16 .17 {18 "inputs": [19 {20 "internalType": "string",21 "name": "_greeting",22 "type": "string"23 }24 ],25 "name": "setGreeting",26 "outputs": [],27 "stateMutability": "nonpayable",28 "type": "function"29 }30] as constExibir tudoPara usar um contrato, precisamos do seu endereço e da para ele. Fornecemos ambos aqui.
Em JavaScript (e, portanto, em TypeScript), você não pode atribuir um novo valor a uma constante, mas você pode modificar o objeto que está armazenado nela. Ao usar o sufixo as const, estamos dizendo ao TypeScript que a lista em si é constante e não pode ser alterada.
1const publicClient = createPublicClient({2 chain: holesky,3 transport: http(),4})Crie um cliente público (opens in a new tab) Viem. Clientes públicos não têm uma chave privada anexada e, portanto, não podem enviar transações. Eles podem chamar funções view (opens in a new tab), ler saldos de contas, etc.
1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)As variáveis de ambiente estão disponíveis em process.env (opens in a new tab). No entanto, o TypeScript é fortemente tipado. Uma variável de ambiente pode ser qualquer string, ou vazia, então o tipo para uma variável de ambiente é string | undefined. No entanto, uma chave é definida no Viem como 0x${string} (0x seguido por uma string). Aqui nós dizemos ao TypeScript que a variável de ambiente PRIVATE_KEY será desse tipo. Se não for, teremos um erro de tempo de execução.
A função privateKeyToAccount (opens in a new tab) então usa esta chave privada para criar um objeto de conta completo.
1const walletClient = createWalletClient({2 account,3 chain: holesky,4 transport: http(),5})Em seguida, usamos o objeto da conta para criar um cliente de carteira (opens in a new tab). Este cliente tem uma chave privada e um endereço, então ele pode ser usado para enviar transações.
1const greeter = getContract({2 address: greeterAddress,3 abi: greeterABI,4 client: { public: publicClient, wallet: walletClient },5})Agora que temos todos os pré-requisitos, podemos finalmente criar uma instância de contrato (opens in a new tab). Usaremos esta instância de contrato para nos comunicarmos com o contrato na cadeia.
Lendo da cadeia de blocos
1console.log(`Saudação atual:`, await greeter.read.greet())As funções de contrato que são somente leitura (view (opens in a new tab) e pure (opens in a new tab)) estão disponíveis em read. Neste caso, usamos ela para acessar a função greet (opens in a new tab), que retorna a saudação.
O JavaScript é de thread único, então, quando disparamos um processo de longa duração, precisamos especificar que o fazemos de forma assíncrona (opens in a new tab). Chamar a cadeia de blocos, mesmo para uma operação de somente leitura, requer uma viagem de ida e volta entre o computador e um nó da cadeia de blocos. É por isso que especificamos aqui que o código precisa await (aguardar) pelo resultado.
Se você estiver interessado em como isso funciona, pode ler sobre isso aqui (opens in a new tab), mas, em termos práticos, tudo o que você precisa saber é que você usa await nos resultados se iniciar uma operação que leva muito tempo, e que qualquer função que faça isso precisa ser declarada como async.
Emitindo transações
1const setGreeting = async (greeting: string): Promise<any> => {Esta é a função que você chama para emitir uma transação que altera a saudação. Como esta é uma operação longa, a função é declarada como async. Devido à implementação interna, qualquer função async precisa retornar um objeto Promise. Neste caso, Promise<any> significa que não especificamos o que exatamente será retornado na Promise.
1const txHash = await greeter.write.setGreeting([greeting])O campo write da instância do contrato tem todas as funções que escrevem no estado da cadeia de blocos (aquelas que exigem o envio de uma transação), como setGreeting (opens in a new tab). Os parâmetros, se houver, são fornecidos como uma lista, e a função retorna o hash da transação.
1 console.log(`Trabalhando em uma correção, veja https://eth-holesky.blockscout.com/tx/${txHash}`)23 return txHash4}Informe o hash da transação (como parte de um URL para o explorador de blocos para visualizá-lo) e retorne-o.
Respondendo a eventos
1greeter.watchEvent.SetGreeting({A função watchEvent (opens in a new tab) permite que você especifique que uma função deve ser executada quando um evento é emitido. Se você se importa apenas com um tipo de evento (neste caso, SetGreeting), pode usar esta sintaxe para se limitar a esse tipo de evento.
1 onLogs: logs => {A função onLogs é chamada quando há entradas de log. No Ethereum, "log" e "evento" são geralmente intercambiáveis.
1console.log(2 `O endereço ${logs[0].args.sender} alterou a saudação para ${logs[0].args.greeting}`3)Pode haver vários eventos, mas por simplicidade nos importamos apenas com o primeiro. logs[0].args são os argumentos do evento, neste caso, sender e greeting.
1 if (logs[0].args.sender != account.address)2 setGreeting(`${account.address} insiste que seja Olá!`)3 }4})Se o remetente não for este servidor, use setGreeting para alterar a saudação.
package.json
Este arquivo (opens in a new tab) controla a configuração do Node.js (opens in a new tab). Este artigo explica apenas as definições importantes.
1{2 "main": "dist/index.js",Esta definição especifica qual arquivo JavaScript executar.
1 "scripts": {2 "start": "tsc && node dist/app.js",3 },Os scripts são várias ações da aplicação. Neste caso, o único que temos é o start, que compila e depois executa o servidor. O comando tsc faz parte do pacote typescript e compila TypeScript em JavaScript. Se você quiser executá-lo manualmente, ele está localizado em node_modules/.bin. O segundo comando executa o servidor.
1 "type": "module",Existem vários tipos de aplicações Node de JavaScript. O tipo module nos permite ter await no código de nível superior, o que é importante quando você faz operações lentas (e, portanto, assíncronas).
1 "devDependencies": {2 "@types/node": "^20.14.2",3 "typescript": "^5.4.5"4 },Estes são pacotes que são necessários apenas para o desenvolvimento. Aqui precisamos do typescript e, como estamos usando com Node.js, também estamos obtendo os tipos para variáveis e objetos do Node, como o process. A notação ^<versão> (opens in a new tab) significa essa versão ou uma versão superior que não tenha alterações que quebrem a compatibilidade. Veja aqui (opens in a new tab) para mais informações sobre o significado dos números de versão.
1 "dependencies": {2 "dotenv": "^16.4.5",3 "viem": "2.14.1"4 }5}Estes são pacotes que são necessários em tempo de execução, ao executar dist/app.js.
Conclusão
O servidor centralizado que criamos aqui cumpre seu papel, que é atuar como um agente para um usuário. Qualquer outra pessoa que queira que o dapp continue funcionando e esteja disposta a gastar o gás pode executar uma nova instância do servidor com seu próprio endereço.
No entanto, isso só funciona quando as ações do servidor centralizado podem ser facilmente verificadas. Se o servidor centralizado tiver alguma informação de estado secreta ou executar cálculos difíceis, é uma entidade centralizada na qual você precisa confiar para usar a aplicação, que é exatamente o que as cadeias de blocos tentam evitar. Em um artigo futuro, pretendo mostrar como usar provas de conhecimento zero para contornar este problema.
Veja aqui mais do meu trabalho (opens in a new tab).
Última atualização da página: 25 de fevereiro de 2026