From 9940fdb3e036e03aa8ede1ca80cd44d86d3b85b7 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Sat, 16 Sep 2023 23:30:29 +0100 Subject: [PATCH] feat(faucet): add faucet service (#1517) --- .changeset/chilled-cougars-smash.md | 23 ++++++++ Dockerfile | 4 ++ .../packages/client-react/package.json | 1 + .../client-react/src/mud/setupNetwork.ts | 31 ++++------ examples/minimal/packages/contracts/.env | 2 + .../minimal/packages/contracts/package.json | 2 + examples/minimal/pnpm-lock.yaml | 6 ++ packages/faucet/.eslintrc | 6 ++ packages/faucet/.gitignore | 1 + packages/faucet/.npmignore | 6 ++ packages/faucet/bin/faucet-server.ts | 56 +++++++++++++++++++ packages/faucet/package.json | 50 +++++++++++++++++ packages/faucet/src/createAppRouter.ts | 42 ++++++++++++++ packages/faucet/src/createClient.ts | 21 +++++++ packages/faucet/src/debug.ts | 3 + packages/faucet/src/index.ts | 2 + packages/faucet/tsconfig.json | 14 +++++ packages/faucet/tsup.config.ts | 11 ++++ pnpm-lock.yaml | 40 +++++++++++++ 19 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 .changeset/chilled-cougars-smash.md create mode 100644 packages/faucet/.eslintrc create mode 100644 packages/faucet/.gitignore create mode 100644 packages/faucet/.npmignore create mode 100644 packages/faucet/bin/faucet-server.ts create mode 100644 packages/faucet/package.json create mode 100644 packages/faucet/src/createAppRouter.ts create mode 100644 packages/faucet/src/createClient.ts create mode 100644 packages/faucet/src/debug.ts create mode 100644 packages/faucet/src/index.ts create mode 100644 packages/faucet/tsconfig.json create mode 100644 packages/faucet/tsup.config.ts diff --git a/.changeset/chilled-cougars-smash.md b/.changeset/chilled-cougars-smash.md new file mode 100644 index 0000000000..0356ce2c82 --- /dev/null +++ b/.changeset/chilled-cougars-smash.md @@ -0,0 +1,23 @@ +--- +"@latticexyz/faucet": minor +--- + +New package to run your own faucet service. We'll use this soon for our testnet in place of `@latticexyz/services`. + +To run the faucet server: + +- Add the package with `pnpm add @latticexyz/faucet` +- Add a `.env` file that has a `RPC_HTTP_URL` and `FAUCET_PRIVATE_KEY` (or pass the environment variables into the next command) +- Run `pnpm faucet-server` to start the server + +You can also adjust the server's `HOST` (defaults to `0.0.0.0`) and `PORT` (defaults to `3002`). The tRPC routes are accessible under `/trpc`. + +To connect a tRPC client, add the package with `pnpm add @latticexyz/faucet` and then use `createClient`: + +```ts +import { createClient } from "@latticexyz/faucet"; + +const faucet = createClient({ url: "http://localhost:3002/trpc" }); + +await faucet.mutate.drip({ address: burnerAccount.address }); +``` diff --git a/Dockerfile b/Dockerfile index 6b4a5ef2c8..31e8acc9eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,3 +50,7 @@ RUN pnpm run -r build FROM mud AS store-indexer WORKDIR /app/packages/store-indexer EXPOSE 3001 + +FROM mud AS faucet +WORKDIR /app/packages/faucet +EXPOSE 3002 diff --git a/examples/minimal/packages/client-react/package.json b/examples/minimal/packages/client-react/package.json index cbbce8e319..0b69a60485 100644 --- a/examples/minimal/packages/client-react/package.json +++ b/examples/minimal/packages/client-react/package.json @@ -13,6 +13,7 @@ "@improbable-eng/grpc-web": "^0.15.0", "@latticexyz/common": "link:../../../../packages/common", "@latticexyz/dev-tools": "link:../../../../packages/dev-tools", + "@latticexyz/faucet": "link:../../../../packages/faucet", "@latticexyz/react": "link:../../../../packages/react", "@latticexyz/recs": "link:../../../../packages/recs", "@latticexyz/schema-type": "link:../../../../packages/schema-type", diff --git a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts index d11cd48e24..895bf70553 100644 --- a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts +++ b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts @@ -7,6 +7,7 @@ import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; import { ContractWrite, createBurnerAccount, createContract, transportObserver } from "@latticexyz/common"; import { Subject, share } from "rxjs"; import mudConfig from "contracts/mud.config"; +import { createClient as createFaucetClient } from "@latticexyz/faucet"; export type SetupNetworkResult = Awaited>; @@ -44,28 +45,20 @@ export async function setupNetwork() { startBlock: BigInt(networkConfig.initialBlockNumber), }); - // Request drip from faucet - if (networkConfig.faucetServiceUrl) { - const address = burnerAccount.address; - console.info("[Dev Faucet]: Player address -> ", address); + try { + console.log("creating faucet client"); + const faucet = createFaucetClient({ url: "http://localhost:3002/trpc" }); - const faucet = createFaucetService(networkConfig.faucetServiceUrl); - - const requestDrip = async () => { - const balance = await publicClient.getBalance({ address }); - console.info(`[Dev Faucet]: Player balance -> ${balance}`); - const lowBalance = balance < parseEther("1"); - if (lowBalance) { - console.info("[Dev Faucet]: Balance is low, dripping funds to player"); - // Double drip - await faucet.dripDev({ address }); - await faucet.dripDev({ address }); - } + const drip = async () => { + console.log("dripping"); + const tx = await faucet.drip.mutate({ address: burnerAccount.address }); + console.log("got drip", tx); }; - requestDrip(); - // Request a drip every 20 seconds - setInterval(requestDrip, 20000); + drip(); + setInterval(drip, 20_000); + } catch (e) { + console.error(e); } return { diff --git a/examples/minimal/packages/contracts/.env b/examples/minimal/packages/contracts/.env index 0f8cc4a305..0c925aa484 100644 --- a/examples/minimal/packages/contracts/.env +++ b/examples/minimal/packages/contracts/.env @@ -6,3 +6,5 @@ # # Anvil default private key: PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +FAUCET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +RPC_HTTP_URL=http://127.0.0.1:8545 diff --git a/examples/minimal/packages/contracts/package.json b/examples/minimal/packages/contracts/package.json index 642f4a17b9..5acf110bed 100644 --- a/examples/minimal/packages/contracts/package.json +++ b/examples/minimal/packages/contracts/package.json @@ -11,6 +11,7 @@ "deploy:local": "pnpm run build && mud deploy", "deploy:testnet": "pnpm run build && mud deploy --profile=lattice-testnet", "dev": "pnpm mud dev-contracts", + "faucet": "DEBUG=mud:faucet pnpm faucet-server", "lint": "pnpm run prettier && pnpm run solhint", "prettier": "prettier --write 'src/**/*.sol'", "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", @@ -19,6 +20,7 @@ "devDependencies": { "@latticexyz/cli": "link:../../../../packages/cli", "@latticexyz/config": "link:../../../../packages/config", + "@latticexyz/faucet": "link:../../../../packages/faucet", "@latticexyz/schema-type": "link:../../../../packages/schema-type", "@latticexyz/store": "link:../../../../packages/store", "@latticexyz/world": "link:../../../../packages/world", diff --git a/examples/minimal/pnpm-lock.yaml b/examples/minimal/pnpm-lock.yaml index b0e57b5a9d..7f09b57e02 100644 --- a/examples/minimal/pnpm-lock.yaml +++ b/examples/minimal/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: '@latticexyz/dev-tools': specifier: link:../../../../packages/dev-tools version: link:../../../../packages/dev-tools + '@latticexyz/faucet': + specifier: link:../../../../packages/faucet + version: link:../../../../packages/faucet '@latticexyz/react': specifier: link:../../../../packages/react version: link:../../../../packages/react @@ -305,6 +308,9 @@ importers: '@latticexyz/config': specifier: link:../../../../packages/config version: link:../../../../packages/config + '@latticexyz/faucet': + specifier: link:../../../../packages/faucet + version: link:../../../../packages/faucet '@latticexyz/schema-type': specifier: link:../../../../packages/schema-type version: link:../../../../packages/schema-type diff --git a/packages/faucet/.eslintrc b/packages/faucet/.eslintrc new file mode 100644 index 0000000000..6db0063ad7 --- /dev/null +++ b/packages/faucet/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": ["../../.eslintrc"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "error" + } +} diff --git a/packages/faucet/.gitignore b/packages/faucet/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/packages/faucet/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/faucet/.npmignore b/packages/faucet/.npmignore new file mode 100644 index 0000000000..84815f1eba --- /dev/null +++ b/packages/faucet/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!src/** +!package.json +!README.md diff --git a/packages/faucet/bin/faucet-server.ts b/packages/faucet/bin/faucet-server.ts new file mode 100644 index 0000000000..f6a6327a3d --- /dev/null +++ b/packages/faucet/bin/faucet-server.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env node +import "dotenv/config"; +import { z } from "zod"; +import fastify from "fastify"; +import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify"; +import { ClientConfig, http, parseEther, isHex, createClient } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { AppRouter, createAppRouter } from "../src/createAppRouter"; +import { getChainId } from "viem/actions"; + +// TODO: refine zod type to be either CHAIN_ID or RPC_HTTP_URL/RPC_WS_URL +const env = z + .object({ + HOST: z.string().default("0.0.0.0"), + PORT: z.coerce.number().positive().default(3002), + RPC_HTTP_URL: z.string(), + FAUCET_PRIVATE_KEY: z.string().refine(isHex), + DRIP_AMOUNT_ETHER: z + .string() + .default("1") + .transform((ether) => parseEther(ether)), + }) + .parse(process.env, { + errorMap: (issue) => ({ + message: `Missing or invalid environment variable: ${issue.path.join(".")}`, + }), + }); + +const client = createClient({ + transport: http(env.RPC_HTTP_URL), +}); + +const faucetAccount = privateKeyToAccount(env.FAUCET_PRIVATE_KEY); + +// @see https://fastify.dev/docs/latest/ +const server = fastify({ + maxParamLength: 5000, +}); + +await server.register(import("@fastify/cors")); + +// @see https://trpc.io/docs/server/adapters/fastify +server.register(fastifyTRPCPlugin, { + prefix: "/trpc", + trpcOptions: { + router: createAppRouter(), + createContext: async () => ({ + client, + faucetAccount, + dripAmount: env.DRIP_AMOUNT_ETHER, + }), + }, +}); + +await server.listen({ host: env.HOST, port: env.PORT }); +console.log(`faucet server listening on http://${env.HOST}:${env.PORT}`); diff --git a/packages/faucet/package.json b/packages/faucet/package.json new file mode 100644 index 0000000000..9bf3daa1e7 --- /dev/null +++ b/packages/faucet/package.json @@ -0,0 +1,50 @@ +{ + "name": "@latticexyz/faucet", + "version": "2.0.0-next.8", + "description": "Faucet API for Lattice testnet", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/faucet" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": "./dist/src/index.js" + }, + "types": "src/index.ts", + "bin": { + "faucet-server": "./dist/bin/faucet-server.js" + }, + "scripts": { + "build": "pnpm run build:js", + "build:js": "tsup", + "clean": "pnpm run clean:js", + "clean:js": "rimraf dist", + "dev": "tsup --watch", + "lint": "eslint .", + "start": "tsx bin/server", + "test": "tsc --noEmit --skipLibCheck", + "test:ci": "pnpm run test" + }, + "dependencies": { + "@fastify/cors": "^8.3.0", + "@trpc/client": "10.34.0", + "@trpc/server": "10.34.0", + "debug": "^4.3.4", + "dotenv": "^16.0.3", + "fastify": "^4.21.0", + "viem": "1.6.0", + "zod": "^3.21.4" + }, + "devDependencies": { + "@types/debug": "^4.1.7", + "tsup": "^6.7.0", + "tsx": "^3.12.6", + "vitest": "0.31.4" + }, + "publishConfig": { + "access": "public" + }, + "gitHead": "914a1e0ae4a573d685841ca2ea921435057deb8f" +} diff --git a/packages/faucet/src/createAppRouter.ts b/packages/faucet/src/createAppRouter.ts new file mode 100644 index 0000000000..6bbe28000d --- /dev/null +++ b/packages/faucet/src/createAppRouter.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { initTRPC } from "@trpc/server"; +import { Client, Hex, LocalAccount, formatEther, isHex } from "viem"; +import { sendTransaction } from "viem/actions"; +import { debug } from "./debug"; + +export type AppContext = { + client: Client; + faucetAccount: LocalAccount; + dripAmount: bigint; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createAppRouter() { + const t = initTRPC.context().create(); + + return t.router({ + drip: t.procedure + .input( + z.object({ + address: z.string().refine(isHex), + }) + ) + .mutation(async (opts): Promise => { + const { client, faucetAccount, dripAmount } = opts.ctx; + + const { address } = opts.input; + const tx = await sendTransaction(client, { + chain: null, + account: faucetAccount, + to: address, + value: dripAmount, + }); + + debug(`Dripped ${formatEther(dripAmount)} ETH from ${faucetAccount.address} to ${address} (tx ${tx})`); + + return tx; + }), + }); +} + +export type AppRouter = ReturnType; diff --git a/packages/faucet/src/createClient.ts b/packages/faucet/src/createClient.ts new file mode 100644 index 0000000000..661d987035 --- /dev/null +++ b/packages/faucet/src/createClient.ts @@ -0,0 +1,21 @@ +import { createTRPCProxyClient, httpBatchLink, CreateTRPCProxyClient } from "@trpc/client"; +import type { AppRouter } from "./createAppRouter"; + +type CreateClientOptions = { + /** + * tRPC endpoint URL like `https://faucet.dev.linfra.xyz/trpc`. + */ + url: string; +}; + +/** + * Creates a tRPC client to talk to a MUD faucet. + * + * @param {CreateClientOptions} options See `CreateClientOptions`. + * @returns {CreateTRPCProxyClient} A typed tRPC client. + */ +export function createClient({ url }: CreateClientOptions): CreateTRPCProxyClient { + return createTRPCProxyClient({ + links: [httpBatchLink({ url })], + }); +} diff --git a/packages/faucet/src/debug.ts b/packages/faucet/src/debug.ts new file mode 100644 index 0000000000..a6dd8eec29 --- /dev/null +++ b/packages/faucet/src/debug.ts @@ -0,0 +1,3 @@ +import createDebug from "debug"; + +export const debug = createDebug("mud:faucet"); diff --git a/packages/faucet/src/index.ts b/packages/faucet/src/index.ts new file mode 100644 index 0000000000..03493d8de2 --- /dev/null +++ b/packages/faucet/src/index.ts @@ -0,0 +1,2 @@ +export * from "./createAppRouter"; +export * from "./createClient"; diff --git a/packages/faucet/tsconfig.json b/packages/faucet/tsconfig.json new file mode 100644 index 0000000000..e590f0c026 --- /dev/null +++ b/packages/faucet/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true + } +} diff --git a/packages/faucet/tsup.config.ts b/packages/faucet/tsup.config.ts new file mode 100644 index 0000000000..385f3fc274 --- /dev/null +++ b/packages/faucet/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "bin/faucet-server.ts"], + target: "esnext", + format: ["esm"], + dts: false, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cedd484c9e..b1dec75481 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,6 +405,46 @@ importers: packages/ecs-browser: {} + packages/faucet: + dependencies: + '@fastify/cors': + specifier: ^8.3.0 + version: 8.3.0 + '@trpc/client': + specifier: 10.34.0 + version: 10.34.0(@trpc/server@10.34.0) + '@trpc/server': + specifier: 10.34.0 + version: 10.34.0 + debug: + specifier: ^4.3.4 + version: 4.3.4(supports-color@8.1.1) + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + fastify: + specifier: ^4.21.0 + version: 4.21.0 + viem: + specifier: 1.6.0 + version: 1.6.0(typescript@5.1.6)(zod@3.21.4) + zod: + specifier: ^3.21.4 + version: 3.21.4 + devDependencies: + '@types/debug': + specifier: ^4.1.7 + version: 4.1.7 + tsup: + specifier: ^6.7.0 + version: 6.7.0(postcss@8.4.23)(typescript@5.1.6) + tsx: + specifier: ^3.12.6 + version: 3.12.6 + vitest: + specifier: 0.31.4 + version: 0.31.4(jsdom@22.1.0) + packages/gas-report: dependencies: chalk: