diff --git a/.env.example b/.env.example index 44c1b653..4841362b 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ +# See src/networkData for available configuration networks. SMF_CONFIG_NETWORK="frequencyPaseo" +#SMF_CONFIG_NETWORK="localhost" SMF_CONFIG_PORT=5555 SMF_CONFIG_DEPLOYED_REF=local @@ -24,4 +26,5 @@ SMF_CONFIG_DEPLOYED_REF=local SMF_CONFIG_FAUCET_ACCOUNT_MNEMONIC="//Alice" # Only used with external access -SMF_CONFIG_RECAPTCHA_SECRET="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" # Public testing secret, will accept all tokens. +# HCaptcha Test secret, see https://docs.hcaptcha.com/#integration-testing-test-keys +SMF_CONFIG_CAPTCHA_SECRET=0x0000000000000000000000000000000000000000 diff --git a/client/env.sample b/client/env.sample index 124dfb56..e1229f9c 100644 --- a/client/env.sample +++ b/client/env.sample @@ -1,9 +1,12 @@ +# leave blank to turn off demo mode PUBLIC_DEMO_MODE= -PUBLIC_CAPTCHA_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI +# test key for hcaptcha, see https://docs.hcaptcha.com/#integration-testing-test-keys +PUBLIC_CAPTCHA_KEY=10000000-ffff-ffff-ffff-000000000001 -# Direct requests to local instance +# Direct requests to local instance. Leave blank to use external default # PUBLIC_FAUCET_URL=http://localhost:5555/drip/web/ PUBLIC_FAUCET_URL= +# No need to edit these. PUBLIC_ISSUE_LINK=https://github.com/frequency-chain/testnet-faucet/issues/new/choose PUBLIC_FORUM="https://discord.com/channels/969001918460469250/969308337864867840" diff --git a/client/package.json b/client/package.json index 2e34f329..91ae0edf 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "postcss-load-config": "^6.0.1", "svelte": "^4.2.19", "svelte-check": "^4.0.2", + "svelte-hcaptcha": "^0.1.1", "svelte-markdown": "^0.4.1", "svelte-meta-tags": "^3.1.4", "svelte-preprocess": "^6.0.2", diff --git a/client/playwright.config.ts b/client/playwright.config.ts index ed75383e..c8e5fa26 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -5,10 +5,8 @@ const config: PlaywrightTestConfig = { command: "npm run build && npm run preview", port: 4173, env: { - PUBLIC_CAPTCHA_KEY: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI", - PUBLIC_DEMO_MODE: "", + PUBLIC_CAPTCHA_KEY: "10000000-ffff-ffff-ffff-000000000001", PUBLIC_FAUCET_URL: "https://example.com/test", - PUBLIC_FORUM: "", }, }, testDir: "tests", diff --git a/client/src/app.html b/client/src/app.html index 5e309a9a..69604632 100644 --- a/client/src/app.html +++ b/client/src/app.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8" /> - <link rel="icon" type="image/x-icon" href="%sveltekit.assets%/favicon-frequency.png" /> + <link rel="icon" type="image/x-icon" href="%sveltekit.assets%/favicon-192.png" /> <meta name="viewport" content="width=device-width" /> %sveltekit.head% </head> diff --git a/client/src/lib/assets/logo.svg b/client/src/lib/assets/logo.svg index c7c199dc..ff90cc6f 100644 --- a/client/src/lib/assets/logo.svg +++ b/client/src/lib/assets/logo.svg @@ -1,13 +1,13 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="461.117" height="68.256" viewBox="0 0 461.117 68.256"> <defs> <clipPath id="clip-path"> - <path id="Path_60" data-name="Path 60" d="M36.1,40.076v65.463H47.625V85.166l23.052-9.945-.309-11.729L47.625,73.177V50.987H76.953V40.076ZM91.965,50.987h15.449c4.976,0,9.078,2.531,9.078,7.943,0,5.324-4.1,8.03-9.078,8.03H91.965ZM80.444,40.076v65.463H91.965V89.479l36.048,16.147V92.97l-35-15.1h14.139c11.173,0,20.861-7.157,20.861-18.94s-9.688-18.854-20.861-18.854Zm51.06,0v65.463h40.849V94.629H143.025V78.219h29.328V67.309H143.025V50.987h29.328V40.076Zm150.651,0V84.853c0,6.721-4.888,11.347-11.522,11.347s-11.521-4.626-11.521-11.347V40.076H247.59v44.6c0,13.18,10.3,22.258,23.043,22.258s23.043-9.078,23.043-22.258v-44.6Zm15.012,0v65.463h40.848V94.629H308.688V78.219h29.327V67.309H308.688V50.987h29.327V40.076Zm80.824,0V84.242l-24-44.166H341.506v65.463h11.347V61.461l24.09,44.078h12.4V40.076Zm106.923,0-9.078,24.615-9.6-24.615H453.492l27.843,65.463H493.38l-11.608-27.32,15.449-38.143ZM186.754,72.808a23.261,23.261,0,1,1,46.522,0,23.816,23.816,0,0,1-3.055,11.7L204.3,68.531V81.536l18.243,11a23.733,23.733,0,0,1-12.569,3.578,23.008,23.008,0,0,1-23.218-23.3M209.972,38.68a34.128,34.128,0,1,0,22.519,59.876l11.609,6.983V92.97l-4.626-2.793A34.838,34.838,0,0,0,244.1,72.808,33.846,33.846,0,0,0,209.972,38.68Zm216.985,0a34.128,34.128,0,1,0,27.407,54.377l-8.03-8.03a21.476,21.476,0,0,1-19.377,11.085c-13.18,0-23.13-10.125-23.13-23.3s9.95-23.392,23.13-23.392a21.548,21.548,0,0,1,19.377,11.172l8.03-8.03A33.331,33.331,0,0,0,426.957,38.68Z" fill="#4b64ff"/> + <path id="Path_60" data-name="Path 60" d="M36.1,40.076v65.463H47.625V85.166l23.052-9.945-.309-11.729L47.625,73.177V50.987H76.953V40.076ZM91.965,50.987h15.449c4.976,0,9.078,2.531,9.078,7.943,0,5.324-4.1,8.03-9.078,8.03H91.965ZM80.444,40.076v65.463H91.965V89.479l36.048,16.147V92.97l-35-15.1h14.139c11.173,0,20.861-7.157,20.861-18.94s-9.688-18.854-20.861-18.854Zm51.06,0v65.463h40.849V94.629H143.025V78.219h29.328V67.309H143.025V50.987h29.328V40.076Zm150.651,0V84.853c0,6.721-4.888,11.347-11.522,11.347s-11.521-4.626-11.521-11.347V40.076H247.59v44.6c0,13.18,10.3,22.258,23.043,22.258s23.043-9.078,23.043-22.258v-44.6Zm15.012,0v65.463h40.848V94.629H308.688V78.219h29.327V67.309H308.688V50.987h29.327V40.076Zm80.824,0V84.242l-24-44.166H341.506v65.463h11.347V61.461l24.09,44.078h12.4V40.076Zm106.923,0-9.078,24.615-9.6-24.615H453.492l27.843,65.463H493.38l-11.608-27.32,15.449-38.143ZM186.754,72.808a23.261,23.261,0,1,1,46.522,0,23.816,23.816,0,0,1-3.055,11.7L204.3,68.531V81.536l18.243,11a23.733,23.733,0,0,1-12.569,3.578,23.008,23.008,0,0,1-23.218-23.3M209.972,38.68a34.128,34.128,0,1,0,22.519,59.876l11.609,6.983V92.97l-4.626-2.793A34.838,34.838,0,0,0,244.1,72.808,33.846,33.846,0,0,0,209.972,38.68Zm216.985,0a34.128,34.128,0,1,0,27.407,54.377l-8.03-8.03a21.476,21.476,0,0,1-19.377,11.085c-13.18,0-23.13-10.125-23.13-23.3s9.95-23.392,23.13-23.392a21.548,21.548,0,0,1,19.377,11.172l8.03-8.03A33.331,33.331,0,0,0,426.957,38.68Z" fill="#55B1AB"/> </clipPath> </defs> <g id="Group_182" data-name="Group 182" transform="translate(-36.104 -38.68)"> <path id="Path_59" data-name="Path 59" d="M36.1,40.076v65.463H47.625V85.166l23.052-9.945-.309-11.729L47.625,73.177V50.987H76.953V40.076ZM91.965,50.987h15.449c4.976,0,9.078,2.531,9.078,7.943,0,5.324-4.1,8.03-9.078,8.03H91.965ZM80.444,40.076v65.463H91.965V89.479l36.048,16.147V92.97l-35-15.1h14.139c11.173,0,20.861-7.157,20.861-18.94s-9.688-18.854-20.861-18.854Zm51.06,0v65.463h40.849V94.629H143.025V78.219h29.328V67.309H143.025V50.987h29.328V40.076Zm150.651,0V84.853c0,6.721-4.888,11.347-11.522,11.347s-11.521-4.626-11.521-11.347V40.076H247.59v44.6c0,13.18,10.3,22.258,23.043,22.258s23.043-9.078,23.043-22.258v-44.6Zm15.012,0v65.463h40.848V94.629H308.688V78.219h29.327V67.309H308.688V50.987h29.327V40.076Zm80.824,0V84.242l-24-44.166H341.506v65.463h11.347V61.461l24.09,44.078h12.4V40.076Zm106.923,0-9.078,24.615-9.6-24.615H453.492l27.843,65.463H493.38l-11.608-27.32,15.449-38.143ZM186.754,72.808a23.261,23.261,0,1,1,46.522,0,23.816,23.816,0,0,1-3.055,11.7L204.3,68.531V81.536l18.243,11a23.733,23.733,0,0,1-12.569,3.578,23.008,23.008,0,0,1-23.218-23.3M209.972,38.68a34.128,34.128,0,1,0,22.519,59.876l11.609,6.983V92.97l-4.626-2.793A34.838,34.838,0,0,0,244.1,72.808,33.846,33.846,0,0,0,209.972,38.68Zm216.985,0a34.128,34.128,0,1,0,27.407,54.377l-8.03-8.03a21.476,21.476,0,0,1-19.377,11.085c-13.18,0-23.13-10.125-23.13-23.3s9.95-23.392,23.13-23.392a21.548,21.548,0,0,1,19.377,11.172l8.03-8.03A33.331,33.331,0,0,0,426.957,38.68Z" fill="#4b64ff"/> <g id="Group_181" data-name="Group 181" clip-path="url(#clip-path)"> - <rect id="Rectangle_137" data-name="Rectangle 137" width="461.117" height="68.256" transform="translate(36.104 38.68)" fill="#4b64ff"/> + <rect id="Rectangle_137" data-name="Rectangle 137" width="461.117" height="68.256" transform="translate(36.104 38.68)" fill="#55B1AB"/> </g> </g> </svg> diff --git a/client/src/lib/components/CaptchaV2.svelte b/client/src/lib/components/CaptchaV2.svelte index fba9b121..f45bddf8 100644 --- a/client/src/lib/components/CaptchaV2.svelte +++ b/client/src/lib/components/CaptchaV2.svelte @@ -1,66 +1,42 @@ <script lang="ts"> - import { createEventDispatcher, onMount } from "svelte"; + import { createEventDispatcher } from "svelte"; import Cross from "./icons/Cross.svelte"; + // @ts-ignore + import HCaptcha from 'svelte-hcaptcha'; + import {PUBLIC_CAPTCHA_KEY} from '$env/static/public'; - export let captchaKey: string; + const siteKey = PUBLIC_CAPTCHA_KEY; - const dispatch = createEventDispatcher<{ token: string }>(); + const dispatch = createEventDispatcher(); const captchaId = "captcha_element"; - let captchaError: boolean = false; - export let theme: "dark" | "light" | "auto" = "auto"; + let captchaError = false; + let captchaKey = ''; - let componentMounted: boolean; - - onMount(() => { - window.captchaLoaded = () => { - const colorTheme = - theme === "auto" - ? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light" - : theme; - const mobileScreen = window.innerHeight > window.innerWidth; - - if (!window.grecaptcha) { - captchaError = true; - throw new Error("grecaptcha is undefined!"); - } - window.grecaptcha.render(captchaId, { - sitekey: captchaKey, - theme: colorTheme, - callback: "onToken", - size: mobileScreen ? "compact" : "normal", - "expired-callback": "onExpiredToken", - }); - }; - - window.onToken = (token) => { - dispatch("token", token); - }; - - // clean the token so the form becomes invalid - window.onExpiredToken = () => { - dispatch("token", ""); - }; - - // once we have mounted all the required methods, we import the script - componentMounted = true; + const handleSuccess = (payload: { detail?: {token: string}}) => { + const token = payload?.detail?.token || ''; + dispatch("token", token); captchaError = false; - }); -</script> + } -<svelte:head> - {#if componentMounted} - <script src="https://www.google.com/recaptcha/api.js?onload=captchaLoaded&render=explicit" async defer></script> - {/if} -</svelte:head> + const handleError = (error: Error) => { + captchaError = true; + console.error(error) + } +</script> +<HCaptcha + sitekey={siteKey} + bind:this={captchaKey} + theme=dark + on:success={handleSuccess} + on:error={handleError} +/> {#if captchaError} <div class="alert alert-error shadow-lg" data-testid="error"> <div> <Cross /> - <span>Error loading Google Captcha. Please reload the page.</span> + <span>Error loading HCaptcha. Please reload the page.</span> </div> </div> {/if} diff --git a/client/src/lib/components/NetworkInput.svelte b/client/src/lib/components/ChainDropdown.svelte similarity index 100% rename from client/src/lib/components/NetworkInput.svelte rename to client/src/lib/components/ChainDropdown.svelte diff --git a/client/src/lib/components/Form.svelte b/client/src/lib/components/Form.svelte index 6433cfdd..7ac9c2f8 100644 --- a/client/src/lib/components/Form.svelte +++ b/client/src/lib/components/Form.svelte @@ -1,11 +1,10 @@ <script lang="ts"> - import { PUBLIC_CAPTCHA_KEY } from "$env/static/public"; import type { NetworkData } from "$lib/utils/networkData"; import { operation, testnet } from "$lib/utils/stores"; import { request as faucetRequest } from "../utils"; import CaptchaV2 from "./CaptchaV2.svelte"; import NetworkDropdown from "./NetworkDropdown.svelte"; - import NetworkInput from "./NetworkInput.svelte"; + import NetworkInput from "./ChainDropdown.svelte"; import {validateAddress} from "@polkadot/util-crypto"; let address: string = ""; @@ -74,7 +73,7 @@ </div> {#if !webRequest} <div class="grid place-items-center"> - <CaptchaV2 captchaKey={PUBLIC_CAPTCHA_KEY ?? ""} on:token={onToken} theme="dark" /> + <CaptchaV2 on:token={onToken} /> </div> <button class="submit-btn" type="submit" data-testid="submit-button" disabled={!formValid}> Get some {$testnet.currency}s diff --git a/client/src/lib/utils/networkData.ts b/client/src/lib/utils/networkData.ts index dca29ab2..f885aa11 100644 --- a/client/src/lib/utils/networkData.ts +++ b/client/src/lib/utils/networkData.ts @@ -30,35 +30,6 @@ export const Frequency: NetworkData = { explorer: null }; -export const Rococo: NetworkData = { - networkName: "Rococo", - currency: "ROC", - chains: [ - { name: "Rococo Relay", id: -1 }, - { name: "AssetHub", id: 1000 }, - { name: "Contracts", id: 1002 }, - { name: "Encointer Lietaer", id: 1003 }, - { name: "Coretime", id: 1005 }, - { name: "Bridgehub", id: 1013 } - ], - endpoint: faucetUrl("https://rococo-faucet.parity-testnet.parity.io/drip/web"), - explorer: "https://rococo.subscan.io" -}; - -export const Westend: NetworkData = { - networkName: "Westend", - currency: "WND", - chains: [ - { name: "Westend Relay", id: -1 }, - { name: "AssetHub", id: 1000 }, - { name: "Collectives", id: 1001 }, - { name: "BridgeHub", id: 1002 }, - { name: "People", id: 1004 } - ], - endpoint: faucetUrl("https://westend-faucet.polkadot.io/drip/web"), - explorer: "https://westend.subscan.io" -}; - export const Paseo: NetworkData = { networkName: "Paseo", currency: "PAS", @@ -67,20 +38,9 @@ export const Paseo: NetworkData = { explorer: null }; -export const Trappist: NetworkData = { - networkName: "Trappist", - currency: "HOP", - chains: [{ name: "Trappist rococo parachain", id: -1 }], - endpoint: faucetUrl("https://trappist-faucet.parity-testnet.parity.io/drip/web"), - explorer: null -}; - export const Networks: { network: NetworkData; url: string }[] = [ - { network: Frequency, url: "/" }, - { network: Rococo, url: "https://faucet.polkadot.io" }, + { network: Frequency, url: "/" }, { network: Paseo, url: "https://faucet.polkadot.io/paseo" }, - { network: Westend, url: "https://faucet.polkadot.io/westend" }, - { network: Trappist, url: "https://faucet.polkadot.io/trappist" } ]; export function getChainName(network: NetworkData, id: number): string | null { diff --git a/client/src/lib/utils/stores.ts b/client/src/lib/utils/stores.ts index 1671507c..45172c09 100644 --- a/client/src/lib/utils/stores.ts +++ b/client/src/lib/utils/stores.ts @@ -1,9 +1,9 @@ import { derived, writable } from "svelte/store"; -import { type NetworkData, Rococo } from "./networkData"; +import { type NetworkData, Frequency } from "./networkData"; // If we want to have a new network we need to change this hardcoded value. -export const testnet = writable<NetworkData>(Rococo); +export const testnet = writable<NetworkData>(Frequency); export const testnetName = derived(testnet, ($net) => $net.networkName); diff --git a/client/src/routes/trappist/+page.svelte b/client/src/routes/trappist/+page.svelte deleted file mode 100644 index 0e0238a1..00000000 --- a/client/src/routes/trappist/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ -<script lang="ts"> - import Faucet from "$lib/components/Faucet.svelte"; - import { Trappist, type NetworkData } from "$lib/utils/networkData"; - import faqMd from "$lib/assets/FAQ.md?raw"; - - let network: NetworkData = Trappist; - let faq: string = faqMd - .replaceAll("<NETWORK-TOKEN>", network.currency) - .replaceAll("<NETWORK-NAME>", network.networkName); -</script> - -<Faucet {network} {faq} /> diff --git a/client/src/routes/westend/+page.svelte b/client/src/routes/westend/+page.svelte deleted file mode 100644 index 06e85b50..00000000 --- a/client/src/routes/westend/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ -<script lang="ts"> - import Faucet from "$lib/components/Faucet.svelte"; - import { Westend, type NetworkData } from "$lib/utils/networkData"; - import faqMd from "$lib/assets/FAQ.md?raw"; - - let network: NetworkData = Westend; - let faq: string = faqMd - .replaceAll("<NETWORK-TOKEN>", network.currency) - .replaceAll("<NETWORK-NAME>", network.networkName); -</script> - -<Faucet {network} {faq} /> diff --git a/client/static/favicon-192.png b/client/static/favicon-192.png new file mode 100644 index 00000000..79c5877c Binary files /dev/null and b/client/static/favicon-192.png differ diff --git a/client/static/favicon-512.png b/client/static/favicon-512.png new file mode 100644 index 00000000..af5d37e9 Binary files /dev/null and b/client/static/favicon-512.png differ diff --git a/client/static/favicon.ico b/client/static/favicon.ico new file mode 100644 index 00000000..f92e3348 Binary files /dev/null and b/client/static/favicon.ico differ diff --git a/client/static/favicon.svg b/client/static/favicon.svg new file mode 100644 index 00000000..057ecfc7 --- /dev/null +++ b/client/static/favicon.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192"> + <defs> + <style> + .cls-1 { + fill: #00b6af; + } + + .cls-2 { + fill: #19455e; + } + </style> + </defs> + <circle class="cls-2" cx="96" cy="96" r="96"/> + <polygon class="cls-1" points="63.87 46.07 63.87 152.6 82.57 152.6 82.57 119.42 120.14 103.32 119.66 84.29 82.73 100.07 82.73 63.8 130.39 63.8 130.39 46.07 63.87 46.07"/> +</svg> \ No newline at end of file diff --git a/client/tests/faucet.ts b/client/tests/faucet.ts index 441edcd9..def5560f 100644 --- a/client/tests/faucet.ts +++ b/client/tests/faucet.ts @@ -1,10 +1,10 @@ import { - type Frame, - type FullConfig, - type Locator, - type Page, - expect, - test + type Frame, + type FullConfig, + type Locator, + type Page, + expect, + test, type ElementHandle, type Route } from "@playwright/test"; type FormSubmit = { @@ -17,29 +17,7 @@ const getFormElements = async (page: Page, getCaptcha = false) => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions let captcha: Locator = {} as Locator; if (getCaptcha) { - // ?: Hack. We need to wait for the frame to load and then invade it. - await page.reload(); - const captchaFrame = await new Promise<Frame>((resolve, reject) => { - let i = 0; - // function that waits for the frame and timeouts after 3 seconds - // FIXME consider "until" from "@eng-automation/js"? - // eslint-disable-next-line no-restricted-syntax - (function waitForFrame() { - const captchaFrames = page - .frames() - .filter((f) => f.url().includes("https://www.google.com/recaptcha/api2/")); - if (captchaFrames.length > 0) { - return resolve(captchaFrames[0]); - } else { - i++; - if (i > 10) { - reject(new Error("Timeout")); - } - } - setTimeout(waitForFrame, 300); - })(); - }); - captcha = captchaFrame?.locator("#recaptcha-anchor") as Locator; + captcha = await page.locator('iframe[title="Widget containing checkbox for hCaptcha security challenge"]'); } return { address: page.getByTestId("address"), @@ -222,10 +200,10 @@ export class FaucetTests { const { address, captcha, submit } = await getFormElements(page, true); await expect(submit).toBeDisabled(); await address.fill(validAddress); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); const faucetUrl = this.getFaucetUrl(config); - await page.route(faucetUrl, (route) => + await page.route(faucetUrl, (route: Route) => route.fulfill({ body: JSON.stringify({ hash: "hash" }) }) ); @@ -254,7 +232,7 @@ export class FaucetTests { const networkBtn = page.getByTestId(`network-${i}`); await expect(networkBtn).toBeVisible(); await networkBtn.click(); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); await expect(submit).toBeEnabled(); const faucetUrl = this.getFaucetUrl(config); await page.route(faucetUrl, (route) => @@ -284,7 +262,7 @@ export class FaucetTests { const customChainDiv = page.getByTestId("custom-network-button"); await customChainDiv.click(); await network.fill("9999"); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); await expect(submit).toBeEnabled(); const faucetUrl = this.getFaucetUrl(config); await page.route(faucetUrl, (route) => diff --git a/client/tests/test.ts b/client/tests/test.ts index a2c144f0..cfa1cc9a 100644 --- a/client/tests/test.ts +++ b/client/tests/test.ts @@ -7,7 +7,7 @@ import { test } from "@playwright/test"; -const chains = [{ name: "Frequency Rococo Testnet Chain", id: -1 }]; +const chains = [{ name: "Frequency Paseo Testnet Chain", id: -1 }]; type FormSubmit = { address: string; @@ -19,28 +19,10 @@ const testAddress = '5G3r2K1cEi4vtdBjMNHpjWCofRdyg2AFSdVVxMGkDGvuJgaG'; const getFormElements = async (page: Page, getCaptcha = false) => { let captcha: Locator = {} as Locator; + let captchaFrame: Locator = {} as Locator; if (getCaptcha) { - // ?: Hack. We need to wait for the frame to load and then invade it. - await page.reload(); - const captchaFrame = await new Promise<Frame>((resolve, reject) => { - let i = 0; - // function that waits for the frame and timeouts after 3 seconds - (function waitForFrame() { - const captchaFrame = page - .frames() - .filter((f) => f.url().includes("https://www.google.com/recaptcha/api2/")); - if (captchaFrame.length > 0) { - return resolve(captchaFrame[0]); - } else { - i++; - if (i > 10) { - reject(new Error("Timout")); - } - } - setTimeout(waitForFrame, 300); - })(); - }); - captcha = captchaFrame?.locator("#recaptcha-anchor") as Locator; + captchaFrame = await page.locator('iframe[title="Widget containing checkbox for hCaptcha security challenge"]'); + captcha = await captchaFrame.locator("#anchor") } return { address: page.getByTestId("address"), @@ -118,7 +100,7 @@ test.describe("form interaction", () => { const { address, captcha, submit } = await getFormElements(page, true); await expect(submit).toBeDisabled(); await address.fill(testAddress); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); await expect(submit).toBeEnabled(); }); @@ -128,7 +110,7 @@ test.describe("form interaction", () => { await expect(submit).toBeDisabled(); const myAddress = "0x000000001"; await address.fill(myAddress); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); const url = getFaucetUrl(config); await page.route(url, (route) => route.fulfill({ @@ -149,43 +131,6 @@ test.describe("form interaction", () => { await request; }); - for (let i = 1; i < chains.length; i++) { - const chain = chains[i]; - test(`sends data with ${chain.name} chain on submit`, async ({ page }, { config }) => { - await page.goto("/"); - const { address, captcha, submit } = await getFormElements(page, true); - const dropdown = page.getByTestId(dropdownId); - await expect(submit).toBeDisabled(); - const myAddress = "0x000000002"; - await address.fill(myAddress); - await dropdown.click(); - const networkBtn = page.getByTestId(`network-${i}`); - await expect(networkBtn).toBeVisible(); - await networkBtn.click(); - await captcha.click(); - await expect(submit).toBeEnabled(); - const url = getFaucetUrl(config); - await page.route(url, (route) => - route.fulfill({ - body: JSON.stringify({ hash: "hash" }) - }) - ); - - const request = page.waitForRequest((req) => { - if (req.url() === url) { - const data = req.postDataJSON() as FormSubmit; - const parachain_id = chain.id > 0 ? chain.id.toString() : undefined; - expect(data).toMatchObject({ address: myAddress, parachain_id }); - return !!data.recaptcha; - } - return false; - }); - - await submit.click(); - await request; - }); - } - // test("display link to transaction", async ({ page }, { config }) => { // await page.goto("/"); // const operationHash = "0x0123435423412343214"; @@ -211,7 +156,7 @@ test.describe("form interaction", () => { const { address, captcha, submit } = await getFormElements(page, true); await expect(submit).toBeDisabled(); await address.fill("0x123"); - await captcha.click(); + await captcha.contentFrame().getByLabel('hCaptcha checkbox with text').click(); await page.route(getFaucetUrl(config), (route) => route.fulfill({ body: JSON.stringify({ error }) diff --git a/client/vite.config.ts b/client/vite.config.ts index 7102f34d..934cc732 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,4 +1,5 @@ import { sveltekit } from "@sveltejs/kit/vite"; +// @ts-ignore import { defineConfig } from "vite"; // eslint-disable-next-line @typescript-eslint/no-unsafe-call diff --git a/client/yarn.lock b/client/yarn.lock index bbf73943..54991b89 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1700,6 +1700,11 @@ svelte-check@^4.0.2: picocolors "^1.0.0" sade "^1.7.4" +svelte-hcaptcha@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/svelte-hcaptcha/-/svelte-hcaptcha-0.1.1.tgz#b2db3ae317d93de3ffba570f27e2f3b32dcd02b1" + integrity sha512-iFF3HwfrCRciJnDs4Y9/rpP/BM2U/5zt+vh+9d4tALPAHVkcANiJIKqYuS835pIaTm6gt+xOzjfFI3cgiRI29A== + svelte-hmr@^0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.16.0.tgz#9f345b7d1c1662f1613747ed7e82507e376c1716" diff --git a/e2e/bootstrap.sh b/e2e/bootstrap.sh index 83d9eb8f..5102e974 100755 --- a/e2e/bootstrap.sh +++ b/e2e/bootstrap.sh @@ -71,5 +71,5 @@ SMF_BACKEND_PORT=5555 SMF_BACKEND_DEPLOYED_REF=local SMF_BACKEND_DEPLOYED_TIME=local SMF_BACKEND_EXTERNAL_ACCESS=true -SMF_BACKEND_RECAPTCHA_SECRET="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" # Public testing secret, will accept all tokens. +SMF_BACKEND_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000" # Public testing secret, will accept all tokens. EOF diff --git a/env.faucet.config.json b/env.faucet.config.json index b6f78426..6559f769 100644 --- a/env.faucet.config.json +++ b/env.faucet.config.json @@ -43,7 +43,7 @@ "default": 5555, "type": "number" }, - "RECAPTCHA_SECRET": { + "CAPTCHA_SECRET": { "description": "A secret recaptcha token used to validate external requests", "default": "", "masked": true, diff --git a/src/dripper/Captcha.spec.ts b/src/dripper/Captcha.spec.ts new file mode 100644 index 00000000..240a49e7 --- /dev/null +++ b/src/dripper/Captcha.spec.ts @@ -0,0 +1,18 @@ +import { Captcha } from "./Captcha"; + +describe("HCaptcha", () => { + it("Validates captcha positively", async () => { + const validTestSecretKey = "0x0000000000000000000000000000000000000000"; + const validTestResponseToken = "10000000-aaaa-bbbb-cccc-000000000001"; + const recaptcha = new Captcha(validTestSecretKey); + const result = await recaptcha.validate(validTestResponseToken); + expect(result).toBeTruthy(); + }); + + it("Validates captcha negatively", async () => { + const badSecretKey = "AAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAAAAAAAAAA"; + const recaptcha = new Captcha(badSecretKey); + const result = await recaptcha.validate("doesnt matter"); + expect(result).toBeFalsy(); + }); +}); diff --git a/src/dripper/Recaptcha.ts b/src/dripper/Captcha.ts similarity index 60% rename from src/dripper/Recaptcha.ts rename to src/dripper/Captcha.ts index ce1af0ad..2f20b82f 100644 --- a/src/dripper/Recaptcha.ts +++ b/src/dripper/Captcha.ts @@ -4,10 +4,10 @@ import { URLSearchParams } from "url"; import { config } from "../config"; import { logger } from "../logger"; -export class Recaptcha { - constructor(private secret: string = config.Get("RECAPTCHA_SECRET")) { +export class Captcha { + constructor(private secret: string = config.Get("CAPTCHA_SECRET")) { if (!this.secret) { - throw new Error(`⭕ Recaptcha is not configured. Check the RECAPTCHA_SECRET variable.`); + throw new Error(`⭕ HCaptcha is not configured. Check the CAPTCHA_SECRET variable.`); } } @@ -16,9 +16,9 @@ export class Recaptcha { const params = new URLSearchParams(); params.append("secret", this.secret); params.append("response", captcha); - const captchaResult = await axios.post("https://www.google.com/recaptcha/api/siteverify", params); + const captchaResult = await axios.post("https://api.hcaptcha.com/siteverify", params); if (captchaResult.data.success === true) return true; - logger.debug("Negative recaptcha validation result", captchaResult.data); + logger.debug("❌Negative recaptcha validation result", captchaResult.data); return false; } catch (e) { logger.error(`⭕ An error occurred when validating captcha`, e); diff --git a/src/dripper/DripRequestHandler.spec.ts b/src/dripper/DripRequestHandler.spec.ts index 9b76c82d..b35fea8a 100644 --- a/src/dripper/DripRequestHandler.spec.ts +++ b/src/dripper/DripRequestHandler.spec.ts @@ -1,8 +1,8 @@ +import type { Captcha } from "./Captcha"; import { hasDrippedToday, saveDrip } from "./dripperStorage"; import { DripRequestHandler } from "./DripRequestHandler"; import type { PolkadotActions } from "./polkadot/PolkadotActions"; import { convertAmountToBn } from "./polkadot/utils"; -import type { Recaptcha } from "./Recaptcha"; jest.mock("./dripperStorage"); @@ -12,7 +12,7 @@ const actionsMock: PolkadotActions = { addr === "unlucky" ? { error: "An error occurred when sending tokens" } : { hash: "0x123" }, } as any; // eslint-disable-line @typescript-eslint/no-explicit-any -const recaptcha: Recaptcha = { validate: async (captcha: string) => captcha === "valid" } as any; // eslint-disable-line @typescript-eslint/no-explicit-any +const recaptcha: Captcha = { validate: async (captcha: string) => captcha === "valid" } as any; // eslint-disable-line @typescript-eslint/no-explicit-any function assumeMocked<R, A extends unknown[]>(f: (...args: A) => R): jest.Mock<R, A> { return f as jest.Mock<R, A>; @@ -70,12 +70,12 @@ describe("DripRequestHandler", () => { it("Parity members are privileged in terms of repeated requests", async () => { assumeMocked(hasDrippedToday).mockResolvedValueOnce(true); - const result = await handler.handleRequest({ ...defaultRequest, sender: "someone:parity.io" }); + const result = await handler.handleRequest({ ...defaultRequest, sender: "@erin:parity.io" }); expect(result).toEqual({ hash: "0x123" }); }); it("Parity members are privileged in terms of balance cap", async () => { - const result = await handler.handleRequest({ ...defaultRequest, sender: "someone:parity.io", address: "rich" }); + const result = await handler.handleRequest({ ...defaultRequest, sender: "@pierre:parity.io", address: "rich" }); expect(result).toEqual({ hash: "0x123" }); }); diff --git a/src/dripper/DripRequestHandler.ts b/src/dripper/DripRequestHandler.ts index f44f14b9..aca8dd94 100644 --- a/src/dripper/DripRequestHandler.ts +++ b/src/dripper/DripRequestHandler.ts @@ -3,9 +3,9 @@ import { logger } from "../logger"; import { counters } from "../metrics"; import { DripRequestType, DripResponse } from "../types"; import { isAccountPrivileged } from "../utils"; +import { Captcha } from "./Captcha"; import { hasDrippedToday, saveDrip } from "./dripperStorage"; import type { PolkadotActions } from "./polkadot/PolkadotActions"; -import { Recaptcha } from "./Recaptcha"; const isParachainValid = (parachain: string): boolean => { if (!parachain) { @@ -22,7 +22,7 @@ const isParachainValid = (parachain: string): boolean => { export class DripRequestHandler { constructor( private actions: PolkadotActions, - private recaptcha: Recaptcha, + private recaptcha: Captcha, ) {} async handleRequest( @@ -66,7 +66,7 @@ export class DripRequestHandler { let instance: DripRequestHandler | undefined; export const getDripRequestHandlerInstance = (polkadotActions: PolkadotActions) => { if (!instance) { - const recaptchaService = new Recaptcha(); + const recaptchaService = new Captcha(); instance = new DripRequestHandler(polkadotActions, recaptchaService); } return instance; diff --git a/src/dripper/Recaptcha.spec.ts b/src/dripper/Recaptcha.spec.ts deleted file mode 100644 index e8263cc5..00000000 --- a/src/dripper/Recaptcha.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Recaptcha } from "./Recaptcha"; - -const PUBLIC_TESTING_RECAPTCHA_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"; -const OTHER_SECRET_KEY = "AAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAAAAAAAAAA"; - -describe("Recaptcha", () => { - it("Validates captcha positively", async () => { - const recaptcha = new Recaptcha(PUBLIC_TESTING_RECAPTCHA_SECRET_KEY); - const result = await recaptcha.validate("something"); - expect(result).toBeTruthy(); - }); - - it("Validates captcha negatively", async () => { - const recaptcha = new Recaptcha(OTHER_SECRET_KEY); - const result = await recaptcha.validate("something"); - expect(result).toBeFalsy(); - }); -}); diff --git a/src/dripper/polkadot/utils.spec.ts b/src/dripper/polkadot/utils.spec.ts index 59484774..83d7994e 100644 --- a/src/dripper/polkadot/utils.spec.ts +++ b/src/dripper/polkadot/utils.spec.ts @@ -3,21 +3,21 @@ import { convertAmountToBn } from "./utils"; describe("utils", () => { [ { amount: "0", expected: "0" }, - { amount: "1", expected: "1000000000000" }, - { amount: "100", expected: "100000000000000" }, - { amount: "1.45", expected: "1450000000000" }, - { amount: "145.454141111", expected: "145454141111000" }, - { amount: "99999999.9", expected: "99999999900000000000" }, - { amount: "999999999", expected: "999999999000000000000" }, - { amount: "999999999.9", expected: "999999999900000000000" }, - { amount: "999999999999999.9", expected: "999999999999999900000000000" }, + { amount: "1", expected: "10000000000" }, + { amount: "100", expected: "1000000000000" }, + { amount: "1.45", expected: "14500000000" }, + { amount: "145.454141111", expected: "1454541411110" }, + { amount: "99999999.9", expected: "999999999000000000" }, + { amount: "999999999", expected: "9999999990000000000" }, + { amount: "999999999.9", expected: "9999999999000000000" }, + { amount: "999999999999999.9", expected: "9999999999999999000000000" }, { amount: "999999999999999999999999999999999999999999999999999999", - expected: "999999999999999999999999999999999999999999999999999999000000000000", + expected: "9999999999999999999999999999999999999999999999999999990000000000", }, { amount: "999999999999999999999999999999999999999999999999999999.7", - expected: "999999999999999999999999999999999999999999999999999999700000000000", + expected: "9999999999999999999999999999999999999999999999999999997000000000", }, ].forEach((t) => test(`convertAmountToBn ${t.amount} => ${t.expected}`, () => { diff --git a/src/networkData.ts b/src/networkData.ts index a249877c..31151875 100644 --- a/src/networkData.ts +++ b/src/networkData.ts @@ -17,24 +17,16 @@ export interface NetworkData { matrixWhitelistPatterns: RegExp[]; } -const parityWhitelist = [/^.*:parity.io$/, /^.*:web3.foundation$/]; - -const rococo: NetworkData = { +const localhost: NetworkData = { balanceCap: 1000, - chains: [ - { name: "Rococo Relay Chain", id: -1 }, - { name: "Rockmine", id: 1000 }, - { name: "Contracts", id: 1002 }, - { name: "Encointer Lietaer", id: 1003 }, - { name: "Bridgehub", id: 1013 }, - ], - currency: "ROC", - decimals: 12, - dripAmount: "100", - explorer: "https://rococo.subscan.io", - networkName: "Rococo", - rpcEndpoint: "wss://rococo-rpc.polkadot.io/", - matrixWhitelistPatterns: parityWhitelist, + chains: [{ name: "Localhost", id: 1000 }], + currency: "UNIT", + decimals: 8, + dripAmount: "10", + explorer: "", + networkName: "Localhost", + rpcEndpoint: "ws://127.0.0.1:9944", + matrixWhitelistPatterns: [], }; const frequencyPaseo: NetworkData = { @@ -49,46 +41,6 @@ const frequencyPaseo: NetworkData = { matrixWhitelistPatterns: [], }; -const westend: NetworkData = { - balanceCap: 100, - chains: [ - { name: "Westend Relay Chain", id: -1 }, - { name: "Westmint", id: 1000 }, - { name: "Collectives", id: 1001 }, - ], - currency: "WND", - decimals: 12, - dripAmount: "10", - explorer: "https://westend.subscan.io", - networkName: "Westend", - rpcEndpoint: "wss://westend-rpc.polkadot.io/", - matrixWhitelistPatterns: parityWhitelist, -}; - -const versi: NetworkData = { - balanceCap: 1000, - chains: [], - currency: "VRS", - decimals: 12, - dripAmount: "100", - explorer: null, - networkName: "Versi", - rpcEndpoint: "wss://versi-rpc-node-0.parity-versi.parity.io/", - matrixWhitelistPatterns: parityWhitelist, -}; - -const trappist: NetworkData = { - balanceCap: 100, - chains: [], - currency: "HOP", - decimals: 12, - dripAmount: "10", - explorer: null, - networkName: "Trappist", - rpcEndpoint: "wss://rococo-trappist-rpc.polkadot.io/", - matrixWhitelistPatterns: parityWhitelist, -}; - const paseo: NetworkData = { balanceCap: 500, chains: [], @@ -111,26 +63,10 @@ const paseo: NetworkData = { ], }; -const e2e: NetworkData = { - balanceCap: 100, - chains: [], - currency: "UNIT", - decimals: 12, - dripAmount: "10", - explorer: null, - networkName: "Rococo", - rpcEndpoint: "ws://host.docker.internal:9933/", - matrixWhitelistPatterns: parityWhitelist, -}; - export const networks: Record<string, NetworkData> = { - rococo, - versi, - westend, - e2e, - trappist, paseo, frequencyPaseo, + localhost, }; export function getNetworkData(networkName: string): NetworkData { diff --git a/src/server/routes/actions.spec.ts b/src/server/routes/actions.spec.ts index 94afb0b7..ef1e0f0a 100644 --- a/src/server/routes/actions.spec.ts +++ b/src/server/routes/actions.spec.ts @@ -36,7 +36,7 @@ jest.mock("../../config", () => { Get: mockConfig.mockImplementation( (key: string) => // eslint-disable-next-line security/detect-object-injection - ({ NETWORK: "rococo" })[key], // minimal viable config on the initial import + ({ NETWORK: "paseo" })[key], // minimal viable config on the initial import ), }, }; diff --git a/src/test/globalSetup.unit.ts b/src/test/globalSetup.unit.ts index b63d9c43..011f918f 100644 --- a/src/test/globalSetup.unit.ts +++ b/src/test/globalSetup.unit.ts @@ -1,3 +1,3 @@ export default function() { - process.env.SMF_CONFIG_NETWORK = "rococo" + process.env.SMF_CONFIG_NETWORK = "paseo" } diff --git a/src/test/setupE2E.ts b/src/test/setupE2E.ts index 4bc25910..7c793977 100644 --- a/src/test/setupE2E.ts +++ b/src/test/setupE2E.ts @@ -112,18 +112,16 @@ export async function teardown(setup: E2ESetup): Promise<void> { async function setupMatrixContainer(): Promise<StartedTestContainer> { const image = await GenericContainer.fromDockerfile("e2e", "matrix_container.Dockerfile").build(); - const matrixContainer = image - .withExposedPorts(8008) - .withEnvironment({ - SYNAPSE_SERVER_NAME: "parity.io", - SYNAPSE_REPORT_STATS: "no" - }) - .withCommand(["run"]) - .withWaitStrategy(Wait.forHealthCheck()) - .withLogConsumer(logConsumer("faucet-test-matrix")) - .start(); - - return matrixContainer; + return image + .withExposedPorts(8008) + .withEnvironment({ + SYNAPSE_SERVER_NAME: "parity.io", + SYNAPSE_REPORT_STATS: "no" + }) + .withCommand(["run"]) + .withWaitStrategy(Wait.forHealthCheck()) + .withLogConsumer(logConsumer("faucet-test-matrix")) + .start(); } async function setupMatrix(matrixContainer: StartedTestContainer): Promise<MatrixSetup> { @@ -224,7 +222,7 @@ async function setupAppContainer(params: { SMF_CONFIG_DB_DATABASE_NAME: "faucet", // Public testing secret, will accept all tokens. - SMF_CONFIG_RECAPTCHA_SECRET: "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" + SMF_CONFIG_CAPTCHA_SECRET: "0x0000000000000000000000000000000000000000" }) .withWaitStrategy(Wait.forListeningPorts()) .withExtraHosts([{ host: "host.docker.internal", ipAddress: "host-gateway" }]) diff --git a/src/utils.spec.ts b/src/utils.spec.ts index 24ef664f..cbd316fb 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -9,22 +9,6 @@ type DataProvider = { jest.mock("./networkData"); -describe("test rococo", () => { - const dataProvider: DataProvider[] = [ - { username: "1", expected: false }, - { username: "", expected: false }, - { username: "@username:matrix.org", expected: false }, - { username: "@1:parity.io", expected: true }, - { username: "@1:matrix.parity.io", expected: false }, - { username: "@1:web3.foundation", expected: true }, - { username: "@1:web3.foundati", expected: false }, - ]; - - test.each(dataProvider)("$username, $expect", ({ username, expected }) => { - expect(isAccountPrivileged(username)).toBe(expected); - }); -}); - describe("test paseo", () => { beforeAll(() => { process.env.SMF_CONFIG_NETWORK = "paseo";