├── .nvmrc ├── apps ├── web │ ├── .eslintrc.json │ ├── public │ │ ├── favicon.ico │ │ └── meta-mask-fox.svg │ ├── utils │ │ ├── inter.ts │ │ ├── capitalize.ts │ │ ├── useLastAddress.tsx │ │ ├── firebase-config.ts │ │ ├── history.ts │ │ ├── use-media-query.ts │ │ ├── balance.ts │ │ ├── captcha-verify.ts │ │ ├── firebase.client.ts │ │ └── firebase.serverside.ts │ ├── @ │ │ ├── lib │ │ │ └── utils.ts │ │ └── components │ │ │ └── ui │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ ├── pages │ │ ├── _document.tsx │ │ ├── _app.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...nextauth].ts │ │ │ └── faucet.ts │ │ └── [chain].tsx │ ├── next.config.js │ ├── components.json │ ├── .gitignore │ ├── tsconfig.json │ ├── styles │ │ ├── FaucetHeader.module.css │ │ ├── Form.module.css │ │ ├── globals.css │ │ └── Home.module.css │ ├── components │ │ ├── mode-toggle.tsx │ │ ├── logo.tsx │ │ ├── faucet-header.tsx │ │ ├── github-auth.tsx │ │ ├── faucet-status.tsx │ │ ├── setup-button.tsx │ │ └── request-form.tsx │ ├── types │ │ └── index.ts │ ├── package.json │ ├── config │ │ └── chains.ts │ └── README.md └── firebase │ ├── firebase.json │ ├── scripts │ ├── tsconfig.json │ └── cli.ts │ ├── jest.config.js │ ├── .firebaserc │ ├── .vscode │ └── extensions.json │ ├── src │ ├── get-qualified-mount.ts │ ├── utils.ts │ ├── index.ts │ ├── get-qualified-mount.test.ts │ ├── celo-adapter.ts │ ├── metrics.ts │ ├── config.ts │ ├── celo-adapter.test.ts │ └── database-helper.ts │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ ├── database-rules.bolt │ └── LICENSE ├── .yarnrc.yml ├── prettier.config.js ├── postcss.config.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── FEATURE-FORM.yml │ └── BUG-FORM.yml ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── ci.yaml │ └── deploy-chain.yaml ├── .husky └── pre-commit ├── .mergify.yml ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── package.json ├── renovate.json ├── .gitignore ├── CONTRIBUTING.md ├── readme.md ├── .eslintrc.js └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.3.1.cjs 4 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celo-org/faucet/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: false, 4 | singleQuote: true, 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/utils/inter.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | export const inter = Inter({ subsets: ['latin'] }) 3 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /apps/web/@/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Github Discussions 4 | url: https://github.com/celo-org/faucet/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /apps/firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": ".", 4 | "predeploy": ["yarn lint", "yarn build", "yarn build:rules"] 5 | }, 6 | "database": [{ "target": "faucet", "rules": "database-rules.bolt" }] 7 | } 8 | -------------------------------------------------------------------------------- /apps/firebase/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "." 6 | }, 7 | "include": ["."], 8 | "references": [{ "path": ".." }] 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(word: string) { 2 | const words = word.split('-') 3 | return words 4 | .map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase()) 5 | .join(' ') 6 | .trim() 7 | // } 8 | } 9 | -------------------------------------------------------------------------------- /apps/firebase/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['**/src/**/*.ts?(x)', '!**/*.d.ts'], 3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 4 | testMatch: ['**/?(*.)(spec|test).ts?(x)'], 5 | transform: { 6 | '\\.(ts|tsx)$': 'ts-jest', 7 | '^.+\\.jsx?$': 'babel-jest', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # For details on acceptable file patterns, please refer to the [Github Documentation](https://help.github.com/articles/about-codeowners/) 4 | 5 | # default owners, overridden by package specific owners below 6 | * @celo-org/devtooling -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | allowedDevOrigins: ['localhost'], 5 | async redirects() { 6 | return [ 7 | { 8 | source: '/', 9 | destination: '/celo-sepolia', 10 | permanent: false, 11 | }, 12 | ] 13 | }, 14 | } 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /apps/web/utils/useLastAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { getAddresses } from 'utils/history' 3 | 4 | export function useLastAddress() { 5 | const [lastAddress, setLastAddress] = useState() 6 | useEffect(() => { 7 | const lastUsedAddress = getAddresses().at(0) 8 | setLastAddress(lastUsedAddress) 9 | }, []) 10 | 11 | return lastAddress 12 | } 13 | -------------------------------------------------------------------------------- /apps/firebase/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "celo-faucet" 4 | }, 5 | "targets": { 6 | "celo-faucet": { 7 | "database": { 8 | "faucet": [ 9 | "celo-faucet" 10 | ] 11 | } 12 | }, 13 | "celo-faucet-staging": { 14 | "database": { 15 | "faucet": [ 16 | "celo-faucet-staging" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | 6 | # This block does prettier formatting and adds it to commit 7 | FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') 8 | [ -z "$FILES" ] && exit 0 9 | 10 | # Prettify all selected files 11 | echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write 12 | 13 | # Add back the modified/prettified files to staging 14 | echo "$FILES" | xargs git add 15 | -------------------------------------------------------------------------------- /apps/firebase/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [ 11 | 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/firebase/src/get-qualified-mount.ts: -------------------------------------------------------------------------------- 1 | import { NetworkConfig } from './config' 2 | import { AuthLevel } from './database-helper' 3 | 4 | export function getQualifiedAmount( 5 | authLevel: AuthLevel, 6 | config: NetworkConfig, 7 | ): { celoAmount: bigint } { 8 | switch (authLevel) { 9 | case undefined: 10 | case AuthLevel.none: 11 | return { 12 | celoAmount: config.faucetGoldAmount, 13 | } 14 | case AuthLevel.authenticated: 15 | return { 16 | celoAmount: config.authenticatedGoldAmount, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | env-config.js 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /apps/firebase/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function withTimeLog( 2 | name: string, 3 | f: (...args: any[]) => Promise, 4 | ) { 5 | return async (...args: Parameters): Promise => { 6 | try { 7 | console.time(name) 8 | return await f(...args) 9 | } finally { 10 | console.timeEnd(name) 11 | } 12 | } 13 | } 14 | 15 | export async function runWithTimeLog( 16 | name: string, 17 | f: () => Promise, 18 | ): Promise { 19 | try { 20 | console.time(name) 21 | return await f() 22 | } finally { 23 | console.timeEnd(name) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/utils/firebase-config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_KEY, 3 | authDomain: `${process.env.NEXT_PUBLIC_FIREBASE_PID}.firebaseapp.com`, 4 | databaseURL: `https://${process.env.NEXT_PUBLIC_FIREBASE_PID}.firebaseio.com`, 5 | projectId: `${process.env.NEXT_PUBLIC_FIREBASE_PID}`, 6 | storageBucket: `${process.env.NEXT_PUBLIC_FIREBASE_PID}.appspot.com`, 7 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_ID, 8 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 9 | measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASURE_ID, 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/@/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | function Label({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | ) 21 | } 22 | 23 | export { Label } 24 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - "-merged" 6 | 7 | pull_request_rules: 8 | - name: Automatically merge on CI success and code review 9 | conditions: 10 | # Add this label when you are ready to automerge the pull request. 11 | - "label=automerge" 12 | # Exclude drafts 13 | - "-draft" 14 | # At least one approval required 15 | - "#approved-reviews-by>=1" 16 | actions: 17 | queue: 18 | method: squash 19 | name: default 20 | commit_message_template: "{{title}}" 21 | delete_head_branch: 22 | force: False -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "baseUrl": ".", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@": ["../../node_modules/shadcn"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/firebase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "typescript-tslint-plugin" 6 | } 7 | ], 8 | "lib": ["ES2020"], 9 | "module": "commonjs", 10 | "strict": true, 11 | "allowJs": false, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "target": "es2020", 17 | "rootDir": "src", 18 | "outDir": "./dist", 19 | "skipLibCheck": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "preserveConstEnums": true, 23 | "composite": true 24 | }, 25 | "include": ["src", "script"], 26 | "exclude": ["dist"], 27 | "compileOnSave": true 28 | } 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | _A few sentences describing the overall effects and goals of the pull request's commits. 4 | What is the current behavior, and what is the updated/expected behavior with this PR?_ 5 | 6 | ### Other changes 7 | 8 | _Describe any minor or "drive-by" changes here._ 9 | 10 | ### Tested 11 | 12 | _An explanation of how the changes were tested or an explanation as to why they don't need to be._ 13 | 14 | ### Related issues 15 | 16 | - Fixes #[issue number here] 17 | 18 | ### Backwards compatibility 19 | 20 | _Brief explanation of why these changes are/are not backwards compatible._ 21 | 22 | ### Documentation 23 | 24 | _The set of community facing docs that have been added/modified because of this change_ -------------------------------------------------------------------------------- /apps/web/utils/history.ts: -------------------------------------------------------------------------------- 1 | const HISTORY = 'fauceted-addresses' 2 | 3 | export function saveAddress(address: string) { 4 | const listOfAddresses = retrieve() 5 | 6 | const nextList = [ 7 | address, 8 | ...listOfAddresses.filter((addr) => addr !== address), 9 | ] 10 | 11 | localStorage.setItem(HISTORY, JSON.stringify(nextList)) 12 | return nextList 13 | } 14 | 15 | export function getAddresses() { 16 | const list = retrieve() 17 | return list 18 | } 19 | 20 | function retrieve() { 21 | const rawList = localStorage.getItem(HISTORY) 22 | 23 | let listOfAddresses: string[] = [] 24 | 25 | if (typeof rawList === 'string') { 26 | listOfAddresses = JSON.parse(rawList) as string[] 27 | } 28 | 29 | return listOfAddresses 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/styles/FaucetHeader.module.css: -------------------------------------------------------------------------------- 1 | .top { 2 | width: 100vw; 3 | } 4 | 5 | .topBar { 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .logo { 12 | margin-top: 20px; 13 | margin-left: 20px; 14 | } 15 | 16 | .notice { 17 | position: relative; 18 | left: 0; 19 | right: 0; 20 | background-color: #1e002b; 21 | font-family: 'Inter', Arial, Helvetica, sans-serif; 22 | text-align: center; 23 | color: white; 24 | padding: 0.5rem; 25 | border-bottom: 2px solid #fcff52; 26 | } 27 | 28 | @media (max-width: 700px) { 29 | .top { 30 | border-bottom: 1px solid var(--foreground-rgb); 31 | } 32 | .logo { 33 | margin-top: 4px; 34 | } 35 | .logo svg { 36 | width: 70px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature 3 | labels: ['feature request'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please ensure that the feature has not already been requested in the issue tracker. 9 | 10 | Thanks for helping us improve the faucet! 11 | - type: textarea 12 | attributes: 13 | label: Describe the feature you would like 14 | description: Please also describe what the feature is aiming to solve, if relevant. 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Additional context 20 | description: Add any other context to the feature (like screenshots, code snippets, resources) 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "eamodio.gitlens", 8 | "dbaeumer.vscode-eslint", 9 | "esbenp.prettier-vscode", 10 | "juanblanco.solidity", 11 | "redhat.vscode-yaml", 12 | "smkamranqadri.vscode-bolt-language", 13 | "pkief.material-icon-theme", 14 | "davidanson.vscode-markdownlint", 15 | "mikestead.dotenv", 16 | "coenraads.bracket-pair-colorizer-2" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/@/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<'input'>) { 6 | return ( 7 | 16 | ) 17 | } 18 | 19 | export { Input } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mono-faucet", 3 | "version": "1.0.0", 4 | "description": "Repo for faucet UI and firebase functions", 5 | "main": "index.js", 6 | "repository": "https://github.com/celo-org/faucet.git", 7 | "author": "cLabs", 8 | "license": "Apache-2.0", 9 | "private": true, 10 | "workspaces": [ 11 | "apps/*" 12 | ], 13 | "scripts": { 14 | "dev": "yarn --cwd apps/web dev", 15 | "build": "yarn workspaces foreach --all --parallel run build", 16 | "lint": "yarn --cwd apps/web lint --fix && yarn --cwd apps/firebase lint --fix", 17 | "postinstall": "husky install", 18 | "prettify": "yarn prettier --write \"apps/**/*.{ts,tsx}\" --ignore-path=./.gitignore" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^8.57.0", 22 | "husky": "9.1.1", 23 | "prettier": "^3.3.3" 24 | }, 25 | "engines": { 26 | "node": "20" 27 | }, 28 | "packageManager": "yarn@4.3.1" 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/react' 2 | import { SessionProvider } from 'next-auth/react' 3 | import { ThemeProvider } from 'next-themes' 4 | import type { AppProps } from 'next/app' 5 | import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3' 6 | import 'styles/globals.css' 7 | 8 | export default function App({ 9 | Component, 10 | pageProps: { session, ...pageProps }, 11 | }: AppProps) { 12 | return ( 13 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/utils/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function useMediaQuery(query: string): boolean { 4 | const getMatches = (query: string): boolean => { 5 | // Prevents SSR issues 6 | if (typeof window !== 'undefined') { 7 | return window.matchMedia(query).matches 8 | } 9 | return false 10 | } 11 | 12 | const [matches, setMatches] = useState(getMatches(query)) 13 | 14 | function handleChange() { 15 | setMatches(getMatches(query)) 16 | } 17 | 18 | useEffect(() => { 19 | const matchMedia = window.matchMedia(query) 20 | 21 | // Triggered at the first client-side load and if query changes 22 | handleChange() 23 | 24 | // Listen matchMedia 25 | matchMedia.addEventListener('change', handleChange) 26 | 27 | return () => { 28 | matchMedia.removeEventListener('change', handleChange) 29 | } 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, [query]) 32 | 33 | return matches 34 | } 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>celo-org/.github:renovate-config", 5 | "local>celo-org/developer-tooling:dt-renovate-base" 6 | ], 7 | "packageRules": [ 8 | { 9 | "matchDepTypes": ["devDependencies"], 10 | "matchUpdateTypes": ["patch", "minor"], 11 | "groupName": "devDependencies (non-major)" 12 | }, 13 | { 14 | "groupName": "celo", 15 | "matchPackageNames": ["/@celo/"] 16 | }, 17 | { 18 | "groupName": "firebase", 19 | "matchPackageNames": ["/firebase/"] 20 | }, 21 | { 22 | "groupName": "eslint", 23 | "enabled": false, 24 | "matchPackageNames": ["/eslint/"] 25 | } 26 | ], 27 | "major": { 28 | "minimumReleaseAge": "12 days" 29 | }, 30 | "minor": { 31 | "minimumReleaseAge": "6 days" 32 | }, 33 | "patch": { 34 | "minimumReleaseAge": "4 days" 35 | }, 36 | "schedule": ["on tuesday and thursday", "every weekend"] 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/styles/Form.module.css: -------------------------------------------------------------------------------- 1 | .center { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .intro { 8 | max-width: 420px; 9 | font-size: 1rem; 10 | margin-top: 1.5rem; 11 | margin-bottom: 1rem; 12 | } 13 | 14 | .label { 15 | font-weight: 500; 16 | margin: 0.25rem; 17 | font-family: 'Inter', Arial, Helvetica, sans-serif; 18 | } 19 | 20 | .address { 21 | font-size: 0.9rem; 22 | font-family: 'Inter', Arial, Helvetica, sans-serif; 23 | } 24 | 25 | .address:focus-visible { 26 | outline-color: var(--outline-color); 27 | } 28 | 29 | .status { 30 | margin-top: 0.67rem; 31 | margin-bottom: 1rem; 32 | } 33 | 34 | .githubAuthenticate { 35 | text-decoration: underline dotted; 36 | text-underline-offset: 3px; 37 | } 38 | .githubAuthenticate:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | /* Mobile */ 43 | @media (max-width: 700px) { 44 | .address { 45 | font-size: 0.85rem; 46 | width: 320px; 47 | max-width: 90vw; 48 | } 49 | 50 | .intro { 51 | margin-top: 1.4rem; 52 | margin-bottom: 1.4rem; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: ['bug report'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please ensure that the bug has not already been filed in the issue tracker. 9 | 10 | Thanks for taking the time to report this bug! 11 | - type: dropdown 12 | attributes: 13 | label: Browser 14 | description: What browser and version are you using? 15 | options: 16 | - Chrome 17 | - Opera 18 | - Safari 19 | - Firefox 20 | - Brave 21 | - Arc 22 | - Other (please describe) 23 | - type: dropdown 24 | attributes: 25 | label: Operating System 26 | description: What operating system are you on? 27 | options: 28 | - Windows 29 | - macOS (Intel) 30 | - macOS (Apple Silicon) 31 | - Linux 32 | - type: textarea 33 | attributes: 34 | label: Describe the bug 35 | description: Please include relevant screenshots if relevant. 36 | validations: 37 | required: true 38 | -------------------------------------------------------------------------------- /apps/firebase/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applicationDefault, 3 | AppOptions, 4 | initializeApp, 5 | } from 'firebase-admin/app' 6 | import { getDatabase } from 'firebase-admin/database' 7 | import { setGlobalOptions } from 'firebase-functions/v2' 8 | import { onValueCreated } from 'firebase-functions/v2/database' 9 | import { 10 | DATABASE_URL, 11 | DB_POOL_OPTS, 12 | getNetworkConfig, 13 | PROCESSOR_RUNTIME_OPTS, 14 | SERVICE_ACCOUNT, 15 | } from './config' 16 | import { AccountPool, processRequest } from './database-helper' 17 | 18 | setGlobalOptions({ 19 | serviceAccount: SERVICE_ACCOUNT, 20 | }) 21 | 22 | const app = initializeApp({ 23 | credential: applicationDefault(), 24 | databaseURL: DATABASE_URL, 25 | } as AppOptions) 26 | 27 | export const faucetRequestProcessor = onValueCreated( 28 | PROCESSOR_RUNTIME_OPTS, 29 | async (event) => { 30 | const network = event.params.network 31 | const config = getNetworkConfig(network) 32 | const pool = new AccountPool(getDatabase(app), network, DB_POOL_OPTS) 33 | return processRequest(event.data, pool, config) 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /apps/web/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from 'lucide-react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { useTheme } from 'next-themes' 5 | import { useCallback } from 'react' 6 | 7 | export function ModeToggle() { 8 | const { theme, setTheme } = useTheme() 9 | 10 | const onClick = useCallback(() => { 11 | let oldTheme = theme 12 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') 13 | .matches 14 | ? 'dark' 15 | : 'light' 16 | 17 | if (theme === 'system' || theme === undefined) { 18 | oldTheme = systemTheme 19 | } 20 | 21 | const newTheme = oldTheme === 'light' ? 'dark' : 'light' 22 | setTheme(newTheme) 23 | }, [theme, setTheme]) 24 | 25 | return ( 26 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/firebase/src/get-qualified-mount.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { NetworkConfig } from './config' 3 | import { AuthLevel } from './database-helper' 4 | import { getQualifiedAmount } from './get-qualified-mount' 5 | 6 | describe('getQualifiedAmount', () => { 7 | const mockConfig: NetworkConfig = { 8 | nodeUrl: '', 9 | faucetGoldAmount: 100n, 10 | authenticatedGoldAmount: 200n, 11 | } 12 | 13 | it('should return faucetGoldAmount for undefined authLevel', () => { 14 | // @ts-expect-error 15 | const result = getQualifiedAmount(undefined, mockConfig) 16 | expect(result.celoAmount).toBe(mockConfig.faucetGoldAmount) 17 | }) 18 | 19 | it('should return faucetGoldAmount for AuthLevel.none', () => { 20 | const result = getQualifiedAmount(AuthLevel.none, mockConfig) 21 | expect(result.celoAmount).toBe(mockConfig.faucetGoldAmount) 22 | }) 23 | 24 | it('should return authenticatedGoldAmount for AuthLevel.authenticated', () => { 25 | const result = getQualifiedAmount(AuthLevel.authenticated, mockConfig) 26 | expect(result.celoAmount).toBe(mockConfig.authenticatedGoldAmount) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /apps/web/utils/balance.ts: -------------------------------------------------------------------------------- 1 | import { FaucetAddress, Network } from 'types' 2 | const MINIMUM_BALANCE = BigInt('5100000000000000000') // IN WEI 3 | 4 | function getApiPath(network: Network) { 5 | const faucetAddress = FaucetAddress[network] 6 | const root = `https://celo-${network}.blockscout.com/api` 7 | const apiPath = `${root}?module=account&action=balance&address=${faucetAddress}` 8 | return apiPath 9 | } 10 | 11 | async function getFaucetBalance(network: Network) { 12 | const apiPath = getApiPath(network) 13 | const result = await fetch(apiPath) 14 | 15 | const data: { result: string | null } = await result.json() 16 | 17 | return data.result 18 | } 19 | 20 | // returns true if faucet has less than 5 CELO 21 | export async function isBalanceBelowPar(network: Network) { 22 | try { 23 | const balance = await getFaucetBalance(network) 24 | if (balance === null) { 25 | // if for some reason the Celo Explore returns an error, just let faucet work as if it had balance 26 | return false 27 | } 28 | const balanceInt = BigInt(balance) 29 | 30 | return balanceInt <= MINIMUM_BALANCE 31 | } catch (error) {} 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/utils/captcha-verify.ts: -------------------------------------------------------------------------------- 1 | const CAPTCHA_URL = 'https://www.google.com/recaptcha/api/siteverify' 2 | 3 | enum Errors { 4 | MissingSecret = 'missing-input-secret', 5 | InvalidSecret = 'invalid-input-secret', 6 | MissingResponse = 'missing-input-response', 7 | InvalidResponse = 'invalid-input-response', 8 | BadRequest = 'bad-request', 9 | Timeout = 'timeout-or-duplicate', 10 | } 11 | 12 | interface RecaptchaResponse { 13 | success: boolean 14 | challenge_ts: string // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) 15 | apk_package_name: string // the package name of the app where the reCAPTCHA was solved 16 | 'error-codes'?: Errors[] // optional 17 | } 18 | 19 | export async function captchaVerify( 20 | captchaToken: string, 21 | ): Promise { 22 | const result = await fetch(CAPTCHA_URL, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/x-www-form-urlencoded', 26 | }, 27 | body: `secret=${encodeURIComponent( 28 | process.env.RECAPTCHA_SECRET as string, 29 | )}&response=${encodeURIComponent(captchaToken)}`, 30 | }) 31 | 32 | return result.json() 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/components/logo.tsx: -------------------------------------------------------------------------------- 1 | export function Logo() { 2 | return ( 3 | 9 | 13 | 19 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/firebase/README.md: -------------------------------------------------------------------------------- 1 | # Celo Faucet 2 | 3 | A firebase function that faucets addresses 4 | 5 | 6 | ## Funding the accounts 7 | 8 | The accounts are normal EOAs. send CELO via transfer or simple transaction. 9 | 10 | 11 | ## How to qa 12 | 13 | push to the staging branch. this branch is the only non master branch that can be used with captcha and deployment 14 | 15 | 16 | ## How to deploy 17 | 18 | Deployment is done thru a github action. see deploy-chain.yaml 19 | 20 | ## ✍️ Contributing 21 | 22 | Feel free to jump on the Celo 🚂🚋🚋🚋. Improvements and contributions are highly encouraged! 🙏👊 23 | 24 | See the [contributing guide](https://docs.celo.org/what-is-celo/joining-celo/contributors/overview) for details on how to participate. 25 | [![GitHub issues by-label](https://img.shields.io/github/issues/celo-org/celo-monorepo/1%20hour%20tasks)](https://github.com/celo-org/celo-monorepo/issues?q=is%3Aopen+is%3Aissue+label%3A%221+hour+tasks%22) 26 | 27 | All communication and contributions to the Celo project are subject to the [Celo Code of Conduct](https://celo.org/code-of-conduct). 28 | 29 | 30 | ## 📜 License 31 | 32 | All packages are licensed under the terms of the [Apache 2.0 License](LICENSE) unless otherwise specified in the LICENSE file at package's root. 33 | -------------------------------------------------------------------------------- /apps/firebase/src/celo-adapter.ts: -------------------------------------------------------------------------------- 1 | import { ensureLeading0x } from '@celo/utils/lib/address' 2 | import { 3 | Account, 4 | Address, 5 | createWalletClient, 6 | Hex, 7 | http, 8 | Transport, 9 | WalletClient, 10 | } from 'viem' 11 | import { privateKeyToAccount } from 'viem/accounts' 12 | import { celoAlfajores, celoSepolia } from 'viem/chains' 13 | 14 | export class CeloAdapter { 15 | public readonly client: WalletClient< 16 | Transport, 17 | typeof celoAlfajores | typeof celoSepolia, 18 | Account 19 | > 20 | private readonly chain: typeof celoAlfajores | typeof celoSepolia 21 | 22 | constructor({ pk, nodeUrl }: { pk: Hex; nodeUrl: string }) { 23 | const account = privateKeyToAccount(ensureLeading0x(pk)) 24 | this.chain = nodeUrl.includes('alfajores') ? celoAlfajores : celoSepolia 25 | this.client = createWalletClient({ 26 | account, 27 | transport: http(nodeUrl), 28 | chain: this.chain, 29 | }) 30 | console.info(`New client from url: ${nodeUrl}`) 31 | console.info(`Using address ${account.address} to send transactions`) 32 | } 33 | 34 | async transferCelo(to: Address, amount: bigint): Promise { 35 | const txHash = await this.client.sendTransaction({ 36 | to, 37 | value: amount, 38 | chain: this.chain, 39 | }) 40 | return txHash 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/components/faucet-header.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { GitHubAuth } from 'components/github-auth' 3 | import { Logo } from 'components/logo' 4 | import { ModeToggle } from 'components/mode-toggle' 5 | import styles from 'styles/FaucetHeader.module.css' 6 | import { Network } from 'types' 7 | 8 | interface Props { 9 | network: Network 10 | isOutOfCELO: boolean 11 | } 12 | 13 | export const FaucetHeader: FC = ({ network, isOutOfCELO }) => ( 14 |
15 | {isOutOfCELO && ( 16 |
17 | The Faucet is out of CELO for now. 18 | {network === 'alfajores' && ( 19 | <> 20 | {' '} 21 | It will be topped up{' '} 22 | 27 | within an hour 28 | 29 | 30 | )} 31 |
32 | )} 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 | ) 44 | -------------------------------------------------------------------------------- /apps/web/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { AuthOptions, Profile, Session } from 'next-auth' 2 | import GithubProvider from 'next-auth/providers/github' 3 | 4 | type ExtendedProfile = { 5 | created_at?: string 6 | } & Profile 7 | 8 | interface ExtendedUser { 9 | created_at?: string 10 | name?: string | null 11 | email?: string | null 12 | image?: string | null 13 | } 14 | 15 | type ExtendedSession = { 16 | user?: ExtendedUser 17 | } & Session 18 | 19 | export const authOptions: AuthOptions = { 20 | providers: [ 21 | GithubProvider({ 22 | clientId: process.env.GITHUB_ID || '', 23 | clientSecret: process.env.GITHUB_SECRET || '', 24 | }), 25 | ], 26 | callbacks: { 27 | async jwt({ token, profile }) { 28 | if (profile) { 29 | const extProfile: ExtendedProfile = profile 30 | token.user_created_at = extProfile.created_at 31 | } 32 | return token 33 | }, 34 | async session({ session, token, user }) { 35 | const extSession: ExtendedSession = session 36 | if (extSession.user) { 37 | extSession.user.created_at = token.user_created_at as string 38 | } 39 | return session 40 | }, 41 | }, 42 | session: { 43 | maxAge: 2 * 60, // 2 minutes in seconds 44 | }, 45 | jwt: { 46 | maxAge: 60, // seconds 47 | }, 48 | } 49 | 50 | export default NextAuth(authOptions) 51 | -------------------------------------------------------------------------------- /apps/web/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Address = string 2 | export type E164Number = string 3 | 4 | export const networks = ['alfajores', 'celo-sepolia'] as const 5 | export type Network = (typeof networks)[number] 6 | 7 | export enum FaucetAddress { 8 | alfajores = '0x22579CA45eE22E2E16dDF72D955D6cf4c767B0eF', 9 | 'celo-sepolia' = '0x22579CA45eE22E2E16dDF72D955D6cf4c767B0eF', 10 | } 11 | 12 | export enum ChainId { 13 | alfajores = 44787, 14 | 'celo-sepolia' = 11142220, 15 | } 16 | 17 | export enum RequestStatus { 18 | Pending = 'Pending', 19 | Working = 'Working', 20 | Done = 'Done', 21 | Failed = 'Failed', 22 | } 23 | 24 | export enum RequestType { 25 | Faucet = 'Faucet', 26 | } 27 | 28 | export enum AuthLevel { 29 | none = 'none', 30 | authenticated = 'authenticated', 31 | } 32 | 33 | export interface RequestRecord { 34 | beneficiary: Address 35 | status: RequestStatus 36 | type: RequestType 37 | dollarTxHash?: string 38 | goldTxHash?: string 39 | tokens?: RequestedTokenSet 40 | authLevel: AuthLevel 41 | timestamp?: number 42 | } 43 | 44 | export enum RequestedTokenSet { 45 | All = 'All', 46 | Stables = 'Stables', 47 | Celo = 'Celo', 48 | } 49 | 50 | export type FaucetAPIResponse = 51 | | { 52 | status: RequestStatus.Done | RequestStatus.Pending | RequestStatus.Pending 53 | key: string | null 54 | } 55 | | { 56 | status: RequestStatus.Failed 57 | message: string 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Faucet CI 2 | run-name: 'Faucet CI: ${{ github.head_ref || github.ref_name }}' 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | concurrency: 13 | group: faucet-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | shell: bash --login -eo pipefail {0} 19 | 20 | jobs: 21 | run-checks: 22 | name: Run Checks 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout Repo 26 | uses: actions/checkout@v4 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20.x 31 | cache: 'yarn' 32 | - name: Install Dependencies 33 | run: yarn 34 | - name: Run Lint Globally 35 | run: yarn lint 36 | - name: Build Firebase 37 | run: yarn --cwd=apps/firebase run build 38 | 39 | - name: Run Tests 40 | run: yarn --cwd=apps/firebase run test:ci 41 | 42 | validate-renovate-config: 43 | name: Validate Renovate Config 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout Repo 47 | uses: actions/checkout@v4 48 | - name: Setup Node.js 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: 20.x 52 | - name: Validate Renovate Config 53 | run: | 54 | npm install --global renovate 55 | renovate-config-validator 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": false 4 | }, 5 | "files.exclude": { 6 | "**/*.js": { "when": "$(basename).ts" }, 7 | "**/*.js.map": true 8 | }, 9 | "files.watcherExclude": { 10 | "**/.git/objects/**": true, 11 | "**/.git/subtree-cache/**": true, 12 | "**/node_modules/*/**": true 13 | }, 14 | "typescript.preferences.importModuleSpecifier": "non-relative", 15 | "typescript.updateImportsOnFileMove.enabled": "always", 16 | "editor.codeActionsOnSave": { 17 | "source.organizeImports": "never" 18 | }, 19 | "[javascript]": { 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.organizeImports": "never" 23 | } 24 | }, 25 | "[javascriptreact]": { 26 | "editor.formatOnSave": true, 27 | "editor.codeActionsOnSave": { 28 | "source.organizeImports": "explicit" 29 | } 30 | }, 31 | "[typescript]": { 32 | "editor.formatOnSave": true, 33 | "editor.codeActionsOnSave": { 34 | "source.organizeImports": "explicit" 35 | } 36 | }, 37 | "[typescriptreact]": { 38 | "editor.formatOnSave": true, 39 | "editor.codeActionsOnSave": { 40 | "source.organizeImports": "explicit" 41 | } 42 | }, 43 | "javascript.format.enable": false, 44 | "editor.tabSize": 2, 45 | "editor.detectIndentation": false, 46 | "[javascriptreact][typescript][typescriptreact]": { 47 | "editor.codeActionsOnSave": { 48 | "source.organizeImports": "explicit" 49 | } 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /apps/web/utils/firebase.client.ts: -------------------------------------------------------------------------------- 1 | import { getAnalytics } from 'firebase/analytics' 2 | import firebase from 'firebase/compat/app' 3 | import 'firebase/compat/database' 4 | import { Network, RequestRecord, RequestStatus } from 'types' 5 | import { config } from './firebase-config' 6 | // Code in this file is sent to the browser. 7 | // Code in FirebaseServerSide.ts is not sent to the browser. 8 | 9 | async function getFirebase() { 10 | if (!firebase.apps.length) { 11 | const app = firebase.initializeApp(config) 12 | getAnalytics(app) 13 | } 14 | return firebase 15 | } 16 | 17 | async function getDB(): Promise { 18 | return (await getFirebase()).database() 19 | } 20 | 21 | // Don't do this. It hangs next.js build process: https://github.com/zeit/next.js/issues/6824 22 | // const db = firebase.database() 23 | 24 | export async function subscribeRequest( 25 | key: string, 26 | onChange: (record: RequestRecord) => void, 27 | network: Network, 28 | ) { 29 | const ref: firebase.database.Reference = (await getDB()).ref( 30 | `${network}/requests/${key}`, 31 | ) 32 | 33 | const listener = ref.on('value', (snap) => { 34 | const record = snap.val() as RequestRecord 35 | 36 | if (record) { 37 | onChange(record) 38 | } else { 39 | console.debug(snap.key, 'exists?', snap.exists()) 40 | } 41 | 42 | if ( 43 | record?.status === RequestStatus.Done || 44 | record?.status === RequestStatus.Failed 45 | ) { 46 | ref.off('value', listener) 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@celo/abis": "^13.0.0", 13 | "@firebase/app-types": "^0.9.3", 14 | "@metamask/detect-provider": "^2.0.0", 15 | "@radix-ui/react-label": "^2.1.7", 16 | "@radix-ui/react-slot": "^1.2.3", 17 | "@upstash/redis": "^1.35.3", 18 | "@vercel/analytics": "^1.3.1", 19 | "@vercel/functions": "^3.1.0", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "firebase": "^12.1.0", 23 | "lucide-react": "^0.542.0", 24 | "next": "15.5.9", 25 | "next-auth": "^4.24.11", 26 | "next-themes": "^0.4.6", 27 | "react": "19.2.1", 28 | "react-dom": "19.2.1", 29 | "react-google-recaptcha-v3": "^1.10.1", 30 | "react-use-async-callback": "^2.1.2", 31 | "viem": "^2.33.2" 32 | }, 33 | "devDependencies": { 34 | "@tailwindcss/postcss": "^4.1.13", 35 | "@types/node": "20.14.11", 36 | "@types/react": "18.3.3", 37 | "@types/react-dom": "18.3.0", 38 | "eslint": "8.57.0", 39 | "eslint-config-next": "13.5.6", 40 | "postcss": "^8.5.6", 41 | "shadcn": "^3.2.1", 42 | "tailwind-merge": "^3.3.1", 43 | "tailwindcss": "^4.1.13", 44 | "tw-animate-css": "^1.3.8", 45 | "typescript": "5.5.3" 46 | }, 47 | "resolutions": { 48 | "is-arrayish": "0.3.2", 49 | "simple-swizzle": "0.2.2", 50 | "error-ex": "1.3.2" 51 | }, 52 | "engines": { 53 | "node": "20" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/firebase/src/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Logging } from '@google-cloud/logging' 2 | 3 | // See https://firebase.google.com/docs/functions/config-env 4 | const ProjectID = process.env.GCLOUD_PROJECT || 'celo-faucet' 5 | 6 | const logging = new Logging({ 7 | projectId: ProjectID, 8 | }) 9 | const log = logging.log('faucetMetrics') 10 | 11 | const METADATA = { 12 | resource: { 13 | labels: { 14 | function_name: 'faucetMetrics', 15 | project_id: ProjectID, 16 | region: 'us-central1', 17 | }, 18 | type: 'cloud_function', 19 | }, 20 | } 21 | 22 | export enum ExecutionResult { 23 | Ok = 'Ok', 24 | /** Enqued Faucet Request has invalid type */ 25 | InvalidRequestErr = 'InvalidRequestErr', 26 | /** Failed to obtain a free acount to faucet from */ 27 | NoFreeAccountErr = 'NoFreeAccountErr', 28 | /** Faucet Action timed out */ 29 | ActionTimedOutErr = 'ActionTimedOutErr', 30 | OtherErr = 'OtherErr', 31 | } 32 | 33 | /** 34 | * Sends an entry but doesn't block 35 | * (we don't want to block waiting for a metric to be sent) 36 | */ 37 | function noBlockingSendEntry(entryData: Record) { 38 | const entry = log.entry(METADATA, entryData) 39 | log.write(entry).catch((err: any) => { 40 | console.error('EventLogger: error sending entry', err) 41 | }) 42 | } 43 | 44 | export function logExecutionResult( 45 | snapKey: string | null, 46 | result: ExecutionResult, 47 | ) { 48 | noBlockingSendEntry({ 49 | event: 'celo/faucet/result', 50 | executionResult: result, 51 | failed: result !== ExecutionResult.Ok, 52 | snapKey, 53 | message: `${snapKey}: Faucet result was ${result}`, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /apps/web/config/chains.ts: -------------------------------------------------------------------------------- 1 | import { Network } from 'types' 2 | import { celoAlfajores, celoSepolia } from 'viem/chains' 3 | 4 | interface ChainParams { 5 | chainId: `0x${string}` 6 | chainName: string 7 | nativeCurrency: { 8 | name: string 9 | symbol: string 10 | decimals: number 11 | } 12 | rpcUrls: string[] 13 | blockExplorerUrls: string[] 14 | iconUrls: string[] 15 | } 16 | 17 | export const CHAIN_PARAMS: Record = [ 18 | { ...celoAlfajores, network: 'alfajores' as Network }, 19 | { ...celoSepolia, network: 'celo-sepolia' as Network }, 20 | ].reduce( 21 | (acc, chain) => { 22 | acc[chain.network] = { 23 | chainId: `0x${chain.id.toString(16)}`, 24 | chainName: chain.name, 25 | nativeCurrency: { 26 | name: chain.nativeCurrency.name, 27 | symbol: chain.nativeCurrency.symbol, 28 | decimals: chain.nativeCurrency.decimals, 29 | }, 30 | rpcUrls: [...chain.rpcUrls.default.http], 31 | blockExplorerUrls: [chain.blockExplorers.default.url], 32 | iconUrls: ['future'], // Placeholder for future icons 33 | } 34 | return acc 35 | }, 36 | {} as Record, 37 | ) 38 | 39 | interface Token { 40 | symbol: string 41 | address: `0x${string}` 42 | } 43 | 44 | export const tokens: Record = { 45 | alfajores: [ 46 | { 47 | symbol: 'cEUR', 48 | address: '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', 49 | }, 50 | { 51 | symbol: 'cREAL', 52 | address: '0xE4D517785D091D3c54818832dB6094bcc2744545', 53 | }, 54 | { 55 | symbol: 'cUSD', 56 | address: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', 57 | }, 58 | ], 59 | 'celo-sepolia': [], 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/components/github-auth.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, signOut, useSession } from 'next-auth/react' 2 | import { Inter } from 'next/font/google' 3 | import { FC } from 'react' 4 | import { Button } from '../@/components/ui/button' 5 | import { useMediaQuery } from 'utils/use-media-query' 6 | 7 | export const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const GitHubAuth: FC = () => { 10 | const { data: session } = useSession() 11 | const isMobile = useMediaQuery('(max-width: 700px)') 12 | 13 | return session?.user ? ( 14 | 18 | ) : ( 19 | 23 | ) 24 | } 25 | 26 | const GitHubIcon: FC = () => ( 27 | 33 | 37 | 38 | ) 39 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /apps/firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@celo/faucet-app", 3 | "version": "2.0.0", 4 | "description": "Faucet Firebase Functions", 5 | "author": "Celo", 6 | "license": "Apache-2.0", 7 | "main": "dist/index.js", 8 | "scripts": { 9 | "preserve": "yarn run build", 10 | "serve": "cross-env NODE_ENV=production firebase serve -p 5001", 11 | "deploy:staging": "echo 'please use the deploy-chain workflow on github' && exit 1", 12 | "deploy:prod": "echo 'please use the deploy-chain workflow on github' && exit 1", 13 | "clean": "tsc -b . --clean", 14 | "build": "tsc -b .", 15 | "lint": "eslint -c ../../.eslintrc.js --ext .ts ./src", 16 | "cli": "ts-node scripts/cli.ts", 17 | "build:rules": "firebase-bolt database-rules.bolt", 18 | "test": "vitest", 19 | "test:ci": "vitest run" 20 | }, 21 | "dependencies": { 22 | "@celo/utils": "6.0.1", 23 | "@firebase/app": "^0.14.1", 24 | "@google-cloud/logging": "^11.1.0", 25 | "debug": "^4.3.5", 26 | "firebase": "^12.1.0", 27 | "firebase-admin": "^13.4.0", 28 | "firebase-functions": "^6.4.0", 29 | "firebase-tools": "^14.12.0", 30 | "viem": "^2.33.2" 31 | }, 32 | "devDependencies": { 33 | "@types/debug": "^4.1.12", 34 | "@types/node": "^20.14.11", 35 | "@types/yargs": "^17.0.32", 36 | "@typescript-eslint/eslint-plugin": "^5.62.0", 37 | "@typescript-eslint/eslint-plugin-tslint": "^5.62.0", 38 | "@typescript-eslint/parser": "^5.62.0", 39 | "cross-env": "7.0.3", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-import": "^2.29.1", 42 | "eslint-plugin-jsdoc": "^48.7.0", 43 | "eslint-plugin-prefer-arrow": "^1.2.3", 44 | "eslint-plugin-react": "^7.34.4", 45 | "firebase-bolt": "^0.8.4", 46 | "ts-node": "^10.9.2", 47 | "typescript": "^5.5.3", 48 | "vitest": "^1.6.1", 49 | "yargs": "17.7.2" 50 | }, 51 | "engines": { 52 | "node": "20" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/firebase/src/config.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import type { ReferenceOptions } from 'firebase-functions/database' 3 | import type { PoolOptions } from './database-helper' 4 | 5 | export interface NetworkConfig { 6 | nodeUrl: string 7 | faucetGoldAmount: bigint 8 | authenticatedGoldAmount: bigint 9 | } 10 | 11 | const ALFAJORES_CONFIG: NetworkConfig = { 12 | nodeUrl: 'https://alfajores-forno.celo-testnet.org', 13 | faucetGoldAmount: 300_000_000_000_000_000n, 14 | authenticatedGoldAmount: 3_000_000_000_000_000_000n, 15 | } 16 | 17 | const CELO_SEPOLIA_CONFIG: NetworkConfig = { 18 | nodeUrl: 'https://forno.celo-sepolia.celo-testnet.org', 19 | faucetGoldAmount: 300_000_000_000_000_000n, 20 | authenticatedGoldAmount: 3_000_000_000_000_000_000n, 21 | } 22 | 23 | const CONFIGS: Record = { 24 | alfajores: ALFAJORES_CONFIG, 25 | 'celo-sepolia': CELO_SEPOLIA_CONFIG, 26 | } 27 | 28 | export function getNetworkConfig(net: string): NetworkConfig { 29 | if (CONFIGS[net] == null) { 30 | throw new Error('No Config for: ' + net) 31 | } 32 | 33 | return CONFIGS[net] 34 | } 35 | 36 | export const PROCESSOR_RUNTIME_OPTS: ReferenceOptions = { 37 | // When changing this, check that `DB_POOL_OPTS.actionTimeoutMS` is less than this number 38 | timeoutSeconds: 120, 39 | memory: '512MiB', 40 | ref: '/{network}/requests/{request}', 41 | } 42 | 43 | export const DB_POOL_OPTS: PoolOptions = { 44 | retryWaitMS: 1_000, 45 | getAccountTimeoutMS: 20_000, 46 | actionTimeoutMS: 90_000, 47 | } 48 | 49 | const GCLOUD_PROJECT = process.env.GCLOUD_PROJECT 50 | assert(GCLOUD_PROJECT, 'Missing in env: GCLOUD_PROJECT') 51 | 52 | const SUFFIX = GCLOUD_PROJECT.includes('staging') ? '-staging' : '' 53 | export const SERVICE_ACCOUNT = `faucet-deploy-firebase${SUFFIX}@celo-faucet${SUFFIX}.iam.gserviceaccount.com` 54 | export const DATABASE_URL = `https://celo-faucet${SUFFIX}.firebaseio.com` 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Expo 2 | # 3 | .expo/ 4 | 5 | # Python 6 | # 7 | *.pyc 8 | 9 | # Vim 10 | # 11 | *.swo 12 | *.swp 13 | *.swm 14 | *.swn 15 | 16 | # OSX 17 | # 18 | .DS_Store 19 | 20 | # Xcode 21 | # 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | xcuserdata 31 | *.xccheckout 32 | *.moved-aside 33 | DerivedData 34 | *.hmap 35 | *.ipa 36 | *.xcuserstate 37 | project.xcworkspace 38 | 39 | # Android/IntelliJ 40 | # 41 | .idea 42 | .gradle 43 | local.properties 44 | *.iml 45 | 46 | # node.js 47 | # 48 | node_modules/ 49 | dist/ 50 | npm-debug.log 51 | yarn-error.log 52 | 53 | # Yarn 54 | # Copied from the official Yarn 4.x documentation. 55 | # Source: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 56 | .pnp.* 57 | .yarn/* 58 | !.yarn/patches 59 | !.yarn/plugins 60 | !.yarn/releases 61 | !.yarn/sdks 62 | !.yarn/versions 63 | 64 | 65 | # BUCK 66 | buck-out/ 67 | \.buckd/ 68 | *.keystore 69 | 70 | # fastlane 71 | # 72 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 73 | # screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/ 76 | 77 | 78 | coverage 79 | coverage.json 80 | 81 | 82 | # Typechain types 83 | **/types/typechain/ 84 | 85 | # keys 86 | .env.mnemonic* 87 | !.env.mnemonic*.enc 88 | 89 | .terraform/ 90 | .terraform.lock.hcl 91 | tsconfig.tsbuildinfo 92 | 93 | # git mergetool 94 | *.orig 95 | 96 | 97 | */.next/* 98 | .env.local 99 | .env.* 100 | 101 | 102 | 103 | 104 | # temp json file for deploy-sdks script 105 | scripts/failedSDKs.json 106 | 107 | dist/ 108 | 109 | # Firebase cache 110 | .firebase/ 111 | 112 | serviceAccountKey.json 113 | firebase-debug.log 114 | database-rules.json 115 | 116 | .vercel 117 | -------------------------------------------------------------------------------- /apps/web/@/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import * as React from 'react' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | const buttonVariants = cva( 9 | 'inline-flex items-center justify-center whitespace-nowrap rounded-base text-sm font-base ring-offset-white transition-all gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | 'text-main-foreground bg-main border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none', 15 | noShadow: 'text-main-foreground bg-main border-2 border-border', 16 | neutral: 17 | 'bg-secondary-background text-foreground border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none', 18 | reverse: 19 | 'text-main-foreground bg-main border-2 border-border hover:translate-x-reverseBoxShadowX hover:translate-y-reverseBoxShadowY hover:shadow-shadow', 20 | }, 21 | size: { 22 | default: 'h-10 px-4 py-2', 23 | sm: 'h-9 px-3', 24 | lg: 'h-11 px-8', 25 | icon: 'size-10', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | 35 | function Button({ 36 | className, 37 | variant, 38 | size, 39 | asChild = false, 40 | ...props 41 | }: React.ComponentProps<'button'> & 42 | VariantProps & { 43 | asChild?: boolean 44 | }) { 45 | const Comp = asChild ? Slot : 'button' 46 | 47 | return ( 48 | 53 | ) 54 | } 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug PhoneNumberPrivacy Combiner Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/.bin/jest", 11 | "--rootDir", 12 | "${workspaceFolder}/packages/phone-number-privacy", 13 | "--runInBand", 14 | "${workspaceFolder}/packages/phone-number-privacy/combiner/test/**", 15 | ], 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "port": 9229 19 | }, 20 | { 21 | "name": "Debug PhoneNumberPrivacy Signer Tests", 22 | "type": "node", 23 | "request": "launch", 24 | "runtimeArgs": [ 25 | "--inspect-brk", 26 | "${workspaceRoot}/node_modules/.bin/jest", 27 | "--rootDir", 28 | "${workspaceFolder}/packages/phone-number-privacy/signer", 29 | "--runInBand", 30 | "${workspaceFolder}/packages/phone-number-privacy/signer/test/**", 31 | ], 32 | "console": "integratedTerminal", 33 | "internalConsoleOptions": "neverOpen", 34 | "port": 9229 35 | }, 36 | { 37 | "name": "Debug ContractKit Tests", 38 | "type": "node", 39 | "request": "launch", 40 | "runtimeArgs": [ 41 | "--inspect-brk", 42 | "${workspaceRoot}/node_modules/.bin/jest", 43 | "--rootDir", 44 | "${workspaceFolder}/packages/contractkit", 45 | "--runInBand", 46 | "${workspaceFolder}/packages/contractkit/src/**/*.test.ts", 47 | ], 48 | "console": "integratedTerminal", 49 | "internalConsoleOptions": "neverOpen", 50 | "port": 9229 51 | }, 52 | { 53 | "name": "Debug Komencikit Tests", 54 | "type": "node", 55 | "request": "launch", 56 | "runtimeArgs": [ 57 | "--inspect-brk", 58 | "${workspaceRoot}/node_modules/.bin/jest", 59 | "--rootDir", 60 | "${workspaceFolder}/packages/komencikit", 61 | "--runInBand", 62 | "${workspaceFolder}/packages/komencikit/src/kit.spec.ts", 63 | ], 64 | "console": "integratedTerminal", 65 | "internalConsoleOptions": "neverOpen", 66 | "port": 9229 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /apps/web/@/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | function Card({ className, ...props }: React.ComponentProps<'div'>) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<'div'>) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<'div'>) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardDescription, 90 | CardContent, 91 | CardAction, 92 | } 93 | -------------------------------------------------------------------------------- /apps/web/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'tw-animate-css'; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | :root { 7 | --background: rgb(252, 246, 241); 8 | --secondary-background: oklch(100% 0 0); 9 | --foreground: oklch(0% 0 0); 10 | --main-foreground: oklch(0% 0 0); 11 | --main: rgb(252, 255, 82); 12 | --border: oklch(0% 0 0); 13 | --ring: oklch(0% 0 0); 14 | --overlay: oklch(0% 0 0 / 0.8); 15 | --shadow: 4px 4px 0px 0px var(--border); 16 | --chart-1: #ca7aff; 17 | --chart-2: #facc00; 18 | --chart-3: #00d696; 19 | --chart-4: #ff7a05; 20 | --chart-5: #0099ff; 21 | --chart-active-dot: #000; 22 | } 23 | 24 | .dark { 25 | --background: oklch(29.68% 0.0791 315.62); 26 | --secondary-background: oklch(23.93% 0 0); 27 | --foreground: oklch(92.49% 0 0); 28 | --main-foreground: oklch(0% 0 0); 29 | --main: rgb(180, 144, 255); 30 | --border: oklch(0% 0 0); 31 | --ring: oklch(100% 0 0); 32 | --shadow: 4px 4px 0px 0px var(--border); 33 | --chart-1: #d494ff; 34 | --chart-2: #e0b700; 35 | --chart-3: #00bd84; 36 | --chart-4: #eb6d00; 37 | --chart-5: #008ae5; 38 | --chart-active-dot: #fff; 39 | } 40 | 41 | @theme inline { 42 | --color-main: var(--main); 43 | --color-background: var(--background); 44 | --color-secondary-background: var(--secondary-background); 45 | --color-foreground: var(--foreground); 46 | --color-main-foreground: var(--main-foreground); 47 | --color-border: var(--border); 48 | --color-overlay: var(--overlay); 49 | --color-ring: var(--ring); 50 | --color-chart-1: var(--chart-1); 51 | --color-chart-2: var(--chart-2); 52 | --color-chart-3: var(--chart-3); 53 | --color-chart-4: var(--chart-4); 54 | --color-chart-5: var(--chart-5); 55 | 56 | --spacing-boxShadowX: 4px; 57 | --spacing-boxShadowY: 4px; 58 | --spacing-reverseBoxShadowX: -4px; 59 | --spacing-reverseBoxShadowY: -4px; 60 | --radius-base: 5px; 61 | --shadow-shadow: var(--shadow); 62 | --font-weight-base: 500; 63 | --font-weight-heading: 900; 64 | } 65 | 66 | @layer base { 67 | body { 68 | @apply text-foreground font-base bg-background; 69 | } 70 | 71 | h1, 72 | h2, 73 | h3, 74 | h4, 75 | h5, 76 | h6 { 77 | @apply font-heading; 78 | } 79 | } 80 | 81 | body { 82 | color: var(--foreground); 83 | background-image: linear-gradient(90deg, #8080804d 1px, #0000 0), 84 | linear-gradient(#80808090 1px, #0000 0); 85 | background-size: 40px 40px; 86 | } 87 | button { 88 | cursor: pointer; 89 | } 90 | -------------------------------------------------------------------------------- /apps/web/public/meta-mask-fox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web/pages/api/faucet.ts: -------------------------------------------------------------------------------- 1 | import { ipAddress } from '@vercel/functions' 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | import { Session } from 'next-auth' 4 | import { getServerSession } from 'next-auth/next' 5 | import { Hex, sha256 } from 'viem' 6 | import { sendRequest } from '../../utils/firebase.serverside' 7 | import { authOptions } from './auth/[...nextauth]' 8 | import { captchaVerify } from 'utils/captcha-verify' 9 | import { AuthLevel, FaucetAPIResponse, networks, RequestStatus } from 'types' 10 | 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse, 14 | ) { 15 | let authLevel = AuthLevel.none 16 | let session: Session | null | undefined 17 | try { 18 | session = await getServerSession(req, res, authOptions) 19 | if (session) { 20 | authLevel = AuthLevel.authenticated 21 | } 22 | } catch (e) { 23 | console.error('Authentication check failed', e) 24 | } 25 | 26 | const { captchaToken, beneficiary, network } = req.body 27 | 28 | if (!networks.includes(network)) { 29 | res.status(400).json({ 30 | status: RequestStatus.Failed, 31 | message: `Invalid network: ${network}`, 32 | }) 33 | return 34 | } 35 | const headers = new Headers() 36 | for (const [key, value] of Object.entries(req.headers)) { 37 | headers.set(key, value as string) 38 | } 39 | 40 | const captchaResponse = await captchaVerify(captchaToken) 41 | if (captchaResponse.success) { 42 | try { 43 | const { key, reason } = await sendRequest( 44 | beneficiary, 45 | true, 46 | network, 47 | authLevel, 48 | ipAddress(headers) || 49 | (req.headers['x-forwarded-for'] as string | undefined), 50 | session?.user?.email ? sha256(session.user.email as Hex) : undefined, 51 | ) 52 | 53 | if (key) { 54 | res.status(200).json({ status: RequestStatus.Pending, key }) 55 | } else if (reason === 'rate_limited') { 56 | res.status(403).json({ 57 | status: RequestStatus.Failed, 58 | message: 'Fauceting denied. Please check the faucet rules below.', 59 | }) 60 | } else { 61 | throw new Error(reason) 62 | } 63 | } catch (error) { 64 | console.error(error) 65 | res.status(404).json({ 66 | status: RequestStatus.Failed, 67 | message: 'Error while fauceting', 68 | }) 69 | } 70 | } else { 71 | console.error( 72 | 'Faucet Failed due to Recaptcha', 73 | captchaResponse['error-codes'], 74 | ) 75 | res.status(401).json({ 76 | status: RequestStatus.Failed, 77 | message: captchaResponse['error-codes']?.toString() || 'unknown', 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/deploy-chain.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Chain and Functions 2 | run-name: 'Deploy Chain and Functions: ${{ github.head_ref || github.ref_name }}' 3 | 4 | on: 5 | push: 6 | branches: 7 | - master # authorized for celo-faucet project & celo-faucet-staging 8 | - staging # only authorized for celo-faucet-staging project 9 | 10 | workflow_dispatch: 11 | inputs: 12 | CHAIN_NAME: 13 | description: 'Chain Name (kebab-case)' 14 | required: false 15 | RPC_URL: 16 | description: 'RPC URL' 17 | required: false 18 | PK: 19 | description: 'Private Key (0x...)' 20 | required: false 21 | PROD: 22 | type: boolean 23 | description: 'Is this a production deployment?' 24 | default: false 25 | env: 26 | IS_PROD: ${{ github.event.inputs.PROD == 'true' || github.ref_name == 'master' }} 27 | jobs: 28 | deploy: 29 | runs-on: ubuntu-latest 30 | permissions: 31 | id-token: write 32 | contents: read 33 | issues: write 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Build project 41 | working-directory: apps/firebase 42 | run: yarn build 43 | - name: Authenticate to Google Cloud 44 | id: auth 45 | uses: 'google-github-actions/auth@v2' 46 | with: 47 | workload_identity_provider: ${{ env.IS_PROD == 'true' && 'projects/1094498259535/locations/global/workloadIdentityPools/gh-faucet-master/providers/github-by-repos' || 'projects/1094498259535/locations/global/workloadIdentityPools/gh-faucet/providers/github-by-repos' }} 48 | service_account: ${{ env.IS_PROD == 'true' && 'faucet-deploy-firebase@celo-faucet.iam.gserviceaccount.com' || 'faucet-deploy-firebase-staging@celo-faucet-staging.iam.gserviceaccount.com' }} 49 | - name: Set Firebase Project 50 | id: set_firebase_project 51 | working-directory: apps/firebase 52 | run: yarn firebase use ${{env.IS_PROD == 'true' && 'celo-faucet' || 'celo-faucet-staging'}} 53 | - name: Set PK 54 | if: ${{ github.event.inputs.PK != '' }} 55 | working-directory: apps/firebase 56 | run: | 57 | yarn cli accounts:add ${{ github.event.inputs.PK }} --net ${{ github.event.inputs.CHAIN_NAME }} 58 | # - name: Setup artifacts policy 59 | # working-directory: apps/firebase 60 | # run: | 61 | # yarn firebase functions:artifacts:setpolicy --days 14d 62 | - name: Deploy Firebase Function faucetRequestProcessor 63 | working-directory: apps/firebase 64 | run: | 65 | yarn firebase deploy --only functions:faucetRequestProcessor 66 | -------------------------------------------------------------------------------- /apps/firebase/database-rules.bolt: -------------------------------------------------------------------------------- 1 | /** 2 | * Node Types 3 | */ 4 | type Request { 5 | beneficiary: String, // Address or phone number for the request's beneficiary 6 | mobileOS: String | Null, 7 | dollarTxHash: String | Null, // Transaction Hash for the executed Request 8 | goldTxHash: String | Null, 9 | escrowTxHash: String | Null, 10 | status: RequestStatus, // Request Status enum 11 | type: RequestType, // Request Type enum 12 | tokens: RequestedTokenSet, 13 | authLevel: AuthLevel | Null, 14 | } 15 | 16 | type Account { 17 | pk: String, // Account's private key 18 | address: String, // Accounts's Address 19 | locked: Boolean, // Lock status 20 | } 21 | 22 | /** 23 | * Leaf Node Types 24 | */ 25 | 26 | type RequestStatus extends String { 27 | validate() { 28 | this == 'Pending' || 29 | this == 'Working' || 30 | this == 'Done' || 31 | this == 'Failed' 32 | } 33 | } 34 | 35 | type RequestedTokenSet extends String { 36 | validate() { 37 | this === 'All' || 38 | this === 'Stables' || 39 | this === 'Celo' 40 | } 41 | } 42 | 43 | type RequestType extends String { 44 | validate() { 45 | this == 'Faucet' || 46 | this == 'Invite' 47 | } 48 | } 49 | 50 | type AuthLevel extends String { 51 | validate() { 52 | this === 'none' || 53 | this === 'authenticated' 54 | } 55 | } 56 | 57 | /** 58 | * Node Paths 59 | */ 60 | 61 | path / { 62 | // Only admin access 63 | read() { false } 64 | write() { false } 65 | } 66 | 67 | path /{net} { 68 | // Only admin access 69 | read() { false } 70 | write() { false } 71 | } 72 | 73 | path /{net}/requests { 74 | // Only admin access 75 | read() { false } 76 | write() { false } 77 | } 78 | 79 | path /{net}/requests/{id} is Request { 80 | read() { true } 81 | write() { isAllowed(this) } 82 | } 83 | 84 | path /{net}/accounts/{account} is Account { 85 | // Only admin access 86 | read() { false } 87 | write() { false } 88 | } 89 | 90 | /** 91 | * Helper Functions 92 | */ 93 | 94 | isLoggedIn() { auth != null } 95 | 96 | isNew(ref) { prior(ref) == null } 97 | 98 | // uid of service_account_firebase_faucet@clabs.co is LFH1B0m3tqdWGSugIvM2EkjARVR2 on celo-faucet 99 | // This account can be seen/modified at https://console.firebase.google.com/project/celo-faucet/authentication/users 100 | // 101 | // uid of service_account_firebase_faucet@clabs.co is ldA2DGvtgWP1xa8QFrtB7BYKc7l2 on celo-faucet-staging 102 | // This account can be seen/modified at https://console.firebase.google.com/project/celo-faucet-staging/authentication/users 103 | isAllowed(ref) { 104 | // TODO(ashishb): In the longer run, it would be better to choose only one uid based on whether 105 | // we are on staging network or the production network. 106 | return auth.uid == "LFH1B0m3tqdWGSugIvM2EkjARVR2" || auth.uid == "ldA2DGvtgWP1xa8QFrtB7BYKc7l2" 107 | } 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in improving [faucet.celo.org](https://faucet.celo.org/). 4 | 5 | If you want to contribute, but aren't sure where to start, you can create a 6 | [new discussion](https://github.com/celo-org/faucet/discussions). 7 | 8 | There are multiple opportunities to contribute. It doesn't matter if you are just 9 | getting started or are an expert. We appreciate your interest in contributing. 10 | 11 | > **IMPORTANT** 12 | > Please ask before starting work on any significant new features. 13 | > 14 | > It's never a fun experience to have your pull request declined after investing time and effort 15 | > into a new feature. To avoid this from happening, we invite contributors to create a 16 | > [new discussion](https://github.com/celo-org/faucet/discussions) to discuss API changes or 17 | > significant new ideas. 18 | 19 | ## Basic guide 20 | 21 | This guide is intended to help you get started with contributing. By following these steps, 22 | you will understand the development process and workflow. 23 | 24 | ### Cloning the repository 25 | 26 | To start contributing to the project, clone it to your local machine using git: 27 | 28 | ```sh 29 | $ git clone https://github.com/celo-org/faucet.git 30 | ``` 31 | 32 | Navigate to the project's root directory: 33 | 34 | ```sh 35 | $ cd faucet 36 | ``` 37 | 38 | ### Installing Node.js 39 | 40 | We use [Node.js](https://nodejs.org/en/) to run the project locally. 41 | You need to install the **Node.js version** specified in [.nvmrc](../.nvmrc). To do so, run: 42 | 43 | ```sh 44 | $ nvm install 45 | $ nvm use 46 | ``` 47 | 48 | ### Installing dependencies 49 | 50 | Once in the project's root directory, run the following command to install the project's 51 | dependencies: 52 | 53 | ```sh 54 | $ yarn install 55 | ``` 56 | 57 | After installing the dependencies, the project is ready to be run. 58 | 59 | ### Navigating the repository 60 | 61 | The project is structured into two apps located in the [`apps/`](./apps/) directory. 62 | 63 | 1. The firebase app contains functions which do the actual fauceting. 64 | 2. The web app contains a UI for making requests. 65 | 66 | ### Running packages 67 | 68 | Once you navigated to the package directory you want to run, inspect the `package.json` file 69 | and look for the `scripts` section. It contains the list of available scripts that can be run. 70 | 71 | ### Running the test suite 72 | 73 | Unfortunately, we don't have a consistent testing suite for the faucet. 74 | This is something we are working on improving. 75 | 76 | When you open a Pull Request, the GitHub CI will run any available test suites for you, but 77 | you can also add and run tests locally. 78 | 79 | > **INFO** 80 | > Some tests are run automatically when you open a Pull Request, while others are run when a 81 | > maintainer approves the Pull Request. This is for security reasons, as some tests require access 82 | > to secrets. 83 | 84 | ### Open a Pull Request 85 | 86 | ✅ Now you're ready to contribute to Celo SDK(s) and CLI(s)! 87 | -------------------------------------------------------------------------------- /apps/web/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 0; 7 | min-height: 100vh; 8 | row-gap: 1rem; 9 | padding-bottom: 2rem; 10 | } 11 | 12 | .grid { 13 | display: grid; 14 | grid-template-columns: repeat(4, minmax(25%, auto)); 15 | padding-right: 2rem; 16 | max-width: 100%; 17 | gap: 1rem; 18 | } 19 | 20 | .grid > :first-child { 21 | grid-column: span 4; 22 | } 23 | .grid > :first-child p { 24 | max-width: 100%; 25 | } 26 | 27 | .card { 28 | text-align: left; 29 | padding: 1rem 1.2rem; 30 | width: 100%; 31 | } 32 | 33 | .card span { 34 | display: inline-block; 35 | transition: transform 200ms; 36 | } 37 | 38 | .card h3 { 39 | font-weight: 500; 40 | margin-bottom: 0.7rem; 41 | display: inline-flex; 42 | } 43 | 44 | .card h3 img { 45 | margin-right: 0.5rem; 46 | } 47 | 48 | .card p { 49 | margin: 0; 50 | opacity: 0.85; 51 | font-size: 0.9rem; 52 | line-height: 1.5; 53 | max-width: 30ch; 54 | text-align: left; 55 | } 56 | 57 | .card h3 span { 58 | margin-left: 4px; 59 | } 60 | 61 | .switchNetwork { 62 | text-decoration: underline dotted; 63 | text-underline-offset: 3px; 64 | } 65 | .switchNetwork:hover { 66 | text-decoration: underline; 67 | } 68 | 69 | /* Enable hover only on non-touch devices */ 70 | @media (hover: hover) and (pointer: fine) { 71 | .card:hover span { 72 | transform: translateX(4px); 73 | } 74 | } 75 | 76 | @media (prefers-reduced-motion) { 77 | .thirteen::before { 78 | animation: none; 79 | } 80 | 81 | .card:hover span { 82 | transform: none; 83 | } 84 | } 85 | 86 | /* Mobile */ 87 | @media (max-width: 700px) { 88 | .main { 89 | padding: 1rem; 90 | } 91 | .grid { 92 | grid-template-columns: 1fr; 93 | margin-top: 40px; 94 | margin-bottom: 40px; 95 | max-width: 100%; 96 | gap: 1rem; 97 | padding-right: 0rem; 98 | } 99 | 100 | .grid > :first-child { 101 | grid-column: span 1; 102 | } 103 | 104 | .title { 105 | font-size: 1.5rem; 106 | } 107 | 108 | .card { 109 | padding: 1rem 2.5rem; 110 | } 111 | 112 | .card h3 { 113 | margin-bottom: 0.5rem; 114 | } 115 | 116 | .container { 117 | flex: none; 118 | } 119 | } 120 | 121 | /* Tablet and Smaller Desktop */ 122 | @media (min-width: 701px) and (max-width: 1600px) { 123 | .main { 124 | padding: 1rem; 125 | justify-content: unset; 126 | } 127 | .grid { 128 | grid-template-columns: repeat(2, 50%); 129 | justify-items: center; 130 | gap: 1rem; 131 | padding-right: 1rem; 132 | } 133 | 134 | .grid > :first-child { 135 | grid-column: span 2; 136 | } 137 | } 138 | 139 | @media (prefers-color-scheme: dark) { 140 | .vercelLogo { 141 | filter: invert(1); 142 | } 143 | 144 | .thirteen img { 145 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /apps/web/components/faucet-status.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useEffect, useState } from 'react' 2 | import { inter } from 'components/request-form' 3 | import { CHAIN_PARAMS } from 'config/chains' 4 | import styles from 'styles/Form.module.css' 5 | import { Network, RequestRecord, RequestStatus } from 'types' 6 | import { subscribeRequest } from 'utils/firebase.client' 7 | 8 | interface StatusProps { 9 | faucetRequestKey: string | null 10 | isExecuting: boolean 11 | failureStatus: string | null 12 | errors: any[] 13 | reset: () => void 14 | network: Network 15 | } 16 | export const FaucetStatus: FC = ({ 17 | reset, 18 | faucetRequestKey, 19 | isExecuting, 20 | errors, 21 | failureStatus, 22 | network, 23 | }: StatusProps) => { 24 | const [faucetRecord, setFaucetRecord] = useState>() 25 | 26 | const onFirebaseUpdate = useCallback( 27 | ({ status, dollarTxHash, goldTxHash }: RequestRecord) => { 28 | setFaucetRecord({ 29 | status, 30 | dollarTxHash, 31 | goldTxHash, 32 | }) 33 | if (status === RequestStatus.Done) { 34 | setTimeout(reset, 2_000) 35 | } 36 | }, 37 | [reset], 38 | ) 39 | 40 | useEffect(() => { 41 | const run = async function () { 42 | if (faucetRequestKey) { 43 | console.info('subscribing to events...') 44 | await subscribeRequest(faucetRequestKey, onFirebaseUpdate, network) 45 | } 46 | } 47 | // eslint-disable-next-line 48 | run().catch(console.error) 49 | }, [faucetRequestKey, onFirebaseUpdate, network]) 50 | 51 | if (!faucetRecord && !isExecuting && !errors?.length && !failureStatus) { 52 | return null 53 | } 54 | 55 | if (errors?.length) { 56 | console.error('Faucet Error', errors) 57 | } 58 | 59 | return ( 60 |
61 |

62 | Status:{' '} 63 | {errors?.length || failureStatus?.length 64 | ? 'Error' 65 | : (faucetRecord?.status ?? 'Initializing')} 66 |

67 | 68 | {failureStatus ? ( 69 | 70 | {failureStatus} 71 | 72 | ) : null} 73 |
74 | ) 75 | } 76 | 77 | const TxMessage = ({ 78 | txHash, 79 | network, 80 | }: { 81 | txHash?: string 82 | network: Network 83 | }) => { 84 | if (!txHash) { 85 | return null 86 | } 87 | if (txHash === 'skipped') { 88 | return ( 89 | 90 | No celo was transferred as the account already has a large celo balance. 91 | 92 | ) 93 | } 94 | const explorerUrl = new URL(CHAIN_PARAMS[network].blockExplorerUrls[0]) 95 | explorerUrl.pathname = `/tx/${txHash}` 96 | return ( 97 | 103 | View on Celo Explorer 104 | 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /apps/firebase/scripts/cli.ts: -------------------------------------------------------------------------------- 1 | import { ensureLeading0x } from '@celo/utils/lib/address' 2 | import { execSync } from 'child_process' 3 | import { privateKeyToAddress } from 'viem/accounts' 4 | import yargs from 'yargs' 5 | 6 | // tslint:disable-next-line: no-unused-expression 7 | yargs 8 | .scriptName('yarn cli') 9 | .recommendCommands() 10 | .demandCommand(1) 11 | .strict(true) 12 | .showHelpOnFail(true) 13 | .command( 14 | 'deploy:functions', 15 | 'Deploy Project firebase functions', 16 | (x) => x, 17 | () => deployFunctions(), 18 | ) 19 | .command( 20 | 'accounts:get', 21 | 'Get Accounts for a network', 22 | (args) => 23 | args.option('net', { 24 | type: 'string', 25 | description: 'Name of network', 26 | demandOption: true, 27 | }), 28 | (args) => printAccounts(args.net), 29 | ) 30 | .command( 31 | 'accounts:clear', 32 | 'Remova all Accounts for a network', 33 | (args) => 34 | args.option('net', { 35 | type: 'string', 36 | description: 'Name of network', 37 | demandOption: true, 38 | }), 39 | (args) => clearAccounts(args.net), 40 | ) 41 | .command( 42 | 'accounts:add ', 43 | 'Add an account', 44 | (args) => 45 | args 46 | .option('net', { 47 | type: 'string', 48 | description: 'Name of network', 49 | demandOption: true, 50 | }) 51 | .positional('pk', { 52 | type: 'string', 53 | description: 'Private Key. Format 0x...', 54 | }) 55 | .demand(['pk']), 56 | (args) => addAccount(args.net, args.pk), 57 | ) 58 | .command( 59 | 'faucet:request ', 60 | 'Request Funds', 61 | (args) => 62 | args 63 | .option('net', { 64 | type: 'string', 65 | description: 'Name of network', 66 | demand: true, 67 | }) 68 | .option('to', { 69 | type: 'string', 70 | description: 'Address', 71 | demand: true, 72 | }), 73 | (args) => enqueueFundRequest(args.net, args.to), 74 | ).argv 75 | 76 | function printAccounts(network: string) { 77 | execSync(`yarn firebase database:get --pretty /${network}/accounts`, { 78 | stdio: 'inherit', 79 | }) 80 | } 81 | 82 | function enqueueFundRequest(network: string, address: string) { 83 | const request = { 84 | beneficiary: address, 85 | status: 'Pending', 86 | type: 'Faucet', 87 | } 88 | const data = JSON.stringify(request) 89 | execSync(`yarn firebase database:push -d '${data}' /${network}/requests`, { 90 | stdio: 'inherit', 91 | }) 92 | } 93 | 94 | function addAccount(network: string, pk: string) { 95 | const account = { 96 | pk, 97 | address: privateKeyToAddress(ensureLeading0x(pk)), 98 | locked: false, 99 | } 100 | const data = JSON.stringify(account) 101 | execSync(`yarn firebase database:push -d '${data}' /${network}/accounts`, { 102 | stdio: 'inherit', 103 | }) 104 | } 105 | 106 | function clearAccounts(network: string) { 107 | execSync(`yarn firebase database:remove /${network}/accounts`, { 108 | stdio: 'inherit', 109 | }) 110 | } 111 | 112 | function deployFunctions() { 113 | execSync(`yarn firebase deploy --only functions:faucetRequestProcessor`, { 114 | stdio: 'inherit', 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Welcome to Celo Faucet app 2 | 3 | This Repo contains the code for the celo testnets faucet. This is contained in 2 apps. 4 | 5 | - The firebase app contains functions which do the actual fauceting. 6 | 7 | - The web app contains a UI for making requests. 8 | 9 | The web app deploys automatically to vercel. 10 | 11 | The deploy-chains gh actions deploys functions to staging and production envronments from staging and master branches respectively 12 | 13 | Note other branches are not deployed autamtically but can be by manually triggering the flow 14 | 15 | 16 | ## Setup 17 | 18 | ### Wep app 19 | 20 | To set up the web app to run locally: 21 | 22 | 1. navigate to the `apps/web` folder 23 | 24 | ```sh 25 | $ cd apps/web 26 | ``` 27 | 28 | 1. link your local repository to the `faucet` project on Vercel 29 | 30 | ```sh 31 | $ yarn dlx vercel@latest link 32 | ``` 33 | 34 | You'll be asked to authenticate with your Vercel account. Once you've done that, you'll be 35 | guided through a series of prompts to link your local project to the `faucet` Vercel project. 36 | 37 | ``` 38 | ? Set up “~/Documents/celo-org/faucet/apps/web”? [Y/n] y 39 | ? Which scope should contain your project? Celo Ecosystem Project Hosting 40 | ? Link to existing project? [y/N] y 41 | ? What’s the name of your existing project? faucet 42 | ✅ Linked to c-labs/faucet (created .vercel) 43 | ``` 44 | 45 | 1. fetch environment variables from Vercel 46 | 47 | ```sh 48 | $ yarn dlx vercel@latest env pull 49 | ``` 50 | 51 | If you get an error like `Error! No project found`, you may need to run `vercel link` again. 52 | If everything worked, you should see a message like this: 53 | 54 | ```sh 55 | > Downloading `development` Environment Variables for Project faucet 56 | ✅ Created .env.local file [249ms] 57 | ``` 58 | 59 | 1. run the app locally 60 | 61 | ```sh 62 | $ yarn dev 63 | ``` 64 | 65 | You should see a message like this: 66 | 67 | ```sh 68 | ready - started server on 0.0.0.0:3000, url: http://localhost:3000 69 | info - Loaded env from /Users/arthur/Documents/celo-org/faucet/apps/web/.env.local 70 | ``` 71 | 72 | You can now view the app in your browser at http://localhost:3000. 73 | 74 | ## Firebase app 75 | 76 | To set up the firebase app to run locally: 77 | 78 | 1. navigate to the `apps/firebase` folder 79 | ```sh 80 | $ cd apps/firebase 81 | ``` 82 | 1. login to firebase 83 | ```sh 84 | $ yarn dlx firebase-tools@latest login 85 | ``` 86 | You'll be asked to authenticate with your Firebase account. 87 | 1. build the firebase app 88 | ```sh 89 | $ yarn run preserve 90 | ``` 91 | 1. ensure that you are on required node version specified in `engines.node` in 92 | `firebase/package.json`. Currently this is Node 20 at the time of writing. 93 | ```sh 94 | $ nvm use 95 | ``` 96 | 1. run the firebase app locally 97 | ```sh 98 | $ yarn run serve 99 | ``` 100 | 101 | ## Adding chains 102 | 103 | ### Web 104 | 105 | - Add the chain config and token info to `config/chains.ts`. 106 | 107 | - Add chain name to the networks array, and `ChainId` and `FaucetAddress` to enums in `types/index.ts`. 108 | 109 | ### Firebase 110 | 111 | Dispatch the deploy-chains workflow. ensure chain name is kebab case and matches a network in `config/chains.ts`. 112 | -------------------------------------------------------------------------------- /apps/web/components/setup-button.tsx: -------------------------------------------------------------------------------- 1 | import detectEthereumProvider from '@metamask/detect-provider' 2 | import Image from 'next/image' 3 | import { FC } from 'react' 4 | import { useAsyncCallback } from 'react-use-async-callback' 5 | import { CHAIN_PARAMS, tokens } from '../config/chains' 6 | import { ChainId, Network } from 'types' 7 | import { capitalize } from 'utils/capitalize' 8 | import { inter } from 'utils/inter' 9 | 10 | interface Props { 11 | network: Network 12 | } 13 | 14 | export const SetupButton: FC = ({ network }) => { 15 | const networkCapitalized = capitalize(network) 16 | 17 | const [importTokens, { isExecuting }] = useAsyncCallback(async () => { 18 | const provider = (await detectEthereumProvider()) as EthProvider 19 | 20 | if (provider?.request) { 21 | try { 22 | if (provider.chainId && Number(provider.chainId) !== ChainId[network]) { 23 | const result = await provider.request({ 24 | method: 'wallet_addEthereumChain', 25 | params: [CHAIN_PARAMS[network]], 26 | }) 27 | 28 | console.info('network added', result) 29 | 30 | // need to wait longer than just the await for the metamask popup to show 31 | await delay(3_000) 32 | } 33 | await Promise.all( 34 | chainTokenParams[network].map((params) => 35 | provider.request({ 36 | method: 'wallet_watchAsset', 37 | params, 38 | }), 39 | ), 40 | ) 41 | } catch (e: any) { 42 | console.error(e) 43 | alert(`Unable to complete: ${e.message}`) 44 | } 45 | } else { 46 | alert('Wallet Not Detected') 47 | } 48 | }, []) 49 | 50 | return ( 51 | 64 | ) 65 | } 66 | 67 | function delay(time: number) { 68 | return new Promise((resolve) => setTimeout(resolve, time)) 69 | } 70 | 71 | interface TokenParams { 72 | type: 'ERC20' 73 | options: { 74 | address: string 75 | symbol: string 76 | decimals: 18 77 | image: string 78 | } 79 | } 80 | const chainTokenParams = Object.keys(tokens).reduce< 81 | Record 82 | >( 83 | (params, network) => { 84 | params[network as Network] = tokens[network as Network].map( 85 | ({ symbol, address }) => ({ 86 | type: 'ERC20', 87 | options: { 88 | address, 89 | symbol, 90 | decimals: 18, 91 | image: `https://reserve.mento.org/assets/tokens/${symbol}.svg`, // A string url of the token logo 92 | }, 93 | }), 94 | ) 95 | return params 96 | }, 97 | {} as Record, 98 | ) 99 | 100 | interface EthProvider { 101 | request: (a: { method: string; params?: unknown }) => Promise 102 | chainId: string 103 | isMetaMask?: boolean 104 | once(eventName: string | symbol, listener: (...args: any[]) => void): this 105 | on(eventName: string | symbol, listener: (...args: any[]) => void): this 106 | off(eventName: string | symbol, listener: (...args: any[]) => void): this 107 | addListener( 108 | eventName: string | symbol, 109 | listener: (...args: any[]) => void, 110 | ): this 111 | removeListener( 112 | eventName: string | symbol, 113 | listener: (...args: any[]) => void, 114 | ): this 115 | removeAllListeners(event?: string | symbol): this 116 | } 117 | -------------------------------------------------------------------------------- /apps/web/components/request-form.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { Inter } from 'next/font/google' 3 | import { FC, FormEvent, useCallback, useRef, useState } from 'react' 4 | import { useGoogleReCaptcha } from 'react-google-recaptcha-v3' 5 | import { useAsyncCallback } from 'react-use-async-callback' 6 | import { Button } from '../@/components/ui/button' 7 | import { Input } from '../@/components/ui/input' 8 | import { Label } from '../@/components/ui/label' 9 | import styles from 'styles/Form.module.css' 10 | import { FaucetAPIResponse, Network } from 'types' 11 | import { saveAddress } from 'utils/history' 12 | import { useLastAddress } from 'utils/useLastAddress' 13 | 14 | const FaucetStatus = dynamic(async () => { 15 | const imported = await import('components/faucet-status') 16 | return imported.FaucetStatus 17 | }, {}) 18 | export const inter = Inter({ subsets: ['latin'] }) 19 | 20 | interface Props { 21 | isOutOfCELO: boolean 22 | network: Network 23 | } 24 | 25 | export const RequestForm: FC = ({ isOutOfCELO, network }) => { 26 | const inputRef = useRef(null) 27 | 28 | const { executeRecaptcha } = useGoogleReCaptcha() 29 | 30 | const previousAddress = useLastAddress() 31 | const [address, setAddress] = useState(previousAddress) 32 | const [faucetRequestKey, setKey] = useState(null) 33 | const [failureStatus, setFailureStatus] = useState(null) 34 | 35 | const disableCELOWhenOut = isOutOfCELO 36 | 37 | const [onSubmit, { isExecuting, errors }] = useAsyncCallback( 38 | async (event: FormEvent) => { 39 | event.preventDefault() 40 | 41 | const beneficiary = address 42 | console.info('begin faucet sequence') 43 | if (!beneficiary?.length || !executeRecaptcha) { 44 | console.info('aborting') 45 | return 46 | } 47 | saveAddress(beneficiary) 48 | 49 | const captchaToken = await executeRecaptcha('faucet') 50 | console.info('received captcha token...posting faucet request') 51 | const response = await fetch('api/faucet', { 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | body: JSON.stringify({ 57 | beneficiary, 58 | captchaToken, 59 | network, 60 | }), 61 | }) 62 | const result = (await response.json()) as FaucetAPIResponse 63 | console.info('faucet request sent...received') 64 | if (result.status === 'Failed') { 65 | console.warn(result.message) 66 | setFailureStatus(result.message) 67 | } else { 68 | setKey(result.key) 69 | } 70 | }, 71 | [address, executeRecaptcha], 72 | ) 73 | 74 | const onInvalid = useCallback((event: FormEvent) => { 75 | const { validity } = event.currentTarget 76 | console.log(event.currentTarget.validity, event.currentTarget.value) 77 | console.debug('validity input', JSON.stringify(validity)) 78 | if (validity.patternMismatch || validity.badInput || !validity.valid) { 79 | event.currentTarget.setCustomValidity('enter an 0x address') 80 | } else { 81 | event.currentTarget.setCustomValidity('') 82 | } 83 | }, []) 84 | 85 | const reset = useCallback(() => { 86 | setFailureStatus(null) 87 | setKey(null) 88 | }, []) 89 | 90 | const buttonDisabled = 91 | !executeRecaptcha || 92 | !!faucetRequestKey || 93 | disableCELOWhenOut || 94 | !address || 95 | isExecuting 96 | 97 | return ( 98 |
104 |
105 | 106 | setAddress(event.target.value)} 112 | pattern="^0x[a-fA-F0-9]{40}" 113 | type="text" 114 | placeholder="0x01F10..." 115 | className={styles.address} 116 | /> 117 | 120 |
121 | 122 | 130 | 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /apps/firebase/src/celo-adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { parseEther } from 'viem' 2 | import { celoAlfajores, celoSepolia } from 'viem/chains' 3 | import { beforeEach, describe, expect, it, vi } from 'vitest' 4 | import { CeloAdapter } from './celo-adapter' 5 | 6 | describe('CeloAdapter Integration Tests', () => { 7 | // Test configuration 8 | const testPrivateKey = 9 | '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as const 10 | // this is NOT the address of the test private key 11 | const validAddress = '0x744a3f56D61487FA2cD5a09262d31E6222DC136E' as const 12 | const testNodeUrls = { 13 | alfajores: celoAlfajores.rpcUrls.default.http[0], 14 | sepolia: celoSepolia.rpcUrls.default.http[0], 15 | } 16 | describe('Constructor', () => { 17 | it('creates an adapter with Alfajores chain when nodeUrl contains alfajores', () => { 18 | const adapter = new CeloAdapter({ 19 | pk: testPrivateKey, 20 | nodeUrl: testNodeUrls.alfajores, 21 | }) 22 | 23 | // Verify the adapter was created successfully 24 | expect(adapter).toBeInstanceOf(CeloAdapter) 25 | 26 | // Test that the transferCelo method exists 27 | expect(typeof adapter.transferCelo).toBe('function') 28 | }) 29 | 30 | it('creates an adapter with Sepolia chain when nodeUrl contains sepolia', () => { 31 | const adapter = new CeloAdapter({ 32 | pk: testPrivateKey, 33 | nodeUrl: testNodeUrls.sepolia, 34 | }) 35 | 36 | expect(adapter).toBeInstanceOf(CeloAdapter) 37 | expect(typeof adapter.transferCelo).toBe('function') 38 | }) 39 | 40 | it('defaults to Sepolia chain when nodeUrl does not contain alfajores', () => { 41 | const adapter = new CeloAdapter({ 42 | pk: testPrivateKey, 43 | nodeUrl: 'https://some-other-node.org', 44 | }) 45 | 46 | expect(adapter).toBeInstanceOf(CeloAdapter) 47 | expect(typeof adapter.transferCelo).toBe('function') 48 | }) 49 | }) 50 | 51 | describe('transferCelo method', () => { 52 | let adapter: CeloAdapter 53 | beforeEach(() => { 54 | vi.resetAllMocks() 55 | adapter = new CeloAdapter({ 56 | pk: testPrivateKey, 57 | nodeUrl: testNodeUrls.alfajores, 58 | }) 59 | vi.spyOn(adapter.client, 'sendTransaction').mockResolvedValue( 60 | '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as const, 61 | ) 62 | }) 63 | it('has the correct method signature', () => { 64 | // Test that the method exists and has the right signature 65 | expect(typeof adapter.transferCelo).toBe('function') 66 | 67 | // The method should accept Address and bigint parameters 68 | // and return a Promise 69 | const method = adapter.transferCelo 70 | expect(method.length).toBe(2) // Should accept 2 parameters 71 | }) 72 | 73 | it('accepts valid parameters without throwing', async () => { 74 | // Use a valid checksummed address 75 | const amount = parseEther('1') // 1 CELO in wei 76 | 77 | await adapter.transferCelo(validAddress, amount) 78 | expect(adapter.client.sendTransaction).toHaveBeenCalledWith({ 79 | to: validAddress, 80 | value: amount, 81 | chain: celoAlfajores, 82 | }) 83 | }) 84 | 85 | it('handles different amount types', async () => { 86 | // Test with different bigint amounts 87 | const amounts = [ 88 | BigInt(0), 89 | parseEther('1'), // 1 CELO 90 | parseEther('100000000'), // Very large amount 91 | ] 92 | 93 | amounts.forEach(async (amount) => { 94 | await adapter.transferCelo(validAddress, amount) 95 | expect(adapter.client.sendTransaction).toHaveBeenCalledWith({ 96 | to: validAddress, 97 | value: amount, 98 | chain: celoAlfajores, 99 | }) 100 | }) 101 | }) 102 | }) 103 | 104 | describe('Chain Configuration', () => { 105 | it('uses correct chain configuration for Alfajores', () => { 106 | const adapter = new CeloAdapter({ 107 | pk: testPrivateKey, 108 | nodeUrl: testNodeUrls.alfajores, 109 | }) 110 | 111 | // Verify the adapter was created with Alfajores configuration 112 | expect(adapter).toBeInstanceOf(CeloAdapter) 113 | }) 114 | 115 | it('uses correct chain configuration for Sepolia', () => { 116 | const adapter = new CeloAdapter({ 117 | pk: testPrivateKey, 118 | nodeUrl: testNodeUrls.sepolia, 119 | }) 120 | 121 | // Verify the adapter was created with Sepolia configuration 122 | expect(adapter).toBeInstanceOf(CeloAdapter) 123 | }) 124 | }) 125 | 126 | describe('Error Handling', () => { 127 | it('handles invalid private key format gracefully', () => { 128 | // This should throw an error due to invalid private key format 129 | expect(() => { 130 | new CeloAdapter({ 131 | pk: 'invalid-private-key' as any, 132 | nodeUrl: testNodeUrls.alfajores, 133 | }) 134 | }).toThrow() 135 | }) 136 | }) 137 | 138 | describe('Real Network Integration', () => { 139 | it('creates adapter instances without throwing network errors', () => { 140 | // Test that we can create adapters for both networks 141 | const alfajoresAdapter = new CeloAdapter({ 142 | pk: testPrivateKey, 143 | nodeUrl: testNodeUrls.alfajores, 144 | }) 145 | 146 | const sepoliaAdapter = new CeloAdapter({ 147 | pk: testPrivateKey, 148 | nodeUrl: testNodeUrls.sepolia, 149 | }) 150 | 151 | expect(alfajoresAdapter).toBeInstanceOf(CeloAdapter) 152 | expect(sepoliaAdapter).toBeInstanceOf(CeloAdapter) 153 | }) 154 | 155 | it('handles method calls without network errors', async () => { 156 | const adapter = new CeloAdapter({ 157 | pk: testPrivateKey, 158 | nodeUrl: testNodeUrls.alfajores, 159 | }) 160 | 161 | // The method should be callable (though it will fail due to insufficient funds) 162 | // We're testing that the method signature and basic functionality work 163 | expect(typeof adapter.transferCelo).toBe('function') 164 | 165 | // We can't actually call it because it would try to send a real transaction 166 | // but we can verify the method exists and has the right signature 167 | }) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /apps/web/utils/firebase.serverside.ts: -------------------------------------------------------------------------------- 1 | import { lockedGoldABI } from '@celo/abis' 2 | import { Redis } from '@upstash/redis' 3 | import firebase from 'firebase/compat/app' 4 | import 'firebase/compat/auth' 5 | import 'firebase/compat/database' 6 | import { 7 | Address, 8 | AuthLevel, 9 | Network, 10 | RequestedTokenSet, 11 | RequestRecord, 12 | RequestStatus, 13 | RequestType, 14 | } from 'types' 15 | import { createPublicClient, getAddress, http } from 'viem' 16 | import { celo, mainnet } from 'viem/chains' 17 | import { config } from './firebase-config' 18 | 19 | async function getFirebase() { 20 | if (!firebase.apps.length) { 21 | firebase.initializeApp(config) 22 | const loginUsername = process.env.FIREBASE_LOGIN_USERNAME 23 | const loginPassword = process.env.FIREBASE_LOGIN_PASSWORD 24 | if ( 25 | loginUsername === undefined || 26 | loginUsername === null || 27 | loginUsername.length === 0 || 28 | loginPassword === undefined 29 | ) { 30 | throw new Error('Login username or password is empty') 31 | } 32 | try { 33 | // Source: https://firebase.google.com/docs/auth 34 | await firebase 35 | .auth() 36 | .signInWithEmailAndPassword(loginUsername, loginPassword) 37 | } catch (e) { 38 | console.error(`Fail to login into Firebase: ${e}`) 39 | throw e 40 | } 41 | } 42 | return firebase 43 | } 44 | 45 | async function getDB(): Promise { 46 | return (await getFirebase()).database() 47 | } 48 | 49 | type RateLimit = Readonly<{ count: number; timePeriodInSeconds: number }> 50 | 51 | const SECONDS = 1 52 | const MINUTES = 60 * SECONDS 53 | const HOURS = 60 * MINUTES 54 | export const RATE_LIMITS: Record = { 55 | [AuthLevel.none]: { count: 4, timePeriodInSeconds: 24 * HOURS }, 56 | [AuthLevel.authenticated]: { count: 10, timePeriodInSeconds: 24 * HOURS }, 57 | } 58 | 59 | export const GLOBAL_RATE_LIMITS: Record = { 60 | [AuthLevel.none]: { count: 3, timePeriodInSeconds: 10 * MINUTES }, 61 | [AuthLevel.authenticated]: { count: 15, timePeriodInSeconds: 10 * MINUTES }, 62 | } 63 | 64 | export const RATE_LIMITS_PER_IP: RateLimit = { 65 | count: 18, // authenticated + none*2 66 | timePeriodInSeconds: 24 * HOURS, 67 | } 68 | 69 | export async function sendRequest( 70 | address: Address, 71 | skipStables: boolean, 72 | network: Network, 73 | authLevel: AuthLevel, 74 | ip?: string, 75 | userId?: string, 76 | ): Promise<{ key?: string; reason?: 'rate_limited' }> { 77 | // NOTE: make sure address is stable (no lowercase/not-prefixed BS) 78 | const beneficiary = getAddress( 79 | address.startsWith('0x') ? address : `0x${address}`, 80 | ) 81 | 82 | const newRequest: RequestRecord = { 83 | beneficiary, 84 | status: RequestStatus.Pending, 85 | type: RequestType.Faucet, 86 | tokens: skipStables ? RequestedTokenSet.Celo : RequestedTokenSet.All, 87 | authLevel, 88 | } 89 | 90 | try { 91 | if (await addressCanBeElevatedToTrusted(beneficiary)) { 92 | authLevel = AuthLevel.authenticated 93 | } 94 | const db = await getDB() 95 | const redis = Redis.fromEnv() 96 | const namespace = 'rate-limits' 97 | const ipNamespace = 'ip-counts' 98 | 99 | const [ 100 | pendingRequestCountGlobal, 101 | pendingRequestCountForBeneficiary, 102 | pendingRequestCountForUser, 103 | pendingRequestCountForIp, 104 | ] = await Promise.all([ 105 | redis.hlen(`${namespace}:global`), 106 | redis.hlen(`${namespace}:${beneficiary}`), 107 | userId ? redis.hlen(`${namespace}:${userId}`) : 0, 108 | redis.hlen(`${ipNamespace}:${ip}`), 109 | ]) 110 | 111 | if (pendingRequestCountGlobal >= GLOBAL_RATE_LIMITS[authLevel].count) { 112 | return { reason: 'rate_limited' } 113 | } 114 | 115 | if (pendingRequestCountForIp >= RATE_LIMITS_PER_IP.count) { 116 | return { reason: 'rate_limited' } 117 | } 118 | 119 | if (userId && pendingRequestCountForUser >= RATE_LIMITS[authLevel].count) { 120 | return { reason: 'rate_limited' } 121 | } 122 | 123 | if (pendingRequestCountForBeneficiary >= RATE_LIMITS[authLevel].count) { 124 | return { reason: 'rate_limited' } 125 | } 126 | 127 | const ref: firebase.database.Reference = await db 128 | .ref(`${network}/requests`) 129 | .push(newRequest) 130 | 131 | const params = { 132 | // INCREASE GLOBAL COUNT 133 | [`${namespace}:global`]: 134 | GLOBAL_RATE_LIMITS[authLevel].timePeriodInSeconds, 135 | 136 | // INCREASE COUNT FOR BENEFICIARY 137 | [`${namespace}:${beneficiary}`]: 138 | RATE_LIMITS[authLevel].timePeriodInSeconds, 139 | 140 | // INCREASE COUNT FOR USER IDENTIFIER IF AUTHENTICATED 141 | ...(userId != null && { 142 | [`${namespace}:${userId}`]: 143 | RATE_LIMITS.authenticated.timePeriodInSeconds, 144 | }), 145 | // INCREASE COUNT FOR IP 146 | [`${ipNamespace}:${ip}`]: RATE_LIMITS_PER_IP.timePeriodInSeconds, 147 | } 148 | 149 | /// BEGIN TRANSACTION 150 | const tx = redis.multi() 151 | for (const [path, ttl] of Object.entries(params)) { 152 | tx.hsetnx(path, ref.key!, 1) 153 | tx.expire(path, ttl) 154 | tx.hexpire(path, ref.key!, ttl) 155 | } 156 | await tx.exec() 157 | /// END TRANSACTION 158 | 159 | return { key: ref.key! } 160 | } catch (e) { 161 | console.error(`Error while sendRequest: ${e}`) 162 | throw e 163 | } 164 | } 165 | 166 | const ethPublicClient = createPublicClient({ 167 | transport: http(), 168 | chain: mainnet, 169 | }) 170 | const celoPublicClient = createPublicClient({ 171 | transport: http(), 172 | chain: celo, 173 | }) 174 | const LOCKED_CELO_CONTRACT_ADDRESS = 175 | '0x6cC083Aed9e3ebe302A6336dBC7c921C9f03349E' 176 | const WEI = BigInt('1000000000000000000') 177 | const MIN_ETH_ON_MAINNET = (BigInt(1) * WEI) / BigInt(100) // 0.01 ETH 178 | const MIN_LOCKED_CELO = BigInt(100) * WEI // 100 LockedCELO 179 | 180 | async function addressCanBeElevatedToTrusted(address: `0x${string}`) { 181 | const [ethOnMainnet, lockedCELO] = await Promise.all([ 182 | ethPublicClient.getBalance({ address }), 183 | celoPublicClient.readContract({ 184 | address: LOCKED_CELO_CONTRACT_ADDRESS, 185 | abi: lockedGoldABI, 186 | functionName: 'getAccountTotalLockedGold', 187 | args: [address], 188 | }), 189 | ]) 190 | 191 | return ethOnMainnet >= MIN_ETH_ON_MAINNET || lockedCELO >= MIN_LOCKED_CELO 192 | } 193 | -------------------------------------------------------------------------------- /apps/web/pages/[chain].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, NextPage } from 'next' 2 | import { useSession } from 'next-auth/react' 3 | import Head from 'next/head' 4 | import Link from 'next/link' 5 | import { 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from '../@/components/ui/card' 12 | import { Card } from '@/components/ui/card' 13 | import { FaucetHeader } from 'components/faucet-header' 14 | import { RequestForm } from 'components/request-form' 15 | import { SetupButton } from 'components/setup-button' 16 | import styles from 'styles/Home.module.css' 17 | import { Network, networks } from 'types' 18 | import { isBalanceBelowPar } from 'utils/balance' 19 | import { capitalize } from 'utils/capitalize' 20 | import { inter } from 'utils/inter' 21 | 22 | interface Props { 23 | isOutOfCELO: boolean 24 | network: Network 25 | } 26 | 27 | const Home: NextPage = ({ isOutOfCELO, network }: Props) => { 28 | const networkCapitalized = capitalize(network) 29 | const { data: session } = useSession() 30 | 31 | const otherNetwork = 32 | networks.indexOf(network) === 0 ? networks[1] : networks[0] 33 | return ( 34 | <> 35 | 36 | Fund Your Testnet Account 37 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | {networkCapitalized} Token Faucet 49 | 50 | {networks.length > 1 && ( 51 | 55 | Switch to {capitalize(otherNetwork)} 56 | 57 | )} 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | {!session && ( 66 | <> 67 | 68 | • To receive 10x the tokens,{' '} 69 | 70 | authenticate with GitHub 71 | 72 | 73 | 74 | )} 75 | 76 | • Need USDC? Get tokens at{' '} 77 | 78 | faucet.circle.com 79 | 80 | 81 | 82 | • {' '} 83 | 84 | Swap CELO for Mento Tokens 85 | 86 | 87 | {network === 'celo-sepolia' && ( 88 | 89 | • Alternative faucet{' '} 90 | 94 | by Google 95 | 96 | 97 | )} 98 |
99 |
100 |
101 | 102 | 171 |
172 | 173 | ) 174 | } 175 | 176 | export default Home 177 | 178 | export const getServerSideProps: GetServerSideProps = async ( 179 | context, 180 | ) => { 181 | const network = context.query.chain 182 | if (typeof network !== 'string' || !networks.includes(network as Network)) { 183 | return { 184 | notFound: true, 185 | } 186 | } 187 | 188 | const isOutOfCELO = await isBalanceBelowPar(network as Network) 189 | return { 190 | props: { isOutOfCELO, network: network as Network }, 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | env: { 16 | browser: true, 17 | es6: true, 18 | node: true, 19 | }, 20 | parser: '@typescript-eslint/parser', 21 | parserOptions: { 22 | project: 'tsconfig.json', 23 | sourceType: 'module', 24 | }, 25 | plugins: [ 26 | 'eslint-plugin-import', 27 | 'eslint-plugin-jsdoc', 28 | 'eslint-plugin-prefer-arrow', 29 | 'eslint-plugin-react', 30 | '@typescript-eslint', 31 | ], 32 | rules: { 33 | '@typescript-eslint/adjacent-overload-signatures': 'error', 34 | '@typescript-eslint/array-type': [ 35 | 'error', 36 | { 37 | default: 'array-simple', 38 | }, 39 | ], 40 | '@typescript-eslint/ban-types': [ 41 | 'error', 42 | { 43 | types: { 44 | Object: { 45 | message: 'Avoid using the `Object` type. Did you mean `object`?', 46 | }, 47 | Function: { 48 | message: 49 | 'Avoid using the `Function` type. Prefer a specific function type, like `() => void`.', 50 | }, 51 | Boolean: { 52 | message: 'Avoid using the `Boolean` type. Did you mean `boolean`?', 53 | }, 54 | Number: { 55 | message: 'Avoid using the `Number` type. Did you mean `number`?', 56 | }, 57 | String: { 58 | message: 'Avoid using the `String` type. Did you mean `string`?', 59 | }, 60 | Symbol: { 61 | message: 'Avoid using the `Symbol` type. Did you mean `symbol`?', 62 | }, 63 | }, 64 | }, 65 | ], 66 | '@typescript-eslint/consistent-type-assertions': 'error', 67 | '@typescript-eslint/consistent-type-definitions': 'error', 68 | '@typescript-eslint/dot-notation': 'error', 69 | '@typescript-eslint/explicit-function-return-type': 'off', 70 | '@typescript-eslint/explicit-member-accessibility': [ 71 | 'off', 72 | { 73 | accessibility: 'explicit', 74 | }, 75 | ], 76 | '@typescript-eslint/explicit-module-boundary-types': 'off', 77 | '@typescript-eslint/indent': 'off', 78 | '@typescript-eslint/member-delimiter-style': [ 79 | 'off', 80 | { 81 | multiline: { 82 | delimiter: 'none', 83 | requireLast: true, 84 | }, 85 | singleline: { 86 | delimiter: 'semi', 87 | requireLast: false, 88 | }, 89 | }, 90 | ], 91 | '@typescript-eslint/member-ordering': 'error', 92 | '@typescript-eslint/naming-convention': 'off', 93 | '@typescript-eslint/no-empty-function': 'error', 94 | '@typescript-eslint/no-empty-interface': 'error', 95 | '@typescript-eslint/no-explicit-any': 'off', 96 | '@typescript-eslint/no-floating-promises': 'error', 97 | '@typescript-eslint/no-misused-new': 'error', 98 | '@typescript-eslint/no-namespace': 'error', 99 | '@typescript-eslint/no-parameter-properties': 'off', 100 | '@typescript-eslint/no-shadow': [ 101 | 'error', 102 | { 103 | hoist: 'all', 104 | }, 105 | ], 106 | '@typescript-eslint/no-this-alias': 'error', 107 | '@typescript-eslint/no-unused-expressions': 'error', 108 | '@typescript-eslint/no-use-before-define': 'off', 109 | '@typescript-eslint/no-var-requires': 'off', 110 | '@typescript-eslint/prefer-for-of': 'error', 111 | '@typescript-eslint/prefer-function-type': 'error', 112 | '@typescript-eslint/prefer-namespace-keyword': 'error', 113 | '@typescript-eslint/quotes': 'off', 114 | '@typescript-eslint/semi': ['off', null], 115 | '@typescript-eslint/triple-slash-reference': [ 116 | 'error', 117 | { 118 | path: 'always', 119 | types: 'prefer-import', 120 | lib: 'always', 121 | }, 122 | ], 123 | '@typescript-eslint/type-annotation-spacing': 'off', 124 | '@typescript-eslint/typedef': 'off', 125 | '@typescript-eslint/unified-signatures': 'error', 126 | 'arrow-body-style': 'error', 127 | 'arrow-parens': ['off', 'always'], 128 | 'brace-style': ['off', 'off'], 129 | 'comma-dangle': 'off', 130 | complexity: 'off', 131 | 'constructor-super': 'error', 132 | curly: 'error', 133 | 'dot-notation': 'error', 134 | 'eol-last': 'off', 135 | eqeqeq: ['error', 'smart'], 136 | 'guard-for-in': 'error', 137 | 'id-denylist': 'error', 138 | 'id-match': 'error', 139 | 'import/no-extraneous-dependencies': 'off', 140 | 'import/no-internal-modules': 'off', 141 | 'import/order': 'warn', 142 | indent: 'off', 143 | 'jsdoc/check-alignment': 'error', 144 | 'jsdoc/check-indentation': 'error', 145 | 'jsdoc/tag-lines': [ 146 | 'error', 147 | 'any', 148 | { 149 | startLines: 1, 150 | }, 151 | ], 152 | 'linebreak-style': 'off', 153 | 'max-classes-per-file': ['error', 1], 154 | 'max-len': 'off', 155 | 'new-parens': 'off', 156 | 'newline-per-chained-call': 'off', 157 | 'no-bitwise': 'error', 158 | 'no-caller': 'error', 159 | 'no-cond-assign': 'error', 160 | 'no-console': 'off', 161 | 'no-constant-condition': 'error', 162 | 'no-debugger': 'error', 163 | 'no-duplicate-case': 'error', 164 | 'no-duplicate-imports': 'error', 165 | 'no-empty': 'error', 166 | 'no-empty-function': 'error', 167 | 'no-eval': 'error', 168 | 'no-extra-bind': 'error', 169 | 'no-extra-semi': 'off', 170 | 'no-fallthrough': 'off', 171 | 'no-invalid-this': 'off', 172 | 'no-irregular-whitespace': 'off', 173 | 'no-multiple-empty-lines': 'off', 174 | 'no-new-func': 'error', 175 | 'no-new-wrappers': 'error', 176 | 'no-redeclare': 'error', 177 | 'no-restricted-imports': [ 178 | 'error', 179 | { 180 | paths: [ 181 | { 182 | name: 'elliptic', 183 | importNames: ['ec'], 184 | }, 185 | ], 186 | }, 187 | ], 188 | 'no-restricted-syntax': ['error', 'ForInStatement'], 189 | 'no-return-await': 'error', 190 | 'no-sequences': 'error', 191 | 'no-shadow': 'off', 192 | 'no-sparse-arrays': 'error', 193 | 'no-template-curly-in-string': 'error', 194 | 'no-throw-literal': 'error', 195 | 'no-trailing-spaces': 'off', 196 | 'no-undef-init': 'error', 197 | 'no-underscore-dangle': 'error', 198 | 'no-unsafe-finally': 'error', 199 | 'no-unused-expressions': 'error', 200 | 'no-unused-labels': 'error', 201 | 'no-use-before-define': 'off', 202 | 'no-var': 'error', 203 | 'object-shorthand': 'error', 204 | 'one-var': ['off', 'never'], 205 | 'padded-blocks': [ 206 | 'off', 207 | { 208 | blocks: 'never', 209 | }, 210 | { 211 | allowSingleLineBlocks: true, 212 | }, 213 | ], 214 | 'prefer-arrow/prefer-arrow-functions': 'off', 215 | 'prefer-const': 'error', 216 | 'prefer-object-spread': 'error', 217 | 'quote-props': 'off', 218 | quotes: 'off', 219 | radix: 'error', 220 | 'react/jsx-curly-spacing': 'off', 221 | 'react/jsx-equals-spacing': 'off', 222 | 'react/jsx-tag-spacing': [ 223 | 'off', 224 | { 225 | afterOpening: 'allow', 226 | closingSlash: 'allow', 227 | }, 228 | ], 229 | 'react/jsx-wrap-multilines': 'off', 230 | semi: 'off', 231 | 'space-before-function-paren': 'off', 232 | 'space-in-parens': ['off', 'never'], 233 | 'spaced-comment': [ 234 | 'error', 235 | 'always', 236 | { 237 | markers: ['/'], 238 | }, 239 | ], 240 | 'use-isnan': 'error', 241 | 'valid-typeof': 'off', 242 | }, 243 | } 244 | -------------------------------------------------------------------------------- /apps/firebase/src/database-helper.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable max-classes-per-file */ 2 | import { retryAsync, sleep } from '@celo/utils/lib/async' 3 | import type { DataSnapshot, Database, Reference } from 'firebase-admin/database' 4 | import type { Address, Hex } from 'viem' 5 | import { CeloAdapter } from './celo-adapter' 6 | import { NetworkConfig } from './config' 7 | import { getQualifiedAmount } from './get-qualified-mount' 8 | import { ExecutionResult, logExecutionResult } from './metrics' 9 | 10 | export interface AccountRecord { 11 | pk: Hex 12 | address: Address 13 | locked: boolean 14 | } 15 | 16 | export enum AuthLevel { 17 | none = 'none', 18 | authenticated = 'authenticated', 19 | } 20 | 21 | export enum RequestStatus { 22 | Pending = 'Pending', 23 | Working = 'Working', 24 | Done = 'Done', 25 | Failed = 'Failed', 26 | } 27 | 28 | export enum RequestType { 29 | Faucet = 'Faucet', 30 | } 31 | 32 | export interface RequestRecord { 33 | beneficiary: Address 34 | status: RequestStatus 35 | type: RequestType 36 | dollarTxHash?: string 37 | goldTxHash?: string 38 | tokens?: RequestedTokenSet 39 | authLevel: AuthLevel 40 | } 41 | 42 | enum RequestedTokenSet { 43 | All = 'All', 44 | Stables = 'Stables', 45 | Celo = 'Celo', 46 | } 47 | 48 | export async function processRequest( 49 | snap: DataSnapshot, 50 | pool: AccountPool, 51 | config: NetworkConfig, 52 | ) { 53 | const request = snap.val() as RequestRecord 54 | if (request.status !== RequestStatus.Pending) { 55 | return 56 | } 57 | 58 | await snap.ref.update({ status: RequestStatus.Working }) 59 | console.info( 60 | `req(${snap.key}): Started working on ${request.type} request for:${request.beneficiary}`, 61 | ) 62 | 63 | try { 64 | if (request.type !== RequestType.Faucet) { 65 | logExecutionResult(snap.key, ExecutionResult.InvalidRequestErr) 66 | return ExecutionResult.InvalidRequestErr 67 | } 68 | const faucetSender = buildFaucetSender(request, snap, config) 69 | 70 | const actionResult = await pool.doWithAccount(faucetSender) 71 | if (actionResult === ActionResult.Ok) { 72 | await snap.ref.update({ status: RequestStatus.Done }) 73 | logExecutionResult(snap.key, ExecutionResult.Ok) 74 | return ExecutionResult.Ok 75 | } else { 76 | await snap.ref.update({ status: RequestStatus.Failed }) 77 | const result = 78 | actionResult === ActionResult.NoFreeAccount 79 | ? ExecutionResult.NoFreeAccountErr 80 | : ExecutionResult.ActionTimedOutErr 81 | logExecutionResult(snap.key, result) 82 | return result 83 | } 84 | } catch (err) { 85 | logExecutionResult(snap.key, ExecutionResult.OtherErr) 86 | console.error(`req(${snap.key}): ERROR proccessRequest`, err) 87 | await snap.ref.update({ status: RequestStatus.Failed }) 88 | throw err 89 | } 90 | } 91 | 92 | const MAX_RETRIES = 3 93 | const RETRY_WAIT_MS = 500 94 | function buildFaucetSender( 95 | request: RequestRecord, 96 | snap: DataSnapshot, 97 | config: NetworkConfig, 98 | ) { 99 | return async (account: AccountRecord) => { 100 | const { nodeUrl } = config 101 | const { celoAmount } = getQualifiedAmount(request.authLevel, config) 102 | const celo = new CeloAdapter({ nodeUrl, pk: account.pk }) 103 | 104 | await retryAsync( 105 | dispatchCeloFunds, 106 | MAX_RETRIES, 107 | [celo, request.beneficiary, celoAmount, snap], 108 | RETRY_WAIT_MS, 109 | ) 110 | } 111 | } 112 | 113 | async function dispatchCeloFunds( 114 | celo: CeloAdapter, 115 | address: Address, 116 | amount: bigint, 117 | snap: DataSnapshot, 118 | ) { 119 | console.info( 120 | `req(${snap.key}): Sending ${amount.toString()} celo to ${address}`, 121 | ) 122 | 123 | const celoTxhash = await celo.transferCelo(address, amount) 124 | console.info( 125 | `req(${snap.key}): CELO Transaction Submited to mempool. txhash:${celoTxhash}`, 126 | ) 127 | await snap.ref.update({ celoTxhash }) 128 | return celoTxhash 129 | } 130 | 131 | function withTimeout( 132 | timeout: number, 133 | fn: () => Promise, 134 | onTimeout?: () => A | Promise, 135 | ): Promise { 136 | return new Promise((resolve, reject) => { 137 | let timeoutHandler: NodeJS.Timeout | null = setTimeout(() => { 138 | timeoutHandler = null 139 | 140 | if (onTimeout) { 141 | resolve(onTimeout()) 142 | } else { 143 | reject(new Error(`Timeout after ${timeout} ms`)) 144 | } 145 | }, timeout) 146 | 147 | fn() 148 | .then((val) => { 149 | if (timeoutHandler !== null) { 150 | clearTimeout(timeoutHandler) 151 | resolve(val) 152 | } 153 | }) 154 | .catch((err) => { 155 | if (timeoutHandler !== null) { 156 | clearTimeout(timeoutHandler) 157 | reject(err) 158 | } 159 | }) 160 | }) 161 | } 162 | 163 | export interface PoolOptions { 164 | retryWaitMS: number 165 | getAccountTimeoutMS: number 166 | actionTimeoutMS: number 167 | } 168 | 169 | const SECOND = 1000 170 | 171 | enum ActionResult { 172 | Ok, 173 | NoFreeAccount, 174 | ActionTimeout, 175 | } 176 | export class AccountPool { 177 | constructor( 178 | private db: Database, 179 | public network: string, 180 | private options: PoolOptions = { 181 | getAccountTimeoutMS: 10 * SECOND, 182 | retryWaitMS: 3000, 183 | actionTimeoutMS: 50 * SECOND, 184 | }, 185 | ) { 186 | // is empty. 187 | } 188 | 189 | get accountsRef() { 190 | const network = this.network 191 | return this.db.ref(`/${network}/accounts`) 192 | } 193 | 194 | removeAll() { 195 | return this.accountsRef.remove() 196 | } 197 | 198 | addAccount(account: AccountRecord) { 199 | return this.accountsRef.push(account) 200 | } 201 | 202 | getAccounts() { 203 | return this.accountsRef.once('value').then((snap) => snap.val()) 204 | } 205 | 206 | async doWithAccount( 207 | action: (account: AccountRecord) => Promise, 208 | ): Promise { 209 | const accountSnap = await this.tryLockAccountWithRetries() 210 | if (!accountSnap) { 211 | return ActionResult.NoFreeAccount 212 | } 213 | 214 | try { 215 | return withTimeout( 216 | this.options.actionTimeoutMS, 217 | async () => { 218 | await action(accountSnap.val()) 219 | return ActionResult.Ok 220 | }, 221 | () => ActionResult.ActionTimeout, 222 | ) 223 | } finally { 224 | await accountSnap.child('locked').ref.set(false) 225 | } 226 | } 227 | 228 | async tryLockAccountWithRetries() { 229 | let end = false 230 | let retries = 0 231 | 232 | const loop = async () => { 233 | while (!end) { 234 | const acc = await this.tryLockAccount() 235 | if (acc != null) { 236 | return acc 237 | } else { 238 | await sleep(this.options.retryWaitMS) 239 | retries++ 240 | } 241 | } 242 | return null 243 | } 244 | 245 | const onTimeout = () => { 246 | end = true 247 | return null 248 | } 249 | 250 | const account = await withTimeout( 251 | this.options.getAccountTimeoutMS, 252 | loop, 253 | onTimeout, 254 | ) 255 | 256 | if (account) { 257 | console.info( 258 | `LockAccount: ${account.val().address} (after ${retries - 1} retries)`, 259 | ) 260 | } else { 261 | console.warn(`LockAccount: Failed`) 262 | } 263 | return account 264 | } 265 | 266 | async tryLockAccount(): Promise { 267 | const accountsSnap = await this.accountsRef.once('value') 268 | 269 | const accountKeys: string[] = [] 270 | accountsSnap.forEach((accSnap) => { 271 | accountKeys.push(accSnap.key!) 272 | }) 273 | console.log(`tryLockAccount: Found ${accountKeys.length} accounts`) 274 | for (const key of accountKeys) { 275 | console.log(`tryLockAccount: Trying to lock account ${key}`) 276 | const lockPath = accountsSnap.child(key + '/locked') 277 | console.info( 278 | `tryLockAccount: Lock path is ${lockPath.ref.toString()} ${lockPath.val()}`, 279 | ) 280 | if (!lockPath.val() && (await this.trySetLockField(lockPath.ref))) { 281 | return accountsSnap.child(key) 282 | } 283 | } 284 | 285 | return null 286 | } 287 | 288 | /** 289 | * Try to set `locked` field to true. 290 | * 291 | * @param lockRef Reference to lock field 292 | * @returns Wether it sucessfully updated the field 293 | */ 294 | private async trySetLockField(lockRef: Reference) { 295 | const txres = await lockRef.transaction((curr: boolean) => { 296 | if (curr) { 297 | return // already locked, abort 298 | } else { 299 | return true 300 | } 301 | }) 302 | return txres.committed 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /apps/firebase/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------