diff --git a/.gitignore b/.gitignore index 4d29575..1e952cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +node_modules /.pnp .pnp.js @@ -9,7 +9,8 @@ /coverage # production -/build +build +dist # misc .DS_Store @@ -21,3 +22,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.aider* diff --git a/LICENSE.md b/LICENSE.md index 62346bd..aa99387 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 oxheadalpha +Copyright (c) 2023 Oxhead Alpha Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index aa23219..c7a8cd6 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,21 @@ A one-click faucet for Tezos, now enhanced with a PoW (Proof of Work) challenge The faucet's backend code can be located at [tezos-faucet-backend](https://github.com/oxheadalpha/tezos-faucet-backend). The backend handles the faucet's private key, CAPTCHA secret, PoW challenge creation and solution verification, and the amounts of Tez sent. -Sent amounts and challenge details are configured via `profiles`. This enforces security, avoiding a user trying to change amounts in frontend javascript code and drying out the faucet. Two profiles are created by default: **user**, to get 1 xtz and **baker** to get 6000 xtz. - ### Proof of Work (PoW) Challenge To mitigate potential abuse and ensure a fair distribution of Tez, users are now required to solve computational challenges before receiving their Tez. This PoW mechanism discourages bots and other malicious actors from exploiting the faucet. ### Application Flow -1. **Initiating the Process**: Upon a Tez request, the frontend communicates with the `/challenge` endpoint of the backend, providing essential details such as the user's address and the profile type. +1. **Initiating the Process**: Upon a Tez request, the frontend communicates with the `/challenge` endpoint of the backend, providing essential details such as the user's address and the amount of Tez requested. 2. **Receiving and Solving the Challenge**: The backend then sends a challenge. The difficulty and amount of challenges to be solved depends on factors such as if a CAPTCHA token was submitted and how much Tez was requested. The browser will create a webworker which will begin the process of finding a solution. 3. **Submitting and Verifying the Solution**: After solving, the frontend sends the solution to the `/verify` endpoint. The backend then checks its validity. Assuming it is valid, if more challenges are pending, the user proceeds to solve them. Once all challenges are cleared, Tez is sent to the user's account. ## Programmatic Faucet Usage -We provide a [`getTez.js`](./scripts/getTez.js) script for programmatic faucet usage. This script can be run from a JavaScript program or directly from a shell. +For programmatic usage of the faucet, we provide an npm package `@oxheadalpha/get-tez`. The code can be found [here](https://github.com/oxheadalpha/tezos-faucet/tree/main/getTez). Please refer to it for more details on how to use it. This script can be run from a JavaScript program or directly from a shell. It interacts with the backend to request Tez, solve the required challenges, and verify the solutions. -Please note that the `getTez.js` script does not use CAPTCHA. Therefore, challenges can be configured to make them more difficult and require more of them to be solved when using the programmatic faucet. +Please note that the programmatic faucet code does not use CAPTCHA and so more challenges can be given when using it. ## Setup @@ -80,13 +78,9 @@ See https://github.com/oxheadalpha/tezos-faucet-backend - `disableChallenges`: If PoW challenges need to be solved before receiving Tez. The backend must also disable challenges. Defaults to `false`. -- `profiles`: backend profiles, must match backend configuration. - -- - `user`: user profile, to get 1 XTZ - -- - `baker`: baker profile, to get 6000 XTZ - -- - - `amount`: amount given for the profile, for display only. +- `minTez`: The minimum amount of Tez that can be requested. +- +- `maxTez`: The maximum amount of Tez that can be requested. **Network configuration:** diff --git a/getTez/LICENSE b/getTez/LICENSE new file mode 100644 index 0000000..aa99387 --- /dev/null +++ b/getTez/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Oxhead Alpha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/getTez/README.md b/getTez/README.md new file mode 100644 index 0000000..6aae29e --- /dev/null +++ b/getTez/README.md @@ -0,0 +1,60 @@ +# get-tez + +This zero dependency package provides a programmatic interface to interact with the [Tezos faucet](https://github.com/oxheadalpha/tezos-faucet-backend). It is a script that can be run from a JavaScript/Typescript program or directly from a shell. + +## Installation + +You can install the package from npm: + +```bash +npm install @oxheadalpha/get-tez +``` + +## Usage + +### JS / TS +After installing the package, you can import it in your Node.js JavaScript or TypeScript project: + +```javascript +const getTez = require("@oxheadalpha/get-tez") + +// OR + +import getTez from "@oxheadalpha/get-tez" +``` + +You can then use the `getTez` function to interact with the Tezos faucet. The function takes an object as an argument, with the following properties: + +- `address`: The address to send Tez to. +- `amount`: The amount of Tez to request. +- `network`: The faucet's network name. Must match a network name with a faucet listed at https://teztnets.xyz. Ignored if `faucetUrl` is set. +- `faucetUrl`: The custom faucet URL. Ignores `network`. + +Here is an example of how to use the `getTez` function: + +```javascript +const txHash = await getTez({ + address: "tz1...", + amount: 10, + network: "ghostnet", +}) +``` + +Example using the `faucetUrl` parameter: +```js +const txHash = await getTez({ + address: "tz1...", + amount: 10, + faucetUrl: "https://my-custom-faucet-url.com", +}) +``` + +### CLI + +You can also run the script directly from the command line with Node.js. When you install the package via npm, the JavaScript file will be located at `node_modules/@oxheadalpha/get-tez/dist/getTez.js`. You can run it with the following command: + +```bash +node node_modules/@oxheadalpha/get-tez/dist/getTez.js tz1... --amount 10 --network ghostnet +``` + +Run the script with the `--help` flag for more information. diff --git a/getTez/getTez.ts b/getTez/getTez.ts new file mode 100755 index 0000000..91cd455 --- /dev/null +++ b/getTez/getTez.ts @@ -0,0 +1,371 @@ +import * as crypto from "crypto" + +const isMainModule = require.main === module + +/* +We use instantiate a "Console" to stderr for logging so that logs are not +written to stdout when the script is run from the CLI. We want the transaction +hash to be the only stdout once the Tez is sent to the user. +*/ +import { Console } from "console" +const console = new Console(isMainModule ? process.stderr : process.stdout) +const { log } = console + +let VERBOSE: boolean, TIME: boolean + +const verboseLog = (message: any) => VERBOSE && log(message) + +const [time, timeLog, timeEnd] = [ + console.time, + console.timeLog, + console.timeEnd, +].map( + (f: Function) => + (...a: any[]) => + TIME && f(...a) +) + +const displayHelp = () => { + log(`CLI Usage: node getTez.js [options]
+Options: + -h, --help Display help information. + -a, --amount The amount of Tez to request. + -n, --network Set the faucet's network name. Must match a + network name with a faucet listed at https://teztnets.xyz. + Ignored if --faucet-url is set. + -f, --faucet-url Set the custom faucet URL. Ignores --network. + -t, --time Enable PoW challenges timer. + -v, --verbose Enable verbose logging.`) +} + +const DISPLAY_HELP = isMainModule && true + +const handleError = (message: string, help?: boolean) => { + if (isMainModule) { + log(`ERROR: ${message}`, "\n") + help && displayHelp() + process.exit(1) + } else { + help && displayHelp() + throw new Error(message) + } +} + +type GetTezArgs = { + /** The address to send Tez to. */ + address: string + /** The amount of Tez to request. */ + amount: number + /** Set the faucet's network name. Must match a network name with a faucet + * listed at https://teztnets.xyz. Ignored if `faucetUrl` is set. */ + network?: string + /** Set the custom faucet URL. Ignores `network`. */ + faucetUrl?: string + /** Enable verbose logging. */ + verbose?: boolean + /** Enable PoW challenges timer */ + time?: boolean +} + +const parseCliArgs = (args: string | string[]) => { + if (typeof args === "string") args = args.split(" ") + + const parsedArgs: GetTezArgs = { + address: "", + amount: 0, + network: "", + faucetUrl: "", + } + + while (args.length > 0) { + const arg = args.shift() + switch (arg) { + case "-h": + case "--help": + if (isMainModule) { + displayHelp() + process.exit(0) + } else { + throw new Error("'--help' passed") + } + case "-a": + case "--amount": + parsedArgs.amount = Number(args.shift()) + break + case "-n": + case "--network": + parsedArgs.network = args.shift()?.toLowerCase() || "" + break + case "-f": + case "--faucet-url": + parsedArgs.faucetUrl = args.shift() || "" + break + case "-v": + case "--verbose": + VERBOSE = true + break + case "-t": + case "--time": + TIME = true + break + default: + parsedArgs.address = arg || "" + break + } + } + + return parsedArgs +} + +type ValidatedArgs = Required> + +const validateArgs = async (args: GetTezArgs): Promise => { + if (!args.address) { + handleError("Tezos address is required.", DISPLAY_HELP) + } + + if (!args.amount || args.amount <= 0) { + handleError("An amount greater than 0 is required.", DISPLAY_HELP) + } + + if (!args.faucetUrl && !args.network) { + handleError( + "Either a network name or faucet URL is required.", + DISPLAY_HELP + ) + } + + if (!args.faucetUrl) { + const teztnetsUrl = "https://teztnets.xyz/teztnets.json" + const response = await fetch(teztnetsUrl, { + signal: AbortSignal.timeout(2000), + }) + + if (!response.ok) { + handleError(`Error fetching networks from ${teztnetsUrl}`) + } + + args.network = args.network?.toLowerCase() + + for (const net of Object.values(await response.json()) as any[]) { + if (net.human_name.toLowerCase() === args.network) { + args.faucetUrl = net.faucet_url + } + } + + if (!args.faucetUrl) { + handleError("Network not found or not supported.") + } + } + + if (args.verbose) VERBOSE = true + if (args.time) TIME = true + + return args as ValidatedArgs +} + +const requestHeaders = { + // `fetch` by default sets "Connection: keep-alive" header. Was causing + // ECONNRESET errors with localhost. + Connection: "close", + "Content-Type": "application/x-www-form-urlencoded", +} + +/* Get Info */ + +const getInfo = async (faucetUrl: string) => { + verboseLog("Requesting faucet info...") + + const response = await fetch(`${faucetUrl}/info`, { + headers: requestHeaders, + signal: AbortSignal.timeout(2000), + }) + + const body = await response.json() + + if (!response.ok) { + handleError(body.message) + } + + return body +} + +/* Get Challenge */ + +const getChallenge = async ({ address, amount, faucetUrl }: ValidatedArgs) => { + verboseLog("Requesting PoW challenge...") + + const response = await fetch(`${faucetUrl}/challenge`, { + method: "POST", + headers: requestHeaders, + signal: AbortSignal.timeout(2000), + body: `address=${address}&amount=${amount}`, + }) + + const body = await response.json() + + if (!response.ok) { + handleError(body.message) + } + + return body +} + +/* Solve Challenge */ + +type SolveChallengeArgs = { + challenge: string + difficulty: number + challengeCounter: number + challengesNeeded: number +} + +type Solution = { + nonce: number + solution: string +} + +const solveChallenge = ({ + challenge, + difficulty, + challengeCounter, + challengesNeeded, +}: SolveChallengeArgs): Solution => { + const progress = Math.min( + 99, + Number((((challengeCounter - 1) / challengesNeeded) * 100).toFixed(1)) + ) + + if (isMainModule && process.stdout.isTTY) { + // Overwrite the same line instead of printing multiple lines. + process.stderr.clearLine(0) + process.stderr.cursorTo(0) + process.stderr.write(`Solving challenges... ${progress}% `) + } else { + verboseLog(`Solving challenges... ${progress}%`) + } + + let nonce = 0 + time("solved") + while (true) { + const input = `${challenge}:${nonce}` + const hash = crypto.createHash("sha256").update(input).digest("hex") + if (hash.startsWith("0".repeat(difficulty))) { + timeEnd("solved") + timeLog("getTez time") + verboseLog(`Solution found`) + return { solution: hash, nonce } + } + nonce++ + } +} + +/* Verify Solution */ + +type VerifySolutionArgs = Solution & ValidatedArgs + +type VerifySolutionResult = { + challenge?: string + challengeCounter?: number + difficulty?: number + txHash?: string +} + +const verifySolution = async ({ + address, + amount, + faucetUrl, + nonce, + solution, +}: VerifySolutionArgs): Promise => { + verboseLog("Verifying solution...") + + const response = await fetch(`${faucetUrl}/verify`, { + method: "POST", + headers: requestHeaders, + signal: AbortSignal.timeout(2000), + body: `address=${address}&amount=${amount}&nonce=${nonce}&solution=${solution}`, + }) + + const { txHash, challenge, challengeCounter, difficulty, message } = + await response.json() + + if (!response.ok) { + handleError(message) + } + + if (txHash) { + verboseLog(`Solution is valid`) + verboseLog(`Tez sent! Check transaction: ${txHash}\n`) + return { txHash } + } else if (challenge && difficulty && challengeCounter) { + verboseLog(`Solution is valid\n`) + return { challenge, difficulty, challengeCounter } + } else { + handleError(`Error verifying solution: ${message}`) + } + return {} +} + +/* Entrypoint */ + +const getTez = async (args: GetTezArgs) => { + const validatedArgs = await validateArgs(args) + + const { challengesEnabled, minTez, maxTez } = await getInfo( + validatedArgs.faucetUrl + ) + + if (!(args.amount >= minTez && args.amount <= maxTez)) { + handleError(`Amount must be between ${minTez} and ${maxTez} tez.`) + } + + if (!challengesEnabled) { + const txHash = ( + await verifySolution({ solution: "", nonce: 0, ...validatedArgs }) + )?.txHash + return txHash + } + + let { challenge, difficulty, challengeCounter, challengesNeeded } = + await getChallenge(validatedArgs) + + time("getTez time") + + while (challenge && difficulty && challengeCounter && challengesNeeded) { + verboseLog({ challenge, difficulty, challengeCounter }) + + const { solution, nonce } = solveChallenge({ + challenge, + difficulty, + challengeCounter, + challengesNeeded, + }) + + verboseLog({ nonce, solution }) + + let txHash + ;({ challenge, difficulty, challengeCounter, txHash } = + await verifySolution({ solution, nonce, ...validatedArgs })) + + if (txHash) { + timeEnd("getTez time") + return txHash + } + } +} + +if (isMainModule) { + log("getTez.js by Oxhead Alpha - Get Free Tez\n") + // If the file is executed directly by node and not via import then argv will + // include the file name. + const args = process.argv.slice(isMainModule ? 2 : 1) + const parsedArgs = parseCliArgs(args) + getTez(parsedArgs).then( + (txHash) => txHash && process.stdout.write("\n" + txHash) + ) +} + +// https://remarkablemark.org/blog/2020/05/05/typescript-export-commonjs-es6-modules +getTez.default = getTez +export = getTez diff --git a/getTez/package-lock.json b/getTez/package-lock.json new file mode 100644 index 0000000..31ac8ff --- /dev/null +++ b/getTez/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "get-tez", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "get-tez", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.6.0", + "typescript": "^5.2.2" + } + }, + "node_modules/@types/node": { + "version": "20.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", + "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/getTez/package.json b/getTez/package.json new file mode 100644 index 0000000..7295d8e --- /dev/null +++ b/getTez/package.json @@ -0,0 +1,40 @@ +{ + "name": "@oxheadalpha/get-tez", + "version": "1.0.0", + "description": "Programmatic script to get Tezos from faucet", + "main": "dist/getTez.js", + "types": "dist/getTez.d.ts", + "files": [ + "dist", + "package.json", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node ./dist/getTez.js", + "build": "tsc", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/oxheadalpha/tezos-faucet/tree/main/getTez" + }, + "keywords": [ + "tezos", + "blockchain", + "faucet", + "ghostnet", + "mondaynet", + "dailynet" + ], + "author": "Aryeh Harris", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.6.0", + "typescript": "^5.2.2" + } +} diff --git a/getTez/tsconfig.json b/getTez/tsconfig.json new file mode 100644 index 0000000..e8f6a25 --- /dev/null +++ b/getTez/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/package.json b/package.json index 0a36b68..d57e062 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "tezos-faucet", "version": "2.0.0", "description": "Tezos Faucet Frontend", + "license": "MIT", "private": true, "dependencies": { "@airgap/beacon-sdk": "^4.0.8", diff --git a/public/config.json b/public/config.json index a05c385..fec7801 100644 --- a/public/config.json +++ b/public/config.json @@ -5,14 +5,8 @@ "backendUrl": "http://localhost:3000", "githubRepo": "https://github.com/oxheadalpha/tezos-faucet", "disableChallenges": false, - "profiles": { - "user": { - "amount": 1 - }, - "baker": { - "amount": 6000 - } - } + "minTez": 1, + "maxTez": 6000 }, "network": { "name": "Ghostnet", diff --git a/scripts/configChecker.js b/scripts/configChecker.js index 57c245f..52f3625 100644 --- a/scripts/configChecker.js +++ b/scripts/configChecker.js @@ -105,6 +105,22 @@ const config: ConfigType = configJson "tmpConfig.ts" ) +;["minTez", "maxTez"].forEach((str) => { + const numValue = Number(Config.application[str]) + if (isNaN(numValue) || numValue <= 0) { + throw new Error( + `config.json is missing the application.${str} property or it's invalid. Please set application.${str} to a number greater than 0.` + ) + } +}) + +if (Config.application.minTez > Config.application.maxTez) { + throw new Error( + `In config.json, application.minTez is greater than application.maxTez. Please ensure that minTez is less than or equal to maxTez.` + ) +} + + const networkKeys = Object.keys(NetworkType); const configNetwork = Config.network.name.toLowerCase(); diff --git a/scripts/getTez.js b/scripts/getTez.js deleted file mode 100755 index ac951dc..0000000 --- a/scripts/getTez.js +++ /dev/null @@ -1,242 +0,0 @@ -"use strict" -const crypto = require("crypto") - -/* -We use console.error for logging so that logs are not written to stdout when the -script is run from the CLI. We want the transaction hash to be the only output -once the Tez is sent to the user. -*/ -const { error: log } = require("console") -let VERBOSE = false -const verboseLog = (message) => VERBOSE && log(message) - -const displayHelp = () => { - log(`Usage: getTez.js [options]
-Options: - -h, --help Display help information. - -p, --profile Set the profile ('user' or 'baker'). - -n, --network Set the faucet's network name. See available networks at https://teztnets.xyz. - Ignored if --faucet-url is set. - -f, --faucet-url Set the custom faucet URL. Ignores --network. - -v, --verbose Enable verbose logging.`) -} - -const isMainModule = require.main === module - -const DISPLAY_HELP = true -const handleError = (message, help) => { - if (isMainModule) { - log(message) - help && displayHelp() - process.exit(1) - } else { - help && displayHelp() - throw new Error(message) - } -} - -let address, - profile, - network, - faucetUrl = "" - -const parseArgs = async (args) => { - // If the file is executed directly by node and not via import then argv will - // include the file name. - args = args || process.argv.slice(isMainModule ? 2 : 1) - if (typeof args === "string") args = args.split(" ") - - while (args.length > 0) { - const arg = args.shift() - switch (arg) { - case "-h": - case "--help": - if (isMainModule) { - displayHelp() - process.exit(0) - } else { - throw new Error("'--help' passed") - } - case "-p": - case "--profile": - profile = args.shift().toLowerCase() - break - case "-n": - case "--network": - network = args.shift().toLowerCase() - break - case "-f": - case "--faucet-url": - faucetUrl = args.shift() - break - case "-v": - case "--verbose": - VERBOSE = true - break - default: - address = arg - break - } - } - - if (!address) { - handleError("Tezos address is required.", DISPLAY_HELP) - } - - if (!profile) { - handleError("Profile is required.", DISPLAY_HELP) - } else if (!["user", "baker"].includes(profile)) { - handleError("Invalid profile. Allowed values are 'user' or 'baker'.") - } - - if (!faucetUrl && !network) { - handleError("Either a network or faucet URL is required.", DISPLAY_HELP) - } - - if (!faucetUrl) { - const teztnetsUrl = "https://teztnets.xyz/teztnets.json" - const response = await fetch(teztnetsUrl) - - if (!response.ok) { - handleError(`Error fetching networks from ${teztnetsUrl}`) - } - - for (const net of Object.values(await response.json())) { - if (net.human_name.toLowerCase() === network) { - faucetUrl = net.faucet_url - } - } - - if (!faucetUrl) { - handleError("Network not found or not supported.") - } - } -} - -const requestHeaders = { - // `fetch` by default sets "Connection: keep-alive" header. Was causing - // ECONNRESET errors with localhost. - Connection: "close", - "Content-Type": "application/x-www-form-urlencoded", -} - -const getInfo = async () => { - verboseLog("Requesting faucet info...") - - const response = await fetch(`${faucetUrl}/info`, { - headers: requestHeaders, - }) - - const body = await response.json() - - if (!response.ok) { - handleError(`ERROR: ${body.message}`) - } - - return body -} - -const getChallenge = async () => { - verboseLog("Requesting PoW challenge...") - - const response = await fetch(`${faucetUrl}/challenge`, { - method: "POST", - headers: requestHeaders, - body: `address=${address}&profile=${profile}`, - }) - - const body = await response.json() - - if (!response.ok) { - handleError(`ERROR: ${body.message}`) - } - - return body -} - -const solvePow = (challenge, difficulty, challengeCounter) => { - if (isMainModule && process.stdout.isTTY) { - process.stderr.clearLine(0) - process.stderr.cursorTo(0) - process.stderr.write(`Solving challenge #${challengeCounter}... `) - } else { - verboseLog(`Solving challenge #${challengeCounter}...`) - } - - let nonce = 0 - while (true) { - const input = `${challenge}:${nonce}` - const hash = crypto.createHash("sha256").update(input).digest("hex") - if (hash.startsWith("0".repeat(difficulty))) { - verboseLog(`Solution found`) - return { solution: hash, nonce } - } - nonce++ - } -} - -const verifySolution = async (solution, nonce) => { - verboseLog("Verifying solution...") - - const response = await fetch(`${faucetUrl}/verify`, { - method: "POST", - headers: requestHeaders, - body: `address=${address}&profile=${profile}&nonce=${nonce}&solution=${solution}`, - }) - - const { txHash, challenge, challengeCounter, difficulty, message } = - await response.json() - - if (!response.ok) { - handleError(`ERROR: ${message}`) - } - - if (txHash) { - verboseLog(`Solution is valid`) - verboseLog(`Tez sent! Check transaction: ${txHash}\n`) - return { txHash } - } else if (challenge && difficulty && challengeCounter) { - verboseLog(`Solution is valid\n`) - return { challenge, difficulty, challengeCounter } - } else { - handleError(`Error verifying solution: ${message}`) - } -} - -const getTez = async (args) => { - await parseArgs(args) - - const faucetInfo = await getInfo() - - if (faucetInfo.challengesDisabled) { - const txHash = (await verifySolution("", 0)).txHash - return txHash - } - - let { challenge, difficulty, challengeCounter } = await getChallenge() - - while (challenge && difficulty && challengeCounter) { - verboseLog({ challenge, difficulty, challengeCounter }) - - const { solution, nonce } = solvePow( - challenge, - difficulty, - challengeCounter - ) - - verboseLog({ nonce, solution }) - - let txHash - ;({ challenge, difficulty, challengeCounter, txHash } = - await verifySolution(solution, nonce)) - - if (txHash) return txHash - } -} - -if (isMainModule) { - log("getTez.js by Oxhead Alpha - Get Free Tez\n") - return getTez().then((txHash) => txHash && process.stdout.write(txHash)) -} - -module.exports = getTez diff --git a/src/App.css b/src/App.css index 732b2e2..8ea179e 100644 --- a/src/App.css +++ b/src/App.css @@ -1,53 +1,46 @@ -div.card { - margin-top: 50px; -} - div.alert { - margin-top: 20px; + margin-top: 20px; } div.balances { - margin-top:10px; + margin-top: 10px; } -.content h1, .content h3 { - margin-top:50px; +.content h1, +.content h3 { + margin-top: 50px; } .faucet-info { - text-align:center; + text-align: center; } .faucet-part { - border-left: 5px solid gray; - padding: 10px; - margin: top 20px!important; -} - -.faucet-part-user { - margin-bottom: 10px; + border-left: 5px solid gray; + padding: 10px; + margin: top 20px !important; } .faucet-address-to { - margin-bottom:10px; + margin-bottom: 10px; } .balance-badge { - font-size:1em!important; + font-size: 0.80em !important; } .logotext_alpha { display: inline-block; font-family: 'Titillium Web'; - --tw-text-opacity: 1; - color: rgba(229, 38, 47, var(--tw-text-opacity)); - font-weight: 700; + --tw-text-opacity: 1; + color: rgba(229, 38, 47, var(--tw-text-opacity)); + font-weight: 700; } .logotext_oxhead { display: inline-block; font-family: 'Titillium Web'; - --tw-text-opacity: 1; - color: rgba(30, 38, 166, var(--tw-text-opacity)); - font-weight: 900; + --tw-text-opacity: 1; + color: rgba(30, 38, 166, var(--tw-text-opacity)); + font-weight: 900; } diff --git a/src/App.tsx b/src/App.tsx index 2d5f3e0..6391a44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,68 +1,70 @@ -import { useEffect, useState } from "react"; -import { Row, Col, Container } from 'react-bootstrap'; -import { TezosToolkit } from "@taquito/taquito"; -import './App.css'; +import { useEffect, useState } from "react" +import { Row, Col, Container } from "react-bootstrap" +import { TezosToolkit } from "@taquito/taquito" +import "./App.css" //import AppLogo from "../public/faucet-logo.png"; -import Header from "./components/Header"; -import Footer from "./components/Footer"; -import SplittedFaucet from "./components/Faucet/SplittedFaucet"; -import SplittedWallet from "./components/Wallet/SplittedWallet"; -import { Network, TestnetContext, UserContext } from "./lib/Types"; -import Config from "./Config"; +import Header from "./components/Header" +import Footer from "./components/Footer" +import SplittedFaucet from "./components/Faucet/SplittedFaucet" +import SplittedWallet from "./components/Wallet/SplittedWallet" +import { Network, TestnetContext, UserContext } from "./lib/Types" +import Config from "./Config" function App() { - // Common user data - const [userAddress, setUserAddress] = useState(""); - const [userBalance, setUserBalance] = useState(0); + const [userAddress, setUserAddress] = useState("") + const [userBalance, setUserBalance] = useState(0) // network data - const [network, setNetwork] = useState(Config.network); - const [Tezos, setTezos] = useState(new TezosToolkit(network.rpcUrl)); - const [wallet, setWallet] = useState(null); + const [network, setNetwork] = useState(Config.network) + const [Tezos, setTezos] = useState( + new TezosToolkit(network.rpcUrl) + ) + const [wallet, setWallet] = useState(null) - let user: UserContext = { userAddress, setUserAddress, userBalance, setUserBalance }; + const user: UserContext = { + userAddress, + setUserAddress, + userBalance, + setUserBalance, + } let testnet: TestnetContext = { network, wallet, setWallet, Tezos, - setTezos + setTezos, } useEffect(() => { - console.log(`Loading ${Config.network.name}`); - }, []); + console.log(`Loading ${Config.network.name}`) + }, []) return ( <>
- - - - Faucet logo - - - + + + Faucet logo - - - - - + + +