Skip to content

Commit

Permalink
feat(faucet): add faucet service (#1517)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Sep 16, 2023
1 parent 99ab9cd commit 9940fdb
Show file tree
Hide file tree
Showing 19 changed files with 302 additions and 19 deletions.
23 changes: 23 additions & 0 deletions .changeset/chilled-cougars-smash.md
Original file line number Diff line number Diff line change
@@ -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 });
```
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions examples/minimal/packages/client-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 12 additions & 19 deletions examples/minimal/packages/client-react/src/mud/setupNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof setupNetwork>>;

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions examples/minimal/packages/contracts/.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
#
# Anvil default private key:
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
FAUCET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
RPC_HTTP_URL=http://127.0.0.1:8545
2 changes: 2 additions & 0 deletions examples/minimal/packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions examples/minimal/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/faucet/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": ["../../.eslintrc"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "error"
}
}
1 change: 1 addition & 0 deletions packages/faucet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
6 changes: 6 additions & 0 deletions packages/faucet/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!src/**
!package.json
!README.md
56 changes: 56 additions & 0 deletions packages/faucet/bin/faucet-server.ts
Original file line number Diff line number Diff line change
@@ -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<AppRouter>, {
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}`);
50 changes: 50 additions & 0 deletions packages/faucet/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
42 changes: 42 additions & 0 deletions packages/faucet/src/createAppRouter.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
dripAmount: bigint;
};

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createAppRouter() {
const t = initTRPC.context<AppContext>().create();

return t.router({
drip: t.procedure
.input(
z.object({
address: z.string().refine(isHex),
})
)
.mutation(async (opts): Promise<Hex> => {
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<typeof createAppRouter>;
21 changes: 21 additions & 0 deletions packages/faucet/src/createClient.ts
Original file line number Diff line number Diff line change
@@ -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<AppRouter>} A typed tRPC client.
*/
export function createClient({ url }: CreateClientOptions): CreateTRPCProxyClient<AppRouter> {
return createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url })],
});
}
3 changes: 3 additions & 0 deletions packages/faucet/src/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createDebug from "debug";

export const debug = createDebug("mud:faucet");
2 changes: 2 additions & 0 deletions packages/faucet/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./createAppRouter";
export * from "./createClient";
14 changes: 14 additions & 0 deletions packages/faucet/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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
}
}
11 changes: 11 additions & 0 deletions packages/faucet/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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,
});
40 changes: 40 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9940fdb

Please sign in to comment.