diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e7b92e0a3c..ff3c13631a6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -465,6 +465,17 @@ jobs: name: "Build and test" command: build aztec-node | add_timestamps + aztec-faucet: + machine: + image: ubuntu-2204:2023.07.2 + resource_class: large + steps: + - *checkout + - *setup_env + - run: + name: "Build and test" + command: build aztec-faucet | add_timestamps + pxe-x86_64: machine: image: ubuntu-2204:2023.07.2 @@ -1263,6 +1274,11 @@ workflows: - yarn-project <<: *defaults + - aztec-faucet: + requires: + - yarn-project + <<: *defaults + - pxe-x86_64: requires: - yarn-project diff --git a/build_manifest.yml b/build_manifest.yml index e1c9d907f78..c3d413b1552 100644 --- a/build_manifest.yml +++ b/build_manifest.yml @@ -113,6 +113,12 @@ aztec-sandbox: dependencies: - yarn-project +aztec-faucet: + buildDir: yarn-project + projectDir: yarn-project/aztec-faucet + dependencies: + - yarn-project + pxe: buildDir: yarn-project projectDir: yarn-project/pxe diff --git a/yarn-project/aztec-faucet/.dockerignore b/yarn-project/aztec-faucet/.dockerignore new file mode 100644 index 00000000000..2b30eaf4896 --- /dev/null +++ b/yarn-project/aztec-faucet/.dockerignore @@ -0,0 +1,4 @@ +data +dest +node_modules +Dockerfile \ No newline at end of file diff --git a/yarn-project/aztec-faucet/.eslintrc.cjs b/yarn-project/aztec-faucet/.eslintrc.cjs new file mode 100644 index 00000000000..e659927475c --- /dev/null +++ b/yarn-project/aztec-faucet/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@aztec/foundation/eslint'); diff --git a/yarn-project/aztec-faucet/.gitignore b/yarn-project/aztec-faucet/.gitignore new file mode 100644 index 00000000000..81efe293f26 --- /dev/null +++ b/yarn-project/aztec-faucet/.gitignore @@ -0,0 +1 @@ +/data* diff --git a/yarn-project/aztec-faucet/Dockerfile b/yarn-project/aztec-faucet/Dockerfile new file mode 100644 index 00000000000..d524e04c8ef --- /dev/null +++ b/yarn-project/aztec-faucet/Dockerfile @@ -0,0 +1,14 @@ +FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/yarn-project AS builder + +WORKDIR /usr/src/yarn-project/aztec-faucet + +# Productionify. See comment in yarn-project-base/Dockerfile. +RUN yarn cache clean && yarn workspaces focus --production + +# Create final, minimal size image. +FROM node:18-alpine +COPY --from=builder /usr/src/ /usr/src/ +WORKDIR /usr/src/yarn-project/aztec-faucet +ENTRYPOINT ["yarn"] +CMD [ "start" ] +EXPOSE 8080 diff --git a/yarn-project/aztec-faucet/README.md b/yarn-project/aztec-faucet/README.md new file mode 100644 index 00000000000..44427ad4f0c --- /dev/null +++ b/yarn-project/aztec-faucet/README.md @@ -0,0 +1,3 @@ +# Aztec Faucet + +This application allows someone to obtain a small amount of eth via a http endpoint. diff --git a/yarn-project/aztec-faucet/package.json b/yarn-project/aztec-faucet/package.json new file mode 100644 index 00000000000..df0541d920c --- /dev/null +++ b/yarn-project/aztec-faucet/package.json @@ -0,0 +1,61 @@ +{ + "name": "@aztec/aztec-faucet", + "version": "0.1.0", + "main": "dest/bin/index.js", + "type": "module", + "bin": "./dest/bin/index.js", + "typedocOptions": { + "entryPoints": [ + "./src/bin/index.ts" + ], + "name": "Aztec Faucet", + "tsconfig": "./tsconfig.json" + }, + "scripts": { + "start": "node --no-warnings ./dest/bin", + "build": "yarn clean && tsc -b", + "build:dev": "tsc -b --watch", + "clean": "rm -rf ./dest .tsbuildinfo", + "formatting": "run -T prettier --check ./src && run -T eslint ./src", + "formatting:fix": "run -T prettier -w ./src", + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests" + }, + "inherits": [ + "../package.common.json" + ], + "jest": { + "preset": "ts-jest/presets/default-esm", + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.m?js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", + "rootDir": "./src" + }, + "dependencies": { + "@aztec/ethereum": "workspace:^", + "@aztec/foundation": "workspace:^", + "koa": "^2.14.2", + "koa-cors": "^0.0.16", + "koa-router": "^12.0.0", + "viem": "^1.2.5" + }, + "devDependencies": { + "@jest/globals": "^29.5.0", + "@rushstack/eslint-patch": "^1.1.4", + "@types/jest": "^29.5.0", + "@types/node": "^18.7.23", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "files": [ + "dest", + "src", + "!*.test.*" + ], + "types": "./dest/index.d.ts", + "engines": { + "node": ">=18" + } +} diff --git a/yarn-project/aztec-faucet/src/bin/index.ts b/yarn-project/aztec-faucet/src/bin/index.ts new file mode 100644 index 00000000000..df33695c57b --- /dev/null +++ b/yarn-project/aztec-faucet/src/bin/index.ts @@ -0,0 +1,153 @@ +#!/usr/bin/env -S node --no-warnings +import { NULL_KEY, createEthereumChain } from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import http from 'http'; +import Koa from 'koa'; +import cors from 'koa-cors'; +import Router from 'koa-router'; +import { Hex, http as ViemHttp, createWalletClient, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +const { + FAUCET_PORT = 8082, + API_PREFIX = '', + API_KEY = '', + RPC_URL = '', + CHAIN_ID = '', + PRIVATE_KEY = '', + INTERVAL = '', + ETH_AMOUNT = '', +} = process.env; + +const logger = createDebugLogger('aztec:faucet'); + +const rpcUrl = RPC_URL; +const apiKey = API_KEY; +const chainId = +CHAIN_ID; +const privateKey: Hex = PRIVATE_KEY ? createHex(PRIVATE_KEY) : NULL_KEY; +const interval = +INTERVAL; +const mapping: { [key: Hex]: Date } = {}; + +/** + * 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(address: Hex) { + if (mapping[address] === undefined) { + return; + } + const last = mapping[address]; + const current = new Date(); + const diff = (current.getTime() - last.getTime()) / 1000; + if (diff < interval) { + throw new Error(`Not funding address ${address}, please try again later`); + } +} + +/** + * Helper function to send some ETH to the given address + * @param address - Address to receive some ETH + */ +async function transferEth(address: string) { + const chain = createEthereumChain(rpcUrl, apiKey); + + const account = privateKeyToAccount(privateKey); + const walletClient = createWalletClient({ + account: account, + chain: chain.chainInfo, + transport: ViemHttp(chain.rpcUrl), + }); + const hexAddress = createHex(address); + checkThrottle(hexAddress); + try { + const hash = await walletClient.sendTransaction({ + account, + to: hexAddress, + value: parseEther(ETH_AMOUNT), + }); + mapping[hexAddress] = new Date(); + logger.info(`Sent ${ETH_AMOUNT} ETH to ${hexAddress} in tx ${hash}`); + } catch (error) { + logger.error(`Failed to send eth to ${hexAddress}`); + throw error; + } +} + +/** + * 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; + await transferEth(EthAddress.fromString(address).toChecksumString()); + ctx.status = 200; + }); + return router; +} + +/** + * Create and start a new Aztec Node HTTP Server + */ +async function main() { + logger.info(`Setting up Aztec Faucet...`); + + const chain = createEthereumChain(rpcUrl, apiKey); + if (chain.chainInfo.id !== chainId) { + 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 = 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}`); + await Promise.resolve(); +} + +main().catch(err => { + logger.error(err); + process.exit(1); +}); diff --git a/yarn-project/aztec-faucet/terraform/main.tf b/yarn-project/aztec-faucet/terraform/main.tf new file mode 100644 index 00000000000..8a1f901fd09 --- /dev/null +++ b/yarn-project/aztec-faucet/terraform/main.tf @@ -0,0 +1,219 @@ +terraform { + backend "s3" { + bucket = "aztec-terraform" + region = "eu-west-2" + } + required_providers { + aws = { + source = "hashicorp/aws" + version = "3.74.2" + } + } +} + +# Define provider and region +provider "aws" { + region = "eu-west-2" +} + +data "terraform_remote_state" "setup_iac" { + backend = "s3" + config = { + bucket = "aztec-terraform" + key = "setup/setup-iac" + region = "eu-west-2" + } +} + +data "terraform_remote_state" "aztec2_iac" { + backend = "s3" + config = { + bucket = "aztec-terraform" + key = "aztec2/iac" + region = "eu-west-2" + } +} + + +resource "aws_cloudwatch_log_group" "aztec-faucet" { + name = "/fargate/service/${var.DEPLOY_TAG}/aztec-faucet" + retention_in_days = 14 +} + +resource "aws_service_discovery_service" "aztec-faucet" { + name = "${var.DEPLOY_TAG}-aztec-faucet" + + health_check_custom_config { + failure_threshold = 1 + } + + dns_config { + namespace_id = data.terraform_remote_state.setup_iac.outputs.local_service_discovery_id + + dns_records { + ttl = 60 + type = "A" + } + + dns_records { + ttl = 60 + type = "SRV" + } + + routing_policy = "MULTIVALUE" + } + + # Terraform just fails if this resource changes and you have registered instances. + provisioner "local-exec" { + when = destroy + command = "${path.module}/../servicediscovery-drain.sh ${self.id}" + } +} + +# Define task definition and service. +resource "aws_ecs_task_definition" "aztec-faucet" { + family = "${var.DEPLOY_TAG}-aztec-faucet" + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + cpu = "2048" + memory = "4096" + execution_role_arn = data.terraform_remote_state.setup_iac.outputs.ecs_task_execution_role_arn + task_role_arn = data.terraform_remote_state.aztec2_iac.outputs.cloudwatch_logging_ecs_role_arn + + container_definitions = <