diff --git a/spartan/aztec-network/templates/faucet.yaml b/spartan/aztec-network/templates/faucet.yaml new file mode 100644 index 00000000000..f132365888f --- /dev/null +++ b/spartan/aztec-network/templates/faucet.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aztec-network.fullname" . }}-faucet + labels: + {{- include "aztec-network.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.faucet.replicas }} + selector: + matchLabels: + {{- include "aztec-network.selectorLabels" . | nindent 6 }} + app: faucet + template: + metadata: + labels: + {{- include "aztec-network.selectorLabels" . | nindent 8 }} + app: faucet + spec: + serviceAccountName: {{ include "aztec-network.fullname" . }}-node + {{- if .Values.network.public }} + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + {{- end }} + volumes: + - name: config + emptyDir: {} + - name: scripts + configMap: + name: {{ include "aztec-network.fullname" . }}-scripts + - name: scripts-output + emptyDir: {} + initContainers: + {{- include "aztec-network.serviceAddressSetupContainer" . | nindent 8 }} + - name: wait-for-dependencies + image: {{ .Values.images.curl.image }} + command: + - /bin/sh + - -c + - | + source /shared/config/service-addresses + cat /shared/config/service-addresses + + echo "Awaiting ethereum node at ${ETHEREUM_HOST}" + until curl -s -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":67}' \ + ${ETHEREUM_HOST} | grep -q reth; do + echo "Waiting for Ethereum node ${ETHEREUM_HOST}..." + sleep 5 + done + echo "Ethereum node is ready!" + volumeMounts: + - name: config + mountPath: /shared/config + - name: scripts + mountPath: /shared/scripts + containers: + - name: faucet + {{ include "aztec-network.image" . | nindent 10 }} + volumeMounts: + - name: config + mountPath: /shared/config + command: + - "/bin/bash" + - "-c" + - | + source /shared/config/service-addresses + cat /shared/config/service-addresses + node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js start --faucet --faucet.apiServer --faucet.apiServerPort {{ .Values.faucet.apiServerPort }} + env: + - name: AZTEC_PORT + value: "{{ .Values.faucet.service.nodePort }}" + - name: L1_CHAIN_ID + value: "{{ .Values.ethereum.chainId }}" + - name: MNEMONIC + value: "{{ .Values.aztec.l1DeploymentMnemonic }}" + - name: FAUCET_MNEMONIC_ACCOUNT_INDEX + value: "{{ .Values.faucet.accountIndex }}" + - name: FAUCET_L1_ASSETS + value: "{{ .Values.faucet.l1Assets }}" + - name: LOG_JSON + value: "1" + - name: LOG_LEVEL + value: "{{ .Values.faucet.logLevel }}" + ports: + - name: http + containerPort: {{ .Values.faucet.service.nodePort }} + protocol: TCP + resources: + {{- toYaml .Values.faucet.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aztec-network.fullname" . }}-faucet + labels: + {{- include "aztec-network.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "aztec-network.selectorLabels" . | nindent 4 }} + app: faucet + ports: + - protocol: TCP + port: {{ .Values.faucet.service.nodePort }} + targetPort: {{ .Values.faucet.service.nodePort }} + {{- if and (eq .Values.faucet.service.type "NodePort") .Values.faucet.service.nodePort }} + nodePort: {{ .Values.faucet.service.nodePort }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aztec-network.fullname" . }}-faucet-api + labels: + {{- include "aztec-network.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "aztec-network.selectorLabels" . | nindent 4 }} + app: faucet + ports: + - protocol: TCP + port: {{ .Values.faucet.apiServerPort }} + targetPort: {{ .Values.faucet.apiServerPort }} diff --git a/spartan/aztec-network/values.yaml b/spartan/aztec-network/values.yaml index 807aa7fb8b6..b1dc6c94dab 100644 --- a/spartan/aztec-network/values.yaml +++ b/spartan/aztec-network/values.yaml @@ -240,3 +240,13 @@ proverBroker: jobs: deployL1Verifier: enable: false + +faucet: + replicas: 1 + service: + nodePort: 8085 + apiServerPort: 8086 + accountIndex: 0 + l1Assets: "" + logLevel: "" + resources: {} diff --git a/yarn-project/aztec-faucet/package.json b/yarn-project/aztec-faucet/package.json index 3852ff8828e..2285a4087c0 100644 --- a/yarn-project/aztec-faucet/package.json +++ b/yarn-project/aztec-faucet/package.json @@ -4,6 +4,9 @@ "main": "dest/bin/index.js", "type": "module", "bin": "./dest/bin/index.js", + "exports": { + ".": "./dest/index.js" + }, "typedocOptions": { "entryPoints": [ "./src/bin/index.ts" @@ -65,14 +68,17 @@ "@aztec/ethereum": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", + "@koa/cors": "^5.0.0", "koa": "^2.14.2", - "koa-cors": "^0.0.16", + "koa-bodyparser": "^4.4.1", "koa-router": "^12.0.0", - "viem": "^2.7.15" + "viem": "^2.7.15", + "zod": "^3.23.8" }, "devDependencies": { "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", + "@types/koa-bodyparser": "^4.3.12", "@types/node": "^18.7.23", "jest": "^29.5.0", "ts-node": "^10.9.1", diff --git a/yarn-project/aztec-faucet/src/bin/index.ts b/yarn-project/aztec-faucet/src/bin/index.ts index b6baf9d6b94..3adaa2a413e 100644 --- a/yarn-project/aztec-faucet/src/bin/index.ts +++ b/yarn-project/aztec-faucet/src/bin/index.ts @@ -1,278 +1,22 @@ #!/usr/bin/env -S node --no-warnings -import { NULL_KEY, createEthereumChain } from '@aztec/ethereum'; -import { EthAddress } from '@aztec/foundation/eth-address'; import { createLogger } from '@aztec/foundation/log'; -import { TestERC20Abi } from '@aztec/l1-artifacts'; -import http from 'http'; -import Koa from 'koa'; -import cors from 'koa-cors'; -import Router from 'koa-router'; -import { - type Hex, - type LocalAccount, - http as ViemHttp, - createPublicClient, - createWalletClient, - getContract, - parseEther, -} from 'viem'; -import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; +import { getFaucetConfigFromEnv } from '../config.js'; +import { Faucet } from '../faucet.js'; +import { createFaucetHttpServer } from '../http.js'; -const { - FAUCET_PORT = 8082, - API_PREFIX = '', - RPC_URL = '', - L1_CHAIN_ID = '', - FORK_MNEMONIC = '', - FAUCET_ACCOUNT_INDEX = '', - PRIVATE_KEY = '', - INTERVAL = '', - ETH_AMOUNT = '', - // asset_name:contract_address - EXTRA_ASSETS = '', - EXTRA_ASSET_AMOUNT = '', -} = process.env; - -const logger = createLogger('faucet'); - -const rpcUrl = RPC_URL; -const l1ChainId = +L1_CHAIN_ID; -const interval = +INTERVAL; -type AssetName = string & { __brand: 'AssetName' }; -type ThrottleKey = `${'eth' | AssetName}/${Hex}`; -type Assets = Record; - -const mapping: { [key: ThrottleKey]: Date } = {}; -const assets: Assets = {}; - -if (EXTRA_ASSETS) { - const assetList = EXTRA_ASSETS.split(','); - assetList.forEach(asset => { - const [name, address] = asset.split(':'); - if (!name || !address) { - throw new Error(`Invalid asset: ${asset}`); - } - assets[name as AssetName] = createHex(address); - }); -} - -class ThrottleError extends Error { - constructor(address: string) { - super(`Not funding address ${address}, please try again later`); - } -} - -/** - * Checks if the requested asset is something the faucet can handle. - * @param asset - The asset to check - * @returns True if the asset is known - */ -function isKnownAsset(asset: any): asset is 'eth' | AssetName { - return asset === 'eth' || asset in assets; -} - -/** - * Helper function to convert a string to a Hex value - * @param hex - The string to convert - * @returns The converted value - */ -function createHex(hex: string) { - return `0x${hex.replace('0x', '')}` as Hex; -} - -/** - * Function to throttle drips on a per address basis - * @param address - Address requesting some ETH - */ -function checkThrottle(asset: 'eth' | AssetName, address: Hex) { - const key: ThrottleKey = `${asset}/${address}`; - if (mapping[key] === undefined) { - return; - } - const last = mapping[key]; - const current = new Date(); - const diff = (current.getTime() - last.getTime()) / 1000; - if (diff < interval) { - throw new ThrottleError(address); - } -} - -/** - * Update the throttle mapping for the given asset and address - * @param asset - The asset to throttle - * @param address - The address to throttle - */ -function updateThrottle(asset: 'eth' | AssetName, address: Hex) { - const key: ThrottleKey = `${asset}/${address}`; - mapping[key] = new Date(); -} - -/** - * Get the account to use for sending ETH - * @returns The account to use for sending ETH - */ -function getFaucetAccount(): LocalAccount { - let account: LocalAccount; - if (FORK_MNEMONIC) { - const index = Number.isNaN(+FAUCET_ACCOUNT_INDEX) ? 0 : +FAUCET_ACCOUNT_INDEX; - account = mnemonicToAccount(FORK_MNEMONIC, { - addressIndex: index, - }); - } else if (PRIVATE_KEY) { - account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); - } else { - logger.warn('No mnemonic or private key provided, using null key'); - account = privateKeyToAccount(NULL_KEY); - } - - return account; -} - -function createClients() { - const chain = createEthereumChain(rpcUrl, l1ChainId); - - const account = getFaucetAccount(); - const walletClient = createWalletClient({ - account: account, - chain: chain.chainInfo, - transport: ViemHttp(chain.rpcUrl), - }); - const publicClient = createPublicClient({ - chain: chain.chainInfo, - transport: ViemHttp(chain.rpcUrl), - }); - - return { account, walletClient, publicClient }; -} - -/** - * Helper function to send some ETH to the given address - * @param address - Address to receive some ETH - */ -async function transferEth(address: string) { - const { account, walletClient, publicClient } = createClients(); - const hexAddress = createHex(address); - checkThrottle('eth', hexAddress); - try { - const hash = await walletClient.sendTransaction({ - account, - to: hexAddress, - value: parseEther(ETH_AMOUNT), - }); - await publicClient.waitForTransactionReceipt({ hash }); - updateThrottle('eth', hexAddress); - logger.info(`Sent ${ETH_AMOUNT} ETH to ${hexAddress} in tx ${hash}`); - } catch (error) { - logger.error(`Failed to send eth to ${hexAddress}`); - throw error; - } -} - -/** - * Mints FeeJuice to the given address - * @param address - Address to receive some FeeJuice - */ -async function transferAsset(assetName: AssetName, address: string) { - const { publicClient, walletClient } = createClients(); - const hexAddress = createHex(address); - checkThrottle(assetName, hexAddress); - - const assetAddress = assets[assetName]; - - try { - const contract = getContract({ - abi: TestERC20Abi, - address: assetAddress, - client: walletClient, - }); - - const amount = BigInt(EXTRA_ASSET_AMOUNT); - const hash = await contract.write.mint([hexAddress, amount]); - await publicClient.waitForTransactionReceipt({ hash }); - updateThrottle(assetName, hexAddress); - logger.info(`Sent ${amount} ${assetName} to ${hexAddress} in tx ${hash}`); - } catch (err) { - logger.error(`Failed to send ${assetName} to ${hexAddress}`); - throw err; - } -} - -/** - * Creates a router for the faucet. - * @param apiPrefix - The prefix to use for all api requests - * @returns - The router for handling status requests. - */ -function createRouter(apiPrefix: string) { - logger.info(`Creating router with prefix ${apiPrefix}`); - const router = new Router({ prefix: `${apiPrefix}` }); - router.get('/status', (ctx: Koa.Context) => { - ctx.status = 200; - }); - router.get('/drip/:address', async (ctx: Koa.Context) => { - const { address } = ctx.params; - const { asset } = ctx.query; - - if (!asset) { - throw new Error('No asset specified'); - } - - if (!isKnownAsset(asset)) { - throw new Error(`Unknown asset: "${asset}"`); - } - - if (asset === 'eth') { - await transferEth(EthAddress.fromString(address).toChecksumString()); - } else { - await transferAsset(asset, EthAddress.fromString(address).toChecksumString()); - } - - ctx.status = 200; - }); - return router; -} +const logger = createLogger('aztec:faucet:http'); /** * Create and start a new Aztec Node HTTP Server */ async function main() { - logger.info(`Setting up Aztec Faucet...`); - - const chain = createEthereumChain(rpcUrl, l1ChainId); - if (chain.chainInfo.id !== l1ChainId) { - throw new Error(`Incorrect chain id, expected ${chain.chainInfo.id}`); - } - - const shutdown = () => { - logger.info('Shutting down...'); - process.exit(0); - }; - - process.once('SIGINT', shutdown); - process.once('SIGTERM', shutdown); - - const app = new Koa(); - app.on('error', error => { - logger.error(`Error on API handler: ${error}`); - }); - const exceptionHandler = async (ctx: Koa.Context, next: () => Promise) => { - try { - await next(); - } catch (err: any) { - logger.error(err); - ctx.status = err instanceof ThrottleError ? 429 : 400; - ctx.body = { error: err.message }; - } - }; - app.use(exceptionHandler); - app.use(cors()); - const apiRouter = createRouter(API_PREFIX); - app.use(apiRouter.routes()); - app.use(apiRouter.allowedMethods()); - - const httpServer = http.createServer(app.callback()); - httpServer.listen(+FAUCET_PORT); - logger.info(`Aztec Faucet listening on port ${FAUCET_PORT}`); + const config = getFaucetConfigFromEnv(); + const faucet = await Faucet.create(config); + const httpServer = createFaucetHttpServer(faucet, '/', logger); + const port = parseInt(process.env?.AZTEC_PORT ?? '', 10) || 8080; + httpServer.listen(port); + logger.info(`Aztec Faucet listening on port ${port}`); await Promise.resolve(); } diff --git a/yarn-project/aztec-faucet/src/config.ts b/yarn-project/aztec-faucet/src/config.ts new file mode 100644 index 00000000000..6beb3b0c49b --- /dev/null +++ b/yarn-project/aztec-faucet/src/config.ts @@ -0,0 +1,58 @@ +import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum'; +import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config'; +import { EthAddress } from '@aztec/foundation/eth-address'; + +export type L1AssetConfig = { + address: EthAddress; + amount: bigint; +}; + +export type FaucetConfig = L1ReaderConfig & { + l1Mnemonic: string; + mnemonicAccountIndex: number; + interval: number; + ethAmount: string; + l1Assets: L1AssetConfig[]; +}; + +export const faucetConfigMapping: ConfigMappingsType = { + ...l1ReaderConfigMappings, + l1Mnemonic: { + env: 'MNEMONIC', + description: 'The mnemonic for the faucet account', + }, + mnemonicAccountIndex: { + env: 'FAUCET_MNEMONIC_ACCOUNT_INDEX', + description: 'The account to use', + ...numberConfigHelper(0), + }, + interval: { + env: 'FAUCET_INTERVAL_MS', + description: 'How often the faucet can be dripped', + ...numberConfigHelper(1 * 60 * 60 * 1000), // 1 hour + }, + ethAmount: { + env: 'FAUCET_ETH_AMOUNT', + description: 'How much eth the faucet should drip per call', + defaultValue: '1.0', + }, + l1Assets: { + env: 'FAUCET_L1_ASSETS', + description: 'Which other L1 assets the faucet is able to drip', + defaultValue: '', + parseEnv(val): L1AssetConfig[] { + const assetConfigs = val.split(','); + return assetConfigs.flatMap(assetConfig => { + if (!assetConfig) { + return []; + } + const [address, amount = '1e9'] = assetConfig.split(':'); + return [{ address: EthAddress.fromString(address), amount: BigInt(amount) }]; + }); + }, + }, +}; + +export function getFaucetConfigFromEnv(): FaucetConfig { + return getConfigFromMappings(faucetConfigMapping); +} diff --git a/yarn-project/aztec-faucet/src/faucet.ts b/yarn-project/aztec-faucet/src/faucet.ts new file mode 100644 index 00000000000..3697d5786d6 --- /dev/null +++ b/yarn-project/aztec-faucet/src/faucet.ts @@ -0,0 +1,168 @@ +import { createEthereumChain } from '@aztec/ethereum'; +import { type EthAddress } from '@aztec/foundation/eth-address'; +import { createLogger } from '@aztec/foundation/log'; +import { TestERC20Abi } from '@aztec/l1-artifacts'; + +import { + type Account, + type Chain, + type GetContractReturnType, + type HttpTransport, + type LocalAccount, + type PublicClient, + http as ViemHttp, + type WalletClient, + createPublicClient, + createWalletClient, + getContract, + parseEther, +} from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; + +import { type FaucetConfig, type L1AssetConfig } from './config.js'; + +type L1Asset = { + contract: GetContractReturnType>; + amount: bigint; +}; + +export class Faucet { + private walletClient: WalletClient; + private publicClient: PublicClient; + + private dripHistory = new Map>(); + private l1Assets = new Map(); + + constructor( + private config: FaucetConfig, + private account: LocalAccount, + private timeFn: () => number = Date.now, + private log = createLogger('aztec:faucet'), + ) { + const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); + + this.walletClient = createWalletClient({ + account: this.account, + chain: chain.chainInfo, + transport: ViemHttp(chain.rpcUrl), + }); + + this.publicClient = createPublicClient({ + chain: chain.chainInfo, + transport: ViemHttp(chain.rpcUrl), + }); + } + + public static async create(config: FaucetConfig): Promise { + if (!config.l1Mnemonic) { + throw new Error('Missing faucet mnemonic'); + } + + const account = mnemonicToAccount(config.l1Mnemonic, { addressIndex: config.mnemonicAccountIndex }); + const faucet = new Faucet(config, account); + + for (const asset of config.l1Assets) { + await faucet.addL1Asset(asset); + } + + return faucet; + } + + public send(to: EthAddress, assetName: string): Promise { + if (assetName.toUpperCase() === 'ETH') { + return this.sendEth(to); + } else { + return this.sendERC20(to, assetName); + } + } + + public async sendEth(to: EthAddress): Promise { + this.checkThrottle(to, 'ETH'); + + const hash = await this.walletClient.sendTransaction({ + account: this.account, + to: to.toString(), + value: parseEther(this.config.ethAmount), + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + + this.updateThrottle(to, 'ETH'); + this.log.info(`Sent ETH ${this.config.ethAmount} to ${to} in tx ${hash}`); + } + + public async sendERC20(to: EthAddress, assetName: string): Promise { + const asset = this.l1Assets.get(assetName); + if (!asset) { + throw new UnknownAsset(assetName); + } + + this.checkThrottle(to, assetName); + + const hash = await asset.contract.write.mint([to.toString(), asset.amount]); + await this.publicClient.waitForTransactionReceipt({ hash }); + + this.updateThrottle(to, assetName); + + this.log.info(`Sent ${assetName} ${asset.amount} to ${to} in tx ${hash}`); + } + + public async addL1Asset(l1AssetConfig: L1AssetConfig): Promise { + const contract = getContract({ + abi: TestERC20Abi, + address: l1AssetConfig.address.toString(), + client: this.walletClient, + }); + + const [name, owner] = await Promise.all([contract.read.name(), contract.read.owner()]); + + if (owner !== this.account.address) { + throw new Error( + `Owner mismatch. Expected contract ${name} to be owned by ${this.account.address}, received ${owner}`, + ); + } + + if ( + this.l1Assets.has(name) && + this.l1Assets.get(name)!.contract.address.toLowerCase() !== l1AssetConfig.address.toString().toLowerCase() + ) { + this.log.warn(`Updating asset ${name} to address=${contract.address}`); + } + + this.l1Assets.set(name, { contract, amount: l1AssetConfig.amount }); + } + + private checkThrottle(address: EthAddress, asset = 'ETH') { + const addressHistory = this.dripHistory.get(address.toString()); + if (!addressHistory) { + return; + } + + const now = this.timeFn(); + const last = addressHistory.get(asset); + if (typeof last === 'number' && last + this.config.interval > now) { + throw new ThrottleError(address.toString(), asset); + } + } + + private updateThrottle(address: EthAddress, asset = 'ETH') { + const addressStr = address.toString(); + if (!this.dripHistory.has(addressStr)) { + this.dripHistory.set(addressStr, new Map()); + } + + const addressHistory = this.dripHistory.get(addressStr)!; + addressHistory.set(asset, this.timeFn()); + } +} + +export class ThrottleError extends Error { + constructor(address: string, asset: string) { + super(`Not funding ${asset}: throttled ${address}`); + } +} + +export class UnknownAsset extends Error { + constructor(asset: string) { + super(`Unknown asset: ${asset}`); + } +} diff --git a/yarn-project/aztec-faucet/src/http.ts b/yarn-project/aztec-faucet/src/http.ts new file mode 100644 index 00000000000..7e2e10723d6 --- /dev/null +++ b/yarn-project/aztec-faucet/src/http.ts @@ -0,0 +1,84 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { createLogger } from '@aztec/foundation/log'; +import { type ApiSchemaFor, schemas } from '@aztec/foundation/schemas'; + +import cors from '@koa/cors'; +import { createServer } from 'http'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import Router from 'koa-router'; +import { z } from 'zod'; + +import { type Faucet, ThrottleError } from './faucet.js'; + +export function createFaucetHttpServer(faucet: Faucet, apiPrefix = '', logger = createLogger('aztec:faucet:http')) { + const router = new Router({ prefix: `${apiPrefix}` }); + router.get('/drip/:address', async ctx => { + const { address } = ctx.params; + const { asset } = ctx.query; + + if (typeof asset !== 'string') { + throw new Error(`Bad asset: [${asset}]`); + } + + await faucet.send(EthAddress.fromString(address), asset); + + ctx.status = 200; + }); + + const L1AssetRequestSchema = z.object({ + address: z.string().transform(str => EthAddress.fromString(str)), + amount: z.string().transform(str => BigInt(str)), + }); + + router.post('/l1-asset', async ctx => { + if (!ctx.request.body) { + throw new Error('Invalid request body'); + } + + const result = L1AssetRequestSchema.safeParse(ctx.request.body); + + if (!result.success) { + throw new Error(`Invalid request: ${result.error.message}`); + } + + const { address, amount } = result.data; + + await faucet.addL1Asset({ address, amount }); + + ctx.status = 200; + }); + + const app = new Koa(); + + app.on('error', error => { + logger.error(`Error on API handler: ${error}`); + }); + + app.use(async (ctx, next) => { + try { + await next(); + } catch (err: any) { + logger.error(err); + ctx.status = err instanceof ThrottleError ? 429 : 400; + ctx.body = { error: err.message }; + } + }); + + app.use(cors()); + app.use(bodyParser()); + app.use(router.routes()); + app.use(router.allowedMethods()); + + return createServer(app.callback()); +} + +export const FaucetSchema: ApiSchemaFor = { + send: z.function().args(schemas.EthAddress, z.string()).returns(z.void()), + sendERC20: z.function().args(schemas.EthAddress, z.string()).returns(z.void()), + sendEth: z.function().args(schemas.EthAddress).returns(z.void()), + addL1Asset: z + .function() + .args(z.object({ address: schemas.EthAddress, amount: z.bigint() })) + .returns(z.void()), +}; diff --git a/yarn-project/aztec-faucet/src/index.ts b/yarn-project/aztec-faucet/src/index.ts new file mode 100644 index 00000000000..e4e8281d7f0 --- /dev/null +++ b/yarn-project/aztec-faucet/src/index.ts @@ -0,0 +1,3 @@ +export * from './faucet.js'; +export * from './http.js'; +export * from './config.js'; diff --git a/yarn-project/aztec/package.json b/yarn-project/aztec/package.json index 7e9611f7afb..4f83270a6b2 100644 --- a/yarn-project/aztec/package.json +++ b/yarn-project/aztec/package.json @@ -30,6 +30,7 @@ "dependencies": { "@aztec/accounts": "workspace:^", "@aztec/archiver": "workspace:^", + "@aztec/aztec-faucet": "workspace:^", "@aztec/aztec-node": "workspace:^", "@aztec/aztec.js": "workspace:^", "@aztec/bb-prover": "workspace:^", diff --git a/yarn-project/aztec/src/cli/aztec_start_options.ts b/yarn-project/aztec/src/cli/aztec_start_options.ts index 6b0b5c90470..06ae9c449d0 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.ts @@ -1,4 +1,5 @@ import { type ArchiverConfig, archiverConfigMappings } from '@aztec/archiver'; +import { faucetConfigMapping } from '@aztec/aztec-faucet'; import { sequencerClientConfigMappings } from '@aztec/aztec-node'; import { botConfigMappings } from '@aztec/bot'; import { @@ -336,4 +337,26 @@ export const aztecStartOptions: { [key: string]: AztecStartOption[] } = { envVar: undefined, }, ], + FAUCET: [ + { + flag: '--faucet', + description: 'Starts the Aztec faucet', + defaultValue: undefined, + envVar: undefined, + }, + { + flag: '--faucet.apiServer', + description: 'Starts a simple HTTP server to access the faucet', + defaultValue: true, + envVar: undefined, + }, + { + flag: '--faucet.apiServerPort ', + description: 'The port on which to start the api server on', + defaultValue: 8080, + envVar: undefined, + parseVal: val => parseInt(val, 10), + }, + ...getOptions('faucet', faucetConfigMapping), + ], }; diff --git a/yarn-project/aztec/src/cli/cli.ts b/yarn-project/aztec/src/cli/cli.ts index 9655f5169a7..3273aa715a7 100644 --- a/yarn-project/aztec/src/cli/cli.ts +++ b/yarn-project/aztec/src/cli/cli.ts @@ -108,6 +108,9 @@ export function injectAztecCommands(program: Command, userLog: LogFn, debugLogge } else if (options.sequencer) { userLog(`Cannot run a standalone sequencer without a node`); process.exit(1); + } else if (options.faucet) { + const { startFaucet } = await import('./cmds/start_faucet.js'); + await startFaucet(options, signalHandlers, services, userLog); } else { userLog(`No module specified to start`); process.exit(1); diff --git a/yarn-project/aztec/src/cli/cmds/start_faucet.ts b/yarn-project/aztec/src/cli/cmds/start_faucet.ts new file mode 100644 index 00000000000..3b0ff4c78aa --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/start_faucet.ts @@ -0,0 +1,34 @@ +import { + Faucet, + FaucetSchema, + createFaucetHttpServer, + faucetConfigMapping, + getFaucetConfigFromEnv, +} from '@aztec/aztec-faucet'; +import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server'; +import { type LogFn } from '@aztec/foundation/log'; + +import { extractNamespacedOptions, extractRelevantOptions } from '../util.js'; + +export async function startFaucet( + options: any, + signalHandlers: (() => Promise)[], + services: NamespacedApiHandlers, + log: LogFn, +) { + const faucetOptions = extractNamespacedOptions(options, 'faucet'); + const config = { + ...getFaucetConfigFromEnv(), + ...extractRelevantOptions(options, faucetConfigMapping, 'faucet'), + }; + + const faucet = await Faucet.create(config); + if (faucetOptions.apiServer) { + const httpServer = createFaucetHttpServer(faucet); + httpServer.listen(faucetOptions.apiServerPort); + signalHandlers.push(() => new Promise(res => httpServer.close(() => res()))); + log(`Faucet now running on port: ${faucetOptions.apiServerPort}`); + } + + services.faucet = [faucet, FaucetSchema]; +} diff --git a/yarn-project/aztec/tsconfig.json b/yarn-project/aztec/tsconfig.json index a37913279c4..6f0e2ced57d 100644 --- a/yarn-project/aztec/tsconfig.json +++ b/yarn-project/aztec/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../archiver" }, + { + "path": "../aztec-faucet" + }, { "path": "../aztec-node" }, diff --git a/yarn-project/foundation/package.json b/yarn-project/foundation/package.json index 1f3bf0f9e97..616a186ef13 100644 --- a/yarn-project/foundation/package.json +++ b/yarn-project/foundation/package.json @@ -140,7 +140,6 @@ "@types/koa": "^2.13.5", "@types/koa-bodyparser": "^4.3.10", "@types/koa-compress": "^4.0.3", - "@types/koa-cors": "^0.0.2", "@types/koa-router": "^7.4.4", "@types/koa__cors": "^4.0.0", "@types/leveldown": "^4.0.3", diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 8f6bd085fd3..cc86e585953 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -184,4 +184,8 @@ export type EnvVar = | 'L1_TX_MONITOR_MAX_ATTEMPTS' | 'L1_TX_MONITOR_CHECK_INTERVAL_MS' | 'L1_TX_MONITOR_STALL_TIME_MS' - | 'L1_TX_MONITOR_TX_TIMEOUT_MS'; + | 'L1_TX_MONITOR_TX_TIMEOUT_MS' + | 'FAUCET_MNEMONIC_ACCOUNT_INDEX' + | 'FAUCET_ETH_AMOUNT' + | 'FAUCET_INTERVAL_MS' + | 'FAUCET_L1_ASSETS'; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index df1012b9498..2a4d38a760d 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -119,7 +119,7 @@ __metadata: languageName: unknown linkType: soft -"@aztec/aztec-faucet@workspace:aztec-faucet": +"@aztec/aztec-faucet@workspace:^, @aztec/aztec-faucet@workspace:aztec-faucet": version: 0.0.0-use.local resolution: "@aztec/aztec-faucet@workspace:aztec-faucet" dependencies: @@ -127,15 +127,18 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/l1-artifacts": "workspace:^" "@jest/globals": ^29.5.0 + "@koa/cors": ^5.0.0 "@types/jest": ^29.5.0 + "@types/koa-bodyparser": ^4.3.12 "@types/node": ^18.7.23 jest: ^29.5.0 koa: ^2.14.2 - koa-cors: ^0.0.16 + koa-bodyparser: ^4.4.1 koa-router: ^12.0.0 ts-node: ^10.9.1 typescript: ^5.0.4 viem: ^2.7.15 + zod: ^3.23.8 bin: aztec-faucet: ./dest/bin/index.js languageName: unknown @@ -234,6 +237,7 @@ __metadata: dependencies: "@aztec/accounts": "workspace:^" "@aztec/archiver": "workspace:^" + "@aztec/aztec-faucet": "workspace:^" "@aztec/aztec-node": "workspace:^" "@aztec/aztec.js": "workspace:^" "@aztec/bb-prover": "workspace:^" @@ -674,7 +678,6 @@ __metadata: "@types/koa": ^2.13.5 "@types/koa-bodyparser": ^4.3.10 "@types/koa-compress": ^4.0.3 - "@types/koa-cors": ^0.0.2 "@types/koa-router": ^7.4.4 "@types/koa__cors": ^4.0.0 "@types/leveldown": ^4.0.3 @@ -5195,7 +5198,7 @@ __metadata: languageName: node linkType: hard -"@types/koa-bodyparser@npm:^4.3.10": +"@types/koa-bodyparser@npm:^4.3.10, @types/koa-bodyparser@npm:^4.3.12": version: 4.3.12 resolution: "@types/koa-bodyparser@npm:4.3.12" dependencies: @@ -5223,15 +5226,6 @@ __metadata: languageName: node linkType: hard -"@types/koa-cors@npm:^0.0.2": - version: 0.0.2 - resolution: "@types/koa-cors@npm:0.0.2" - dependencies: - "@types/koa": "*" - checksum: 7218bd8f4600fede227626e01fabe2022c691ee8721945792eb3dba3b348b10ddc438c3a679734de783172be512eb6b780d0600ed7052c3f881ed234a601656e - languageName: node - linkType: hard - "@types/koa-router@npm:^7.4.4, @types/koa-router@npm:^7.4.8": version: 7.4.8 resolution: "@types/koa-router@npm:7.4.8" @@ -13688,7 +13682,7 @@ __metadata: languageName: node linkType: hard -"koa-bodyparser@npm:^4.4.0": +"koa-bodyparser@npm:^4.4.0, koa-bodyparser@npm:^4.4.1": version: 4.4.1 resolution: "koa-bodyparser@npm:4.4.1" dependencies: @@ -13728,13 +13722,6 @@ __metadata: languageName: node linkType: hard -"koa-cors@npm:^0.0.16": - version: 0.0.16 - resolution: "koa-cors@npm:0.0.16" - checksum: 66d85b7a4834c436a6bfbe77248a19edebc2082c5e73d907c680a96f75ec1963146024b29cc3bb04c7790e52c6f0067f3dd71a0e341bdcc83dbc6971b86a3339 - languageName: node - linkType: hard - "koa-etag@npm:^4.0.0": version: 4.0.0 resolution: "koa-etag@npm:4.0.0"