├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build-latest-v2.yml │ ├── release-v1.yml │ ├── release-v2-docker.yml │ ├── release-v2.yml │ └── test-build.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .nycrc ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── PoWFaucetVault.sol ├── demo │ ├── README.md │ ├── demo1-config.yaml │ ├── demo2-config.yaml │ ├── demo3-config.yaml │ ├── demo4-config.yaml │ └── demo5-config.yaml ├── sitecfg-apache2.conf ├── sitecfg-nginx.conf └── webserver-setup.md ├── faucet-client ├── .gitignore ├── README.md ├── build-client.js ├── package-lock.json ├── package.json ├── src │ ├── @types │ │ └── global.d.ts │ ├── common │ │ ├── FaucetApi.ts │ │ ├── FaucetConfig.ts │ │ ├── FaucetContext.ts │ │ ├── FaucetSession.ts │ │ ├── FaucetTime.ts │ │ └── index.ts │ ├── components │ │ ├── FaucetPage.scss │ │ ├── FaucetPage.tsx │ │ ├── claim │ │ │ ├── ClaimInput.tsx │ │ │ ├── ClaimNotificationClient.ts │ │ │ ├── ClaimPage.css │ │ │ ├── ClaimPage.tsx │ │ │ └── index.ts │ │ ├── details │ │ │ ├── DetailsPage.css │ │ │ ├── DetailsPage.tsx │ │ │ └── index.ts │ │ ├── frontpage │ │ │ ├── FaucetInput.tsx │ │ │ ├── FrontPage.tsx │ │ │ ├── RestoreSession.tsx │ │ │ ├── github │ │ │ │ ├── GithubLogin.css │ │ │ │ ├── GithubLogin.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── voucher │ │ │ │ └── VoucherInput.tsx │ │ │ └── zupass │ │ │ │ ├── ZupassLogin.css │ │ │ │ ├── ZupassLogin.tsx │ │ │ │ ├── ZupassTypes.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── mining │ │ │ ├── ConnectionAlert.tsx │ │ │ ├── MiningPage.tsx │ │ │ ├── PoWMinerStatus.tsx │ │ │ └── index.ts │ │ ├── passport │ │ │ ├── PassportInfo.css │ │ │ ├── PassportInfo.tsx │ │ │ └── index.ts │ │ ├── shared │ │ │ ├── FaucetCaptcha.tsx │ │ │ ├── FaucetDialog.tsx │ │ │ ├── FaucetNotification.tsx │ │ │ └── index.ts │ │ └── status │ │ │ ├── FaucetStatus.css │ │ │ ├── FaucetStatusPage.tsx │ │ │ ├── QueueStatusPage.tsx │ │ │ └── index.ts │ ├── index.ts │ ├── main.ts │ ├── pow │ │ ├── PoWClient.ts │ │ ├── PoWMiner.ts │ │ ├── PoWSession.ts │ │ └── index.ts │ ├── types │ │ ├── FaucetStatus.ts │ │ ├── PassportInfo.ts │ │ ├── PoWMinerSrc.ts │ │ └── index.ts │ ├── utils │ │ ├── ConvertHelpers.ts │ │ ├── DateUtils.ts │ │ ├── PoWParamsHelper.ts │ │ ├── PromiseDfd.ts │ │ ├── QueryUtils.ts │ │ └── index.ts │ └── worker │ │ ├── PoWWorker.ts │ │ ├── worker-argon2.ts │ │ ├── worker-cryptonight.ts │ │ ├── worker-nickminer.ts │ │ └── worker-scrypt.ts ├── tsconfig.json ├── utils │ └── CliArgs.js └── webpack.config.js ├── faucet-config.example.yaml ├── faucet-wasm ├── argon2 │ ├── .gitignore │ ├── build_wasm.js │ ├── build_wasm.sh │ ├── hash_a2.c │ ├── package-lock.json │ ├── package.json │ └── wasm-pre.js ├── cryptonight │ ├── .gitignore │ ├── Makefile │ ├── build_wasm.js │ ├── build_wasm.sh │ ├── package-lock.json │ ├── package.json │ └── wasm-pre.js ├── didkit │ ├── .gitignore │ ├── build_wasm.js │ ├── package-lock.json │ └── package.json ├── groth16 │ ├── .gitignore │ ├── build_lib.sh │ ├── lib.js │ ├── package-lock.json │ ├── package.json │ └── webpack.config.js ├── nickminer │ ├── .gitignore │ ├── build_wasm.js │ ├── build_wasm.sh │ ├── miner │ │ ├── keccak256.c │ │ ├── keccak256.h │ │ └── nickminer.c │ ├── nickminer-compile.sh │ ├── nickminer.Dockerfile │ ├── package-lock.json │ ├── package.json │ └── wasm-pre.js ├── scrypt │ ├── .gitignore │ ├── build_wasm.js │ ├── build_wasm.sh │ ├── package-lock.json │ └── package.json └── sqlite3 │ ├── .gitignore │ ├── build_wasm.js │ ├── package-lock.json │ └── package.json ├── libs ├── argon2_wasm.cjs ├── argon2_wasm.d.ts ├── cryptonight_wasm.cjs ├── cryptonight_wasm.d.ts ├── didkit_wasm.cjs ├── didkit_wasm.d.ts ├── groth16.cjs ├── nickminer_wasm.cjs ├── nickminer_wasm.d.ts ├── scrypt_wasm.cjs ├── scrypt_wasm.d.ts ├── sqlite3_wasm.cjs └── sqlite3_wasm.d.ts ├── package-lock.json ├── package.json ├── res ├── run-faucet.bat └── run-faucet.sh ├── src ├── @types │ ├── global.d.ts │ └── global.ts ├── abi │ └── ERC20.ts ├── app.ts ├── common │ ├── FaucetError.ts │ ├── FaucetProcess.ts │ ├── FaucetWorker.ts │ └── ServiceManager.ts ├── config │ ├── ConfigSchema.ts │ ├── ConfigShared.ts │ ├── DefaultConfig.ts │ └── FaucetConfig.ts ├── db │ ├── DatabaseWorker.ts │ ├── FaucetDatabase.ts │ ├── FaucetModuleDB.ts │ ├── SQL.ts │ └── driver │ │ ├── BaseDriver.ts │ │ ├── MySQLDriver.ts │ │ ├── SQLiteDriver.ts │ │ └── WorkerDriver.ts ├── eth │ ├── EthClaimManager.ts │ ├── EthClaimNotificationClient.ts │ ├── EthWalletManager.ts │ └── EthWalletRefill.ts ├── modules │ ├── BaseModule.ts │ ├── MODULE_HOOKS.md │ ├── ModuleManager.ts │ ├── captcha │ │ ├── CaptchaConfig.ts │ │ └── CaptchaModule.ts │ ├── concurrency-limit │ │ ├── ConcurrencyLimitConfig.ts │ │ └── ConcurrencyLimitModule.ts │ ├── ensname │ │ ├── EnsNameConfig.ts │ │ └── EnsNameModule.ts │ ├── ethinfo │ │ ├── EthInfoConfig.ts │ │ └── EthInfoModule.ts │ ├── faucet-balance │ │ ├── FaucetBalanceConfig.ts │ │ └── FaucetBalanceModule.ts │ ├── faucet-outflow │ │ ├── FaucetOutflowConfig.ts │ │ └── FaucetOutflowModule.ts │ ├── github │ │ ├── GithubConfig.ts │ │ ├── GithubDB.ts │ │ ├── GithubModule.ts │ │ └── GithubResolver.ts │ ├── ipinfo │ │ ├── IPInfoConfig.ts │ │ ├── IPInfoDB.ts │ │ ├── IPInfoModule.ts │ │ └── IPInfoResolver.ts │ ├── mainnet-wallet │ │ ├── MainnetWalletConfig.ts │ │ └── MainnetWalletModule.ts │ ├── modules.ts │ ├── passport │ │ ├── PassportConfig.ts │ │ ├── PassportDB.ts │ │ ├── PassportModule.ts │ │ └── PassportResolver.ts │ ├── pow │ │ ├── PoWClient.ts │ │ ├── PoWConfig.ts │ │ ├── PoWModule.ts │ │ ├── PoWServer.ts │ │ ├── PoWServerWorker.ts │ │ ├── PoWSession.ts │ │ ├── PoWShareVerification.ts │ │ └── validator │ │ │ ├── IPoWValidator.ts │ │ │ ├── PoWValidator.ts │ │ │ └── PoWValidatorWorker.ts │ ├── recurring-limits │ │ ├── RecurringLimitsConfig.ts │ │ └── RecurringLimitsModule.ts │ ├── voucher │ │ ├── VoucherConfig.ts │ │ ├── VoucherDB.ts │ │ └── VoucherModule.ts │ ├── whitelist │ │ ├── WhitelistConfig.ts │ │ └── WhitelistModule.ts │ └── zupass │ │ ├── ZupassConfig.ts │ │ ├── ZupassDB.ts │ │ ├── ZupassModule.ts │ │ ├── ZupassPCD.ts │ │ ├── ZupassUtils.ts │ │ ├── ZupassWorker.ts │ │ └── circuit.json ├── services │ ├── FaucetStatsLog.ts │ └── FaucetStatus.ts ├── session │ ├── FaucetSession.ts │ ├── SessionManager.ts │ └── SessionRewardFactor.ts ├── tools │ ├── createVoucher.ts │ └── migrateDB.ts ├── utils │ ├── ConvertHelpers.ts │ ├── CryptoUtils.ts │ ├── DateUtils.ts │ ├── FetchUtil.ts │ ├── GuidUtils.ts │ ├── HashedInfo.ts │ ├── ProcessLoadTracker.ts │ ├── PromiseDfd.ts │ ├── PromiseUtils.ts │ ├── StringUtils.ts │ └── VersionCompare.ts └── webserv │ ├── FaucetHttpServer.ts │ ├── FaucetWebApi.ts │ └── api │ └── faucetStatus.ts ├── static ├── css │ └── gh-fork-ribbon.css ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── images │ ├── fauceth_420.jpg │ ├── progress.gif │ ├── progress.png │ ├── spinner.gif │ └── zupass_logo.jpg ├── index.html └── site.webmanifest ├── tea.yaml ├── tests ├── EthClaimManager.spec.ts ├── EthWalletManager.spec.ts ├── EthWalletRefill.spec.ts ├── FaucetHttpServer.spec.ts ├── FaucetProcess.spec.ts ├── FaucetServices.spec.ts ├── FaucetSession.spec.ts ├── FaucetWebApi.spec.ts ├── ModuleManager.spec.ts ├── MySQLDatabase.spec.ts ├── Utils.spec.ts ├── common.ts ├── modules │ ├── CaptchaModule.spec.ts │ ├── ConcurrencyLimitModule.spec.ts │ ├── EnsNameModule.spec.ts │ ├── EthInfoModule.spec.ts │ ├── FaucetBalanceModule.spec.ts │ ├── FaucetOutflowModule.spec.ts │ ├── GithubModule.spec.ts │ ├── IpInfoModule.spec.ts │ ├── MainnetWalletModule.spec.ts │ ├── PassportModule.data.ts │ ├── PassportModule.spec.ts │ ├── PoWModule.spec.ts │ ├── RecurringLimitsModule.spec.ts │ ├── VoucherModule.spec.ts │ ├── WhitelistModule.spec.ts │ └── ZupassModule.spec.ts └── stubs │ ├── FakeProvider.ts │ └── FakeWebSocket.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | static/index.seo.html 4 | faucet-config.json 5 | faucet-config.yaml 6 | faucet-status.json 7 | faucet-store.json 8 | faucet-stats.log 9 | faucet-events.log 10 | faucet-pid.txt 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | libs/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | groups: 9 | "Server dependencies": 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "npm" 13 | directory: "/faucet-client" 14 | schedule: 15 | interval: "weekly" 16 | open-pull-requests-limit: 10 17 | groups: 18 | "Client dependencies": 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/release-v1.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build latest v1 release 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | run_tests: 10 | name: Run Tests 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.15.0 18 | 19 | - run: npm install 20 | - run: npm run test 21 | 22 | build_docker: 23 | name: Build Docker Image 24 | needs: [run_tests] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - 28 | name: Set up QEMU 29 | uses: docker/setup-qemu-action@v2 30 | - 31 | name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v2 33 | - 34 | name: Login to Docker Hub 35 | uses: docker/login-action@v2 36 | with: 37 | username: ${{ secrets.DOCKERHUB_USERNAME }} 38 | password: ${{ secrets.DOCKERHUB_TOKEN }} 39 | - 40 | name: Build and push 41 | uses: docker/build-push-action@v3 42 | with: 43 | push: true 44 | tags: pk910/powfaucet:v1-latest -------------------------------------------------------------------------------- /.github/workflows/release-v2-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build docker image for latest release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | build_docker: 10 | name: Build Docker Image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v3 16 | - name: Login to Docker Hub 17 | uses: docker/login-action@v3 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: Get project versions 22 | id: version 23 | run: | 24 | echo "server_version=$(cat package.json | jq ".version" | tr -d '"\r\n')" >> $GITHUB_OUTPUT 25 | echo "client_version=$(cat faucet-client/package.json | jq ".version" | tr -d '"\r\n')" >> $GITHUB_OUTPUT 26 | - name: Build and push multi-arch images 27 | run: | 28 | docker buildx build \ 29 | --platform linux/amd64,linux/arm64 \ 30 | --file Dockerfile \ 31 | --tag pk910/powfaucet:v${{ steps.version.outputs.server_version }} \ 32 | --tag pk910/powfaucet:v2-stable \ 33 | --push . 34 | -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Development Build 3 | 4 | on: 5 | pull_request: 6 | branches: [ "master" ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | 11 | run_tests: 12 | name: Run Tests 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v2 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | 23 | - run: npm install 24 | - run: npm run test 25 | 26 | build_binaries: 27 | name: Build Binaries 28 | needs: [run_tests] 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: 22 36 | - run: npm install -g pkg 37 | 38 | - run: npm install 39 | - run: npm run bundle 40 | 41 | # build client project 42 | - run: | 43 | cd faucet-client 44 | npm install 45 | node ./build-client.js 46 | 47 | - run: pkg --compress Brotli . 48 | 49 | - name: Upload powfaucet.js artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | path: ./bundle/powfaucet.cjs 53 | name: powfaucet.js 54 | - name: Upload linux binary artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | path: ./bin/pow-faucet-server-linux 58 | name: powfaucet-server-linux 59 | - name: Upload windows binary artifact 60 | uses: actions/upload-artifact@v4 61 | with: 62 | path: ./bin/pow-faucet-server-win.exe 63 | name: powfaucet-server-win.exe 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | bin/ 3 | build/ 4 | bundle/ 5 | coverage/ 6 | dist/ 7 | node_modules/ 8 | static/index.seo.html 9 | static/css/powfaucet* 10 | static/js/powfaucet* 11 | faucet-config.json 12 | faucet-config.yaml 13 | faucet-status.json 14 | faucet-status.yaml 15 | faucet-store.* 16 | faucet-stats.log 17 | faucet-events.log 18 | faucet-pid.txt 19 | tmp* -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "loader": "ts-node/esm" 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | bin/ 3 | build/ 4 | coverage/ 5 | faucet-client/ 6 | faucet-wasm/ 7 | node_modules/ 8 | res/ 9 | static/index.seo.html 10 | faucet-config.json 11 | faucet-config.yaml 12 | faucet-status.json 13 | faucet-status.yaml 14 | faucet-store.* 15 | faucet-stats.log 16 | faucet-events.log 17 | faucet-pid.txt 18 | tmp* -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["text", "lcov"], 3 | "include": [ 4 | "src/**" 5 | ], 6 | "exclude": [ 7 | "libs/*.js", 8 | "tests/**" 9 | ] 10 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build-server env 2 | FROM --platform=$BUILDPLATFORM node:22-slim AS build-server-env 3 | WORKDIR /build 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY ./libs libs 7 | COPY ./tsconfig.json . 8 | COPY ./webpack.config.js . 9 | COPY ./src src 10 | RUN npm run bundle 11 | 12 | # build-client env 13 | FROM --platform=$BUILDPLATFORM node:22-slim AS build-client-env 14 | WORKDIR /build 15 | COPY faucet-client/package*.json ./faucet-client/ 16 | COPY ./libs libs 17 | COPY ./static static 18 | RUN cd faucet-client && npm install 19 | COPY ./faucet-client faucet-client 20 | RUN cd faucet-client && node ./build-client.js 21 | 22 | # final stage 23 | FROM node:22-slim 24 | WORKDIR /app 25 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates 26 | RUN update-ca-certificates 27 | COPY --from=build-server-env /build/bundle ./bundle 28 | COPY --from=build-client-env /build/static ./static 29 | COPY ./faucet-config.example.yaml . 30 | RUN cp ./static/index.html ./static/index.seo.html && chmod 777 ./static/index.seo.html 31 | 32 | EXPOSE 8080 33 | ENTRYPOINT [ "node", "--no-deprecation", "bundle/powfaucet.cjs" ] 34 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The only supported version of PoWFaucet is the latest version 2.x.x release. The older version, 1.x.x, isn't getting updates anymore. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.x.x | :white_check_mark: | 10 | | < 2.0 | :x: | 11 | 12 | ## How to Tell Us About Security Problems 13 | 14 | Keeping PoWFaucet safe is very important. If you find a security problem, I'd really appreciate your help in telling me about it in a safe way. 15 | 16 | ### For Regular Bugs 17 | 18 | If you find a bug that's not about security, please just tell me on GitHub Issues. That way, everyone can see it and help fix it. 19 | 20 | ### For Serious Security Bugs 21 | 22 | If you find a big security issue that shouldn't be shared with everyone for safety reasons, please tell me directly: 23 | 24 | - **Matrix**: `@pk910:matrix.org` 25 | - **Twitter**: `@_pk910_` 26 | 27 | Please don't tell everyone about these big issues until I've had a chance to look at them. I promise to check out what you tell me quickly and work with you to fix any problems safely. 28 | 29 | ## Helping Out 30 | 31 | I'm the main person looking after PoWFaucet, but I'm always happy to get help from others, especially if it's about making PoWFaucet safer or more protective. 32 | 33 | Thanks for helping keep PoWFaucet safe and secure! 34 | -------------------------------------------------------------------------------- /docs/sitecfg-apache2.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | ServerAdmin webmaster@pk910.de 4 | ServerName kiln-faucet.pk910.de 5 | ServerAlias www.kiln-faucet.pk910.de 6 | 7 | # Document root of the faucet site (static folder) 8 | DocumentRoot /home/powfaucet/kiln-faucet/static 9 | 10 | Options -Indexes +FollowSymLinks +MultiViews 11 | AllowOverride All 12 | Require all granted 13 | 14 | 15 | # Prefer index.seo.html as index for better search engine results 16 | DirectoryIndex index.seo.html index.html 17 | 18 | # Redirect websocket calls (/pow) to nodejs faucet process 19 | RewriteEngine On 20 | RewriteCond %{HTTP:Upgrade} =websocket [NC] 21 | RewriteRule /ws/(.*)$ ws://localhost:8080/ws/$1 [P,L] 22 | RewriteRule /api/(.*)$ http://localhost:8080/api/$1 [P,L] 23 | 24 | LogLevel warn 25 | CustomLog ${APACHE_LOG_DIR}/access-kiln-faucet.pk910.de.log vhost_combined 26 | ErrorLog ${APACHE_LOG_DIR}/error-kiln-faucet.pk910.de.log 27 | 28 | 29 | 30 | 31 | ServerAdmin webmaster@pk910.de 32 | ServerName kiln-faucet.pk910.de 33 | ServerAlias www.kiln-faucet.pk910.de 34 | 35 | # Document root of the faucet site (static folder) 36 | DocumentRoot /home/powfaucet/kiln-faucet/static 37 | 38 | Options -Indexes +FollowSymLinks +MultiViews 39 | AllowOverride All 40 | Require all granted 41 | 42 | 43 | # Prefer index.seo.html as index for better search engine results 44 | DirectoryIndex index.seo.html index.html 45 | 46 | # Redirect websocket calls (/pow) to nodejs faucet process 47 | RewriteEngine On 48 | RewriteCond %{HTTP:Upgrade} =websocket [NC] 49 | RewriteRule /ws/(.*)$ ws://localhost:8080/ws/$1 [P,L] 50 | RewriteRule /api/(.*)$ http://localhost:8080/api/$1 [P,L] 51 | 52 | LogLevel warn 53 | CustomLog ${APACHE_LOG_DIR}/access-kiln-faucet.pk910.de.log vhost_combined 54 | ErrorLog ${APACHE_LOG_DIR}/error-kiln-faucet.pk910.de.log 55 | 56 | Include /etc/letsencrypt/options-ssl-apache.conf 57 | SSLCertificateFile /etc/letsencrypt/live/kiln-faucet.pk910.de/fullchain.pem 58 | SSLCertificateKeyFile /etc/letsencrypt/live/kiln-faucet.pk910.de/privkey.pem 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/sitecfg-nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | #listen [::]:80; # don't listen to IPv6 here! IPv4 limitations are kind of a natural barrier to prevent farming ;) 5 | 6 | server_name holesky-faucet.pk910.de www.holesky-faucet.pk910.de; 7 | 8 | root /home/powfaucet/holesky-faucet/static; 9 | index index.seo.html index.html; 10 | 11 | access_log /var/log/nginx/access-holesky-faucet.pk910.de.log; 12 | error_log /var/log/nginx/error-holesky-faucet.pk910.de.log error; 13 | 14 | location /ws/ { 15 | proxy_pass http://localhost:8080/ws/; 16 | proxy_http_version 1.1; 17 | proxy_set_header Upgrade $http_upgrade; 18 | proxy_set_header Connection "upgrade"; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | } 21 | location /api/ { 22 | proxy_pass http://localhost:8080/api/; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | } 25 | 26 | listen 443 ssl; # managed by Certbot 27 | ssl_certificate /etc/letsencrypt/live/holesky-faucet.pk910.de/fullchain.pem; # managed by Certbot 28 | ssl_certificate_key /etc/letsencrypt/live/holesky-faucet.pk910.de/privkey.pem; # managed by Certbot 29 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 30 | } 31 | -------------------------------------------------------------------------------- /docs/webserver-setup.md: -------------------------------------------------------------------------------- 1 | # Productive webserver setup 2 | 3 | For productive setups I'd suggest using a more complex webserver than the built in low-level static server as it does not support ssl, caching and stuff. 4 | I preferred `apache2` for a long time, which works fine till there are more than ~4000 concurrent sessions (websocket connections) at the same time. 5 | Beyond that point I had to switch to `nginx`, which works just fine with a incredible high number of connections :) 6 | 7 | To setup the faucet with a proper webserver, you just need to point the document root to the /static folder of the faucet and forward websocket (Endpoint: `/ws/*`) and api (Endpoint: `/api/*`) calls to the faucet process. 8 | 9 | ## Apache2 webserver config 10 | 11 | Apache has a limit of 150 concurrent connections in its default mpm_prefork configuration. 12 | I'd suggest mpm_event with high limits instead. There is not much traffic going through the client websockets, but it can be a high number of concurrent and long running connections. 13 | 14 | See [sitecfg-apache2.conf](https://github.com/pk910/PoWFaucet/blob/master/docs/sitecfg-apache2.conf) for example apache2 site config (used for kiln-faucet.pk910.de) 15 | 16 | required apache2 modules: 17 | - proxy 18 | - proxy_wstunnel 19 | - rewrite 20 | 21 | Note: Even with mpm_event, I ran into really bad connection issues when serving more than 4000 connections at the same time. 22 | If you expect such a high activity, switch over to nginx. 23 | Nginx seems to work much more reliable with its websocket handling. 24 | 25 | ## Nginx webserver config 26 | 27 | See [sitecfg-nginx.conf](https://github.com/pk910/PoWFaucet/blob/master/docs/sitecfg-nginx.conf) for example nginx site config (used for holesky-faucet.pk910.de) 28 | 29 | ## Common issue: Connection limits too low 30 | 31 | Keep in mind the connection limits of the webserver. 32 | 33 | Per default there is a OS-enforced limit of `1024` concurrent file descriptors in most linux distributions. 34 | 35 | You can check the limit via `ulimit -n` 36 | 37 | To avoid issues with many sessions, increase this limit for the webserver user (`www-data`) & faucet process user. 38 | -------------------------------------------------------------------------------- /faucet-client/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /faucet-client/README.md: -------------------------------------------------------------------------------- 1 | # faucet-client 2 | 3 | This is the client side code for the PoWFaucet. 4 | 5 | This builds: 6 | - /static/js/powfaucet.js (entry src/main.ts) 7 | - /static/js/powfaucet-worker-sc.js (entry src/worker/worker-scrypt.ts) 8 | - /static/js/powfaucet-worker-cn.js (entry src/worker/worker-cryptonight.ts) 9 | - /static/js/powfaucet-worker-a2.js (entry src/worker/worker-argon2.ts) 10 | - /static/css/powfaucet.css (all css imports) 11 | 12 | # How to build 13 | 14 | `npm install` 15 | 16 | `node ./build-client.js` -------------------------------------------------------------------------------- /faucet-client/build-client.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import util from 'util'; 4 | import webpack from 'webpack'; 5 | import babel from "@babel/core"; 6 | import webpackConfig from './webpack.config.js'; 7 | 8 | let distDir = path.join(import.meta.dirname, 'dist'); 9 | if(fs.existsSync(distDir)) 10 | fs.rmdirSync(distDir, { recursive: true }); 11 | fs.mkdirSync(distDir); 12 | 13 | function copyIfFound(filename, dstpath, dstname) { 14 | let srcFile = path.join(distDir, filename); 15 | if(!dstname) 16 | dstname = filename; 17 | if(!fs.existsSync(srcFile)) 18 | return false; 19 | fs.copyFileSync(srcFile, path.join(dstpath, dstname)); 20 | return true; 21 | } 22 | 23 | function copyIfFoundOrRemove(filename, dstpath, dstname) { 24 | if(!copyIfFound(filename, dstpath, dstname)) { 25 | let dstfile = path.join(dstpath, dstname || filename); 26 | if(fs.existsSync(dstfile)) 27 | fs.rmSync(dstfile); 28 | } 29 | } 30 | 31 | function checkFileExists(filename, dstpath) { 32 | let dstfile = path.join(dstpath, filename); 33 | if(!fs.existsSync(dstfile)) { 34 | throw "file not found: " + dstfile; 35 | } 36 | } 37 | 38 | (new Promise((resolve, reject) => { 39 | console.log("Building pow-faucet-client..."); 40 | let compiler = webpack(webpackConfig); 41 | compiler.options.stats = { modulesSpace: 999 }; 42 | compiler.run((err, res) => { 43 | if (err) 44 | return reject(err); 45 | fs.writeFileSync(path.join(distDir, "webpack-stats.json"), JSON.stringify(res.toJson({ 46 | assets: false, 47 | hash: true, 48 | }))); 49 | 50 | resolve(res.toString({ 51 | colors: true 52 | })); 53 | }); 54 | })).then((res) => { 55 | console.log(res); 56 | 57 | let staticPath = path.join(import.meta.dirname, "..", "static"); 58 | if(!fs.existsSync(path.join(staticPath, "js"))) 59 | fs.mkdirSync(path.join(staticPath, "js")); 60 | if(!fs.existsSync(path.join(staticPath, "css"))) 61 | fs.mkdirSync(path.join(staticPath, "css")); 62 | 63 | copyIfFound("powfaucet.css", path.join(staticPath, "css")); 64 | copyIfFoundOrRemove("powfaucet.css.map", path.join(staticPath, "css")); 65 | 66 | copyIfFound("powfaucet.js", path.join(staticPath, "js")); 67 | copyIfFoundOrRemove("powfaucet.js.map", path.join(staticPath, "js")); 68 | 69 | copyIfFound("powfaucet-worker-sc.js", path.join(staticPath, "js")); 70 | copyIfFoundOrRemove("powfaucet-worker-sc.js.map", path.join(staticPath, "js")); 71 | 72 | copyIfFound("powfaucet-worker-cn.js", path.join(staticPath, "js")); 73 | copyIfFoundOrRemove("powfaucet-worker-cn.js.map", path.join(staticPath, "js")); 74 | 75 | copyIfFound("powfaucet-worker-a2.js", path.join(staticPath, "js")); 76 | copyIfFoundOrRemove("powfaucet-worker-a2.js.map", path.join(staticPath, "js")); 77 | 78 | copyIfFound("powfaucet-worker-nm.js", path.join(staticPath, "js")); 79 | copyIfFoundOrRemove("powfaucet-worker-nm.js.map", path.join(staticPath, "js")); 80 | 81 | console.log("finished"); 82 | }).then(() => { 83 | let staticPath = path.join(import.meta.dirname, "..", "static"); 84 | checkFileExists("powfaucet.css", path.join(staticPath, "css")); 85 | checkFileExists("powfaucet.js", path.join(staticPath, "js")); 86 | checkFileExists("powfaucet-worker-sc.js", path.join(staticPath, "js")); 87 | checkFileExists("powfaucet-worker-cn.js", path.join(staticPath, "js")); 88 | checkFileExists("powfaucet-worker-a2.js", path.join(staticPath, "js")); 89 | checkFileExists("powfaucet-worker-nm.js", path.join(staticPath, "js")); 90 | }).catch((err) => { 91 | console.log("build failed: ", err); 92 | process.exit(1); 93 | }); 94 | 95 | -------------------------------------------------------------------------------- /faucet-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powfaucet/client", 3 | "version": "2.4.2", 4 | "description": "PoW Faucet Client", 5 | "type": "module", 6 | "scripts": { 7 | "bundle": "webpack", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "webpack serve", 10 | "build": "webpack --mode production", 11 | "check-types": "tsc" 12 | }, 13 | "author": "pk910 (https://pk910.de)", 14 | "license": "AGPL-3.0", 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pk910/PoWFaucet" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.25.2", 24 | "@babel/plugin-proposal-class-properties": "^7.18.6", 25 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 26 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 27 | "@babel/preset-env": "^7.25.3", 28 | "@babel/preset-flow": "^7.24.7", 29 | "@babel/preset-react": "^7.24.7", 30 | "@babel/preset-typescript": "^7.24.7", 31 | "@types/react-google-recaptcha": "^2.1.9", 32 | "babel-loader": "^10.0.0", 33 | "clean-webpack-plugin": "^4.0.0", 34 | "css-loader": "^7.1.2", 35 | "mini-css-extract-plugin": "^2.9.1", 36 | "sass": "^1.77.8", 37 | "sass-loader": "^16.0.1", 38 | "style-loader": "^4.0.0", 39 | "typescript": "^5.5.4", 40 | "url-loader": "^4.1.1", 41 | "webpack": "^5.93.0", 42 | "webpack-cli": "^6.0.1", 43 | "webpack-visualizer-plugin2": "^1.1.0" 44 | }, 45 | "dependencies": { 46 | "@hcaptcha/react-hcaptcha": "^1.11.0", 47 | "@types/react": "^19.0.1", 48 | "@types/react-dom": "^19.0.1", 49 | "bootstrap": "^5.3.3", 50 | "country-flag-icons": "^1.5.13", 51 | "react": "^19.0.0", 52 | "react-bootstrap": "^2.10.4", 53 | "react-dom": "^19.0.0", 54 | "react-google-recaptcha": "^3.1.0", 55 | "react-router-dom": "^7.0.1", 56 | "react-turnstile": "^1.1.3", 57 | "tiny-typed-emitter": "^2.1.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /faucet-client/src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare const FAUCET_CLIENT_VERSION: string; 3 | declare const FAUCET_CLIENT_BUILDTIME: number; 4 | -------------------------------------------------------------------------------- /faucet-client/src/common/FaucetConfig.ts: -------------------------------------------------------------------------------- 1 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 2 | 3 | export interface IFaucetConfig { 4 | faucetTitle: string; 5 | faucetStatus: IFaucetStatus[]; 6 | faucetImage: string; 7 | faucetHtml: string; 8 | faucetCoinSymbol: string; 9 | faucetCoinType: string; 10 | faucetCoinContract: string; 11 | faucetCoinDecimals: number; 12 | minClaim: number; 13 | maxClaim: number; 14 | sessionTimeout: number; 15 | ethTxExplorerLink: string; 16 | time: number; 17 | resultSharing: { 18 | preHtml?: string; 19 | postHtml?: string; 20 | caption?: string; 21 | [provider: string]: string; 22 | }; 23 | modules: { 24 | captcha?: ICaptchaModuleConfig; 25 | ensname?: IEnsNameModuleConfig; 26 | github?: IGithubModuleConfig; 27 | pow?: IPoWModuleConfig; 28 | passport?: IPassportModuleConfig; 29 | voucher?: IVoucherModuleConfig; 30 | zupass?: IZupassModuleConfig; 31 | }; 32 | } 33 | 34 | export interface ICaptchaModuleConfig { 35 | provider: string; 36 | siteKey: string; 37 | requiredForStart: boolean; 38 | requiredForClaim: boolean; 39 | } 40 | 41 | export interface IEnsNameModuleConfig { 42 | required: boolean; 43 | } 44 | 45 | export interface IGithubModuleConfig { 46 | clientId: string; 47 | authTimeout: number; 48 | redirectUrl: string; 49 | callbackState: string; 50 | } 51 | 52 | export interface IZupassModuleConfig { 53 | url: string; 54 | api: string; 55 | redirectUrl: string; 56 | event: { 57 | name: string; 58 | eventIds: string[]; 59 | productIds: string[]; 60 | }; 61 | watermark: string; 62 | nullifier: string; 63 | loginLogo: string; 64 | loginLabel: string; 65 | userLabel: string; 66 | infoHtml: string; 67 | } 68 | 69 | export interface IPoWModuleConfig { 70 | powWsUrl: string; 71 | powTimeout: number; 72 | powIdleTimeout: number; 73 | powParams: PoWParams; 74 | powDifficulty: number; 75 | powHashrateLimit: number; 76 | } 77 | 78 | export type PoWParams = { 79 | a: PoWHashAlgo.SCRYPT, 80 | n: number; // cpu and memory cost 81 | r: number; // block size 82 | p: number; // parallelization 83 | l: number; // key length 84 | } | { 85 | a: PoWHashAlgo.CRYPTONIGHT, 86 | c: number; // cn-algo 87 | v: number; // variant 88 | h: number; // height 89 | } | { 90 | a: PoWHashAlgo.ARGON2; 91 | t: number; // type 92 | v: number; // version 93 | i: number; // timeCost 94 | m: number; // memoryCost 95 | p: number; // parallelization, 96 | l: number; // keyLength 97 | } | { 98 | a: PoWHashAlgo.NICKMINER; 99 | i: string; // input hash 100 | r: string; // sigR 101 | v: number; // sigV 102 | c: number; // count 103 | s: string; // suffix 104 | p: string; // prefix 105 | } 106 | 107 | export interface IPassportModuleConfig { 108 | refreshTimeout: number; 109 | manualVerification: boolean; 110 | stampScoring: {[stamp: string]: number}; 111 | boostFactor: {[score: number]: number}; 112 | overrideScores: [number, number, number]; 113 | guestRefresh: number | boolean; 114 | } 115 | 116 | export interface IVoucherModuleConfig { 117 | voucherLabel: string; 118 | infoHtml: string; 119 | } 120 | 121 | export interface IFaucetStatus { 122 | text: string; 123 | level: string; 124 | prio: number; 125 | ishtml: boolean; 126 | } 127 | -------------------------------------------------------------------------------- /faucet-client/src/common/FaucetContext.ts: -------------------------------------------------------------------------------- 1 | import { IFaucetDialogProps } from "../components/shared/FaucetDialog"; 2 | import { PoWMinerWorkerSrc } from "../types/PoWMinerSrc"; 3 | import { FaucetApi } from "./FaucetApi"; 4 | import { FaucetSession } from "./FaucetSession"; 5 | 6 | export interface IFaucetContextUrls { 7 | baseUrl: string; 8 | apiUrl: string; 9 | wsBaseUrl: string; 10 | minerSrc: PoWMinerWorkerSrc; 11 | imagesUrl: string; 12 | } 13 | 14 | export interface IFaucetContext { 15 | faucetUrls: IFaucetContextUrls; 16 | faucetApi: FaucetApi; 17 | activeSession?: FaucetSession; 18 | 19 | showStatusAlert(level: string, prio: number, body: React.ReactElement): number; 20 | hideStatusAlert(alertId: number): void 21 | 22 | showNotification(type: string, message: string, time?: number|boolean, timeout?: number): number; 23 | hideNotification(notificationId: number): void; 24 | 25 | showDialog(dialogProps: IFaucetDialogProps): number; 26 | hideDialog(dialogId: number): void; 27 | getContainer(): HTMLElement; 28 | 29 | refreshConfig(): void; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /faucet-client/src/common/FaucetTime.ts: -------------------------------------------------------------------------------- 1 | 2 | export class FaucetTime { 3 | private offset: number; 4 | 5 | public constructor() { 6 | this.offset = 0; 7 | } 8 | 9 | public syncTimeOffset(remoteTime: number) { 10 | let localTime = Math.floor((new Date()).getTime() / 1000); 11 | this.offset = localTime - remoteTime; 12 | } 13 | 14 | public getLocalDate(): Date { 15 | return new Date(); 16 | } 17 | 18 | public getLocalTime(): number { 19 | return Math.floor(this.getLocalDate().getTime() / 1000); 20 | } 21 | 22 | public getSyncedDate(): Date { 23 | let localDate = new Date(); 24 | return new Date(localDate.getTime() - (this.offset * 1000)); 25 | } 26 | 27 | public getSyncedTime(): number { 28 | return Math.floor(this.getSyncedDate().getTime() / 1000); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /faucet-client/src/common/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './FaucetApi' 3 | export * from './FaucetConfig' 4 | export * from './FaucetContext' 5 | export * from './FaucetSession' 6 | export * from './FaucetTime' 7 | -------------------------------------------------------------------------------- /faucet-client/src/components/claim/ClaimInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IFaucetConfig } from '../../common/FaucetConfig'; 3 | import { FaucetCaptcha } from '../shared/FaucetCaptcha'; 4 | 5 | export interface IClaimInputProps { 6 | faucetConfig: IFaucetConfig 7 | submitInputs(inputs: any): Promise; 8 | } 9 | 10 | export interface IClaimInputState { 11 | submitting: boolean; 12 | } 13 | 14 | export class ClaimInput extends React.PureComponent { 15 | private faucetCaptcha = React.createRef(); 16 | 17 | constructor(props: IClaimInputProps) { 18 | super(props); 19 | 20 | this.state = { 21 | submitting: false, 22 | }; 23 | } 24 | 25 | public render(): React.ReactElement { 26 | let requestCaptcha = !!this.props.faucetConfig.modules.captcha?.requiredForClaim; 27 | 28 | return ( 29 |
30 | {requestCaptcha ? 31 |
32 |
33 | Captcha: 34 |
35 |
36 |
37 | 42 |
43 |
44 |
45 | : null} 46 |
47 |
48 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | private async onSubmitBtnClick() { 61 | this.setState({ 62 | submitting: true 63 | }); 64 | 65 | try { 66 | let inputData: any = {}; 67 | 68 | if(this.props.faucetConfig.modules.captcha?.requiredForClaim) { 69 | inputData.captchaToken = await this.faucetCaptcha.current?.getToken(); 70 | } 71 | 72 | await this.props.submitInputs(inputData); 73 | } catch(ex) { 74 | if(this.faucetCaptcha.current) 75 | this.faucetCaptcha.current.resetToken(); 76 | throw ex; 77 | } finally { 78 | this.setState({ 79 | submitting: false 80 | }); 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /faucet-client/src/components/claim/ClaimPage.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .faucet-page .claim-status { 4 | margin-top: 16px; 5 | } 6 | 7 | .faucet-page .result-sharing { 8 | margin-top: 12px; 9 | } 10 | 11 | .faucet-page .result-sharing .sh-link > a { 12 | position: relative; 13 | margin-left: 2px; 14 | margin-right: 2px; 15 | height: 20px; 16 | box-sizing: border-box; 17 | padding: 1px 10px 1px 10px; 18 | border-radius: 9999px; 19 | font-weight: 500; 20 | cursor: pointer; 21 | outline: 0; 22 | text-decoration: none; 23 | } 24 | 25 | .faucet-page .result-sharing .sh-link > a > i { 26 | position: relative; 27 | display: inline-block; 28 | background: transparent 0 0 no-repeat; 29 | } 30 | 31 | .faucet-page .result-sharing .sh-link > a > span { 32 | font-size: 13px; 33 | line-height: 26px; 34 | margin-left: 4px; 35 | white-space: nowrap; 36 | display: inline-block; 37 | vertical-align: top; 38 | } 39 | 40 | 41 | .faucet-page .result-sharing .sh-link.sh-tw > a { 42 | background-color: #1d9bf0; 43 | color: #fff; 44 | } 45 | 46 | .faucet-page .result-sharing .sh-link.sh-tw > a > i { 47 | top: 4px; 48 | height: 18px; 49 | width: 18px; 50 | background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 72'%3E%3Cpath fill='none' d='M0 0h72v72H0z'/%3E%3Cpath class='icon' fill='%23fff' d='M68.812 15.14c-2.348 1.04-4.87 1.744-7.52 2.06 2.704-1.62 4.78-4.186 5.757-7.243-2.53 1.5-5.33 2.592-8.314 3.176C56.35 10.59 52.948 9 49.182 9c-7.23 0-13.092 5.86-13.092 13.093 0 1.026.118 2.02.338 2.98C25.543 24.527 15.9 19.318 9.44 11.396c-1.125 1.936-1.77 4.184-1.77 6.58 0 4.543 2.312 8.552 5.824 10.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163 0 6.345 4.513 11.638 10.504 12.84-1.1.298-2.256.457-3.45.457-.845 0-1.666-.078-2.464-.23 1.667 5.2 6.5 8.985 12.23 9.09-4.482 3.51-10.13 5.605-16.26 5.605-1.055 0-2.096-.06-3.122-.184 5.794 3.717 12.676 5.882 20.067 5.882 24.083 0 37.25-19.95 37.25-37.25 0-.565-.013-1.133-.038-1.693 2.558-1.847 4.778-4.15 6.532-6.774z'/%3E%3C/svg%3E"); 51 | } 52 | 53 | 54 | .faucet-page .result-sharing .sh-link.sh-md > a { 55 | background-color: #4d5160; 56 | color: #fff; 57 | } 58 | 59 | .faucet-page .result-sharing .sh-link.sh-md > a > i { 60 | top: 4px; 61 | height: 18px; 62 | width: 18px; 63 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 216.4 232'%3E%3Cpath fill='%232b90d9' d='M212 139c-3 16-29 34-58 38-15 2-30 3-46 3-26-2-46-7-46-7v8c4 25 26 27 47 28 21 0 39-6 39-6l1 19s-14 8-41 10c-14 1-32-1-53-6C9 214 1 165 0 116V76c0-50 33-65 33-65C50 3 78 0 108 0h1c29 0 58 3 74 11 0 0 33 15 33 65 0 0 1 37-4 63' /%3E%3Cpath fill='%23fff' d='M178 80v61h-25V82c0-13-5-19-15-19-12 0-18 8-18 22v33H96V85c0-14-6-22-17-22s-16 6-16 19v59H39V80c0-12 3-22 9-30 7-7 16-11 26-11 13 0 22 5 28 15l6 10 6-10c6-10 16-15 28-15 11 0 19 4 26 11 6 8 10 18 10 30' /%3E%3C/svg%3E"); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /faucet-client/src/components/claim/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './ClaimInput' 3 | export * from './ClaimNotificationClient' 4 | export * from './ClaimPage' 5 | -------------------------------------------------------------------------------- /faucet-client/src/components/details/DetailsPage.css: -------------------------------------------------------------------------------- 1 | 2 | .faucet-page .details-advanced { 3 | margin-bottom: 8px; 4 | } 5 | 6 | .faucet-page .details-advanced a { 7 | font-size: 9pt; 8 | margin: 4px 0; 9 | color: darkgray; 10 | } 11 | 12 | .faucet-page .details-advanced .session-json { 13 | border: 1px solid darkgray; 14 | font-size: 11pt; 15 | padding: 16px; 16 | } -------------------------------------------------------------------------------- /faucet-client/src/components/details/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './DetailsPage' 3 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/RestoreSession.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IFaucetConfig } from '../../common/FaucetConfig'; 3 | import { IFaucetSessionStatus } from '../../common/FaucetSession'; 4 | import { toReadableAmount } from '../../utils/ConvertHelpers'; 5 | import { renderDate } from '../../utils/DateUtils'; 6 | 7 | export interface IRestoreSessionProps { 8 | faucetConfig: IFaucetConfig; 9 | sessionStatus: IFaucetSessionStatus; 10 | } 11 | 12 | export interface IRestoreSessionState { 13 | } 14 | 15 | export class RestoreSession extends React.PureComponent { 16 | 17 | constructor(props: IRestoreSessionProps) { 18 | super(props); 19 | 20 | this.state = {}; 21 | } 22 | 23 | public render(): React.ReactElement { 24 | return ( 25 |
26 |
27 |
28 | Do you want to continue with your previous session? 29 |
30 |
31 |
32 |
33 | Address: 34 |
35 |
36 | {this.props.sessionStatus.target} 37 |
38 |
39 |
40 |
41 | Start Time: 42 |
43 |
44 | {renderDate(new Date(this.props.sessionStatus.start * 1000), true)} 45 |
46 |
47 |
48 |
49 | Balance: 50 |
51 |
52 | {toReadableAmount(BigInt(this.props.sessionStatus.balance), this.props.faucetConfig.faucetCoinDecimals, this.props.faucetConfig.faucetCoinSymbol)} 53 |
54 |
55 |
56 |
57 | Status: 58 |
59 |
60 | {this.renderSessionStatus()} 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | private renderSessionStatus(): React.ReactElement { 68 | switch(this.props.sessionStatus.status) { 69 | case "running": 70 | return (Running); 71 | case "claimable": 72 | return (Claimable); 73 | case "claiming": 74 | return (Claiming); 75 | case "finished": 76 | return (Finished); 77 | case "failed": 78 | return (Failed); 79 | default: 80 | return (Unknown: {this.props.sessionStatus.status}); 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/github/GithubLogin.css: -------------------------------------------------------------------------------- 1 | 2 | .faucet-page .logo.logo-github { 3 | background-image: url("data:image/svg+xml,%0A%3Csvg width='98' height='96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z' fill='%2324292f'/%3E%3C/svg%3E"); 4 | background-size: contain; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .faucet-page .faucet-auth { 10 | margin: 16px 0; 11 | display: flex; 12 | } 13 | 14 | .faucet-page .faucet-auth .auth-icon { 15 | height: 50px; 16 | width: 50px; 17 | padding: 4px; 18 | } 19 | 20 | .faucet-page .faucet-auth .auth-field { 21 | margin: 0 16px; 22 | } 23 | 24 | .faucet-page .faucet-auth .auth-field .auth-info { 25 | color: #343a40; 26 | } 27 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/github/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './GithubLogin' 3 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * as github from './github' 3 | export * as zupass from './zupass' 4 | 5 | export * from './FaucetInput' 6 | export * from './FrontPage' 7 | export * from './RestoreSession' 8 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/voucher/VoucherInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle, forwardRef } from 'react'; 2 | import { IFaucetConfig } from '../../../common/FaucetConfig'; 3 | import { IFaucetContext } from '../../../common/FaucetContext'; 4 | import { OverlayTrigger, Popover, Form, Row, Col } from 'react-bootstrap'; // Assuming react-bootstrap is available 5 | 6 | export interface IVoucherInputProps { 7 | faucetContext: IFaucetContext; 8 | faucetConfig: IFaucetConfig; 9 | } 10 | 11 | export interface IVoucherInputRef { 12 | getCode(): string | undefined; 13 | } 14 | 15 | const VoucherInput = forwardRef((props, ref) => { 16 | const [voucherCode, setVoucherCode] = useState(''); 17 | const voucherConfig = props.faucetConfig.modules.voucher; 18 | 19 | useImperativeHandle(ref, () => ({ 20 | getCode: () => voucherCode || undefined, 21 | })); 22 | 23 | if (!voucherConfig) { 24 | return null; // Should not happen if rendered conditionally 25 | } 26 | 27 | const infoPopover = voucherConfig.infoHtml ? ( 28 | 29 | 30 |
31 | 32 | 33 | ) : null; 34 | 35 | return ( 36 |
37 | 38 | {voucherConfig.voucherLabel && ( 39 | 40 | {voucherConfig.voucherLabel} 41 | {infoPopover && ( 42 | 43 | 44 | ⓘ {/* Unicode INFO symbol */} 45 | 46 | 47 | )} 48 | 49 | )} 50 | 51 | setVoucherCode(e.target.value)} 56 | style={{ fontFamily: 'monospace' }} 57 | /> 58 | 59 | 60 |
61 | ); 62 | }); 63 | 64 | export default VoucherInput; -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/zupass/ZupassLogin.css: -------------------------------------------------------------------------------- 1 | .faucet-page .logo.logo-zupass { 2 | background-size: contain; 3 | background-repeat: no-repeat; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | .faucet-page .faucet-zupass-auth { 9 | margin: 16px 0; 10 | display: flex; 11 | } 12 | 13 | .faucet-page .faucet-zupass-auth .auth-icon { 14 | height: 50px; 15 | width: 50px; 16 | padding: 0; 17 | } 18 | 19 | .faucet-page .faucet-zupass-auth .auth-field { 20 | margin: 0 8px; 21 | flex-grow: 1; 22 | } 23 | 24 | .faucet-page .faucet-zupass-auth .zupass-info-icon { 25 | margin-left: 8px; 26 | } 27 | 28 | .faucet-page .faucet-zupass-auth .auth-field .auth-info { 29 | color: #343a40; 30 | } 31 | 32 | .faucet-page .faucet-zupass-auth .auth-ident-truncated { 33 | display: inline-block; 34 | max-width: 200px; 35 | text-overflow: ellipsis; 36 | overflow: hidden; 37 | vertical-align: bottom; 38 | margin-left: 4px; 39 | } 40 | 41 | .faucet-page .faucet-zupass-auth .auth-logout { 42 | float: right; 43 | } 44 | 45 | #zupass-tooltip .tooltip-inner { 46 | max-width: 500px; 47 | } 48 | 49 | #zupass-tooltip .zupass-info, 50 | #zupass-tooltip .zupass-info td { 51 | text-align: left; 52 | } 53 | 54 | #zupass-tooltip .zupass-info td.zupass-title { 55 | width: 80px; 56 | vertical-align: top; 57 | } 58 | 59 | #zupass-tooltip .zupass-info td.zupass-value { 60 | max-width: 280px; 61 | } 62 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/zupass/ZupassTypes.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum PCDTypeName { 3 | EdDSATicket = "eddsa-ticket-pcd", 4 | SemaphoreIdentity = "semaphore-identity-pcd", 5 | } 6 | 7 | export enum ArgumentTypeName { 8 | String = "String", 9 | Number = "Number", 10 | BigInt = "BigInt", 11 | Boolean = "Boolean", 12 | Object = "Object", 13 | StringArray = "StringArray", 14 | PCD = "PCD", 15 | ToggleList = "ToggleList", 16 | Unknown = "Unknown" 17 | } 18 | 19 | export enum PCDRequestType { 20 | Get = "Get", 21 | } 22 | 23 | export interface PCDRequest { 24 | returnUrl: string; 25 | type: PCDRequestType; 26 | } 27 | 28 | export interface ProveOptions { 29 | genericProveScreen?: boolean; 30 | title?: string; 31 | description?: string; 32 | debug?: boolean; 33 | proveOnServer?: boolean; 34 | signIn?: boolean; 35 | } 36 | 37 | export interface PCDGetRequest 38 | extends PCDRequest { 39 | type: PCDRequestType.Get; 40 | pcdType: string; 41 | args: any; 42 | options?: ProveOptions; 43 | } 44 | -------------------------------------------------------------------------------- /faucet-client/src/components/frontpage/zupass/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './ZupassLogin' 3 | export * from './ZupassTypes' 4 | -------------------------------------------------------------------------------- /faucet-client/src/components/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * as claim from './claim' 4 | export * as details from './details' 5 | export * as frontpage from './frontpage' 6 | export * as mining from './mining' 7 | export * as passport from './passport' 8 | export * as shared from './shared' 9 | export * as status from './status' 10 | 11 | export * from './FaucetPage' 12 | -------------------------------------------------------------------------------- /faucet-client/src/components/mining/ConnectionAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IFaucetConfig } from '../../common/FaucetConfig'; 3 | import { renderTimespan } from '../../utils/DateUtils'; 4 | import { FaucetTime } from '../../common/FaucetTime'; 5 | 6 | export interface IConnectionAlertProps { 7 | faucetConfig: IFaucetConfig; 8 | disconnectTime: number; 9 | initialConnection: boolean; 10 | timeoutCb?: () => void; 11 | } 12 | 13 | export interface IConnectionAlertState { 14 | refreshIndex: number; 15 | } 16 | 17 | export class ConnectionAlert extends React.PureComponent { 18 | private updateTimer: NodeJS.Timer; 19 | private timeoutCbCalled: boolean; 20 | 21 | constructor(props: IConnectionAlertProps) { 22 | super(props); 23 | 24 | this.state = { 25 | refreshIndex: 0, 26 | }; 27 | } 28 | 29 | public componentDidMount() { 30 | if(!this.updateTimer) { 31 | this.setUpdateTimer(); 32 | } 33 | } 34 | 35 | public componentWillUnmount() { 36 | if(this.updateTimer) { 37 | clearTimeout(this.updateTimer); 38 | this.updateTimer = null; 39 | } 40 | } 41 | 42 | private setUpdateTimer() { 43 | let now = (new Date()).getTime(); 44 | let timeLeft = (1000 - (now % 1000)) + 2; 45 | this.updateTimer = setTimeout(() => { 46 | this.updateTimer = null; 47 | this.setState({ 48 | refreshIndex: this.state.refreshIndex + 1, 49 | }); 50 | this.setUpdateTimer(); 51 | }, timeLeft); 52 | } 53 | 54 | public render(): React.ReactElement { 55 | let now = Math.floor((new Date()).getTime() / 1000); 56 | let timeout = this.props.faucetConfig.modules.pow.powIdleTimeout ? this.props.disconnectTime + this.props.faucetConfig.modules.pow.powIdleTimeout - now : 0; 57 | if(timeout < 0 && !this.timeoutCbCalled) { 58 | this.timeoutCbCalled = true; 59 | if(this.props.timeoutCb) 60 | this.props.timeoutCb(); 61 | } 62 | 63 | let errorCaption: string; 64 | if(this.props.initialConnection) 65 | errorCaption = "Connecting to the faucet server..."; 66 | else 67 | errorCaption = "Connection to faucet server has been lost. Reconnecting..."; 68 | 69 | return ( 70 |
71 |
{errorCaption}
72 | {now - this.props.disconnectTime > 10 && timeout > 0 ? ( 73 |
74 | Please check your internet connection. The connection needs to be restored within the next {renderTimespan(timeout, 2)} or your session will be closed. 75 |
76 | ) : null} 77 | {timeout < 0 ? ( 78 |
79 | Connection couldn't be restored in time. Session timed out. 80 |
81 | ) : null} 82 |
83 | ); 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /faucet-client/src/components/mining/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './ConnectionAlert' 3 | export * from './MiningPage' 4 | export * from './PoWMinerStatus' 5 | -------------------------------------------------------------------------------- /faucet-client/src/components/passport/PassportInfo.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .pow-boost-info .boost-descr { 4 | font-weight: bold; 5 | } 6 | 7 | .pow-boost-info .boost-passport { 8 | margin-top: 16px; 9 | } 10 | 11 | .pow-boost-info .passport-factor { 12 | margin-left: 16px; 13 | } 14 | 15 | .pow-boost-info .passport-details { 16 | margin-top: 16px; 17 | } 18 | 19 | .pow-boost-info .passport-factor-info { 20 | display: inline-block; 21 | margin-left: 4px; 22 | } 23 | 24 | .pow-boost-info .summary-val { 25 | padding-left: 24px; 26 | } 27 | 28 | .pow-boost-info .passport-refresh { 29 | margin-top: 16px; 30 | } 31 | 32 | .pow-boost-info .passport-refresh .refresh-auto button { 33 | width: 100%; 34 | } 35 | 36 | .pow-boost-info .passport-refresh .refresh-btndv { 37 | padding-right: 24px; 38 | } 39 | 40 | .pow-boost-info .passport-refresh-status .alert { 41 | background-color: transparent; 42 | border-color: transparent; 43 | padding: 0; 44 | margin-top: 4px; 45 | margin-bottom: 0; 46 | white-space: pre; 47 | } 48 | 49 | .pow-boost-info .passport-details { 50 | max-height: 600px; 51 | overflow-x: auto; 52 | } 53 | 54 | .pow-boost-info .passport-details .row.details-header > div { 55 | padding: 0; 56 | font-weight: 600; 57 | } 58 | 59 | .passport-factor-info .header-row > div { 60 | white-space: nowrap; 61 | } 62 | 63 | .pow-boost-info .passport-manual-refresh { 64 | margin-top: 16px; 65 | } 66 | 67 | .pow-boost-info .passport-manual-refresh .passport-upload-input { 68 | visibility: hidden; 69 | } 70 | 71 | .pow-boost-info .passport-manual-refresh .form-header { 72 | font-weight: bold; 73 | } 74 | 75 | .pow-boost-info .passport-manual-refresh .passport-json { 76 | width: 100%; 77 | min-height: 150px; 78 | } 79 | 80 | -------------------------------------------------------------------------------- /faucet-client/src/components/passport/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './PassportInfo' 3 | -------------------------------------------------------------------------------- /faucet-client/src/components/shared/FaucetDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { Button, Modal } from 'react-bootstrap'; 3 | 4 | export interface IFaucetDialogProps { 5 | title: string; 6 | body: ReactElement; 7 | size?: string; 8 | closeButton?: { 9 | caption: string; 10 | }, 11 | applyButton?: { 12 | caption: string; 13 | applyFn: () => void, 14 | }, 15 | closeFn?: () => void, 16 | } 17 | 18 | export interface IFaucetDialogFullProps extends IFaucetDialogProps { 19 | container: HTMLElement; 20 | } 21 | 22 | export interface IFaucetDialogState { 23 | } 24 | 25 | export class FaucetDialog extends React.PureComponent { 26 | 27 | constructor(props: IFaucetDialogFullProps) { 28 | super(props); 29 | 30 | this.state = {}; 31 | } 32 | 33 | public render(): React.ReactElement { 34 | return ( 35 | { 36 | if(this.props.closeFn) 37 | this.props.closeFn(); 38 | }}> 39 | 40 | 41 | {this.props.title} 42 | 43 | 44 | 45 | {this.props.body} 46 | 47 | 48 | {this.props.applyButton ? 49 | 56 | : null} 57 | {this.props.closeButton ? 58 | 62 | : null} 63 | 64 | 65 | ); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /faucet-client/src/components/shared/FaucetNotification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderTime } from '../../utils/DateUtils'; 3 | 4 | export interface IFaucetNotificationProps { 5 | type: string; 6 | message: string; 7 | time: number; 8 | hideFn?: () => void, 9 | } 10 | 11 | export interface IFaucetNotificationState { 12 | } 13 | 14 | export class FaucetNotification extends React.PureComponent { 15 | 16 | constructor(props: IFaucetNotificationProps) { 17 | super(props); 18 | 19 | this.state = {}; 20 | } 21 | 22 | public render(): React.ReactElement { 23 | let alertClass: string[] = [ "alert" ]; 24 | switch(this.props.type) { 25 | case "success": 26 | alertClass.push("alert-success"); 27 | break; 28 | case "error": 29 | alertClass.push("alert-danger"); 30 | break; 31 | case "warning": 32 | alertClass.push("alert-warning"); 33 | break; 34 | case "info": 35 | alertClass.push("alert-info"); 36 | break; 37 | } 38 | 39 | return ( 40 |
this.props.hideFn ? this.props.hideFn() : null}> 41 |
42 | {this.props.time ? renderTime(new Date(this.props.time), true) + " - " : ""} 43 | {this.props.message} 44 |
45 |
46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /faucet-client/src/components/shared/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './FaucetCaptcha' 3 | export * from './FaucetDialog' 4 | export * from './FaucetNotification' 5 | -------------------------------------------------------------------------------- /faucet-client/src/components/status/FaucetStatus.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ipinfo-tooltip .tooltip-inner { 4 | max-width: none; 5 | } 6 | 7 | #ipinfo-tooltip .ipaddr-info { 8 | min-width: 200px; 9 | text-align: left; 10 | } 11 | 12 | #ipinfo-tooltip .ipaddr-info.claim-error { 13 | max-width: 500px; 14 | } 15 | 16 | #ipinfo-tooltip .ipaddr-info > table { 17 | width: 100%; 18 | } 19 | 20 | #ipinfo-tooltip .ipaddr-info .ipinfo-title { 21 | min-width: 80px; 22 | } 23 | 24 | #ipinfo-tooltip .ipaddr-info .ipinfo-value { 25 | min-width: 50px; 26 | } 27 | 28 | .faucet-page .faucet-status .status-sessions td, 29 | .faucet-page .faucet-status .status-sessions th { 30 | padding: 0 12px; 31 | } 32 | 33 | .faucet-page .faucet-status .status-general { 34 | max-width: none; 35 | } 36 | 37 | .faucet-page .faucet-status .status-general .status-title { 38 | display: inline-block; 39 | width: 180px; 40 | } 41 | -------------------------------------------------------------------------------- /faucet-client/src/components/status/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './FaucetStatusPage' 3 | export * from './QueueStatusPage' 4 | -------------------------------------------------------------------------------- /faucet-client/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * as common from './common' 4 | export * as components from './components' 5 | export * as pow from './pow' 6 | export * as types from './types' 7 | export * as utils from './utils' 8 | -------------------------------------------------------------------------------- /faucet-client/src/main.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { FaucetPage, IFaucetPageProps } from './components/FaucetPage'; 4 | import * as powfaucet from '.' 5 | 6 | export function initializeFaucet(container: Element, faucetProps: IFaucetPageProps): { element: ReactElement, instance: FaucetPage } { 7 | let res: { element: ReactElement, instance: FaucetPage } = { 8 | element: null, 9 | instance: null, 10 | }; 11 | res.element = React.createElement(FaucetPage, { 12 | ...faucetProps, 13 | ref: (ref) => { 14 | res.instance = ref; 15 | } 16 | }, []); 17 | container.innerHTML = ""; 18 | let root = createRoot(container); 19 | root.render(res.element); 20 | return res; 21 | } 22 | 23 | (() => { 24 | let PoWFaucet = (window as any).PoWFaucet = { 25 | ...powfaucet, 26 | page: null, 27 | initializeFaucet: initializeFaucet, 28 | }; 29 | 30 | var container = document.querySelector(".pow-faucet"); 31 | if(container && container.hasAttribute("data-powfaucet")) { 32 | let faucetProps: IFaucetPageProps = {}; 33 | PoWFaucet.page = initializeFaucet(container, faucetProps); 34 | } 35 | })(); 36 | -------------------------------------------------------------------------------- /faucet-client/src/pow/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './PoWClient' 3 | export * from './PoWMiner' 4 | export * from './PoWSession' 5 | -------------------------------------------------------------------------------- /faucet-client/src/types/FaucetStatus.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IClientClaimStatus { 3 | time: number; 4 | session: string; 5 | target: string; 6 | amount: string; 7 | status: string; 8 | error: string; 9 | nonce: number; 10 | hash: string; 11 | txhex: string; 12 | } 13 | 14 | export interface IClientSessionStatus { 15 | id: string; 16 | start: number; 17 | target: string; 18 | ip: string; 19 | ipInfo: IClientSessionIPInfo, 20 | balance: string; 21 | nonce: number; 22 | hashrate: number; 23 | status: string; 24 | restr: IClientSessionRestrictionStatus; 25 | cliver: string; 26 | boost: any; 27 | connected: boolean; 28 | idle: number; 29 | factors: ISessionRewardFactor[]; 30 | } 31 | 32 | export interface IClientClaimStatusRsp { 33 | claims: IClientClaimStatus[]; 34 | } 35 | 36 | export interface IClientFaucetStatusRsp { 37 | status: IFaucetStatusGeneralStatus; 38 | refill: IFaucetStatusRefillStatus; 39 | outflowRestriction: IFaucetStatusOutflowStatus; 40 | sessions: IClientSessionStatus[]; 41 | claims: IClientClaimStatus[]; 42 | } 43 | 44 | export interface IFaucetStatusGeneralStatus { 45 | walletBalance: number; 46 | unclaimedBalance: number; 47 | queuedBalance: number; 48 | balanceRestriction: number; 49 | } 50 | 51 | export interface IFaucetStatusRefillStatus { 52 | balance: number; 53 | trigger: number; 54 | amount: number; 55 | cooldown: number; 56 | } 57 | 58 | export interface IFaucetStatusOutflowStatus { 59 | now: number; 60 | trackTime: number; 61 | balance: number; 62 | dustAmount: number; 63 | restriction: number; 64 | amount: number; 65 | duration: number; 66 | lowerLimit: number; 67 | upperLimit: number; 68 | } 69 | 70 | 71 | export interface IClientSessionIPInfo { 72 | status: string; 73 | country?: string; 74 | countryCode?: string; 75 | region?: string; 76 | regionCode?: string; 77 | city?: string; 78 | cityCode?: string; 79 | locLat?: number; 80 | locLon?: number; 81 | zone?: string; 82 | isp?: string; 83 | org?: string; 84 | as?: string; 85 | proxy?: boolean; 86 | hosting?: boolean; 87 | } 88 | 89 | export interface IClientSessionRestrictionStatus { 90 | reward: number; 91 | messages: { 92 | text: string; 93 | notify: boolean|string; 94 | }[]; 95 | blocked: false|"close"|"kill"; 96 | } 97 | 98 | export interface ISessionRewardFactor { 99 | factor: number; 100 | module: string; 101 | name?: string; 102 | } 103 | -------------------------------------------------------------------------------- /faucet-client/src/types/PassportInfo.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IPassportScoreInfo { 3 | score: number; 4 | factor: number; 5 | } 6 | 7 | export interface IPassportInfo { 8 | passport: IPassportData; 9 | score: IPassportScoreInfo; 10 | } 11 | 12 | export interface IPassportData { 13 | found: boolean; 14 | parsed: number; 15 | newest: number; 16 | stamps?: IPassportStampInfo[]; 17 | } 18 | 19 | export interface IPassportStampInfo { 20 | provider: string; 21 | expiration: number; 22 | duplicate?: string; 23 | } 24 | -------------------------------------------------------------------------------- /faucet-client/src/types/PoWMinerSrc.ts: -------------------------------------------------------------------------------- 1 | import { joinUrl } from "../utils/QueryUtils"; 2 | 3 | export enum PoWHashAlgo { 4 | SCRYPT = "scrypt", 5 | CRYPTONIGHT = "cryptonight", 6 | ARGON2 = "argon2", 7 | NICKMINER = "nickminer", 8 | } 9 | 10 | export type PoWMinerWorkerSrc = { 11 | [algo in PoWHashAlgo]: string; 12 | }; 13 | 14 | export function getPoWMinerDefaultSrc(baseUrl: string): PoWMinerWorkerSrc { 15 | return { 16 | [PoWHashAlgo.SCRYPT]: joinUrl(baseUrl, "/js/powfaucet-worker-sc.js?" + FAUCET_CLIENT_BUILDTIME), 17 | [PoWHashAlgo.CRYPTONIGHT]: joinUrl(baseUrl, "/js/powfaucet-worker-cn.js?" + FAUCET_CLIENT_BUILDTIME), 18 | [PoWHashAlgo.ARGON2]: joinUrl(baseUrl, "/js/powfaucet-worker-a2.js?" + FAUCET_CLIENT_BUILDTIME), 19 | [PoWHashAlgo.NICKMINER]: joinUrl(baseUrl, "/js/powfaucet-worker-nm.js?" + FAUCET_CLIENT_BUILDTIME), 20 | }; 21 | } -------------------------------------------------------------------------------- /faucet-client/src/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './FaucetStatus' 3 | export * from './PassportInfo' 4 | -------------------------------------------------------------------------------- /faucet-client/src/utils/ConvertHelpers.ts: -------------------------------------------------------------------------------- 1 | 2 | export function base64ToHex(str) { 3 | const raw = atob(str); 4 | let result = ''; 5 | for (let i = 0; i < raw.length; i++) { 6 | const hex = raw.charCodeAt(i).toString(16); 7 | result += (hex.length === 2 ? hex : '0' + hex); 8 | } 9 | return result; 10 | } 11 | 12 | export function toDecimalUnit(amount: number, decimals?: number): number { 13 | let factor = Math.pow(10, decimals || 18); 14 | return amount / factor; 15 | } 16 | 17 | export function toReadableAmount(amount: number | bigint, decimals?: number, unit?: string, precision?: number): string { 18 | if(!decimals) 19 | decimals = 18; 20 | if(!precision) 21 | precision = 3; 22 | if(!amount) 23 | return "0"+ (unit ? " " + unit : ""); 24 | if(typeof amount === "bigint") 25 | amount = Number(amount); 26 | 27 | let decimalAmount = toDecimalUnit(amount, decimals); 28 | let precisionFactor = Math.pow(10, precision); 29 | let amountStr = (Math.round(decimalAmount * precisionFactor) / precisionFactor).toString(); 30 | 31 | return amountStr + (unit ? " " + unit : ""); 32 | } 33 | -------------------------------------------------------------------------------- /faucet-client/src/utils/DateUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | function padLeft(str: any, len: number, pad: string): string { 3 | str = str.toString(); 4 | while(str.length < len) 5 | str = pad + str; 6 | return str; 7 | } 8 | 9 | export const renderDate = (date: Date, withTime?: boolean, withSec?: boolean): string => { 10 | return date.getFullYear() + "-" + padLeft(date.getMonth() + 1, 2, '0') + '-' + padLeft(date.getDate(), 2, '0') + 11 | (withTime ? " " + padLeft(date.getHours(), 2, '0') + ":" + padLeft(date.getMinutes(), 2, '0') + (withSec ? ":" + padLeft(date.getSeconds(), 2, '0') : "") : "") 12 | } 13 | 14 | export const renderTime = (date: Date, withSec?: boolean): string => { 15 | return padLeft(date.getHours(), 2, '0') + ":" + padLeft(date.getMinutes(), 2, '0') + (withSec ? ":" + padLeft(date.getSeconds(), 2, '0') : ""); 16 | } 17 | 18 | export const renderTimespan = (time: number, maxParts?: number): string => { 19 | let resParts: string[] = []; 20 | let group: number; 21 | if(!maxParts) 22 | maxParts = 2; 23 | 24 | group = 60 * 60 * 24; 25 | if(time >= group) { 26 | let groupVal = Math.floor(time / group); 27 | time -= groupVal * group; 28 | resParts.push(groupVal + "d"); 29 | } 30 | 31 | group = 60 * 60; 32 | if(time >= group) { 33 | let groupVal = Math.floor(time / group); 34 | time -= groupVal * group; 35 | resParts.push(groupVal + "h"); 36 | } 37 | 38 | group = 60; 39 | if(time >= group) { 40 | let groupVal = Math.floor(time / group); 41 | time -= groupVal * group; 42 | resParts.push(groupVal + "min"); 43 | } 44 | 45 | group = 1; 46 | if(time >= group) { 47 | let groupVal = Math.floor(time / group); 48 | time -= groupVal * group; 49 | resParts.push(groupVal + "sec"); 50 | } 51 | 52 | if(resParts.length > maxParts) { 53 | resParts = resParts.slice(0, maxParts); 54 | } 55 | return resParts.join(" "); 56 | } 57 | -------------------------------------------------------------------------------- /faucet-client/src/utils/PoWParamsHelper.ts: -------------------------------------------------------------------------------- 1 | import { PoWParams } from "../common/FaucetConfig"; 2 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 3 | 4 | export function getPoWParamsStr(params: PoWParams, difficulty: number): string { 5 | switch(params.a) { 6 | case PoWHashAlgo.SCRYPT: 7 | return params.a+"|"+params.n + "|" + params.r + "|" + params.p + "|" + params.l + "|" + difficulty; 8 | case PoWHashAlgo.CRYPTONIGHT: 9 | return params.a+"|"+params.c + "|" + params.v + "|" + params.h + "|" + difficulty; 10 | case PoWHashAlgo.ARGON2: 11 | return params.a+"|"+params.t + "|" + params.v + "|" + params.i + "|" + params.m + "|" + params.p + "|" + params.l + "|" + difficulty; 12 | case PoWHashAlgo.NICKMINER: 13 | return params.a+"|"+params.i + "|" + params.r + "|" + params.v + "|" + params.c + "|" + params.s + "|" + params.p + "|" + difficulty; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /faucet-client/src/utils/PromiseDfd.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IPromiseFns { 3 | resolve(result?: any): void; 4 | reject(error?: any): void; 5 | } 6 | 7 | export class PromiseDfd { 8 | public readonly promise: Promise; 9 | public readonly resolve: (result?: T) => void; 10 | public readonly reject: (error?: any) => void; 11 | 12 | public constructor() { 13 | let promiseFns: IPromiseFns; 14 | this.promise = new Promise((resolve, reject) => { 15 | promiseFns = { 16 | resolve: resolve, 17 | reject: reject 18 | }; 19 | }); 20 | this.resolve = promiseFns.resolve; 21 | this.reject = promiseFns.reject; 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /faucet-client/src/utils/QueryUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function joinUrl(base: string, add: string): string { 3 | let result = (base || "").replace(/\/+$/, ""); 4 | add = add || ""; 5 | if(add.match(/^\//)) 6 | result += add; 7 | else 8 | result += "/" + add.replace(/^\/+/, ""); 9 | return result; 10 | } 11 | 12 | export function toQuery(params, delimiter = '&'): string { 13 | const keys = Object.keys(params); 14 | 15 | return keys.reduce((str, key, index) => { 16 | let query = `${str}${key}=${params[key]}`; 17 | 18 | if (index < (keys.length - 1)) { 19 | query += delimiter; 20 | } 21 | 22 | return query; 23 | }, ''); 24 | } 25 | -------------------------------------------------------------------------------- /faucet-client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './ConvertHelpers' 3 | export * from './DateUtils' 4 | export * from './PoWParamsHelper' 5 | export * from './PromiseDfd' 6 | export * from './QueryUtils' 7 | -------------------------------------------------------------------------------- /faucet-client/src/worker/worker-argon2.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PoWWorker } from "./PoWWorker"; 3 | import { getArgon2, getArgon2ReadyPromise } from "../../../libs/argon2_wasm.cjs"; 4 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 5 | 6 | (() => { 7 | getArgon2ReadyPromise().then(() => { 8 | let argon2 = getArgon2(); 9 | new PoWWorker({ 10 | hashFn: (nonce, preimg, params) => { 11 | if(params.a !== PoWHashAlgo.ARGON2) 12 | return null; 13 | return argon2(nonce, preimg, params.l, params.i, params.m, params.p, params.t, params.v); 14 | } 15 | }); 16 | }) 17 | })(); -------------------------------------------------------------------------------- /faucet-client/src/worker/worker-cryptonight.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PoWWorker } from "./PoWWorker"; 3 | import { getCryptoNight, getCryptoNightReadyPromise } from "../../../libs/cryptonight_wasm.cjs"; 4 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 5 | 6 | (() => { 7 | getCryptoNightReadyPromise().then(() => { 8 | let cryptonight = getCryptoNight(); 9 | new PoWWorker({ 10 | hashFn: (nonce, preimg, params) => { 11 | if(params.a !== PoWHashAlgo.CRYPTONIGHT) 12 | return null; 13 | return cryptonight(preimg + nonce, params.c, params.v, params.h); 14 | } 15 | }); 16 | }) 17 | })(); -------------------------------------------------------------------------------- /faucet-client/src/worker/worker-nickminer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PoWWorker } from "./PoWWorker"; 3 | import { getNickMiner, getNickMinerReadyPromise } from "../../../libs/nickminer_wasm.cjs"; 4 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 5 | 6 | (() => { 7 | getNickMinerReadyPromise().then(() => { 8 | let nickMiner = getNickMiner(); 9 | nickMiner.miner_init(); 10 | 11 | new PoWWorker({ 12 | hashFn: (nonce, preimg, params) => { 13 | if(params.a !== PoWHashAlgo.NICKMINER) 14 | return null; 15 | return nickMiner.miner_run(nonce); 16 | }, 17 | configFn: (preimg, params) => { 18 | if(params.a !== PoWHashAlgo.NICKMINER) 19 | return null; 20 | nickMiner.miner_set_config(params.i, params.r, params.v, params.s, params.p, params.c, preimg); 21 | } 22 | }); 23 | 24 | }) 25 | })(); -------------------------------------------------------------------------------- /faucet-client/src/worker/worker-scrypt.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PoWWorker } from "./PoWWorker"; 3 | import { getScrypt, getScryptReadyPromise } from "../../../libs/scrypt_wasm.cjs"; 4 | import { PoWHashAlgo } from "../types/PoWMinerSrc"; 5 | 6 | (() => { 7 | getScryptReadyPromise().then(() => { 8 | let scrypt = getScrypt(); 9 | new PoWWorker({ 10 | hashFn: (nonce, preimg, params) => { 11 | if(params.a !== PoWHashAlgo.SCRYPT) 12 | return null; 13 | return scrypt(nonce, preimg, params.n, params.r, params.p, params.l); 14 | } 15 | }); 16 | }) 17 | })(); -------------------------------------------------------------------------------- /faucet-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target latest version of ECMAScript. 4 | "target": "esnext", 5 | // path to output directory 6 | "outDir": "./dist/", 7 | // Search under node_modules for non-relative imports. 8 | "moduleResolution": "node", 9 | // Process & infer types from .js files. 10 | "allowJs": true, 11 | // Don't emit; allow Babel to transform files. 12 | "noEmit": false, 13 | // Import non-ES modules as default imports. 14 | "esModuleInterop": true, 15 | // use typescript to transpile jsx to js 16 | "jsx": "react", 17 | 18 | "lib": [ 19 | "es2015", 20 | "dom.iterable", 21 | "es2016.array.include", 22 | "es2017.object", 23 | "dom" 24 | ], 25 | "module": "ESNext", 26 | "removeComments": true, 27 | "alwaysStrict": true, 28 | "importHelpers": true 29 | }, 30 | } -------------------------------------------------------------------------------- /faucet-client/utils/CliArgs.js: -------------------------------------------------------------------------------- 1 |  2 | export default (function() { 3 | var args = {}; 4 | var arg, key; 5 | for(var i = 0; i < process.argv.length; i++) { 6 | if((arg = /^--([^=]+)(?:=(.+))?$/.exec(process.argv[i]))) { 7 | key = arg[1]; 8 | args[arg[1]] = arg[2] || true; 9 | } 10 | else if(key) { 11 | args[key] = process.argv[i]; 12 | key = null; 13 | } 14 | } 15 | return args; 16 | })(); 17 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/.gitignore: -------------------------------------------------------------------------------- 1 | argon2-wasm/ 2 | emsdk/ 3 | node_modules/ 4 | hash_a2.wasm 5 | hash_a2.js 6 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | import { encode } from "base32768"; 3 | import fs from "fs"; 4 | 5 | const base32768Module = fs.readFileSync("node_modules/base32768/src/index.js", { encoding: "utf8" }).replace(/^export .*$/m, ""); 6 | const base32768WASM = encode(fs.readFileSync("hash_a2.wasm")); 7 | 8 | const wasmWrappperJS = fs.readFileSync("hash_a2.js", { encoding: "utf8" }); 9 | let lines = wasmWrappperJS.replace(/import\.meta/g, "wasmMeta").split("\n"); 10 | // filter out the "export default Module" line 11 | lines = lines.filter(line => !line.startsWith("export default Module")); 12 | const customWASMWrappperJS = lines.join("\n"); 13 | 14 | // -------------------------------------------------------------------------- 15 | // Output the composited webworker JS 16 | 17 | // first, include the warning about this file being automatically generated 18 | console.log(` 19 | 20 | // THIS FILE IS GENERATED AUTOMATICALLY 21 | // Don't edit this file by hand. 22 | // Edit the build located in the faucet-wasm folder. 23 | 24 | var argon2Promise, argon2; 25 | 26 | module.exports = { 27 | getArgon2: function() { return argon2; }, 28 | getArgon2ReadyPromise: function() { return argon2Promise; } 29 | }; 30 | 31 | function getWasmBinary() { 32 | ${base32768Module} 33 | const base32768WASM = "${base32768WASM}"; 34 | return decode(base32768WASM); 35 | } 36 | 37 | (function() { 38 | var wasmMeta = {}; 39 | if(typeof self === "undefined") { 40 | var self = {location:{href:""}}; 41 | } 42 | 43 | ${customWASMWrappperJS} 44 | argon2Promise = Module(); 45 | })(); 46 | 47 | `); 48 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the faucet-wasm folder.\n" 5 | fi 6 | 7 | if [ ! -d argon2-wasm ]; then 8 | printf "Cloning https://github.com/urbit/argon2.git... \n" 9 | git clone https://github.com/urbit/argon2.git argon2-wasm 10 | fi 11 | 12 | emcc_is_installed="$(which emcc | wc -l)" 13 | 14 | if [ "$emcc_is_installed" == "0" ]; then 15 | if [ ! -d ./emsdk ]; then 16 | git clone https://github.com/emscripten-core/emsdk.git 17 | fi 18 | cd emsdk 19 | ./emsdk install latest 20 | ./emsdk activate latest 21 | source ./emsdk_env.sh 22 | cd .. 23 | fi 24 | 25 | printf "compiling argon2 wasm... \n" 26 | emcc -O3 -s NO_FILESYSTEM=1 -s TOTAL_MEMORY=67108864 -s 'EXPORTED_RUNTIME_METHODS=["ccall", "cwrap"]' -s EXPORTED_FUNCTIONS="['_hash_a2','_argon2_hash']" -s WASM=1 -s ENVIRONMENT=worker -s MODULARIZE=1 -s EXPORT_ES6=1 -s STANDALONE_WASM --no-entry --pre-js ./wasm-pre.js -I./argon2-wasm/include ./hash_a2.c -o hash_a2.js 27 | 28 | nodejs_is_installed="$(which node | wc -l)" 29 | npm_is_installed="$(which npm | wc -l)" 30 | 31 | if [ "$nodejs_is_installed" == "0" ] || [ "$npm_is_installed" == "0" ]; then 32 | printf "nodejs and npm are required for the next step. Please install them manually 😇" 33 | exit 1 34 | fi 35 | 36 | if [ ! -d node_modules ]; then 37 | printf "running npm install \n" 38 | npm install 39 | fi 40 | 41 | node build_wasm.js > "../../libs/argon2_wasm.cjs" 42 | 43 | printf "\n\nbuilt ../libs/argon2_wasm.cjs successfully!\n\n" 44 | 45 | 46 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/hash_a2.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "./argon2-wasm/src/blake2/blake2b.c" 9 | #include "./argon2-wasm/src/core.c" 10 | #include "./argon2-wasm/src/encoding.c" 11 | #include "./argon2-wasm/src/ref.c" 12 | #include "./argon2-wasm/src/thread.c" 13 | #include "./argon2-wasm/src/argon2.c" 14 | #include "./argon2-wasm/include/argon2.h" 15 | 16 | char output[(258 * 2) + 1]; 17 | 18 | char* hash_a2(char *input_hex, char *salt_hex, int hash_len, int time_cost, int mem_cost, int parallelism, int type, int version) 19 | { 20 | char *pos; 21 | 22 | int input_len = strlen(input_hex) / 2; 23 | unsigned char input[input_len]; 24 | pos = input_hex; 25 | for(size_t i = 0; i < input_len; i++) { sscanf(pos, "%2hhx", &input[i]); pos += 2; } 26 | 27 | int salt_len = strlen(salt_hex) / 2; 28 | unsigned char salt[salt_len]; 29 | pos = salt_hex; 30 | for(size_t i = 0; i < salt_len; i++) { sscanf(pos, "%2hhx", &salt[i]); pos += 2; } 31 | 32 | if(hash_len > 258) 33 | hash_len = 258; 34 | 35 | unsigned char hash[hash_len]; 36 | int res = argon2_hash(time_cost, mem_cost, parallelism, input, input_len, salt, salt_len, hash, hash_len, NULL, 0, type, version); 37 | if(res == 0) { 38 | char *ptr = &output[0]; 39 | for (size_t i = 0; i < hash_len; i++) { ptr += sprintf (ptr, "%02x",hash[i]); } 40 | } 41 | else { 42 | output[0] = '!'; 43 | strcpy(output+1, argon2_error_message(res)); 44 | } 45 | 46 | return &output[0]; 47 | } -------------------------------------------------------------------------------- /faucet-wasm/argon2/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm_build", 9 | "version": "0.0.0", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "base32768": "^3.0.1" 13 | } 14 | }, 15 | "node_modules/base32768": { 16 | "version": "3.0.1", 17 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-3.0.1.tgz", 18 | "integrity": "sha512-dNGY49X0IKN1kDl9y/6sii1Vced+f+4uAqOeRz/PshjNdPwSD+ntnHOg/YgDbLSZetp94d/XxGdpfbXDKv8BVQ==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm_webworker.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "GPL-3.0-or-later", 12 | "dependencies": { 13 | "base32768": "^3.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /faucet-wasm/argon2/wasm-pre.js: -------------------------------------------------------------------------------- 1 | 2 | Module["wasmBinary"] = getWasmBinary(); 3 | Module["locateFile"] = function() {}; 4 | Module["onRuntimeInitialized"] = function() { 5 | argon2 = cwrap("hash_a2", "string", ["string", "string", "number", "number", "number", "number", "number", "number"]); 6 | } 7 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/.gitignore: -------------------------------------------------------------------------------- 1 | cryptonight-wasm/ 2 | emsdk/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = prog 2 | LIBS = -lm 3 | 4 | CC = emcc -O3 -s NO_FILESYSTEM=1 -s 'EXPORTED_RUNTIME_METHODS=["ccall", "cwrap"]' -s TOTAL_MEMORY=67108864 -s EXPORTED_FUNCTIONS="['_hash_cn']" -s WASM=1 -s ENVIRONMENT=worker -s MODULARIZE=1 -sWASM_BIGINT -s EXPORT_ES6=1 --no-entry --pre-js ../../../wasm-pre.js 5 | CFLAGS = 6 | 7 | # -s ASSERTIONS=1 8 | # -s SINGLE_FILE=1 9 | .PHONY: default all clean 10 | 11 | default: $(TARGET) 12 | all: default 13 | 14 | OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c)) 15 | HEADERS = $(wildcard *.h) 16 | 17 | %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $< -o $@ 18 | 19 | .PRECIOUS: $(TARGET) $(OBJECTS) 20 | 21 | $(TARGET): $(OBJECTS) 22 | $(CC) $(OBJECTS) -Wall $(LIBS) -o cn.js 23 | 24 | clean: 25 | -rm -f *.o 26 | -rm -f $(TARGET) 27 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | import { encode } from "base32768"; 3 | import fs from "fs"; 4 | 5 | const base32768Module = fs.readFileSync("node_modules/base32768/src/index.js", { encoding: "utf8" }).replace(/^export .*$/m, ""); 6 | const base32768WASM = encode(fs.readFileSync("cryptonight-wasm/hash_cn/webassembly/cn.wasm")); 7 | 8 | const wasmWrappperJS = fs.readFileSync("cryptonight-wasm/hash_cn/webassembly/cn.js", { encoding: "utf8" }); 9 | let lines = wasmWrappperJS.replace(/import\.meta/g, "wasmMeta").split("\n"); 10 | // filter out the "export default Module" line 11 | lines = lines.filter(line => !line.startsWith("export default Module")); 12 | const customWASMWrappperJS = lines.join("\n"); 13 | 14 | // -------------------------------------------------------------------------- 15 | // Output the composited webworker JS 16 | 17 | // first, include the warning about this file being automatically generated 18 | console.log(` 19 | 20 | // THIS FILE IS GENERATED AUTOMATICALLY 21 | // Don't edit this file by hand. 22 | // Edit the build located in the faucet-wasm folder. 23 | 24 | var cryptonightPromise, cryptonight; 25 | 26 | module.exports = { 27 | getCryptoNight: function() { return cryptonight; }, 28 | getCryptoNightReadyPromise: function() { 29 | return cryptonightPromise; 30 | } 31 | }; 32 | 33 | function getWasmBinary() { 34 | ${base32768Module} 35 | const base32768WASM = "${base32768WASM}"; 36 | return decode(base32768WASM); 37 | } 38 | 39 | (function() { 40 | var wasmMeta = {}; 41 | if(typeof self === "undefined") { 42 | var self = {location:{href:""}}; 43 | } 44 | 45 | ${customWASMWrappperJS} 46 | cryptonightPromise = Module(); 47 | })(); 48 | 49 | `); 50 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the faucet-wasm folder.\n" 5 | fi 6 | 7 | if [ ! -d cryptonight-wasm ]; then 8 | printf "Cloning https://github.com/notgiven688/webminerpool... \n" 9 | git clone https://github.com/notgiven688/webminerpool.git cryptonight-wasm 10 | fi 11 | 12 | emcc_is_installed="$(which emcc | wc -l)" 13 | 14 | if [ "$emcc_is_installed" == "0" ]; then 15 | if [ ! -d ./emsdk ]; then 16 | git clone https://github.com/emscripten-core/emsdk.git 17 | fi 18 | cd emsdk 19 | ./emsdk install latest 20 | ./emsdk activate latest 21 | source ./emsdk_env.sh 22 | cd .. 23 | fi 24 | 25 | cd cryptonight-wasm/hash_cn/webassembly 26 | 27 | printf "running ./Makefile for webminerpool/hash_cn/webassembly... \n" 28 | 29 | # included Makefile is not working... 30 | mv Makefile Makefile.org 31 | cp ../../../Makefile ./Makefile 32 | make 33 | mv Makefile.org Makefile 34 | 35 | cd ../../.. 36 | 37 | nodejs_is_installed="$(which node | wc -l)" 38 | npm_is_installed="$(which npm | wc -l)" 39 | 40 | if [ "$nodejs_is_installed" == "0" ] || [ "$npm_is_installed" == "0" ]; then 41 | printf "nodejs and npm are required for the next step. Please install them manually 😇" 42 | exit 1 43 | fi 44 | 45 | if [ ! -d node_modules ]; then 46 | printf "running npm install \n" 47 | npm install 48 | fi 49 | 50 | node build_wasm.js > "../../libs/cryptonight_wasm.cjs" 51 | 52 | printf "\n\nbuilt ../../libs/cryptonight_wasm.cjs successfully!\n\n" 53 | 54 | 55 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm_build", 9 | "version": "0.0.0", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "base32768": "^3.0.1" 13 | } 14 | }, 15 | "node_modules/base32768": { 16 | "version": "3.0.1", 17 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-3.0.1.tgz", 18 | "integrity": "sha512-dNGY49X0IKN1kDl9y/6sii1Vced+f+4uAqOeRz/PshjNdPwSD+ntnHOg/YgDbLSZetp94d/XxGdpfbXDKv8BVQ==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm_webworker.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "GPL-3.0-or-later", 12 | "dependencies": { 13 | "base32768": "^3.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /faucet-wasm/cryptonight/wasm-pre.js: -------------------------------------------------------------------------------- 1 | 2 | Module["wasmBinary"] = getWasmBinary(); 3 | Module["locateFile"] = function() {}; 4 | Module["onRuntimeInitialized"] = function() { 5 | cryptonight = cwrap('hash_cn', 'string', ['string','number','number','number']); 6 | } 7 | -------------------------------------------------------------------------------- /faucet-wasm/didkit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /faucet-wasm/didkit/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | const base32768 = require('base32768'); 3 | const fs = require('fs'); 4 | 5 | const base32768WASM = base32768.encode(fs.readFileSync("node_modules/@spruceid/didkit-wasm/node/didkit_wasm_bg.wasm")); 6 | 7 | const wasmWrappperJS = fs.readFileSync("node_modules/@spruceid/didkit-wasm/node/didkit_wasm.js", { encoding: "utf8" }); 8 | let wasmWrappperLines = wasmWrappperJS.split("\n"); 9 | 10 | const customLoaderSrc = [ 11 | fs.readFileSync("node_modules/base32768/dist/iife/base32768.js", { encoding: "utf8" }), 12 | `const base32768WASM = "${base32768WASM}";`, 13 | `const bytes = base32768.decode(base32768WASM);` 14 | ]; 15 | 16 | // inject wasm binary 17 | 18 | /* 19 | const path = require('path').join(__dirname, 'didkit_wasm_bg.wasm'); 20 | const bytes = require('fs').readFileSync(path); 21 | */ 22 | 23 | wasmWrappperLines = wasmWrappperLines.map(line => { 24 | if(line.startsWith("const path") && line.match(/didkit_wasm_bg\.wasm/)) { 25 | return ""; 26 | } 27 | if(line.startsWith("const bytes = require('fs').readFileSync(path);")) { 28 | return customLoaderSrc.join("\n"); 29 | } 30 | return line; 31 | }); 32 | 33 | // -------------------------------------------------------------------------- 34 | // Output the composited library 35 | 36 | const librarySrc = []; 37 | librarySrc.push(` 38 | 39 | // THIS FILE IS GENERATED AUTOMATICALLY 40 | // Don't edit this file by hand. 41 | // Edit the build located in the faucet-wasm folder. 42 | 43 | `); 44 | librarySrc.push(wasmWrappperLines.join("\n")); 45 | 46 | fs.writeFileSync("../../libs/didkit_wasm.cjs", librarySrc.join("\n")); 47 | -------------------------------------------------------------------------------- /faucet-wasm/didkit/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm_build", 9 | "version": "0.0.0", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "@spruceid/didkit-wasm": "^0.3.0-alpha0", 13 | "base32768": "^2.0.1" 14 | } 15 | }, 16 | "node_modules/@spruceid/didkit-wasm": { 17 | "version": "0.3.0-alpha0", 18 | "resolved": "https://registry.npmjs.org/@spruceid/didkit-wasm/-/didkit-wasm-0.3.0-alpha0.tgz", 19 | "integrity": "sha512-NKyw1MM3ZqFmL9WLbFR1l3XdVmukcmP4nV0aEaztBhxeFyEBS31lxdd2WE4tPAl83cUOhxJ+AfX2iZS5ES9rZw==" 20 | }, 21 | "node_modules/base32768": { 22 | "version": "2.0.2", 23 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-2.0.2.tgz", 24 | "integrity": "sha512-0QKwYLdhM2/5t3SYibZQRbGbSOcu7aptVx2jxasQhkbrhXXb3XxTmdWSsnFyDxT/906Cd4vqeltD+Rf6VSYPwQ==" 25 | } 26 | }, 27 | "dependencies": { 28 | "@spruceid/didkit-wasm": { 29 | "version": "0.3.0-alpha0", 30 | "resolved": "https://registry.npmjs.org/@spruceid/didkit-wasm/-/didkit-wasm-0.3.0-alpha0.tgz", 31 | "integrity": "sha512-NKyw1MM3ZqFmL9WLbFR1l3XdVmukcmP4nV0aEaztBhxeFyEBS31lxdd2WE4tPAl83cUOhxJ+AfX2iZS5ES9rZw==" 32 | }, 33 | "base32768": { 34 | "version": "2.0.2", 35 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-2.0.2.tgz", 36 | "integrity": "sha512-0QKwYLdhM2/5t3SYibZQRbGbSOcu7aptVx2jxasQhkbrhXXb3XxTmdWSsnFyDxT/906Cd4vqeltD+Rf6VSYPwQ==" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /faucet-wasm/didkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "base32768": "^2.0.1", 13 | "@spruceid/didkit-wasm": "^0.3.0-alpha0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /faucet-wasm/groth16/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /faucet-wasm/groth16/build_lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the faucet-wasm folder.\n" 5 | fi 6 | 7 | nodejs_is_installed="$(which node | wc -l)" 8 | npm_is_installed="$(which npm | wc -l)" 9 | 10 | if [ "$nodejs_is_installed" == "0" ] || [ "$npm_is_installed" == "0" ]; then 11 | printf "nodejs and npm are required for the next step. Please install them manually 😇" 12 | exit 1 13 | fi 14 | 15 | if [ ! -d node_modules ]; then 16 | printf "running npm install \n" 17 | npm install 18 | fi 19 | 20 | npx webpack --stats-error-details 21 | 22 | cp ./dist/ ../../libs/groth16.cjs 23 | 24 | printf "\n\nbuilt ../../libs/groth16.cjs successfully!\n\n" 25 | 26 | 27 | -------------------------------------------------------------------------------- /faucet-wasm/groth16/lib.js: -------------------------------------------------------------------------------- 1 | 2 | const groth16 = require('@zk-kit/groth16'); 3 | const ffjavascript = require('ffjavascript'); 4 | 5 | function init() { 6 | if(!globalThis.curve_bn128) { 7 | return ffjavascript.buildBn128(true).then(function(curve_bn128) { 8 | globalThis.curve_bn128 = curve_bn128; 9 | }); 10 | } 11 | } 12 | 13 | module.exports = { 14 | init: init, 15 | groth16: groth16, 16 | }; 17 | -------------------------------------------------------------------------------- /faucet-wasm/groth16/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groth16_lib", 3 | "version": "0.0.0", 4 | "description": "build groth16 library", 5 | "main": "lib.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "GPL-3.0-or-later", 11 | "devDependencies": { 12 | "webpack": "^5.90.3", 13 | "webpack-cli": "^5.1.4" 14 | }, 15 | "dependencies": { 16 | "base32768": "^2.0.1", 17 | "@zk-kit/groth16": "^0.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /faucet-wasm/groth16/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = [ 5 | { 6 | entry: './lib.js', 7 | target: 'node', 8 | mode: 'production', 9 | resolve: { 10 | extensions: ['.js'], 11 | }, 12 | output: { 13 | filename: 'groth16.cjs', 14 | path: __dirname + '/dist', 15 | library: 'libpack', 16 | libraryTarget:'umd' 17 | }, 18 | plugins: [ 19 | new webpack.optimize.LimitChunkCountPlugin({ 20 | maxChunks: 1, 21 | }), 22 | ], 23 | optimization: { 24 | minimize: true, 25 | minimizer: [ 26 | new TerserPlugin({ 27 | extractComments: false, 28 | terserOptions: { 29 | format: { 30 | comments: false, 31 | }, 32 | }, 33 | }), 34 | ], 35 | }, 36 | } 37 | ]; 38 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | build/ 3 | secp256k1/ 4 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | import { encode } from "base32768"; 3 | import fs from "fs"; 4 | 5 | const base32768Module = fs.readFileSync("node_modules/base32768/src/index.js", { encoding: "utf8" }).replace(/^export .*$/m, ""); 6 | const base32768WASM = encode(fs.readFileSync("build/nickminer.wasm")); 7 | 8 | const wasmWrappperJS = fs.readFileSync("build/nickminer.js", { encoding: "utf8" }); 9 | let lines = wasmWrappperJS.replace(/import\.meta/g, "wasmMeta").split("\n"); 10 | // filter out the "export default Module" line 11 | lines = lines.filter(line => !line.startsWith("export default Module")); 12 | const customWASMWrappperJS = lines.join("\n"); 13 | 14 | // -------------------------------------------------------------------------- 15 | // Output the composited webworker JS 16 | 17 | // first, include the warning about this file being automatically generated 18 | console.log(` 19 | 20 | // THIS FILE IS GENERATED AUTOMATICALLY 21 | // Don't edit this file by hand. 22 | // Edit the build located in the faucet-wasm folder. 23 | 24 | var nickMinerPromise, nickMiner; 25 | 26 | module.exports = { 27 | getNickMiner: function() { return nickMiner; }, 28 | getNickMinerReadyPromise: function() { return nickMinerPromise; } 29 | }; 30 | 31 | function getWasmBinary() { 32 | ${base32768Module} 33 | const base32768WASM = "${base32768WASM}"; 34 | return decode(base32768WASM); 35 | } 36 | 37 | (function() { 38 | var wasmMeta = {}; 39 | if(typeof self === "undefined") { 40 | var self = {location:{href:""}}; 41 | } 42 | 43 | ${customWASMWrappperJS} 44 | nickMinerPromise = Module(); 45 | })(); 46 | 47 | `); 48 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the faucet-wasm folder.\n" 5 | fi 6 | 7 | if [ ! -d secp256k1 ]; then 8 | printf "Cloning https://github.com/bitcoin-core/secp256k1.git... \n" 9 | git clone https://github.com/bitcoin-core/secp256k1.git secp256k1 10 | fi 11 | 12 | docker_is_installed="$(which docker | wc -l)" 13 | 14 | if [ "$docker_is_installed" == "0" ] ; then 15 | printf "docker is required for the next step. Please install it manually 😇" 16 | exit 1 17 | fi 18 | 19 | printf "compiling nickminer wasm... \n" 20 | 21 | mkdir -p build 22 | docker build -f nickminer.Dockerfile . -t wasm-secp256k1 23 | docker run --rm -v $(pwd)/build:/out wasm-secp256k1 ./nickminer-compile.sh 24 | 25 | nodejs_is_installed="$(which node | wc -l)" 26 | npm_is_installed="$(which npm | wc -l)" 27 | 28 | if [ "$nodejs_is_installed" == "0" ] || [ "$npm_is_installed" == "0" ]; then 29 | printf "nodejs and npm are required for the next step. Please install them manually 😇" 30 | exit 1 31 | fi 32 | 33 | if [ ! -d node_modules ]; then 34 | printf "running npm install \n" 35 | npm install 36 | fi 37 | 38 | node build_wasm.js > "../../libs/nickminer_wasm.cjs" 39 | 40 | printf "\n\nbuilt ../libs/nickminer_wasm.cjs successfully!\n\n" 41 | 42 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/miner/keccak256.h: -------------------------------------------------------------------------------- 1 | /* sha3 - an implementation of Secure Hash Algorithm 3 (Keccak). 2 | * based on the 3 | * The Keccak SHA-3 submission. Submission to NIST (Round 3), 2011 4 | * by Guido Bertoni, Joan Daemen, Michaël Peeters and Gilles Van Assche 5 | * 6 | * Copyright: 2013 Aleksey Kravchenko 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a 9 | * copy of this software and associated documentation files (the "Software"), 10 | * to deal in the Software without restriction, including without limitation 11 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | * and/or sell copies of the Software, and to permit persons to whom the 13 | * Software is furnished to do so. 14 | * 15 | * This program is distributed in the hope that it will be useful, but 16 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 17 | * or FITNESS FOR A PARTICULAR PURPOSE. Use this program at your own risk! 18 | */ 19 | 20 | #ifndef __KECCAK256_H_ 21 | #define __KECCAK256_H_ 22 | 23 | #include 24 | 25 | #define sha3_max_permutation_size 25 26 | #define sha3_max_rate_in_qwords 24 27 | 28 | typedef struct SHA3_CTX { 29 | /* 1600 bits algorithm hashing state */ 30 | uint64_t hash[sha3_max_permutation_size]; 31 | /* 1536-bit buffer for leftovers */ 32 | uint64_t message[sha3_max_rate_in_qwords]; 33 | /* count of bytes in the message[] buffer */ 34 | uint16_t rest; 35 | /* size of a message block processed at once */ 36 | //unsigned block_size; 37 | } SHA3_CTX; 38 | 39 | 40 | #ifdef __cplusplus 41 | extern "C" { 42 | #endif /* __cplusplus */ 43 | 44 | 45 | void keccak_init(SHA3_CTX *ctx); 46 | void keccak_update(SHA3_CTX *ctx, const unsigned char *msg, uint16_t size); 47 | void keccak_final(SHA3_CTX *ctx, unsigned char* result); 48 | 49 | 50 | #ifdef __cplusplus 51 | } 52 | #endif /* __cplusplus */ 53 | 54 | #endif /* __KECCAK256_H_ */ 55 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/nickminer-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | if [ "$SCRIPT_DIR" != "/app" ]; then 5 | echo "This script should be run from within the build container! run ./build_wasm.sh to build the wasm" 6 | exit 1 7 | fi 8 | 9 | # method for joining a multiline string list using a delimiter 10 | join() { 11 | s_list=$1; s_delim=$2 12 | 13 | echo -n "${s_list/$'\n'/}" | tr '\n' "$s_delim" | sed "s/$s_delim$//" 14 | } 15 | 16 | # list of functions to export 17 | s_exports=''' 18 | "_malloc" 19 | "_free" 20 | "_miner_init" 21 | "_miner_set_config" 22 | "_miner_get_input" 23 | "_miner_get_sigrv" 24 | "_miner_get_suffix" 25 | "_miner_get_preimage" 26 | "_miner_run" 27 | ''' 28 | 29 | # join list to string 30 | sx_funcs=$(join "$s_exports" ',') 31 | 32 | # clean 33 | emmake make clean 34 | 35 | # workaround for 36 | echo '{"type":"commonjs"}' > package.json 37 | 38 | # autogen 39 | ./autogen.sh 40 | 41 | # configure 42 | emconfigure ./configure \ 43 | --enable-module-ecdh \ 44 | --enable-module-recovery \ 45 | --enable-module-schnorrsig=no \ 46 | --enable-module-ellswift=no \ 47 | --enable-module-extrakeys=no \ 48 | --with-ecmult-window=4 \ 49 | --with-ecmult-gen-precision=2 \ 50 | --disable-shared \ 51 | CFLAGS="-fdata-sections -ffunction-sections -O2" \ 52 | LDFLAGS="-Wl,--gc-sections" 53 | 54 | # make 55 | emmake make FORMAT=wasm 56 | emmake make src/precompute_ecmult-precompute_ecmult FORMAT=wasm 57 | 58 | # reset output dir 59 | rm -rf out 60 | mkdir -p out 61 | 62 | echo "library build complete, building miner wasm" 63 | 64 | # compile 65 | emcc src/precompute_ecmult-precompute_ecmult.o \ 66 | src/libsecp256k1_precomputed_la-precomputed_ecmult.o \ 67 | src/libsecp256k1_precomputed_la-precomputed_ecmult_gen.o \ 68 | src/libsecp256k1_la-secp256k1.o \ 69 | src/miner/nickminer.c \ 70 | -O3 \ 71 | -s WASM=1 \ 72 | -s NO_FILESYSTEM=1 \ 73 | -s TOTAL_MEMORY=$(( 64 * 1024 * 3 )) \ 74 | -s "BINARYEN_METHOD='native-wasm'" \ 75 | -s DETERMINISTIC=1 \ 76 | -s 'EXPORTED_RUNTIME_METHODS=["ccall", "cwrap"]' \ 77 | -s EXPORTED_FUNCTIONS="[$sx_funcs]" \ 78 | -s ENVIRONMENT=worker \ 79 | -s MODULARIZE=1 \ 80 | -s EXPORT_ES6=1 \ 81 | -s STANDALONE_WASM \ 82 | --no-entry --pre-js ./wasm-pre.js \ 83 | -o out/nickminer.js 84 | 85 | # verify 86 | ls -lah out/ 87 | cp -r out/* /out/ 88 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/nickminer.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM emscripten/emsdk 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | autoconf \ 6 | libtool \ 7 | build-essential 8 | 9 | COPY secp256k1 /app 10 | COPY nickminer-compile.sh /app 11 | COPY wasm-pre.js /app 12 | COPY miner /app/src/miner 13 | 14 | WORKDIR /app 15 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm_build", 9 | "version": "0.0.0", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "base32768": "^3.0.1" 13 | } 14 | }, 15 | "node_modules/base32768": { 16 | "version": "3.0.1", 17 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-3.0.1.tgz", 18 | "integrity": "sha512-dNGY49X0IKN1kDl9y/6sii1Vced+f+4uAqOeRz/PshjNdPwSD+ntnHOg/YgDbLSZetp94d/XxGdpfbXDKv8BVQ==", 19 | "license": "MIT" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm_webworker.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "GPL-3.0-or-later", 12 | "dependencies": { 13 | "base32768": "^3.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /faucet-wasm/nickminer/wasm-pre.js: -------------------------------------------------------------------------------- 1 | 2 | Module["wasmBinary"] = getWasmBinary(); 3 | Module["locateFile"] = function() {}; 4 | Module["onRuntimeInitialized"] = function() { 5 | nickMiner = { 6 | miner_init: cwrap("miner_init", "void", []), 7 | miner_set_config: cwrap("miner_set_config", "void", ["string", "string", "number", "string", "string", "number", "string"]), 8 | miner_get_input: cwrap("miner_get_input", "string", []), 9 | miner_get_sigrv: cwrap("miner_get_sigrv", "string", []), 10 | miner_get_suffix: cwrap("miner_get_suffix", "string", []), 11 | miner_get_preimage: cwrap("miner_get_preimage", "string", []), 12 | miner_run: cwrap("miner_run", "string", ["string"]), 13 | 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /faucet-wasm/scrypt/.gitignore: -------------------------------------------------------------------------------- 1 | scrypt-wasm/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /faucet-wasm/scrypt/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | import { encode } from "base32768"; 3 | import fs from "fs"; 4 | 5 | const base32768WASM = encode(fs.readFileSync("scrypt-wasm/pkg/scrypt_wasm_bg.wasm")); 6 | 7 | const wasmWrappperJS = fs.readFileSync("scrypt-wasm/pkg/scrypt_wasm_bg.js", { encoding: "utf8" }); 8 | let lines = wasmWrappperJS.split("\n"); 9 | 10 | // filter out the first line "import * as wasm from './scrypt_wasm_bg.wasm';" 11 | // because we are using global namespace, not es6 modules 12 | lines = lines.filter(line => !line.includes("scrypt_wasm_bg.wasm")) 13 | 14 | // replace export with global namespace for the same reason. 15 | lines = lines.map(line => { 16 | if(line.startsWith("export function scrypt")) { 17 | return line.replace("export function scrypt", "scrypt = function"); 18 | } 19 | else if(line.startsWith("export function")) { 20 | return line.replace("export function", "function"); 21 | } 22 | return line; 23 | }); 24 | const customWASMWrappperJS = lines.join("\n"); 25 | 26 | // -------------------------------------------------------------------------- 27 | // Output the composited webworker JS 28 | 29 | // first, include the warning about this file being automatically generated 30 | console.log(` 31 | 32 | // THIS FILE IS GENERATED AUTOMATICALLY 33 | // Don't edit this file by hand. 34 | // Edit the build located in the faucet-wasm folder. 35 | 36 | let scrypt; 37 | let scryptPromise; 38 | 39 | module.exports = { 40 | getScrypt: function() { return scrypt; }, 41 | getScryptReadyPromise: function() { return scryptPromise; } 42 | }; 43 | 44 | `); 45 | 46 | // Now its time to load the wasm module. 47 | // first, load the base32768 module into a global variable called "base32768" 48 | console.log(fs.readFileSync("node_modules/base32768/src/index.js", { encoding: "utf8" }).replace(/^export .*$/m, "")) 49 | 50 | // now, decode the base32768 string into an ArrayBuffer and tell WebAssembly to load it 51 | console.log(` 52 | const base32768WASM = "${base32768WASM}"; 53 | const wasmBinary = decode(base32768WASM); 54 | 55 | scryptPromise = WebAssembly.instantiate(wasmBinary, {}).then(instantiatedModule => { 56 | `); 57 | 58 | // Output the WASM wrapper JS code that came from the Rust WASM compiler, 59 | // slightly modified to use global namespace instead of es6 modules 60 | console.log(customWASMWrappperJS.split("\n").map(x => ` ${x}`).join("\n")); 61 | 62 | console.log("__wbg_set_wasm(instantiatedModule.instance.exports);"); 63 | // finish off by closing scryptPromise 64 | console.log("});"); -------------------------------------------------------------------------------- /faucet-wasm/scrypt/build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -f build_wasm.sh ]; then 4 | printf "Please run this script from the faucet-wasm folder.\n" 5 | fi 6 | 7 | nodejs_is_installed="$(which node | wc -l)" 8 | npm_is_installed="$(which npm | wc -l)" 9 | 10 | if [ "$nodejs_is_installed" == "0" ] || [ "$npm_is_installed" == "0" ]; then 11 | printf "nodejs and npm are required for the next step. Please install them manually 😇" 12 | exit 1 13 | fi 14 | 15 | if [ ! -d node_modules ]; then 16 | printf "running npm install \n" 17 | npm install 18 | fi 19 | 20 | if [ ! -d scrypt-wasm ]; then 21 | printf "Cloning https://github.com/pk910/scrypt-wasm... \n" 22 | git clone https://github.com/pk910/scrypt-wasm.git 23 | fi 24 | 25 | cd scrypt-wasm 26 | 27 | rust_is_installed="$(which rustc | wc -l)" 28 | 29 | if [ "$rust_is_installed" == "0" ]; then 30 | printf "rust language compilers & tools will need to be installed." 31 | printf "using rustup.rs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh \n" 32 | read -p "is this ok? [y] " -n 1 -r 33 | printf "\n" 34 | if [[ $REPLY =~ ^[Yy]$ ]] 35 | then 36 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 37 | else 38 | printf "exiting due to no rust compiler" 39 | exit 1 40 | fi 41 | fi 42 | 43 | if [ ! -d pkg ]; then 44 | printf "running Makefile for scrypt-wasm... \n" 45 | rustup target add wasm32-unknown-unknown 46 | $(pwd)/../node_modules/.bin/wasm-pack build --target browser 47 | fi 48 | 49 | cd ../ 50 | 51 | node build_wasm.js > "../../libs/scrypt_wasm.cjs" 52 | 53 | printf "\n\nbuilt ../../libs/scrypt_wasm.cjs successfully!\n\n" 54 | 55 | 56 | -------------------------------------------------------------------------------- /faucet-wasm/scrypt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm_webworker.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "GPL-3.0-or-later", 12 | "dependencies": { 13 | "base32768": "^3.0.1" 14 | }, 15 | "devDependencies": { 16 | "wasm-pack": "^0.12.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /faucet-wasm/sqlite3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /faucet-wasm/sqlite3/build_wasm.js: -------------------------------------------------------------------------------- 1 | 2 | import { encode } from "base32768"; 3 | import fs from "fs"; 4 | 5 | const base32768WASM = encode(fs.readFileSync("node_modules/node-sqlite3-wasm/dist/node-sqlite3-wasm.wasm")); 6 | 7 | const wasmWrappperJS = fs.readFileSync("node_modules/node-sqlite3-wasm/dist/node-sqlite3-wasm.js", { encoding: "utf8" }); 8 | let wasmWrappperLines = wasmWrappperJS.split("\n"); 9 | 10 | const customLoaderSrc = [ 11 | fs.readFileSync("node_modules/base32768/src/index.js", { encoding: "utf8" }).replace(/^export .*$/m, ""), 12 | `const base32768WASM = "${base32768WASM}";`, 13 | `moduleArg['wasmBinary'] = decode(base32768WASM);` 14 | ]; 15 | 16 | // inject wasm binary 17 | wasmWrappperLines = wasmWrappperLines.map(line => { 18 | if(line.startsWith("function(moduleArg = {}) {")) { 19 | return line + "\n" + customLoaderSrc.join("\n"); 20 | } 21 | return line; 22 | }); 23 | 24 | // -------------------------------------------------------------------------- 25 | // Output the composited library 26 | 27 | const librarySrc = []; 28 | librarySrc.push(` 29 | 30 | // THIS FILE IS GENERATED AUTOMATICALLY 31 | // Don't edit this file by hand. 32 | // Edit the build located in the faucet-wasm folder. 33 | 34 | `); 35 | librarySrc.push(wasmWrappperLines.join("\n")); 36 | 37 | fs.writeFileSync("../../libs/sqlite3_wasm.cjs", librarySrc.join("\n")); 38 | -------------------------------------------------------------------------------- /faucet-wasm/sqlite3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm_build", 9 | "version": "0.0.0", 10 | "license": "GPL-3.0-or-later", 11 | "dependencies": { 12 | "base32768": "^3.0.1", 13 | "node-sqlite3-wasm": "^0.8.5" 14 | } 15 | }, 16 | "node_modules/base32768": { 17 | "version": "3.0.1", 18 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-3.0.1.tgz", 19 | "integrity": "sha512-dNGY49X0IKN1kDl9y/6sii1Vced+f+4uAqOeRz/PshjNdPwSD+ntnHOg/YgDbLSZetp94d/XxGdpfbXDKv8BVQ==" 20 | }, 21 | "node_modules/node-sqlite3-wasm": { 22 | "version": "0.8.5", 23 | "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.5.tgz", 24 | "integrity": "sha512-taIyscSqtQ6ODfgeLkQsrUyc25PZO9Sa0TZxcKIcvhVrlrsz+CFFvMsFeE7SM+PPNarRXCB4rYlyIquePwW0Rg==" 25 | } 26 | }, 27 | "dependencies": { 28 | "base32768": { 29 | "version": "3.0.1", 30 | "resolved": "https://registry.npmjs.org/base32768/-/base32768-3.0.1.tgz", 31 | "integrity": "sha512-dNGY49X0IKN1kDl9y/6sii1Vced+f+4uAqOeRz/PshjNdPwSD+ntnHOg/YgDbLSZetp94d/XxGdpfbXDKv8BVQ==" 32 | }, 33 | "node-sqlite3-wasm": { 34 | "version": "0.8.5", 35 | "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.5.tgz", 36 | "integrity": "sha512-taIyscSqtQ6ODfgeLkQsrUyc25PZO9Sa0TZxcKIcvhVrlrsz+CFFvMsFeE7SM+PPNarRXCB4rYlyIquePwW0Rg==" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /faucet-wasm/sqlite3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_build", 3 | "version": "0.0.0", 4 | "description": "build wasm module into webworker", 5 | "main": "build_wasm.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "GPL-3.0-or-later", 12 | "dependencies": { 13 | "base32768": "^3.0.1", 14 | "node-sqlite3-wasm": "^0.8.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/argon2_wasm.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Argon2 = (input: string, salt: string, hashlen: number, iterations: number, memory: number, parallelism: number, type: number, version: number) => string; 3 | 4 | /* type 5 | * 0: Argon2d 6 | * 1: Argon2i 7 | * 2: Argon2id 8 | * 10: Argon2u 9 | */ 10 | 11 | /* version 12 | * 10: Argon2 v10 13 | * 13: Argon2 v13 14 | */ 15 | 16 | export function getArgon2(): Argon2; 17 | export function getArgon2ReadyPromise(): Promise; 18 | -------------------------------------------------------------------------------- /libs/cryptonight_wasm.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type CryptoNight = (input: string, algo: number, variant: number, height: number) => string; 3 | 4 | /* algo 5 | * 0: cn 6 | * 1: cn-lite 7 | * 2: cn-pico 8 | * 3: cn-half 9 | * 4: cn-rwz 10 | */ 11 | 12 | /* 13 | Supported algorithms 14 | # name algo / variant description 15 | 1 cn algo="cn", variant=-1 autodetect cryptonight variant (block.major - 6) 16 | 2 cn/0 algo="cn", variant=0 original cryptonight 17 | 3 cn/1 algo="cn", variant=1 also known as monero7 and cryptonight v7 18 | 4 cn/2 algo="cn", variant=2 or 3 cryptonight variant 2 19 | 5 cn/r algo="cn", variant=4 cryptonight variant 4 also known as cryptonightR 20 | 6 cn-lite algo="cn-lite", variant=-1 same as #1 with memory/2, iterations/2 21 | 7 cn-lite/0 algo="cn-lite", variant=0 same as #2 with memory/2, iterations/2 22 | 8 cn-lite/1 algo="cn-lite", variant=1 same as #3 with memory/2, iterations/2 23 | 9 cn-pico/trtl algo="cn-pico", variant=2 or 3 same as #4 with memory/8, iterations/8 24 | 10 cn-half algo="cn-half", variant=2 or 3 same as #4 with memory/1, iterations/2 25 | 11 cn/rwz algo="cn-rwz", variant=2 or 3 same as #4 with memory/1, iterations*3/4 26 | */ 27 | 28 | export function getCryptoNight(): CryptoNight; 29 | export function getCryptoNightReadyPromise(): Promise; 30 | -------------------------------------------------------------------------------- /libs/nickminer_wasm.d.ts: -------------------------------------------------------------------------------- 1 | 2 | interface NickMiner { 3 | miner_init(): void; 4 | miner_set_config(inputHash: string, sigR: string, sigV: number, suffixMask: string, prefixMask: string, rounds: number, preimage: string): void; 5 | miner_run(nonce: string): string; 6 | } 7 | 8 | export function getNickMiner(): NickMiner; 9 | export function getNickMinerReadyPromise(): Promise; 10 | -------------------------------------------------------------------------------- /libs/scrypt_wasm.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Scrypt = (password: string, salt: string, n: number, r: number, p: number, dklen: number) => string; 3 | 4 | export function getScrypt(): Scrypt; 5 | export function getScryptReadyPromise(): Promise; 6 | -------------------------------------------------------------------------------- /libs/sqlite3_wasm.d.ts: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022-2023 Tobias Enderle 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | export interface RunResult { 24 | changes: number; 25 | lastInsertRowid: number | bigint; 26 | } 27 | 28 | export interface QueryOptions { 29 | expand?: boolean; 30 | } 31 | 32 | export type SQLiteValue = number | bigint | string | Uint8Array | null; 33 | export type JSValue = boolean | SQLiteValue; 34 | 35 | export type BindValues = JSValue | JSValue[] | Record; 36 | 37 | export type NormalQueryResult = Record; 38 | export type ExpandedQueryResult = Record; 39 | export type QueryResult = NormalQueryResult | ExpandedQueryResult; 40 | 41 | export class Database { 42 | constructor(filename?: string, options?: { fileMustExist?: boolean }); 43 | 44 | get isOpen(): boolean; 45 | get inTransaction(): boolean; 46 | 47 | close(): void; 48 | function( 49 | name: string, 50 | func: (...params: SQLiteValue[]) => JSValue, 51 | options?: { deterministic?: boolean } 52 | ): this; 53 | exec(sql: string): void; 54 | prepare(sql: string): Statement; 55 | run(sql: string, values?: BindValues): RunResult; 56 | all( 57 | sql: string, 58 | values?: BindValues, 59 | options?: QueryOptions 60 | ): QueryResult[]; 61 | get( 62 | sql: string, 63 | values?: BindValues, 64 | options?: QueryOptions 65 | ): QueryResult | null; 66 | } 67 | 68 | export class Statement { 69 | get database(): Database; 70 | get isFinalized(): boolean; 71 | 72 | run(values?: BindValues): RunResult; 73 | all(values?: BindValues, options?: QueryOptions): QueryResult[]; 74 | get(values?: BindValues, options?: QueryOptions): QueryResult | null; 75 | finalize(): void; 76 | } 77 | 78 | export class SQLite3Error extends Error {} 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powfaucet/server", 3 | "version": "2.4.2", 4 | "description": "PoW Faucet Server", 5 | "main": "dist/app.js", 6 | "bin": "bundle/powfaucet.cjs", 7 | "type": "module", 8 | "scripts": { 9 | "build": "tsc", 10 | "build-client": "cd faucet-client && node build-client.js", 11 | "start": "tsc && node dist/app.js", 12 | "bundle": "tsc && webpack --mode production", 13 | "test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 mocha --exit --trace-warnings 'tests/**/*.ts'", 14 | "test-coverage": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 c8 --reporter=text --reporter=lcov mocha --exit -r ts-node/register 'tests/**/*.ts'" 15 | }, 16 | "author": "pk910 (https://pk910.de)", 17 | "license": "AGPL-3.0", 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/pk910/PoWFaucet" 24 | }, 25 | "pkg": { 26 | "scripts": "bundle/*.cjs", 27 | "assets": [ 28 | "faucet-config.example.yaml", 29 | "static/**/*" 30 | ], 31 | "targets": [ 32 | "node18-linux-x64", 33 | "node18-win-x64" 34 | ], 35 | "outputPath": "bin" 36 | }, 37 | "devDependencies": { 38 | "@types/better-sqlite3": "^7.6.11", 39 | "@types/chai": "^5.0.0", 40 | "@types/json-bigint": "^1.0.4", 41 | "@types/mocha": "^10.0.7", 42 | "@types/mysql": "^2.15.26", 43 | "@types/node-fetch": "^2.6.11", 44 | "@types/node-static": "^0.7.11", 45 | "@types/randombytes": "^2.0.3", 46 | "@types/sinon": "^17.0.3", 47 | "@types/uuid": "^10.0.0", 48 | "@types/ws": "^8.5.12", 49 | "c8": "^10.1.2", 50 | "chai": "^5.1.1", 51 | "mocha": "^11.0.1", 52 | "mysql-memory-server": "^1.3.0", 53 | "nyc": "^17.0.0", 54 | "sinon": "^20.0.0", 55 | "ts-loader": "^9.5.1", 56 | "ts-node": "^10.9.2", 57 | "tslint": "^6.1.3", 58 | "typescript": "^5.5.4", 59 | "webpack": "^5.93.0", 60 | "webpack-cli": "^6.0.1" 61 | }, 62 | "dependencies": { 63 | "@brettz9/node-static": "^0.1.1", 64 | "@ethereumjs/common": "^10.0.0", 65 | "@ethereumjs/tx": "^10.0.0", 66 | "@types/node": "^22.5.0", 67 | "bignumber.js": "^9.1.2", 68 | "commander": "^13.1.0", 69 | "hcaptcha": "^0.2.0", 70 | "html-entities": "^2.5.2", 71 | "json-bigint": "^1.0.0", 72 | "mysql2": "^3.11.0", 73 | "node-fetch": "^3.3.2", 74 | "randombytes": "^2.1.0", 75 | "tiny-typed-emitter": "^2.1.0", 76 | "uuid": "^11.0.2", 77 | "web3": "^4.11.1", 78 | "web3-eth-ens": "^4.4.0", 79 | "web3-providers-ipc": "^4.0.7", 80 | "ws": "^8.18.0", 81 | "yaml": "^2.5.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /res/run-faucet.bat: -------------------------------------------------------------------------------- 1 | 2 | node --no-deprecation .\bundle\powfaucet.cjs 3 | -------------------------------------------------------------------------------- /res/run-faucet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd $( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P ) 3 | 4 | until node --no-deprecation ./bundle/powfaucet.cjs; do 5 | echo "powfaucet.cjs crashed with exit code $?. Respawning.." >&2 6 | sleep 1 7 | done -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare const POWFAUCET_VERSION: string; 3 | -------------------------------------------------------------------------------- /src/@types/global.ts: -------------------------------------------------------------------------------- 1 | 2 | const POWFAUCET_VERSION: string = ""; 3 | -------------------------------------------------------------------------------- /src/abi/ERC20.ts: -------------------------------------------------------------------------------- 1 | 2 | export const Erc20Abi = [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "account", 8 | "type": "address" 9 | } 10 | ], 11 | "name": "balanceOf", 12 | "outputs": [ 13 | { 14 | "internalType": "uint256", 15 | "name": "", 16 | "type": "uint256" 17 | } 18 | ], 19 | "stateMutability": "view", 20 | "type": "function" 21 | }, 22 | { 23 | "inputs": [], 24 | "name": "decimals", 25 | "outputs": [ 26 | { 27 | "internalType": "uint8", 28 | "name": "", 29 | "type": "uint8" 30 | } 31 | ], 32 | "stateMutability": "view", 33 | "type": "function" 34 | }, 35 | { 36 | "inputs": [ 37 | { 38 | "internalType": "address", 39 | "name": "to", 40 | "type": "address" 41 | }, 42 | { 43 | "internalType": "uint256", 44 | "name": "value", 45 | "type": "uint256" 46 | } 47 | ], 48 | "name": "transfer", 49 | "outputs": [], 50 | "stateMutability": "nonpayable", 51 | "type": "function" 52 | } 53 | ] as const; 54 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname, basename } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { isMainThread, workerData } from "node:worker_threads"; 4 | import { faucetConfig, loadFaucetConfig, setAppBasePath } from "./config/FaucetConfig.js"; 5 | import { FaucetWorkers } from "./common/FaucetWorker.js"; 6 | import { EthWalletManager } from "./eth/EthWalletManager.js"; 7 | import { FaucetHttpServer } from "./webserv/FaucetHttpServer.js"; 8 | import { FaucetDatabase } from "./db/FaucetDatabase.js"; 9 | import { ServiceManager } from "./common/ServiceManager.js"; 10 | import { FaucetStatsLog } from "./services/FaucetStatsLog.js"; 11 | import { FaucetLogLevel, FaucetProcess } from "./common/FaucetProcess.js"; 12 | import { EthClaimManager } from "./eth/EthClaimManager.js"; 13 | import { ModuleManager } from "./modules/ModuleManager.js"; 14 | import { SessionManager } from "./session/SessionManager.js"; 15 | import { FaucetStatus } from "./services/FaucetStatus.js"; 16 | import { createVoucher } from "./tools/createVoucher.js"; 17 | 18 | (async () => { 19 | let srcfile: string; 20 | if(typeof require !== "undefined") { 21 | srcfile = require.main.filename; 22 | } else { 23 | srcfile = fileURLToPath(import.meta.url); 24 | } 25 | let basepath = path.join(dirname(srcfile), ".."); 26 | 27 | setAppBasePath(basepath); 28 | ServiceManager.GetService(FaucetWorkers).initialize(srcfile); 29 | 30 | if(!isMainThread) { 31 | FaucetWorkers.loadWorkerClass(); 32 | return; 33 | } 34 | 35 | if(process.argv.length >= 3) { 36 | switch(process.argv[2]) { 37 | case "worker": 38 | FaucetWorkers.loadWorkerClass(process.argv[3]); 39 | return; 40 | case "create-voucher": 41 | createVoucher(); 42 | return; 43 | } 44 | } 45 | 46 | try { 47 | loadFaucetConfig(); 48 | ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.INFO, "Initializing PoWFaucet v" + faucetConfig.faucetVersion + " (AppBasePath: " + faucetConfig.appBasePath + ", InternalBasePath: " + basepath + ")"); 49 | ServiceManager.GetService(FaucetProcess).initialize(); 50 | ServiceManager.GetService(FaucetStatus).initialize(); 51 | ServiceManager.GetService(FaucetStatsLog).initialize(); 52 | await ServiceManager.GetService(FaucetDatabase).initialize(); 53 | await ServiceManager.GetService(EthWalletManager).initialize(); 54 | await ServiceManager.GetService(ModuleManager).initialize(); 55 | await ServiceManager.GetService(SessionManager).initialize(); 56 | await ServiceManager.GetService(EthClaimManager).initialize(); 57 | ServiceManager.GetService(FaucetHttpServer).initialize(); 58 | 59 | ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.INFO, "Faucet initialization complete."); 60 | } catch(ex) { 61 | ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.ERROR, "Faucet initialization failed: " + ex.toString() + " " + ex.stack); 62 | process.exit(0); 63 | } 64 | })(); 65 | -------------------------------------------------------------------------------- /src/common/FaucetError.ts: -------------------------------------------------------------------------------- 1 | 2 | export class FaucetError extends Error { 3 | private code: string; 4 | public data: {[key: string]: any}; 5 | 6 | public constructor(code: string, message: string) { 7 | super(message); 8 | this.code = code; 9 | } 10 | 11 | public toString(): string { 12 | return "[" + this.code + "] " + super.toString(); 13 | } 14 | 15 | public getCode(): string { 16 | return this.code; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/common/FaucetWorker.ts: -------------------------------------------------------------------------------- 1 | 2 | import { MessagePort, Worker, parentPort, workerData } from "node:worker_threads"; 3 | import { fork, ChildProcess } from "node:child_process"; 4 | import { DatabaseWorker } from "../db/DatabaseWorker.js"; 5 | import { PoWServerWorker } from "../modules/pow/PoWServerWorker.js"; 6 | import { PoWValidatorWorker } from "../modules/pow/validator/PoWValidatorWorker.js"; 7 | import { ZupassWorker } from "../modules/zupass/ZupassWorker.js"; 8 | 9 | class TestWorker { 10 | constructor(port: MessagePort) { 11 | if(port) { 12 | port.postMessage({ action: "test" }); 13 | } else if(process.send) { 14 | process.send({ action: "test" }); 15 | } 16 | } 17 | } 18 | 19 | const WORKER_CLASSES = { 20 | "test": TestWorker, 21 | "database": DatabaseWorker, 22 | "pow-server": PoWServerWorker, 23 | "pow-validator": PoWValidatorWorker, 24 | "zupass-worker": ZupassWorker, 25 | }; 26 | 27 | interface IFaucetWorkerData { 28 | classKey: string; 29 | } 30 | 31 | export interface IFaucetChildProcess { 32 | childProcess: ChildProcess; 33 | controller: AbortController; 34 | } 35 | 36 | export class FaucetWorkers { 37 | 38 | public static loadWorkerClass(workerClassKey?: string, workerPort?: MessagePort|ChildProcess) { 39 | let workerClass = WORKER_CLASSES[workerClassKey || workerData?.classKey]; 40 | return new workerClass(workerPort || parentPort); 41 | } 42 | 43 | private initialized: boolean; 44 | private workerSrc: string; 45 | 46 | public initialize(workerSrc: string) { 47 | if(this.initialized) 48 | return; 49 | this.initialized = true; 50 | this.workerSrc = workerSrc; 51 | } 52 | 53 | public createWorker(classKey: string): Worker { 54 | if(!WORKER_CLASSES[classKey]) 55 | throw "unknown worker class-key '" + classKey + "'"; 56 | let worker = new Worker(this.workerSrc, { 57 | workerData: { 58 | classKey: classKey, 59 | } as IFaucetWorkerData, 60 | }); 61 | return worker; 62 | } 63 | 64 | public createChildProcess(classKey: string): IFaucetChildProcess { 65 | if(!WORKER_CLASSES[classKey]) 66 | throw "unknown worker class-key '" + classKey + "'"; 67 | 68 | let controller = new AbortController(); 69 | let childProcess = fork(this.workerSrc, ["worker", classKey], { 70 | signal: controller.signal, 71 | }); 72 | 73 | return { 74 | childProcess: childProcess, 75 | controller: controller, 76 | }; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/common/ServiceManager.ts: -------------------------------------------------------------------------------- 1 | 2 | export class ServiceManager { 3 | private static _serviceSymbol = (globalThis.Symbol ? Symbol("ServiceInstances") : "__SvcInstances"); 4 | private static _serviceClasses: object[] = []; 5 | private static _serviceInstances: object[][][] = []; 6 | 7 | private static GetServiceIdx(serviceClass: new(props: SvcP) => SvcT): number { 8 | let serviceIdx: number; 9 | 10 | if(serviceClass.hasOwnProperty(this._serviceSymbol)) 11 | serviceIdx = serviceClass[this._serviceSymbol]; 12 | else { 13 | serviceIdx = this._serviceClasses.length; 14 | Object.defineProperty(serviceClass, this._serviceSymbol, { 15 | value: serviceIdx, 16 | writable: false 17 | }); 18 | this._serviceClasses.push(serviceClass); 19 | this._serviceInstances.push([]); 20 | } 21 | 22 | return serviceIdx; 23 | } 24 | 25 | private static GetServiceObj(serviceIdx: number, identObj: object): object { 26 | let objListLen = this._serviceInstances[serviceIdx].length; 27 | for(let idx = 0; idx < objListLen; idx++) { 28 | if(this._serviceInstances[serviceIdx][idx][0] === identObj) 29 | return this._serviceInstances[serviceIdx][idx][1]; 30 | } 31 | return null; 32 | } 33 | 34 | private static AddServiceObj(serviceIdx: number, identObj: object, serviceObj: object) { 35 | this._serviceInstances[serviceIdx].push([ 36 | identObj, 37 | serviceObj 38 | ]); 39 | } 40 | 41 | public static GetService(serviceClass: new(props: SvcP) => SvcT, serviceIdent: object = null, serviceProps: SvcP = null): SvcT { 42 | if(!serviceClass) 43 | return null; 44 | 45 | let serviceIdx = this.GetServiceIdx(serviceClass); 46 | let serviceObj = this.GetServiceObj(serviceIdx, serviceIdent) as SvcT; 47 | if(!serviceObj) { 48 | serviceObj = new serviceClass(serviceProps); 49 | this.AddServiceObj(serviceIdx, serviceIdent, serviceObj); 50 | } 51 | 52 | return serviceObj; 53 | } 54 | 55 | public static DisposeAllServices(): Promise { 56 | let promises: Promise[] = []; 57 | this._serviceInstances.forEach((instanceArr) => { 58 | if(instanceArr.length > 0) { 59 | instanceArr.forEach((instance) => { 60 | if(typeof (instance[1] as any).dispose === "function") { 61 | promises.push((instance[1] as any).dispose()); 62 | } 63 | }); 64 | instanceArr.splice(0, instanceArr.length); 65 | } 66 | }); 67 | return Promise.all(promises).then(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/config/ConfigShared.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IFaucetResultSharingConfig { 3 | preHtml: string; 4 | postHtml: string; 5 | caption: string; 6 | [provider: string]: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/config/DefaultConfig.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDbDriver } from "../db/FaucetDatabase.js"; 2 | import { FaucetCoinType } from "../eth/EthWalletManager.js"; 3 | import { IConfigSchema } from "./ConfigSchema.js"; 4 | import { resolveRelativePath } from "./FaucetConfig.js"; 5 | 6 | export function getDefaultConfig(): IConfigSchema { 7 | return { 8 | version: 2, 9 | 10 | appBasePath: null, 11 | faucetVersion: "", 12 | staticPath: resolveRelativePath("~app/static"), 13 | faucetPidFile: null, // path to file to write the process pid to 14 | 15 | buildSeoIndex: true, 16 | buildSeoMeta: {}, 17 | database: { 18 | driver: FaucetDbDriver.SQLITE, 19 | file: resolveRelativePath("faucet-store.db"), 20 | }, 21 | 22 | faucetTitle: "Test Faucet", 23 | faucetImage: "/images/fauceth_420.jpg", 24 | faucetHomeHtml: "", 25 | faucetCoinSymbol: "ETH", 26 | faucetCoinType: FaucetCoinType.NATIVE, 27 | faucetCoinContract: null, 28 | faucetLogFile: null, 29 | faucetLogStatsInterval: 600, 30 | serverPort: 8080, 31 | httpProxyCount: 0, 32 | faucetSecret: null, // mandatory 33 | 34 | ethRpcHost: null, // mandatory 35 | ethWalletKey: null, // mandatory 36 | ethChainId: null, 37 | ethTxGasLimit: 100000, 38 | ethLegacyTx: false, 39 | ethTxMaxFee: 100000000000, 40 | ethTxPrioFee: 2000000000, 41 | ethMaxPending: 20, 42 | ethQueueNoFunds: false, 43 | ethTxExplorerLink: null, 44 | 45 | maxDropAmount: 1000000000000000000, // 1 ETH 46 | minDropAmount: 10000000000000000, // 0.01 ETH 47 | sessionTimeout: 86400, 48 | sessionCleanup: 2592000, 49 | sessionSaveTime: 120, 50 | 51 | modules: {}, 52 | 53 | spareFundsAmount: 10000000000000000, // 0.01 ETH 54 | noFundsBalance: 100000000000000000, // 0.1 ETH 55 | lowFundsBalance: 10000000000000000000, // 10 ETH 56 | lowFundsWarning: true, 57 | noFundsError: true, 58 | rpcConnectionError: true, 59 | denyNewSessions: false, 60 | corsAllowOrigin: [], 61 | ethRefillContract: null, 62 | faucetStats: null, 63 | faucetStatus: { 64 | json: "faucet-status.json", 65 | yaml: "faucet-status.yaml", 66 | }, 67 | resultSharing: { 68 | preHtml: '
Do you like the faucet? Give that project a
', 69 | postHtml: '', 70 | caption: null, 71 | }, 72 | }; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/db/DatabaseWorker.ts: -------------------------------------------------------------------------------- 1 | import { MessagePort } from "worker_threads"; 2 | import assert from 'node:assert'; 3 | import { BaseDriver } from "./driver/BaseDriver.js"; 4 | import { FaucetDatabaseOptions } from "./FaucetDatabase.js"; 5 | import { SQLiteDriver } from "./driver/SQLiteDriver.js"; 6 | import { MySQLDriver } from "./driver/MySQLDriver.js"; 7 | 8 | export class DatabaseWorker { 9 | private port: MessagePort; 10 | private driver: BaseDriver; 11 | 12 | public constructor(port: MessagePort) { 13 | this.port = port; 14 | this.port.on("message", (evt) => this.onControlMessage(evt)); 15 | this.port.postMessage({ cmd: "init" }); 16 | } 17 | 18 | private async onControlMessage(msg: any) { 19 | assert.equal(msg && (typeof msg === "object"), true); 20 | 21 | //console.log(evt); 22 | 23 | let result: any = { 24 | req: msg.req, 25 | }; 26 | try { 27 | switch(msg.cmd) { 28 | case "open": 29 | result.result = await this.onCtrlOpen(msg.args); 30 | break; 31 | case "close": 32 | result.result = await this.driver.close(); 33 | break; 34 | case "exec": 35 | result.result = await this.driver.exec(msg.args[0]); 36 | break; 37 | case "run": 38 | result.result = await this.driver.run(msg.args[0], msg.args[1]); 39 | break; 40 | case "all": 41 | result.result = await this.driver.all(msg.args[0], msg.args[1]); 42 | break; 43 | case "get": 44 | result.result = await this.driver.get(msg.args[0], msg.args[1]); 45 | break; 46 | 47 | } 48 | } 49 | catch(ex) { 50 | result.error = ex; 51 | } 52 | this.port.postMessage(result); 53 | } 54 | 55 | private async onCtrlOpen(args: any[]) { 56 | let driverOpts: FaucetDatabaseOptions = args[0]; 57 | switch(driverOpts.driver) { 58 | case "sqlite": 59 | this.driver = new SQLiteDriver(); 60 | await this.driver.open(driverOpts); 61 | break; 62 | case "mysql": 63 | this.driver = new MySQLDriver(); 64 | await this.driver.open(driverOpts); 65 | break; 66 | default: 67 | throw "unknown database driver: " + (driverOpts as any).driver; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/db/FaucetModuleDB.ts: -------------------------------------------------------------------------------- 1 | import { BaseModule } from "../modules/BaseModule.js"; 2 | import { BaseDriver } from "./driver/BaseDriver.js"; 3 | import { FaucetDatabase } from "./FaucetDatabase.js"; 4 | 5 | export abstract class FaucetModuleDB { 6 | protected abstract readonly latestSchemaVersion: number; 7 | protected module: BaseModule; 8 | protected faucetStore: FaucetDatabase; 9 | 10 | public constructor(module: BaseModule, faucetStore: FaucetDatabase) { 11 | this.module = module; 12 | this.faucetStore = faucetStore; 13 | } 14 | 15 | public dispose() { 16 | this.faucetStore.disposeModuleDb(this); 17 | } 18 | 19 | public getModuleName(): string { 20 | return this.module.getModuleName(); 21 | } 22 | 23 | protected get db(): BaseDriver { 24 | return this.faucetStore.getDatabase(); 25 | } 26 | 27 | protected now(): number { 28 | return Math.floor((new Date()).getTime() / 1000); 29 | } 30 | 31 | public async initSchema(): Promise { 32 | await this.faucetStore.upgradeIfNeeded(this.getModuleName(), this.latestSchemaVersion, (version) => this.upgradeSchema(version)); 33 | } 34 | protected abstract upgradeSchema(version: number): Promise; 35 | 36 | public async cleanStore(): Promise { 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/db/SQL.ts: -------------------------------------------------------------------------------- 1 | import { faucetConfig } from "../config/FaucetConfig.js"; 2 | import { FaucetDbDriver } from "./FaucetDatabase.js"; 3 | 4 | export class SQL { 5 | 6 | public static driverSql(sqlMap: {[driver in FaucetDbDriver]: string}, driver?: FaucetDbDriver): string { 7 | return sqlMap[driver || faucetConfig.database.driver]; 8 | } 9 | 10 | public static field(name: string, driver?: FaucetDbDriver): string { 11 | switch(driver || faucetConfig.database.driver) { 12 | case FaucetDbDriver.MYSQL: return "`" + name + "`"; 13 | default: return name; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/db/driver/BaseDriver.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface RunResult { 3 | changes: number; 4 | lastInsertRowid: number | bigint; 5 | } 6 | 7 | export type SQLValue = number | bigint | string | Uint8Array | null; 8 | export type JSValue = boolean | SQLValue; 9 | 10 | export type BindValues = JSValue | JSValue[] | Record; 11 | 12 | export type NormalQueryResult = Record; 13 | export type ExpandedQueryResult = Record; 14 | export type QueryResult = NormalQueryResult | ExpandedQueryResult; 15 | 16 | export abstract class BaseDriver { 17 | public abstract open(options: TDriverOpts): Promise; 18 | public abstract close(): Promise; 19 | 20 | public abstract exec(sql: string): Promise; 21 | public abstract run(sql: string, values?: BindValues): Promise; 22 | public abstract all(sql: string, values?: BindValues): Promise; 23 | public abstract get(sql: string, values?: BindValues): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /src/db/driver/SQLiteDriver.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDbDriver } from "../FaucetDatabase.js"; 2 | import { BaseDriver, BindValues, QueryResult, RunResult } from "./BaseDriver.js"; 3 | 4 | export interface ISQLiteOptions { 5 | driver: FaucetDbDriver.SQLITE; 6 | 7 | file: string; 8 | } 9 | 10 | export class SQLiteDriver extends BaseDriver { 11 | private static sqlite: Promise; 12 | 13 | private static loadSQLite(): Promise { 14 | if(!this.sqlite) { 15 | this.sqlite = import("../../../libs/sqlite3_wasm.cjs"); 16 | } 17 | return this.sqlite; 18 | } 19 | 20 | private db: any; 21 | 22 | public override async open(options: ISQLiteOptions): Promise { 23 | let sqlite = await SQLiteDriver.loadSQLite(); 24 | this.db = new sqlite.default.Database(options.file); 25 | } 26 | public override async close(): Promise { 27 | this.db.close(); 28 | } 29 | 30 | public override async exec(sql: string): Promise { 31 | try { 32 | this.db.exec(sql); 33 | } catch(ex) { 34 | return Promise.reject("sqlite exec() error: " + sql + " [] " + ex.toString()); 35 | } 36 | } 37 | 38 | public override async run(sql: string, values?: BindValues): Promise { 39 | try { 40 | return this.db.run(sql, values); 41 | } catch(ex) { 42 | return Promise.reject("sqlite run() error: " + sql + " [] " + ex.toString()); 43 | } 44 | } 45 | 46 | public override async all(sql: string, values?: BindValues): Promise { 47 | try { 48 | return this.db.all(sql, values); 49 | } catch(ex) { 50 | return Promise.reject("sqlite all() error: " + sql + " [] " + ex.toString()); 51 | } 52 | } 53 | 54 | public override async get(sql: string, values?: BindValues): Promise { 55 | try { 56 | return this.db.get(sql, values); 57 | } catch(ex) { 58 | return Promise.reject("sqlite get() error: " + sql + " [] " + ex.toString()); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/db/driver/WorkerDriver.ts: -------------------------------------------------------------------------------- 1 | import { BaseDriver, BindValues, QueryResult, RunResult } from "./BaseDriver.js"; 2 | import { Worker } from "worker_threads"; 3 | import { PromiseDfd } from "../../utils/PromiseDfd.js"; 4 | 5 | export interface IWorkerOptions { 6 | port: Worker; 7 | } 8 | 9 | export class WorkerDriver extends BaseDriver { 10 | private port: Worker; 11 | private reqDict: {[idx: number]: PromiseDfd} = {}; 12 | private reqIdx = 1; 13 | private readyDfd: PromiseDfd; 14 | 15 | public constructor(port: Worker) { 16 | super(); 17 | this.port = port; 18 | this.readyDfd = new PromiseDfd(); 19 | this.port.on("message", (msg) => this.onWorkerMessage(msg)); 20 | } 21 | 22 | private sendRequest(cmd: string, args: any[]): Promise { 23 | return this.readyDfd.promise.then(() => { 24 | let reqIdx = this.reqIdx++; 25 | let resDfd = this.reqDict[reqIdx] = new PromiseDfd(); 26 | this.port.postMessage({ 27 | req: reqIdx, 28 | cmd: cmd, 29 | args: args, 30 | }); 31 | return resDfd.promise; 32 | }); 33 | } 34 | 35 | private onWorkerMessage(msg: any) { 36 | if(msg.cmd === "init") { 37 | this.readyDfd.resolve(); 38 | return; 39 | } 40 | if(!msg.req) 41 | return; 42 | let reqDfd = this.reqDict[msg.req]; 43 | if(!reqDfd) 44 | return; 45 | delete this.reqDict[msg.req]; 46 | if(msg.hasOwnProperty("result")) 47 | reqDfd.resolve(msg.result); 48 | else 49 | reqDfd.reject(msg.error); 50 | } 51 | 52 | public override async open(options: IWorkerOptions): Promise { 53 | return this.sendRequest("open", [options]); 54 | } 55 | 56 | public override async close(): Promise { 57 | return this.sendRequest("close", []); 58 | } 59 | 60 | public override async exec(sql: string): Promise { 61 | return this.sendRequest("exec", [sql]); 62 | } 63 | 64 | public override async run(sql: string, values?: BindValues): Promise { 65 | return this.sendRequest("run", [sql, values]); 66 | } 67 | 68 | public override async all(sql: string, values?: BindValues): Promise { 69 | return this.sendRequest("all", [sql, values]); 70 | } 71 | 72 | public override async get(sql: string, values?: BindValues): Promise { 73 | return this.sendRequest("get", [sql, values]); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/eth/EthClaimNotificationClient.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'ws'; 2 | import { FaucetLogLevel, FaucetProcess } from "../common/FaucetProcess.js"; 3 | import { ServiceManager } from "../common/ServiceManager.js"; 4 | 5 | export interface IEthClaimNotificationData { 6 | processedIdx: number; 7 | confirmedIdx: number; 8 | } 9 | 10 | export class EthClaimNotificationClient { 11 | public static cfgPingInterval = 30; 12 | public static cfgPingTimeout = 120; 13 | 14 | private static activeClients: EthClaimNotificationClient[] = []; 15 | private static lastNotificationData: IEthClaimNotificationData; 16 | 17 | public static broadcastClaimNotification(data: IEthClaimNotificationData) { 18 | this.lastNotificationData = data; 19 | for(let i = this.activeClients.length - 1; i >= 0; i--) { 20 | this.activeClients[i].sendClaimNotification(data); 21 | } 22 | } 23 | 24 | public static resetClaimNotification() { 25 | this.lastNotificationData = null; 26 | } 27 | 28 | private socket: WebSocket; 29 | private pingTimer: NodeJS.Timeout = null; 30 | private lastPingPong: Date; 31 | private claimIdx: number; 32 | 33 | public constructor(socket: WebSocket, claimIdx: number) { 34 | this.socket = socket; 35 | this.claimIdx = claimIdx; 36 | this.lastPingPong = new Date(); 37 | 38 | this.socket.on("ping", (data) => { 39 | this.lastPingPong = new Date(); 40 | this.socket?.pong(data) 41 | }); 42 | this.socket.on("pong", (data) => { 43 | this.lastPingPong = new Date(); 44 | }); 45 | this.socket.on("error", (err) => { 46 | ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.WARNING, "WebSocket error: " + err.toString()); 47 | try { 48 | this.socket?.close(); 49 | } catch(ex) {} 50 | this.dispose(); 51 | }); 52 | this.socket.on("close", () => { 53 | this.dispose(); 54 | }); 55 | this.pingClientLoop(); 56 | EthClaimNotificationClient.activeClients.push(this); 57 | 58 | if(EthClaimNotificationClient.lastNotificationData) { 59 | this.sendClaimNotification(EthClaimNotificationClient.lastNotificationData); 60 | } 61 | } 62 | 63 | private dispose() { 64 | this.socket = null; 65 | 66 | if(this.pingTimer) { 67 | clearInterval(this.pingTimer); 68 | this.pingTimer = null; 69 | } 70 | 71 | let clientIdx = EthClaimNotificationClient.activeClients.indexOf(this); 72 | if(clientIdx !== -1) { 73 | EthClaimNotificationClient.activeClients.splice(clientIdx, 1); 74 | } 75 | } 76 | 77 | public killClient(reason?: string) { 78 | try { 79 | this.sendMessage("error", { 80 | reason: reason, 81 | }); 82 | this.socket?.close(); 83 | } catch(ex) {} 84 | this.dispose(); 85 | } 86 | 87 | private pingClientLoop() { 88 | this.pingTimer = setInterval(() => { 89 | let pingpongTime = Math.floor(((new Date()).getTime() - this.lastPingPong.getTime()) / 1000); 90 | if(pingpongTime > EthClaimNotificationClient.cfgPingTimeout) { 91 | this.killClient("ping timeout"); 92 | return; 93 | } 94 | 95 | this.socket?.ping(); 96 | }, EthClaimNotificationClient.cfgPingInterval * 1000); 97 | } 98 | 99 | private sendMessage(action: string, data: any) { 100 | this.socket?.send(JSON.stringify({ 101 | action: action, 102 | data: data, 103 | })); 104 | } 105 | 106 | private sendClaimNotification(data: IEthClaimNotificationData) { 107 | this.sendMessage("update", data); 108 | if(data.confirmedIdx >= this.claimIdx) { 109 | this.killClient("claim confirmed"); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/modules/BaseModule.ts: -------------------------------------------------------------------------------- 1 | import { ModuleManager } from "./ModuleManager.js"; 2 | 3 | export interface IBaseModuleConfig { 4 | enabled: boolean; 5 | } 6 | 7 | export abstract class BaseModule { 8 | protected abstract readonly moduleDefaultConfig: TModCfg; 9 | protected moduleManager: ModuleManager; 10 | protected moduleName: string; 11 | protected moduleConfig: TModCfg 12 | protected enabled: boolean; 13 | 14 | public constructor(manager: ModuleManager, name: string) { 15 | this.moduleManager = manager; 16 | this.moduleName = name; 17 | } 18 | 19 | public getModuleName(): string { 20 | return this.moduleName; 21 | } 22 | 23 | public enableModule(): Promise { 24 | if(this.enabled) 25 | return Promise.reject("cannot enable module '" + this.moduleName + "': already enabled"); 26 | this.enabled = true; 27 | return this.startModule(); 28 | } 29 | 30 | public disableModule(): Promise { 31 | if(!this.enabled) 32 | return Promise.reject("cannot disable module '" + this.moduleName + "': not enabled"); 33 | this.enabled = false; 34 | return this.stopModule(); 35 | } 36 | 37 | public getModuleConfig(): TModCfg { 38 | return this.moduleConfig; 39 | } 40 | 41 | public setModuleConfig(config: TModCfg): void { 42 | this.moduleConfig = Object.assign({}, this.moduleDefaultConfig, config); 43 | if(this.enabled) 44 | this.onConfigReload(); 45 | } 46 | 47 | public isEnabled(): boolean { 48 | return this.enabled; 49 | } 50 | 51 | protected abstract startModule(): Promise; 52 | protected abstract stopModule(): Promise; 53 | protected onConfigReload(): void {}; 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/MODULE_HOOKS.md: -------------------------------------------------------------------------------- 1 | 2 | ClientConfig 3 | prio 1: captcha, ensname, github, passport, pow, zupass 4 | 5 | SessionStart 6 | prio 1: captcha, *maintenance_mode_check 7 | prio 2: whitelist, zupass 8 | prio 3: ensname 9 | prio 5: *eth_address_check 10 | prio 6: passport 11 | prio 7: concurrency-limit, ethinfo, ipinfo, mainnet-wallet, recurring-limits 12 | prio 10: pow 13 | 14 | SessionRestore 15 | prio 10: pow 16 | 17 | SessionInfo 18 | prio 1: passport, pow 19 | 20 | SessionRewardFactor 21 | prio 5: faucet-outflow, github, passport 22 | prio 6: faucet-balance, ipinfo, whitelist 23 | 24 | SessionRewarded 25 | prio 5: faucet-outflow 26 | 27 | SessionIpChange 28 | prio 2: whitelist 29 | prio 6: concurrency-limit, ipinfo 30 | 31 | SessionComplete 32 | prio 5: github 33 | prio 10: pow 34 | 35 | SessionClaim 36 | prio 1: captcha 37 | 38 | SessionClaimed 39 | 40 | SessionClose 41 | 42 | -------------------------------------------------------------------------------- /src/modules/captcha/CaptchaConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface ICaptchaConfig extends IBaseModuleConfig { 4 | provider: "hcaptcha"|"recaptcha"|"turnstile"|"custom"; 5 | siteKey: string; // site key 6 | secret: string; // secret key 7 | checkSessionStart: boolean; // require captcha to start a new mining session 8 | checkBalanceClaim: boolean; // require captcha to claim mining rewards 9 | } 10 | 11 | export const defaultConfig: ICaptchaConfig = { 12 | enabled: false, 13 | provider: null, 14 | siteKey: null, 15 | secret: null, 16 | checkSessionStart: false, 17 | checkBalanceClaim: false, 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/concurrency-limit/ConcurrencyLimitConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IConcurrencyLimitConfig extends IBaseModuleConfig { 4 | concurrencyLimit: number; 5 | byAddrOnly: boolean; 6 | byIPOnly: boolean; 7 | messageByAddr: string; 8 | messageByIP: string; 9 | } 10 | 11 | export const defaultConfig: IConcurrencyLimitConfig = { 12 | enabled: false, 13 | concurrencyLimit: 0, 14 | byAddrOnly: false, 15 | byIPOnly: false, 16 | messageByAddr: null, 17 | messageByIP: null, 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/concurrency-limit/ConcurrencyLimitModule.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from "../../common/ServiceManager.js"; 2 | import { FaucetSession } from "../../session/FaucetSession.js"; 3 | import { BaseModule } from "../BaseModule.js"; 4 | import { ModuleHookAction } from "../ModuleManager.js"; 5 | import { defaultConfig, IConcurrencyLimitConfig } from './ConcurrencyLimitConfig.js'; 6 | import { FaucetError } from '../../common/FaucetError.js'; 7 | import { SessionManager } from "../../session/SessionManager.js"; 8 | 9 | export class ConcurrencyLimitModule extends BaseModule { 10 | protected readonly moduleDefaultConfig = defaultConfig; 11 | 12 | protected override startModule(): Promise { 13 | this.moduleManager.addActionHook( 14 | this, ModuleHookAction.SessionStart, 7, "Recurring limits check", 15 | (session: FaucetSession) => this.processSessionStart(session) 16 | ); 17 | this.moduleManager.addActionHook( 18 | this, ModuleHookAction.SessionIpChange, 6, "Recurring limits check", 19 | (session: FaucetSession) => this.processSessionStart(session) 20 | ); 21 | return Promise.resolve(); 22 | } 23 | 24 | protected override stopModule(): Promise { 25 | return Promise.resolve(); 26 | } 27 | 28 | private async processSessionStart(session: FaucetSession): Promise { 29 | if(session.getSessionData>("skip.modules", []).indexOf(this.moduleName) !== -1) 30 | return; 31 | this.checkLimit(session); 32 | } 33 | 34 | private checkLimit(session: FaucetSession): void { 35 | if(this.moduleConfig.concurrencyLimit === 0) 36 | return; 37 | 38 | let activeSessions = ServiceManager.GetService(SessionManager).getActiveSessions(); 39 | let concurrentSessionCount = 0; 40 | let concurrentLimitMessage: string = null; 41 | 42 | if(!this.moduleConfig.byAddrOnly) { 43 | let sessCount = activeSessions.filter((sess) => sess !== session && sess.getRemoteIP() === session.getRemoteIP()).length; 44 | if(sessCount > concurrentSessionCount) { 45 | concurrentSessionCount = sessCount; 46 | concurrentLimitMessage = this.moduleConfig.messageByIP || ("Only " + this.moduleConfig.concurrencyLimit + " concurrent sessions allowed per IP"); 47 | } 48 | } 49 | if(!this.moduleConfig.byIPOnly) { 50 | let sessCount = activeSessions.filter((sess) => sess !== session && sess.getTargetAddr() === session.getTargetAddr()).length; 51 | if(sessCount > concurrentSessionCount) { 52 | concurrentSessionCount = sessCount; 53 | concurrentLimitMessage = this.moduleConfig.messageByAddr || ("Only " + this.moduleConfig.concurrencyLimit + " concurrent sessions allowed per wallet address"); 54 | } 55 | } 56 | 57 | if(concurrentSessionCount >= this.moduleConfig.concurrencyLimit) { 58 | throw new FaucetError( 59 | "CONCURRENCY_LIMIT", 60 | concurrentLimitMessage, 61 | ); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/modules/ensname/EnsNameConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IEnsNameConfig extends IBaseModuleConfig { 4 | rpcHost: string; // ETH execution layer RPC host for ENS resolver 5 | ensAddr: string | null; // ENS Resolver contract address or null for default resolver 6 | required: boolean; 7 | } 8 | 9 | export const defaultConfig: IEnsNameConfig = { 10 | enabled: false, 11 | rpcHost: null, 12 | ensAddr: null, 13 | required: false, 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/ensname/EnsNameModule.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { ENS } from 'web3-eth-ens'; 3 | import { FaucetSession } from "../../session/FaucetSession.js"; 4 | import { BaseModule } from "../BaseModule.js"; 5 | import { ModuleHookAction } from "../ModuleManager.js"; 6 | import { defaultConfig, IEnsNameConfig } from './EnsNameConfig.js'; 7 | import { FaucetError } from '../../common/FaucetError.js'; 8 | import { EthWalletManager } from '../../eth/EthWalletManager.js'; 9 | 10 | export class EnsNameModule extends BaseModule { 11 | protected readonly moduleDefaultConfig = defaultConfig; 12 | private ens: ENS; 13 | private web3: Web3; 14 | 15 | protected override startModule(): Promise { 16 | this.initEnsResolver(); 17 | this.moduleManager.addActionHook( 18 | this, ModuleHookAction.ClientConfig, 1, "ens config", 19 | async (clientConfig: any) => { 20 | clientConfig[this.moduleName] = { 21 | required: !!this.moduleConfig.required, 22 | }; 23 | } 24 | ); 25 | this.moduleManager.addActionHook(this, ModuleHookAction.SessionStart, 3, "resolve ens name", (session: FaucetSession, userInput: any) => this.processSessionStart(session, userInput)); 26 | return Promise.resolve(); 27 | } 28 | 29 | protected override stopModule(): Promise { 30 | // nothing to do 31 | return Promise.resolve(); 32 | } 33 | 34 | protected override onConfigReload(): void { 35 | this.initEnsResolver(); 36 | } 37 | 38 | private initEnsResolver() { 39 | let provider = EthWalletManager.getWeb3Provider(this.moduleConfig.rpcHost); 40 | this.ens = new ENS(this.moduleConfig.ensAddr || "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", provider); 41 | } 42 | 43 | private async processSessionStart(session: FaucetSession, userInput: any): Promise { 44 | let targetAddr: string = userInput.addr; 45 | let isEnsName = false; 46 | if(typeof targetAddr === "string" && targetAddr.match(/^[-a-zA-Z0-9@:%._\+~#=]{1,256}\.eth$/)) { 47 | try { 48 | targetAddr = (await this.ens.getAddress(targetAddr)) as string; 49 | session.setTargetAddr(targetAddr); 50 | isEnsName = true; 51 | } catch(ex) { 52 | throw new FaucetError("INVALID_ENSNAME", "Could not resolve ENS Name '" + targetAddr + "': " + ex.toString()); 53 | } 54 | } 55 | 56 | if(this.moduleConfig.required && !isEnsName) { 57 | throw new FaucetError("REQUIRE_ENSNAME", "Only ENS Names allowed."); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/ethinfo/EthInfoConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IEthInfoConfig extends IBaseModuleConfig { 4 | maxBalance: number; 5 | denyContract: boolean; 6 | } 7 | 8 | export const defaultConfig: IEthInfoConfig = { 9 | enabled: false, 10 | maxBalance: 0, 11 | denyContract: false, 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/ethinfo/EthInfoModule.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from "../../common/ServiceManager.js"; 2 | import { EthWalletManager } from "../../eth/EthWalletManager.js"; 3 | import { FaucetSession } from "../../session/FaucetSession.js"; 4 | import { BaseModule } from "../BaseModule.js"; 5 | import { ModuleHookAction } from "../ModuleManager.js"; 6 | import { defaultConfig, IEthInfoConfig } from './EthInfoConfig.js'; 7 | import { FaucetError } from '../../common/FaucetError.js'; 8 | 9 | export class EthInfoModule extends BaseModule { 10 | protected readonly moduleDefaultConfig = defaultConfig; 11 | 12 | protected override startModule(): Promise { 13 | this.moduleManager.addActionHook(this, ModuleHookAction.SessionStart, 7, "ETH Info check", (session: FaucetSession, userInput: any) => this.processSessionStart(session, userInput)); 14 | return Promise.resolve(); 15 | } 16 | 17 | protected override stopModule(): Promise { 18 | // nothing to do 19 | return Promise.resolve(); 20 | } 21 | 22 | private async processSessionStart(session: FaucetSession, userInput: any): Promise { 23 | if(session.getSessionData>("skip.modules", []).indexOf(this.moduleName) !== -1) 24 | return; 25 | 26 | let targetAddr = session.getTargetAddr(); 27 | let ethWalletManager = ServiceManager.GetService(EthWalletManager); 28 | 29 | if(this.moduleConfig.maxBalance && this.moduleConfig.maxBalance > 0) { 30 | let walletBalance: bigint; 31 | try { 32 | walletBalance = await ServiceManager.GetService(EthWalletManager).getWalletBalance(targetAddr); 33 | } catch(ex) { 34 | throw new FaucetError("BALANCE_ERROR", "Could not get balance of Wallet " + targetAddr + ": " + ex.toString()); 35 | } 36 | if(walletBalance > this.moduleConfig.maxBalance) 37 | throw new FaucetError("BALANCE_LIMIT", "You're already holding " + ServiceManager.GetService(EthWalletManager).readableAmount(walletBalance) + " in your wallet. Please give others a chance to get some funds too."); 38 | } 39 | 40 | if(this.moduleConfig.denyContract) { 41 | try { 42 | if(await ethWalletManager.checkIsContract(targetAddr)) { 43 | throw new FaucetError("CONTRACT_ADDR", "Cannot start session for " + targetAddr + " (address is a contract)"); 44 | } 45 | } catch(ex) { 46 | if(!(ex instanceof FaucetError)) 47 | ex = new FaucetError("CONTRACT_LIMIT", "Could not check contract status of wallet " + targetAddr + ": " + ex.toString()); 48 | throw ex; 49 | } 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/faucet-balance/FaucetBalanceConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IFaucetBalanceConfig extends IBaseModuleConfig { 4 | fixedRestriction?: { 5 | [limit: number]: number; // limit: min balance in wei, value: percent of normal reward (eg. 50 = half rewards) 6 | }; 7 | dynamicRestriction?: { 8 | targetBalance: number; 9 | } 10 | } 11 | 12 | export const defaultConfig: IFaucetBalanceConfig = { 13 | enabled: false, 14 | fixedRestriction: null, 15 | dynamicRestriction: null, 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/faucet-outflow/FaucetOutflowConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IFaucetOutflowConfig extends IBaseModuleConfig { 4 | amount: number; 5 | duration: number; 6 | lowerLimit: number; 7 | upperLimit: number; 8 | } 9 | 10 | export const defaultConfig: IFaucetOutflowConfig = { 11 | enabled: false, 12 | amount: 0, 13 | duration: 86400, 14 | lowerLimit: 0, 15 | upperLimit: 0, 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/github/GithubConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IGithubConfig extends IBaseModuleConfig { 4 | appClientId: string; 5 | appSecret: string; 6 | callbackState: string; 7 | redirectUrl: string; 8 | authTimeout: number; 9 | cacheTime: number; 10 | checks: IGithubCheckConfig[]; 11 | restrictions: IGithubRestrictionConfig[]; 12 | } 13 | 14 | export interface IGithubCheckConfig { 15 | minAccountAge?: number; 16 | minRepoCount?: number; 17 | minFollowers?: number; 18 | minOwnRepoCount?: number; 19 | minOwnRepoStars?: number; 20 | required?: boolean; 21 | message?: string; 22 | rewardFactor?: number; 23 | } 24 | 25 | export interface IGithubRestrictionConfig { 26 | limitCount: number; 27 | limitAmount: number; 28 | duration: number; 29 | message?: string; 30 | } 31 | 32 | export const defaultConfig: IGithubConfig = { 33 | enabled: false, 34 | appClientId: null, 35 | appSecret: null, 36 | callbackState: null, 37 | redirectUrl: null, 38 | authTimeout: 86400, 39 | cacheTime: 86400, 40 | checks: [], 41 | restrictions: [], 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/ipinfo/IPInfoConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IIPInfoConfig extends IBaseModuleConfig { 4 | apiUrl: string; // ip info lookup api url (defaults: http://ip-api.com/json/{ip}?fields=21155839) 5 | cacheTime: number; // ip info caching time 6 | required: boolean; // require valid ip info for session start / resume / recovery 7 | restrictions: null | { // ip based restrictions 8 | hosting?: number | IIPInfoRestrictionConfig; // percentage of reward per share if IP is in a hosting range 9 | proxy?: number | IIPInfoRestrictionConfig; // percentage of reward per share if IP is in a proxy range 10 | [country: string]: number | IIPInfoRestrictionConfig; // percentage of reward per share if IP is from given country code (DE/US/...) 11 | }; 12 | restrictionsPattern: null | { // ip info pattern based restrictions 13 | [pattern: string]: number | IIPInfoRestrictionConfig; // percentage of reward per share if IP info matches regex pattern 14 | }; 15 | restrictionsFile: null | { // ip info pattern based restrictions from file 16 | file?: string; // path to file 17 | yaml?: string|string[]; // path to yaml file (for more actions/kill messages/etc.) 18 | refresh: number; // refresh interval 19 | }; 20 | } 21 | 22 | export interface IIPInfoRestrictionConfig { 23 | reward: number; 24 | msgkey?: string; 25 | message?: string; 26 | notify?: boolean|string; 27 | blocked?: boolean|"close"|"kill"; 28 | } 29 | 30 | export const defaultConfig: IIPInfoConfig = { 31 | enabled: false, 32 | apiUrl: "http://ip-api.com/json/{ip}?fields=21155839", 33 | cacheTime: 86400, 34 | required: false, 35 | restrictions: null, 36 | restrictionsPattern: {}, 37 | restrictionsFile: null, 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/ipinfo/IPInfoDB.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDbDriver } from '../../db/FaucetDatabase.js'; 2 | import { FaucetModuleDB } from '../../db/FaucetModuleDB.js'; 3 | import { SQL } from '../../db/SQL.js'; 4 | import { IIPInfo } from './IPInfoResolver.js'; 5 | 6 | export class IPInfoDB extends FaucetModuleDB { 7 | protected override latestSchemaVersion = 1; 8 | 9 | protected override async upgradeSchema(version: number): Promise { 10 | switch(version) { 11 | case 0: 12 | version = 1; 13 | await this.db.exec(SQL.driverSql({ 14 | [FaucetDbDriver.SQLITE]: ` 15 | CREATE TABLE "IPInfoCache" ( 16 | "IP" TEXT NOT NULL UNIQUE, 17 | "Json" TEXT NOT NULL, 18 | "Timeout" INTEGER NOT NULL, 19 | PRIMARY KEY("IP") 20 | );`, 21 | [FaucetDbDriver.MYSQL]: ` 22 | CREATE TABLE IPInfoCache ( 23 | IP VARCHAR(40) NOT NULL, 24 | Json TEXT NOT NULL, 25 | Timeout INT(11) NOT NULL, 26 | PRIMARY KEY(IP) 27 | );`, 28 | })); 29 | await this.db.exec(SQL.driverSql({ 30 | [FaucetDbDriver.SQLITE]: `CREATE INDEX "IPInfoCacheTimeIdx" ON "IPInfoCache" ("Timeout" ASC);`, 31 | [FaucetDbDriver.MYSQL]: `ALTER TABLE IPInfoCache ADD INDEX IPInfoCacheTimeIdx (Timeout);`, 32 | })); 33 | } 34 | return version; 35 | } 36 | 37 | public override async cleanStore(): Promise { 38 | await this.db.run("DELETE FROM IPInfoCache WHERE Timeout < ?", [this.now()]); 39 | } 40 | 41 | public async getIPInfo(ip: string): Promise { 42 | let row = await this.db.get( 43 | "SELECT Json FROM IPInfoCache WHERE IP = ? AND Timeout > ?", 44 | [ip.toLowerCase(), this.now()] 45 | ) as {Json: string}; 46 | if(!row) 47 | return null; 48 | return JSON.parse(row.Json); 49 | } 50 | 51 | public async setIPInfo(ip: string, info: IIPInfo, duration?: number): Promise { 52 | let now = this.now(); 53 | let row = await this.db.get("SELECT Timeout FROM IPInfoCache WHERE IP = ?", [ip.toLowerCase()]); 54 | 55 | let timeout = now + (typeof duration === "number" ? duration : 86400); 56 | let infoJson = JSON.stringify(info); 57 | 58 | if(row) { 59 | await this.db.run("UPDATE IPInfoCache SET Json = ?, Timeout = ? WHERE IP = ?", [infoJson, timeout, ip.toLowerCase()]); 60 | } else { 61 | await this.db.run("INSERT INTO IPInfoCache (IP, Json, Timeout) VALUES (?, ?, ?)", [ip.toLowerCase(), infoJson, timeout]); 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/modules/ipinfo/IPInfoResolver.ts: -------------------------------------------------------------------------------- 1 | import { FetchUtil } from '../../utils/FetchUtil.js'; 2 | import { IPInfoDB } from './IPInfoDB.js'; 3 | 4 | 5 | export interface IIPInfo { 6 | status: string; 7 | country?: string; 8 | countryCode?: string; 9 | region?: string; 10 | regionCode?: string; 11 | city?: string; 12 | cityCode?: string; 13 | locLat?: number; 14 | locLon?: number; 15 | zone?: string; 16 | isp?: string; 17 | org?: string; 18 | as?: string; 19 | proxy?: boolean; 20 | hosting?: boolean; 21 | } 22 | 23 | export class IPInfoResolver { 24 | private ipInfoDb: IPInfoDB; 25 | private ipInfoApi: string; 26 | private ipInfoCache: {[ip: string]: [number, Promise]} = {}; 27 | private ipInfoCacheCleanupTimer: NodeJS.Timeout; 28 | private ipInfoCacheCleanupInterval: number = 20 * 1000; 29 | private ipInfoCacheCleanupTimeout: number = 6 * 60 * 60; 30 | 31 | public constructor(ipInfoDb: IPInfoDB, api: string) { 32 | this.ipInfoDb = ipInfoDb; 33 | this.ipInfoApi = api; 34 | this.ipInfoCacheCleanupTimer = setInterval(() => { 35 | this.cleanIpInfoCache(); 36 | }, this.ipInfoCacheCleanupInterval); 37 | } 38 | 39 | public dispose() { 40 | clearInterval(this.ipInfoCacheCleanupTimer); 41 | } 42 | 43 | public setApi(api: string) { 44 | this.ipInfoApi = api; 45 | } 46 | 47 | public async getIpInfo(ipAddr: string): Promise { 48 | let cachedIpInfo = await this.ipInfoDb.getIPInfo(ipAddr); 49 | if(cachedIpInfo) 50 | return cachedIpInfo; 51 | if(this.ipInfoCache.hasOwnProperty(ipAddr)) 52 | return await this.ipInfoCache[ipAddr][1]; 53 | 54 | let ipApiUrl = this.ipInfoApi.replace(/{ip}/, ipAddr); 55 | let promise = FetchUtil.fetchWithTimeout(ipApiUrl, null, 20000) 56 | .then((rsp) => rsp.json()) 57 | .then((rsp: any) => { 58 | if(!rsp || !rsp.status) 59 | throw "invalid ip info response"; 60 | let ipInfo: IIPInfo = { 61 | status: rsp.status, 62 | }; 63 | if(rsp.status === "success") { 64 | ipInfo.country = rsp.country; 65 | ipInfo.countryCode = rsp.countryCode; 66 | ipInfo.region = rsp.regionName; 67 | ipInfo.regionCode = rsp.region; 68 | ipInfo.city = rsp.city; 69 | ipInfo.cityCode = rsp.zip; 70 | ipInfo.locLat = rsp.lat; 71 | ipInfo.locLon = rsp.lon; 72 | ipInfo.zone = rsp.timezone; 73 | ipInfo.isp = rsp.isp; 74 | ipInfo.org = rsp.org; 75 | ipInfo.as = rsp.as; 76 | ipInfo.proxy = rsp.proxy; 77 | ipInfo.hosting = rsp.hosting; 78 | 79 | this.ipInfoDb.setIPInfo(ipAddr, ipInfo); 80 | } 81 | return ipInfo; 82 | }, (err) => { 83 | return { 84 | status: "error" + (err ? ": " + err.toString() : ""), 85 | }; 86 | }); 87 | 88 | this.ipInfoCache[ipAddr] = [ 89 | Math.floor((new Date()).getTime() / 1000), 90 | promise, 91 | ]; 92 | return await promise; 93 | } 94 | 95 | private cleanIpInfoCache() { 96 | let now = Math.floor((new Date()).getTime() / 1000); 97 | Object.keys(this.ipInfoCache).forEach((ipAddr) => { 98 | if(now - this.ipInfoCache[ipAddr][0] > this.ipInfoCacheCleanupTimeout) { 99 | delete this.ipInfoCache[ipAddr]; 100 | } 101 | }); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/mainnet-wallet/MainnetWalletConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IMainnetWalletConfig extends IBaseModuleConfig { 4 | rpcHost: string; 5 | minTxCount: number; 6 | minBalance: number; 7 | minErc20Balances: { 8 | name: string; 9 | address: string; 10 | decimals?: number; 11 | minBalance: number; 12 | }[] 13 | } 14 | 15 | export const defaultConfig: IMainnetWalletConfig = { 16 | enabled: false, 17 | rpcHost: null, 18 | minTxCount: 0, 19 | minBalance: 0, 20 | minErc20Balances: [], 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/modules.ts: -------------------------------------------------------------------------------- 1 | import { CaptchaModule } from "./captcha/CaptchaModule.js"; 2 | import { ConcurrencyLimitModule } from "./concurrency-limit/ConcurrencyLimitModule.js"; 3 | import { EnsNameModule } from "./ensname/EnsNameModule.js"; 4 | import { EthInfoModule } from "./ethinfo/EthInfoModule.js"; 5 | import { FaucetBalanceModule } from "./faucet-balance/FaucetBalanceModule.js"; 6 | import { FaucetOutflowModule } from "./faucet-outflow/FaucetOutflowModule.js"; 7 | import { GithubModule } from "./github/GithubModule.js"; 8 | import { IPInfoModule } from "./ipinfo/IPInfoModule.js"; 9 | import { MainnetWalletModule } from "./mainnet-wallet/MainnetWalletModule.js"; 10 | import { PassportModule } from "./passport/PassportModule.js"; 11 | import { PoWModule } from "./pow/PoWModule.js"; 12 | import { RecurringLimitsModule } from "./recurring-limits/RecurringLimitsModule.js"; 13 | import { VoucherModule } from "./voucher/VoucherModule.js"; 14 | import { WhitelistModule } from "./whitelist/WhitelistModule.js"; 15 | import { ZupassModule } from "./zupass/ZupassModule.js"; 16 | 17 | export const MODULE_CLASSES = { 18 | "captcha": CaptchaModule, 19 | "concurrency-limit": ConcurrencyLimitModule, 20 | "ensname": EnsNameModule, 21 | "ethinfo": EthInfoModule, 22 | "faucet-balance": FaucetBalanceModule, 23 | "faucet-outflow": FaucetOutflowModule, 24 | "github": GithubModule, 25 | "ipinfo": IPInfoModule, 26 | "mainnet-wallet": MainnetWalletModule, 27 | "passport": PassportModule, 28 | "pow": PoWModule, 29 | "recurring-limits": RecurringLimitsModule, 30 | "voucher": VoucherModule, 31 | "whitelist": WhitelistModule, 32 | "zupass": ZupassModule, 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/passport/PassportConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IPassportConfig extends IBaseModuleConfig { 4 | scorerApiKey: string; 5 | cachePath: string; 6 | cacheTime: number; 7 | trustedIssuers: string[]; 8 | refreshCooldown: number; 9 | stampDeduplicationTime: number; 10 | stampScoring: {[stamp: string]: number}; 11 | boostFactor: {[score: number]: number}; 12 | requireMinScore: number; 13 | skipProxyCheckScore: number; 14 | skipHostingCheckScore: number; 15 | allowGuestRefresh: boolean; 16 | guestRefreshCooldown: number; 17 | } 18 | 19 | export const defaultConfig: IPassportConfig = { 20 | enabled: false, 21 | scorerApiKey: null, 22 | cachePath: null, 23 | cacheTime: 86400, 24 | trustedIssuers: [ "did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC" ], 25 | refreshCooldown: 300, 26 | stampDeduplicationTime: 86400 * 3, 27 | stampScoring: {}, 28 | boostFactor: {}, 29 | requireMinScore: 0, 30 | skipProxyCheckScore: 0, 31 | skipHostingCheckScore: 0, 32 | allowGuestRefresh: false, 33 | guestRefreshCooldown: 0, 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/pow/validator/IPoWValidator.ts: -------------------------------------------------------------------------------- 1 | import { PoWCryptoParams, PoWHashAlgo } from "../PoWConfig.js"; 2 | 3 | export interface IPoWValidatorValidateRequest { 4 | shareId: string; 5 | nonce: number; 6 | data: string; 7 | preimage: string; 8 | algo: PoWHashAlgo; 9 | params: PoWCryptoParams; 10 | difficulty: number; 11 | } -------------------------------------------------------------------------------- /src/modules/pow/validator/PoWValidator.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { isMainThread, parentPort, Worker } from 'worker_threads'; 3 | import { FaucetWorkers } from '../../../common/FaucetWorker.js'; 4 | import { ServiceManager } from '../../../common/ServiceManager.js'; 5 | import { PromiseDfd } from '../../../utils/PromiseDfd.js'; 6 | import { PoWHashAlgo } from '../PoWConfig.js'; 7 | import { PoWModule } from '../PoWModule.js'; 8 | import { IPoWValidatorValidateRequest } from './IPoWValidator.js'; 9 | import { PoWServerWorker } from '../PoWServerWorker.js'; 10 | 11 | export class PoWValidator { 12 | private server: PoWServerWorker; 13 | private worker: Worker; 14 | private readyDfd: PromiseDfd; 15 | private validateQueue: {[shareId: string]: PromiseDfd} = {}; 16 | 17 | public constructor(module: PoWServerWorker, worker?: Worker) { 18 | this.server = module; 19 | this.readyDfd = new PromiseDfd(); 20 | this.worker = worker || ServiceManager.GetService(FaucetWorkers).createWorker("pow-validator"); 21 | this.worker.on("message", (msg) => this.onWorkerMessage(msg)) 22 | } 23 | 24 | public dispose() { 25 | if(this.worker) { 26 | this.worker.terminate(); 27 | this.worker = null; 28 | } 29 | } 30 | 31 | public getValidationQueueLength(): number { 32 | return Object.keys(this.validateQueue).length; 33 | } 34 | 35 | public validateShare(shareId: string, nonce: number, preimg: string, data: string): Promise { 36 | let resDfd = new PromiseDfd(); 37 | let config = this.server.getModuleConfig(); 38 | let req: IPoWValidatorValidateRequest = { 39 | shareId: shareId, 40 | nonce: nonce, 41 | data: data, 42 | preimage: preimg, 43 | algo: config.powHashAlgo, 44 | params: (() => { 45 | switch(config.powHashAlgo) { 46 | case PoWHashAlgo.SCRYPT: 47 | return config.powScryptParams; 48 | case PoWHashAlgo.CRYPTONIGHT: 49 | return config.powCryptoNightParams; 50 | case PoWHashAlgo.ARGON2: 51 | return config.powArgon2Params; 52 | case PoWHashAlgo.NICKMINER: 53 | return config.powNickMinerParams; 54 | } 55 | })(), 56 | difficulty: config.powDifficulty, 57 | }; 58 | this.validateQueue[req.shareId] = resDfd; 59 | this.readyDfd.promise.then(() => { 60 | this.worker.postMessage({ 61 | action: "validate", 62 | data: req 63 | }); 64 | }); 65 | 66 | return resDfd.promise; 67 | } 68 | 69 | private onWorkerMessage(msg: any) { 70 | assert.equal(msg && (typeof msg === "object"), true); 71 | 72 | switch(msg.action) { 73 | case "init": 74 | this.readyDfd.resolve(); 75 | break; 76 | case "validated": 77 | this.onWorkerValidated(msg.data); 78 | break; 79 | } 80 | } 81 | 82 | private onWorkerValidated(msg: any) { 83 | assert.equal(this.validateQueue.hasOwnProperty(msg.shareId), true); 84 | 85 | let resDfd = this.validateQueue[msg.shareId]; 86 | delete this.validateQueue[msg.shareId]; 87 | 88 | resDfd.resolve(msg.isValid); 89 | } 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/modules/recurring-limits/RecurringLimitsConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IRecurringLimitsConfig extends IBaseModuleConfig { 4 | limits: IRecurringLimitConfig[]; 5 | } 6 | 7 | export interface IRecurringLimitConfig { 8 | limitCount: number; 9 | limitAmount: number; 10 | duration: number; 11 | byAddrOnly?: true; 12 | byIPOnly?: true; 13 | ip4Subnet?: number; 14 | action?: string; 15 | message?: string; 16 | rewards?: number; 17 | } 18 | 19 | export const defaultConfig: IRecurringLimitsConfig = { 20 | enabled: false, 21 | limits: [], 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/voucher/VoucherConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IVoucherConfig extends IBaseModuleConfig { 4 | voucherLabel: string | null; 5 | infoHtml: string | null; 6 | } 7 | 8 | export const defaultConfig: IVoucherConfig = { 9 | enabled: false, 10 | voucherLabel: null, 11 | infoHtml: null, 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/voucher/VoucherDB.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDbDriver } from '../../db/FaucetDatabase.js'; 2 | import { FaucetModuleDB } from '../../db/FaucetModuleDB.js'; 3 | import { SQL } from '../../db/SQL.js'; 4 | import { FaucetSessionStoreData } from '../../session/FaucetSession.js'; 5 | 6 | export interface IVoucher { 7 | code: string; 8 | dropAmount: string; 9 | sessionId?: string; 10 | targetAddr?: string; 11 | startTime?: number; 12 | } 13 | 14 | export class VoucherDB extends FaucetModuleDB { 15 | protected override latestSchemaVersion = 1; 16 | 17 | protected override async upgradeSchema(version: number): Promise { 18 | switch(version) { 19 | case 0: 20 | version = 1; 21 | await this.db.exec(SQL.driverSql({ 22 | [FaucetDbDriver.SQLITE]: ` 23 | CREATE TABLE "Vouchers" ( 24 | "Code" TEXT NOT NULL UNIQUE, 25 | "DropAmount" TEXT NOT NULL, 26 | "SessionId" TEXT NULL, 27 | "TargetAddr" TEXT NULL, 28 | "StartTime" INTEGER NULL, 29 | PRIMARY KEY("Code") 30 | );`, 31 | [FaucetDbDriver.MYSQL]: ` 32 | CREATE TABLE Vouchers ( 33 | Code VARCHAR(50) NOT NULL, 34 | DropAmount VARCHAR(50) NOT NULL, 35 | SessionId CHAR(36) NULL, 36 | TargetAddr CHAR(42) NULL, 37 | StartTime INT(11) NULL, 38 | PRIMARY KEY(Code) 39 | );`, 40 | })); 41 | } 42 | return version; 43 | } 44 | 45 | public async getVoucher(code: string): Promise { 46 | let sql = [ 47 | "SELECT Code, DropAmount, SessionId, TargetAddr, StartTime", 48 | "FROM Vouchers", 49 | "WHERE Code = ?", 50 | ].join(" "); 51 | let rows = await this.db.all(sql, [ code ]) as { 52 | Code: string; 53 | DropAmount: string; 54 | SessionId: string; 55 | TargetAddr: string; 56 | StartTime: number; 57 | }[]; 58 | 59 | let vouchers = rows.map((row) => { 60 | return { 61 | code: row.Code, 62 | dropAmount: row.DropAmount, 63 | sessionId: row.SessionId, 64 | targetAddr: row.TargetAddr, 65 | startTime: row.StartTime, 66 | }; 67 | }); 68 | 69 | if(vouchers.length === 0) 70 | return null; 71 | 72 | return vouchers[0]; 73 | } 74 | 75 | public async updateVoucher(code: string, sessionId: string, startTime: number, oldSessionId: string): Promise { 76 | let sql = "UPDATE Vouchers SET SessionId = ?, StartTime = ? WHERE Code = ?"; 77 | let args = [ 78 | sessionId, 79 | startTime, 80 | code, 81 | ]; 82 | if(oldSessionId) { 83 | sql += " AND SessionId = ?"; 84 | args.push(oldSessionId); 85 | } else { 86 | sql += " AND SessionId IS NULL"; 87 | } 88 | let res = await this.db.run( 89 | sql, 90 | args, 91 | ); 92 | return res.changes > 0; 93 | } 94 | 95 | public async updateVoucherTarget(code: string, sessionId: string, targetAddr: string): Promise { 96 | let sql = "UPDATE Vouchers SET TargetAddr = ? WHERE Code = ? AND SessionId = ?"; 97 | let args = [ 98 | targetAddr, 99 | code, 100 | sessionId, 101 | ]; 102 | await this.db.run( 103 | sql, 104 | args, 105 | ); 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /src/modules/whitelist/WhitelistConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IWhitelistConfig extends IBaseModuleConfig { 4 | whitelistPattern: { // ip info pattern based restrictions 5 | [pattern: string]: IWhitelistEntryConfig; // percentage of reward per share if IP info matches regex pattern 6 | }; 7 | whitelistFile: null | { // ip info pattern based restrictions from file 8 | yaml: string|string[]; // path to yaml file (for more actions/kill messages/etc.) 9 | refresh: number; // refresh interval 10 | }; 11 | } 12 | 13 | export interface IWhitelistEntryConfig { 14 | reward?: number; 15 | skipModules?: string[]; 16 | msgkey?: string; 17 | message?: string; 18 | notify?: boolean|string; 19 | } 20 | 21 | export const defaultConfig: IWhitelistConfig = { 22 | enabled: false, 23 | whitelistPattern: {}, 24 | whitelistFile: null, 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/zupass/ZupassConfig.ts: -------------------------------------------------------------------------------- 1 | import { IBaseModuleConfig } from "../BaseModule.js"; 2 | 3 | export interface IZupassConfig extends IBaseModuleConfig { 4 | zupassUrl: string | null; 5 | zupassApiUrl: string | null; 6 | zupassWatermark: string; 7 | zupassExternalNullifier: string; 8 | redirectUrl: string | null; 9 | requireLogin: boolean; 10 | concurrencyLimit: number; 11 | event: IZupassEventConfig | null; 12 | verify: { 13 | signer?: string[]; 14 | productId?: string[]; 15 | eventId?: string[]; 16 | } | null; 17 | grants: IZupassGrantConfig[]; 18 | loginLogo: string | null; 19 | loginLabel: string | null; 20 | userLabel: string | null; 21 | infoHtml: string | null; 22 | } 23 | 24 | export interface IZupassEventConfig { 25 | name: string; 26 | eventIds: string[]; 27 | productIds: string[]; 28 | } 29 | 30 | export interface IZupassGrantConfig { 31 | limitCount: number; 32 | limitAmount: number; 33 | duration: number; 34 | skipModules?: string[]; 35 | rewardFactor?: number; 36 | overrideMaxDrop?: number; 37 | required?: boolean; 38 | message?: string; 39 | } 40 | 41 | export const defaultConfig: IZupassConfig = { 42 | enabled: false, 43 | zupassUrl: null, 44 | zupassApiUrl: null, 45 | zupassWatermark: "powfaucet challenge", 46 | zupassExternalNullifier: "powfaucet", 47 | redirectUrl: null, 48 | requireLogin: false, 49 | concurrencyLimit: 0, 50 | event: null, 51 | verify: null, 52 | grants: [], 53 | loginLogo: null, 54 | loginLabel: null, 55 | userLabel: null, 56 | infoHtml: null, 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/zupass/ZupassDB.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDbDriver } from '../../db/FaucetDatabase.js'; 2 | import { FaucetModuleDB } from '../../db/FaucetModuleDB.js'; 3 | import { SQL } from '../../db/SQL.js'; 4 | import { FaucetSessionStoreData } from '../../session/FaucetSession.js'; 5 | 6 | export class ZupassDB extends FaucetModuleDB { 7 | protected override latestSchemaVersion = 1; 8 | 9 | protected override async upgradeSchema(version: number): Promise { 10 | switch(version) { 11 | case 0: 12 | version = 1; 13 | await this.db.exec(SQL.driverSql({ 14 | [FaucetDbDriver.SQLITE]: ` 15 | CREATE TABLE "ZupassSessions" ( 16 | "SessionId" TEXT NOT NULL UNIQUE, 17 | "TicketId" TEXT NOT NULL, 18 | "EventId" TEXT NOT NULL, 19 | "ProductId" TEXT NOT NULL, 20 | "AttendeeId" TEXT NOT NULL, 21 | PRIMARY KEY("SessionId") 22 | );`, 23 | [FaucetDbDriver.MYSQL]: ` 24 | CREATE TABLE ZupassSessions ( 25 | SessionId CHAR(36) NOT NULL, 26 | TicketId CHAR(36) NOT NULL, 27 | EventId CHAR(36) NOT NULL, 28 | ProductId CHAR(36) NOT NULL, 29 | AttendeeId VARCHAR(150) NOT NULL, 30 | PRIMARY KEY(SessionId) 31 | );`, 32 | })); 33 | await this.db.exec(SQL.driverSql({ 34 | [FaucetDbDriver.SQLITE]: `CREATE INDEX "ZupassSessionsAttendeeIdx" ON "ZupassSessions" ("AttendeeId" ASC);`, 35 | [FaucetDbDriver.MYSQL]: `ALTER TABLE ZupassSessions ADD INDEX ZupassSessionsAttendeeIdx (AttendeeId);`, 36 | })); 37 | } 38 | return version; 39 | } 40 | 41 | public override async cleanStore(): Promise { 42 | let rows = await this.db.all([ 43 | "SELECT ZupassSessions.SessionId", 44 | "FROM ZupassSessions", 45 | "LEFT JOIN Sessions ON Sessions.SessionId = ZupassSessions.SessionId", 46 | "WHERE Sessions.SessionId IS NULL", 47 | ].join(" ")); 48 | let dataIdx = 0; 49 | let promises: Promise[] = []; 50 | while(dataIdx < rows.length) { 51 | let batchLen = Math.min(rows.length - dataIdx, 100); 52 | let dataBatch = rows.slice(dataIdx, dataIdx + batchLen); 53 | dataIdx += batchLen; 54 | promises.push(this.db.run( 55 | "DELETE FROM ZupassSessions WHERE SessionId IN (" + dataBatch.map(b => "?").join(",") + ")", 56 | dataBatch.map(b => b.SessionId) as any[] 57 | ).then()) 58 | } 59 | await Promise.all(promises); 60 | } 61 | 62 | public getZupassSessions(attendeeId: string, duration: number, skipData?: boolean): Promise { 63 | let now = this.now(); 64 | return this.faucetStore.selectSessionsSql([ 65 | "FROM ZupassSessions", 66 | "INNER JOIN Sessions ON Sessions.SessionId = ZupassSessions.SessionId", 67 | "WHERE ZupassSessions.AttendeeId = ? AND Sessions.StartTime > ? AND Sessions.Status IN ('claimable','claiming','finished')", 68 | ].join(" "), [ attendeeId, now - duration ], skipData); 69 | } 70 | 71 | public async setZupassSession(sessionId: string, attendeeId: string, ticketId: string, eventId: string, productId: string): Promise { 72 | await this.db.run( 73 | SQL.driverSql({ 74 | [FaucetDbDriver.SQLITE]: "INSERT OR REPLACE INTO ZupassSessions (SessionId,TicketId,EventId,ProductId,AttendeeId) VALUES (?,?,?,?,?)", 75 | [FaucetDbDriver.MYSQL]: "REPLACE INTO ZupassSessions (SessionId,TicketId,EventId,ProductId,AttendeeId) VALUES (?,?,?,?,?)", 76 | }), 77 | [ 78 | sessionId, 79 | ticketId, 80 | eventId, 81 | productId, 82 | attendeeId 83 | ] 84 | ); 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/modules/zupass/ZupassUtils.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from "../../utils/CryptoUtils.js"; 2 | import { parse as uuidParse } from "uuid"; 3 | 4 | /** 5 | * Encoding of -1 in a Baby Jubjub field element (as p-1). 6 | */ 7 | export const BABY_JUB_NEGATIVE_ONE = BigInt( 8 | "21888242871839275222246405745257275088548364400416034343698204186575808495616" 9 | ); 10 | 11 | /** 12 | * Hashes a message to be signed with sha256 and truncates to fit into a 13 | * baby jub jub field element. The result includes the top 248 bits of 14 | * the 256 bit hash. 15 | * 16 | * @param signal The initial message. 17 | * @returns The outputted hash, fed in as a signal to the Semaphore proof. 18 | */ 19 | export function generateSnarkMessageHash(signal: string): bigint { 20 | // right shift to fit into a field element, which is 254 bits long 21 | // shift by 8 ensures we have a 253 bit element 22 | return BigInt("0x" + sha256(signal)) >> BigInt(8); 23 | } 24 | 25 | /** 26 | * Converts a boolean to a bigint value of 0 or 1. 27 | */ 28 | export function booleanToBigInt(v: boolean): bigint { 29 | return BigInt(v ? 1 : 0); 30 | } 31 | 32 | /** 33 | * Converts a hex number to a bigint. 34 | */ 35 | export function hexToBigInt(v: string): bigint { 36 | if (!v.startsWith("0x")) { 37 | v = "0x" + v; 38 | } 39 | 40 | return BigInt(v); 41 | } 42 | 43 | /** 44 | * Converts a native number to a bigint. 45 | */ 46 | export function numberToBigInt(v: number): bigint { 47 | return BigInt(v); 48 | } 49 | 50 | /** 51 | * Converts a UUID string into a bigint. 52 | */ 53 | export function uuidToBigInt(v: string): bigint { 54 | // a uuid is just a particular representation of 16 bytes 55 | const bytes = uuidParse(v); 56 | const hex = "0x" + Buffer.from(bytes).toString("hex"); 57 | return BigInt(hex); 58 | } 59 | 60 | /** 61 | * Check if a parameter is defined. If not, it throws an error. 62 | * @param parameter Parameter to be checked. 63 | * @param parameterName Name of the parameter. 64 | */ 65 | export function requireDefinedParameter(parameter: any, parameterName: string) { 66 | if (typeof parameter === "undefined") { 67 | throw new Error(`${parameterName} must be defined`); 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/modules/zupass/ZupassWorker.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { MessagePort } from "worker_threads"; 3 | 4 | // @ts-ignore 5 | import vkey from "./circuit.json" with { type: "json" }; 6 | import { IZupassVerifyRequest } from './ZupassPCD'; 7 | 8 | export class ZupassWorker { 9 | private port: MessagePort; 10 | private groth16: any; 11 | 12 | public constructor(port: MessagePort) { 13 | this.port = port; 14 | this.port.on("message", (evt) => this.onControlMessage(evt)); 15 | 16 | this.initLibrary().then(() => { 17 | this.port.postMessage({ action: "init" }); 18 | }, (err) => { 19 | this.port.postMessage({ action: "error" }); 20 | }); 21 | } 22 | 23 | private async initLibrary(): Promise { 24 | let module = await import("../../../libs/groth16.cjs"); 25 | if(module.default) { 26 | module = module.default; 27 | } 28 | await module.init(); 29 | this.groth16 = module.groth16; 30 | } 31 | 32 | private onControlMessage(msg: any) { 33 | assert.equal(msg && (typeof msg === "object"), true); 34 | 35 | //console.log(evt); 36 | 37 | switch(msg.action) { 38 | case "verify": 39 | this.onCtrlVerify(msg.data); 40 | break; 41 | } 42 | } 43 | 44 | private async onCtrlVerify(req: IZupassVerifyRequest) { 45 | return this.groth16.verify(vkey, { 46 | publicSignals: req.publicSignals, 47 | proof: req.proof 48 | }).catch((ex) => { 49 | console.error(ex); 50 | return false 51 | }).then((res) => { 52 | this.port.postMessage({ 53 | action: "verified", 54 | data: { 55 | reqId: req.reqId, 56 | isValid: res 57 | } 58 | }); 59 | }); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/session/SessionRewardFactor.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ISessionRewardFactor { 3 | factor: number; 4 | module: string; 5 | name?: string; 6 | } -------------------------------------------------------------------------------- /src/tools/createVoucher.ts: -------------------------------------------------------------------------------- 1 | import { FaucetDatabase } from "../db/FaucetDatabase.js"; 2 | import crypto from "crypto"; 3 | import { Command } from "commander"; 4 | import { basename } from "path"; 5 | import { loadFaucetConfig, cliArgs } from "../config/FaucetConfig.js"; 6 | import { ServiceManager } from "../common/ServiceManager.js"; 7 | import { FaucetProcess } from "../common/FaucetProcess.js"; 8 | 9 | function generateRandomString(length: number, prefix?: string): string { 10 | const chars = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"; 11 | let result = prefix || ""; 12 | 13 | while (result.length < (prefix ? prefix.length : 0) + length) { 14 | const randomByte = crypto.randomBytes(1)[0]; 15 | if (randomByte < 256 - (256 % chars.length)) { 16 | result += chars[randomByte % chars.length]; 17 | } 18 | } 19 | 20 | return result; 21 | } 22 | 23 | // Convert ETH to Wei if needed 24 | function parseAmount(amount: string): string { 25 | if (amount.toUpperCase().endsWith("ETH")) { 26 | const ethAmount = parseFloat(amount.slice(0, -3)); 27 | return BigInt(ethAmount * 1e18).toString(); 28 | } 29 | return amount; 30 | } 31 | 32 | export async function createVoucher() { 33 | const program = new Command(); 34 | var argv = process.argv.slice(); 35 | argv.splice(2, 1); 36 | 37 | ServiceManager.GetService(FaucetProcess).hideLogOutput = true; 38 | 39 | program 40 | .name(basename(process.argv[1])) 41 | .description('Mass create voucher codes for PoWFaucet') 42 | .option('-c, --count ', 'Number of voucher codes to generate', parseInt) 43 | .option('-a, --amount [value]', 'Override drop amount (supports ETH as unit, otherwise wei)') 44 | .option('-p, --prefix [string]', 'Code prefix') 45 | .parse(argv); 46 | 47 | const options = program.opts(); 48 | 49 | if (!options.count || options.count <= 0) { 50 | console.error("Error: Count must be a positive number"); 51 | program.help(); 52 | process.exit(1); 53 | } 54 | 55 | // Initialize database 56 | loadFaucetConfig(); 57 | let faucetDb = ServiceManager.GetService(FaucetDatabase); 58 | await faucetDb.initialize(); 59 | 60 | // Add a function to create vouchers directly to the database 61 | async function addVoucher(code: string, dropAmount: string): Promise { 62 | const sql = "INSERT INTO Vouchers (Code, DropAmount, SessionId, TargetAddr, StartTime) VALUES (?, ?, NULL, NULL, NULL)"; 63 | await faucetDb.getDatabase().run(sql, [code, dropAmount]); 64 | } 65 | 66 | // Generate vouchers 67 | const amount = options.amount ? parseAmount(options.amount) : "0"; 68 | const prefix = options.prefix || ""; 69 | const codeLength = Math.max(10, 20 - prefix.length); // Ensure codes are sufficient length 70 | 71 | for (let i = 0; i < options.count; i++) { 72 | let code = generateRandomString(codeLength, prefix); 73 | try { 74 | await addVoucher(code, amount.toString() == "0" ? "" : amount); 75 | code = code.match(/.{1,5}/g).join(" "); 76 | console.log(code); 77 | } catch (error) { 78 | // If code already exists, try again 79 | i--; 80 | } 81 | } 82 | 83 | process.exit(0); 84 | } 85 | -------------------------------------------------------------------------------- /src/tools/migrateDB.ts: -------------------------------------------------------------------------------- 1 | import { MySQLDriver } from "../db/driver/MySQLDriver.js"; 2 | import { SQLiteDriver } from "../db/driver/SQLiteDriver.js"; 3 | import { FaucetDbDriver } from "../db/FaucetDatabase.js"; 4 | import { SQL } from "../db/SQL.js"; 5 | 6 | (async function() { 7 | // migration config 8 | let sourceDB = new SQLiteDriver(); 9 | let sourceDrv = FaucetDbDriver.SQLITE; 10 | await sourceDB.open({ 11 | driver: sourceDrv, 12 | file: "./faucet-store.db" 13 | }); 14 | 15 | let targetDB = new MySQLDriver(); 16 | let targetDrv = FaucetDbDriver.MYSQL; 17 | await targetDB.open({ 18 | driver: targetDrv, 19 | host: "10.16.71.107", 20 | username: "dev-faucet", 21 | password: "**censored**", 22 | database: "dev-faucet", 23 | }); 24 | 25 | let migrations = { 26 | keyValueStore: true, 27 | sessions: true, 28 | ipInfoCache: true, 29 | passportCache: true, 30 | passportStamps: true, 31 | }; 32 | 33 | 34 | // migration script 35 | if(migrations.keyValueStore) { 36 | let data = await sourceDB.all("SELECT " + SQL.field("Key", sourceDrv) + ",Value FROM KeyValueStore"); 37 | for(let i = 0; i < data.length; i++) { 38 | await targetDB.run( 39 | SQL.driverSql({ 40 | [FaucetDbDriver.SQLITE]: "INSERT OR REPLACE INTO KeyValueStore (Key,Value) VALUES (?,?)", 41 | [FaucetDbDriver.MYSQL]: "REPLACE INTO KeyValueStore (`Key`,Value) VALUES (?,?)", 42 | }, targetDrv), 43 | [ data[i].Key as any, data[i].Value as any ] 44 | ); 45 | } 46 | } 47 | 48 | async function migrateTable(table: string, fields: string[], batchSize?: number) { 49 | if(!batchSize) 50 | batchSize = 10; 51 | console.log("migrating table " + table) 52 | await targetDB.run("DELETE FROM " + table); 53 | let data = await sourceDB.all("SELECT " + fields.map((f) => SQL.field(f, sourceDrv)).join(",") + " FROM " + table); 54 | let dataIdx = 0; 55 | while(dataIdx < data.length) { 56 | let batchLen = Math.min(data.length - dataIdx, batchSize); 57 | let dataBatch = data.slice(dataIdx, dataIdx + batchLen); 58 | console.log(" migrate batch " + dataIdx + " - " + (dataIdx + batchLen)); 59 | dataIdx += batchLen; 60 | let args = []; 61 | let sql = [ 62 | "INSERT INTO " + table, 63 | " (" + fields.map((f) => SQL.field(f, sourceDrv)).join(",") + ") ", 64 | "VALUES ", 65 | dataBatch.map(b => { 66 | return "(" + fields.map((f) => { 67 | args.push(b[f]); 68 | return "?"; 69 | }).join(",") + ")" 70 | }).join(",") 71 | ].join(""); 72 | await targetDB.run(sql, args); 73 | } 74 | } 75 | 76 | if(migrations.sessions) 77 | await migrateTable("Sessions", ["SessionId", "Status", "StartTime", "TargetAddr", "DropAmount", "RemoteIP", "Tasks", "Data", "ClaimData"]); 78 | if(migrations.ipInfoCache) 79 | await migrateTable("IPInfoCache", ["IP", "Json", "Timeout"]); 80 | if(migrations.passportCache) 81 | await migrateTable("PassportCache", ["Address", "Json", "Timeout"]); 82 | if(migrations.passportStamps) 83 | await migrateTable("PassportStamps", ["StampHash", "Address", "Timeout"]); 84 | 85 | console.log("migration complete!"); 86 | process.exit(0); 87 | 88 | })(); 89 | 90 | -------------------------------------------------------------------------------- /src/utils/ConvertHelpers.ts: -------------------------------------------------------------------------------- 1 | 2 | export function base64ToHex(str) { 3 | const raw = atob(str); 4 | let result = ''; 5 | for (let i = 0; i < raw.length; i++) { 6 | const hex = raw.charCodeAt(i).toString(16); 7 | result += (hex.length === 2 ? hex : '0' + hex); 8 | } 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/CryptoUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | import crypto from "crypto" 3 | 4 | export function sha256(input: string): string { 5 | let sha256 = crypto.createHash('sha256'); 6 | sha256.update(input); 7 | return sha256.digest("hex"); 8 | } 9 | 10 | export function encryptStr(input: string, passphrase: string): string { 11 | let iv = crypto.randomBytes(16); 12 | let key = Buffer.from(sha256(passphrase), "hex"); 13 | let cipher = crypto.createCipheriv('aes-256-cbc', key, iv); 14 | let encrypted = cipher.update(input); 15 | encrypted = Buffer.concat([encrypted, cipher.final()]); 16 | let final = Buffer.concat([iv, encrypted]); 17 | return final.toString("base64"); 18 | } 19 | 20 | export function decryptStr(input: string, passphrase: string): string { 21 | let inputBuf = Buffer.from(input, "base64"); 22 | if(inputBuf.length <= 16) 23 | return null; 24 | let iv = inputBuf.slice(0, 16); 25 | let key = Buffer.from(sha256(passphrase), "hex"); 26 | let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); 27 | let decrypted = decipher.update(inputBuf.slice(16)); 28 | decrypted = Buffer.concat([decrypted, decipher.final()]); 29 | return decrypted.toString(); 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/DateUtils.ts: -------------------------------------------------------------------------------- 1 | import { strPadLeft } from "./StringUtils.js"; 2 | 3 | 4 | export const renderDate = (date: Date, withTime?: boolean, withSec?: boolean): string => { 5 | return date.getFullYear() + "-" + strPadLeft(date.getMonth() + 1, 2, '0') + '-' + strPadLeft(date.getDate(), 2, '0') + 6 | (withTime ? " " + strPadLeft(date.getHours(), 2, '0') + ":" + strPadLeft(date.getMinutes(), 2, '0') + (withSec ? ":" + strPadLeft(date.getSeconds(), 2, '0') : "") : "") 7 | } 8 | 9 | export const renderTimespan = (time: number, maxParts?: number): string => { 10 | let resParts: string[] = []; 11 | let group: number; 12 | if(!maxParts) 13 | maxParts = 2; 14 | 15 | group = 60 * 60 * 24; 16 | if(time >= group) { 17 | let groupVal = Math.floor(time / group); 18 | time -= groupVal * group; 19 | resParts.push(groupVal + "d"); 20 | } 21 | 22 | group = 60 * 60; 23 | if(time >= group) { 24 | let groupVal = Math.floor(time / group); 25 | time -= groupVal * group; 26 | resParts.push(groupVal + "h"); 27 | } 28 | 29 | group = 60; 30 | if(time >= group) { 31 | let groupVal = Math.floor(time / group); 32 | time -= groupVal * group; 33 | resParts.push(groupVal + "min"); 34 | } 35 | 36 | group = 1; 37 | if(time >= group) { 38 | let groupVal = Math.floor(time / group); 39 | time -= groupVal * group; 40 | resParts.push(groupVal + "sec"); 41 | } 42 | 43 | if(resParts.length > maxParts) { 44 | resParts = resParts.slice(0, maxParts); 45 | } 46 | return resParts.join(" "); 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/FetchUtil.ts: -------------------------------------------------------------------------------- 1 | import { default as nodeFetch, RequestInfo, RequestInit, Response } from 'node-fetch'; 2 | 3 | export class FetchUtil { 4 | public static fetch( 5 | url: RequestInfo, 6 | init?: RequestInit, 7 | ): Promise { 8 | if(init) 9 | return nodeFetch(url, init); 10 | else 11 | return nodeFetch(url); 12 | } 13 | 14 | public static fetchWithTimeout( 15 | url: RequestInfo, 16 | init?: RequestInit, 17 | timeout: number = 5000, 18 | ): Promise { 19 | return new Promise((resolve, reject) => { 20 | const timeoutId = setTimeout(() => { 21 | reject(new Error('Request timed out')); 22 | }, timeout); 23 | 24 | FetchUtil.fetch(url, init).then((res) => { 25 | clearTimeout(timeoutId); 26 | resolve(res); 27 | }).catch((err) => { 28 | clearTimeout(timeoutId); 29 | reject(err); 30 | }); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/GuidUtils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | 3 | export const getNewGuid = (): string => { 4 | return crypto.randomUUID(); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/HashedInfo.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | export function getHashedIp(remoteAddr: string, secret: string): string { 4 | let ipMatch: RegExpExecArray; 5 | let hashParts: string[] = []; 6 | let hashGlue: string; 7 | let getHash = (input: string, len?: number) => { 8 | let hash = crypto.createHash("sha256"); 9 | hash.update(secret + "\r\n"); 10 | hash.update("iphash\r\n"); 11 | hash.update(input); 12 | let hashStr = hash.digest("hex"); 13 | if(len) 14 | hashStr = hashStr.substring(0, len); 15 | return hashStr; 16 | }; 17 | 18 | let hashBase = ""; 19 | if((ipMatch = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.exec(remoteAddr))) { 20 | // IPv4 21 | hashGlue = "."; 22 | 23 | for(let i = 0; i < 4; i++) { 24 | hashParts.push(getHash(hashBase + ipMatch[i+1], 3)); 25 | hashBase += (hashBase ? "." : "") + ipMatch[i+1]; 26 | } 27 | } 28 | else { 29 | // IPv6 30 | hashGlue = ":"; 31 | 32 | let ipSplit = remoteAddr.split(":"); 33 | let ipParts: string[] = []; 34 | for(let i = 0; i < ipSplit.length; i++) { 35 | if(ipSplit[i] === "") { 36 | let skipLen = 8 - ipSplit.length + 1; 37 | for(let j = 0; j < skipLen; j++) 38 | ipParts.push("0"); 39 | break; 40 | } 41 | ipParts.push(ipSplit[i]); 42 | } 43 | for(let i = 0; i < 8; i++) { 44 | hashParts.push(ipParts[i] === "0" ? "0" : getHash(hashBase + ipParts[i], 3)); 45 | hashBase += (hashBase ? "." : "") + ipParts[i]; 46 | } 47 | } 48 | 49 | return hashParts.join(hashGlue); 50 | } 51 | 52 | export function getHashedSessionId(sessionId: string, secret: string): string { 53 | let sessionIdHash = crypto.createHash("sha256"); 54 | sessionIdHash.update(secret + "\r\n"); 55 | sessionIdHash.update(sessionId); 56 | return sessionIdHash.digest("hex").substring(0, 20); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/utils/PromiseDfd.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IPromiseFns { 3 | resolve(result?: any): void; 4 | reject(error?: any): void; 5 | } 6 | 7 | export class PromiseDfd { 8 | public readonly promise: Promise; 9 | public readonly resolve: (result?: T) => void; 10 | public readonly reject: (error?: any) => void; 11 | 12 | public constructor() { 13 | let promiseFns: IPromiseFns; 14 | this.promise = new Promise((resolve, reject) => { 15 | promiseFns = { 16 | resolve: resolve, 17 | reject: reject 18 | }; 19 | }); 20 | this.resolve = promiseFns.resolve; 21 | this.reject = promiseFns.reject; 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/PromiseUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function sleepPromise(delay: number): Promise { 3 | return new Promise((resolve) => { 4 | setTimeout(resolve, delay); 5 | }); 6 | } 7 | 8 | export function timeoutPromise(promise: Promise, timeout: number, rejectReason?: string): Promise { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | reject(rejectReason || "promise timeout"); 12 | }, timeout); 13 | promise.then(resolve, reject); 14 | }); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/utils/StringUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function strPadLeft(str: any, len: number, pad: string): string { 3 | str = str.toString(); 4 | while(str.length < len) 5 | str = pad + str; 6 | return str; 7 | } 8 | 9 | export function strPadRight(str: any, len: number, pad: string): string { 10 | str = str.toString(); 11 | while(str.length < len) 12 | str += pad; 13 | return str; 14 | } 15 | 16 | export function strFormatPlaceholder(format: string, ..._) { 17 | var args = arguments; 18 | return format.replace(/{(\d+)}/g, function(match, number) { 19 | return typeof args[number] != 'undefined' 20 | ? args[number] 21 | : match 22 | ; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/VersionCompare.ts: -------------------------------------------------------------------------------- 1 | 2 | export function isVersionLower(v1: string, v2: string): boolean { 3 | if(!v1 || !v2) 4 | return null; 5 | let v1parts = v1.split("."); 6 | let v2parts = v2.split("."); 7 | let parts = Math.max(v1parts.length, v2parts.length); 8 | 9 | for(let i = 0; i < parts; i++) { 10 | let v1part = i < v1parts.length ? parseInt(v1parts[i]) : 0; 11 | let v2part = i < v2parts.length ? parseInt(v2parts[i]) : 0; 12 | if(v1part < v2part) 13 | return true; 14 | else if(v1part > v2part) 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/favicon/favicon.ico -------------------------------------------------------------------------------- /static/images/fauceth_420.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/images/fauceth_420.jpg -------------------------------------------------------------------------------- /static/images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/images/progress.gif -------------------------------------------------------------------------------- /static/images/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/images/progress.png -------------------------------------------------------------------------------- /static/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/images/spinner.gif -------------------------------------------------------------------------------- /static/images/zupass_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pk910/PoWFaucet/a32d5d6da98468f2fc0eec9d53ff7a8d94bb82ae/static/images/zupass_logo.jpg -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PoW Faucet 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 | Fork me on GitHub 29 | 30 | 31 | -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x4d2FD97560f498Ed0Fe66e6B6FF2329F6b17848b' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tests/Utils.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import sinon from 'sinon'; 3 | import { expect } from 'chai'; 4 | import { renderDate, renderTimespan } from '../src/utils/DateUtils.js'; 5 | import { timeoutPromise } from '../src/utils/PromiseUtils.js'; 6 | import { getHashedIp } from '../src/utils/HashedInfo.js'; 7 | import { isVersionLower } from '../src/utils/VersionCompare.js'; 8 | import { strFormatPlaceholder } from '../src/utils/StringUtils.js'; 9 | 10 | 11 | describe("Utility Functions", () => { 12 | 13 | it("PromiseUtils.timeoutPromise", async () => { 14 | let now = new Date().getTime(); 15 | let err = false; 16 | try { 17 | await timeoutPromise(new Promise((resolve, reject) => { 18 | setTimeout(() => resolve(), 1000); 19 | }), 100); 20 | } catch(e) { 21 | err = true; 22 | } 23 | 24 | expect(new Date().getTime() - now).to.be.lessThan(200, "unexpected result"); 25 | expect(err).to.equal(true, "no timeout error thrown") 26 | }); 27 | 28 | it("DateUtils.renderTimespan", async () => { 29 | expect(renderTimespan(130, 2)).to.equal("2min 10sec", "unexpected result"); 30 | expect(renderTimespan(439964, 5)).to.equal("5d 2h 12min 44sec", "unexpected result"); 31 | expect(renderTimespan(439964, 2)).to.equal("5d 2h", "unexpected result"); 32 | }); 33 | 34 | it("DateUtils.renderDate", async () => { 35 | expect(renderDate(new Date(1970, 2, 3, 5, 11, 12), true)).to.equal("1970-03-03 05:11", "unexpected result 1"); 36 | expect(renderDate(new Date(1970, 2, 3, 5, 11, 12), false)).to.equal("1970-03-03", "unexpected result 2"); 37 | expect(renderDate(new Date(1970, 2, 3, 5, 11, 12), true, true)).to.equal("1970-03-03 05:11:12", "unexpected result 3"); 38 | }); 39 | 40 | it("HashedInfo.getHashedIp", async () => { 41 | expect(getHashedIp("1.2.3.4", "test")).to.equal("df6.60b.ef9.b3e", "unexpected result"); 42 | expect(getHashedIp("2003:DE:C711::ECFF:FE0E:21F1", "test")).to.equal("f84:d47:e32:0:0:dc0:d1e:d8c", "unexpected result"); 43 | }); 44 | 45 | it("VersionCompare.isVersionLower", async () => { 46 | expect(isVersionLower(null as any, "1.2")).to.equal(null, "unexpected result 1") 47 | expect(isVersionLower("1.2.3", "1.2.4")).to.equal(true, "unexpected result 2") 48 | expect(isVersionLower("1.2", "1.2.4")).to.equal(true, "unexpected result 3") 49 | expect(isVersionLower("1.2.3", "1.2")).to.equal(false, "unexpected result 4") 50 | expect(isVersionLower("1.2.3", "1.2.3")).to.equal(undefined, "unexpected result 5") 51 | }); 52 | 53 | it("StringUtils.strFormatPlaceholder", async () => { 54 | expect(strFormatPlaceholder("1: {1}, 2: {2}", "A")).to.equal("1: A, 2: {2}", "unexpected result 1") 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /tests/stubs/FakeWebSocket.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket, RawData } from 'ws'; 2 | import { IncomingMessage } from 'http'; 3 | import { Duplex } from 'stream'; 4 | import { ServiceManager } from '../../src/common/ServiceManager.js'; 5 | import { FaucetHttpServer } from '../../src/webserv/FaucetHttpServer.js'; 6 | import { sleepPromise } from '../../src/utils/PromiseUtils.js'; 7 | 8 | export let fakeWebSockets: FakeWebSocket[] = []; 9 | 10 | export function disposeFakeWebSockets() { 11 | fakeWebSockets.forEach((fakeWs) => fakeWs.dispose()); 12 | } 13 | 14 | export async function injectFakeWebSocket(url: string, ip: string): Promise { 15 | let fakeWs = new FakeWebSocket(); 16 | let faucetHttpServer: any = ServiceManager.GetService(FaucetHttpServer); 17 | let wsHandler: (req: IncomingMessage, ws: WebSocket, remoteIp: string) => Promise = null as any; 18 | let rawHandler: (req: IncomingMessage, socket: Duplex, head: Buffer, remoteIp: string) => Promise = null as any; 19 | for(let endpoint in faucetHttpServer.wssEndpoints) { 20 | if(faucetHttpServer.wssEndpoints[endpoint].pattern.test(url)) { 21 | wsHandler = faucetHttpServer.wssEndpoints[endpoint].wssHandler; 22 | rawHandler = faucetHttpServer.wssEndpoints[endpoint].rawHandler; 23 | } 24 | } 25 | if(wsHandler) { 26 | await wsHandler({ 27 | url: url, 28 | } as any, fakeWs, ip) 29 | } 30 | else if(rawHandler) { 31 | let fakeSocket = {_testWs: fakeWs} as any; 32 | fakeSocket.write = (data: string) => { 33 | data = data.replace(/^.*[\r\n]{2}/gm, ""); 34 | fakeWs.send(data); 35 | }; 36 | fakeSocket.end = () => { fakeWs.close(); }; 37 | fakeSocket.destroy = () => { }; 38 | fakeSocket.removeAllListeners = () => {}; 39 | fakeSocket.pause = () => {}; 40 | fakeSocket.resume = () => {}; 41 | await rawHandler({ 42 | url: url, 43 | } as any, fakeSocket, Buffer.from(""), ip) 44 | await sleepPromise(10); 45 | } 46 | else { 47 | throw "no ws handler for url"; 48 | } 49 | 50 | return fakeWs; 51 | } 52 | 53 | export class FakeWebSocket extends WebSocket { 54 | private sentMessages: any[] = []; 55 | public isReady = true; 56 | 57 | constructor() { 58 | super(null as any, undefined, {}); 59 | fakeWebSockets.push(this); 60 | } 61 | 62 | public dispose() { 63 | let fakeWsIdx = fakeWebSockets.indexOf(this); 64 | if(fakeWsIdx !== -1) { 65 | fakeWebSockets.splice(fakeWsIdx, 1); 66 | } 67 | } 68 | 69 | public override send(data: any): void { 70 | this.sentMessages.push(JSON.parse(data)); 71 | } 72 | 73 | public getSentMessage(action?: string) { 74 | return this.sentMessages.filter((msg) => !action || msg.action === action); 75 | } 76 | 77 | public override ping() { 78 | setTimeout(() => { 79 | this.emit("pong"); 80 | }, 50); 81 | } 82 | 83 | public override close() { 84 | this.isReady = false; 85 | } 86 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "esModuleInterop": true, 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true 11 | }, 12 | "lib": ["es2015"], 13 | "include": [ 14 | "src/@types", 15 | "src/app.ts", 16 | "src/tools/migrateDB.ts" 17 | ], 18 | "exclude": [ 19 | "tests", 20 | "tests-old", 21 | "faucet-client", 22 | "faucet-wasm", 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-console": false 9 | }, 10 | "rulesDirectory": [] 11 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path'; 2 | import fs from 'fs'; 3 | import { fileURLToPath } from "url"; 4 | import webpack from "webpack"; 5 | 6 | let importUrl = fileURLToPath(import.meta.url); 7 | const basedir = dirname(importUrl); 8 | 9 | let packageJson = JSON.parse(fs.readFileSync(path.join(basedir, "package.json"), 'utf8')); 10 | 11 | export default { 12 | entry: './dist/app.js', 13 | target: 'node', 14 | resolve: { 15 | extensions: ['.ts', '.js'], 16 | }, 17 | output: { 18 | filename: 'powfaucet.cjs', 19 | path: path.resolve(basedir, 'bundle'), 20 | }, 21 | plugins: [ 22 | new webpack.optimize.LimitChunkCountPlugin({ 23 | maxChunks: 1, 24 | }), 25 | new webpack.DefinePlugin({ 26 | POWFAUCET_VERSION: JSON.stringify(packageJson.version), 27 | }), 28 | ], 29 | optimization: { 30 | minimize: false 31 | } 32 | }; 33 | --------------------------------------------------------------------------------