diff --git a/.github/workflows/cypress-testing.yml b/.github/workflows/cypress-testing.yml
new file mode 100644
index 00000000..b338e0a1
--- /dev/null
+++ b/.github/workflows/cypress-testing.yml
@@ -0,0 +1,66 @@
+name: test
+
+on:
+ push:
+ branches:
+ - main
+ - development
+ pull_request:
+ branches:
+ - main
+ - development
+
+env:
+ FOUNDRY_PROFILE: ci
+
+jobs:
+ tests:
+ name: Cypress tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20.10.0
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+ with:
+ version: nightly
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Start Anvil
+ run: yarn test:anvil &
+
+ - name: Cypress run
+ uses: cypress-io/github-action@v6
+ with:
+ build: yarn run build
+ start: yarn test:fund, yarn start
+
+ env:
+ SUPABASE_URL: "https://wfzpewmlyiozupulbuur.supabase.co"
+ SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndmenBld21seWlvenVwdWxidXVyIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTU2NzQzMzksImV4cCI6MjAxMTI1MDMzOX0.SKIL3Q0NOBaMehH0ekFspwgcu3afp3Dl9EDzPqs1nKs"
+ AMOUNT_IN_ETH: "0.1"
+ BENEFICIARY_ADDRESS: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
+ CHAIN_ID: "31337"
+ FRONTEND_URL: "http://localhost:8080"
+ PAYMENT_TOKEN_ADDRESS: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
+ RPC_PROVIDER_URL: "http://localhost:8545"
+ UBIQUIBOT_PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: cypress-screenshots
+ path: cypress/screenshots
+ if-no-files-found: ignore
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: cypress-videos
+ path: cypress/videos
+ if-no-files-found: ignore
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9b78ea45..09b6dad8 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -58,7 +58,7 @@ jobs:
script: |
const { owner, repo } = context.repo;
const sha = "${{ github.event.workflow_run.head_sha }}";
-
+
const response = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr sha:${sha}`,
per_page: 1,
@@ -82,11 +82,11 @@ jobs:
repo,
issue_number,
});
-
+
// Find the comment to update or create a new one if not found
let existingComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]');
let body = '| Preview Deployment |\n| ------------------ |\n';
-
+
// If the comment exists, update its body
if (existingComment) {
// Check if the SHA already exists in the comment body to avoid duplicates
diff --git a/.gitignore b/.gitignore
index d01f79ae..704a6abe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,7 @@ commit.txt
# yarn2
.pnp.cjs
.pnp.loader.mjs
-static/dist
\ No newline at end of file
+static/dist
+
+cypress/screenshots
+cypress/videos
\ No newline at end of file
diff --git a/.knip.jsonc b/.knip.jsonc
index 3b28c2af..27f07c24 100644
--- a/.knip.jsonc
+++ b/.knip.jsonc
@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["static/scripts/rewards/init.ts"],
- "ignore": ["lib/**"]
+ "ignore": ["lib/**"],
}
diff --git a/cypress.config.ts b/cypress.config.ts
new file mode 100644
index 00000000..8eee7a78
--- /dev/null
+++ b/cypress.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "cypress";
+import { config } from "dotenv";
+
+config();
+
+export default defineConfig({
+ e2e: {
+ setupNodeEvents() {},
+ baseUrl: "http://localhost:8080",
+ experimentalStudio: true,
+ },
+ viewportHeight: 900,
+ viewportWidth: 1440,
+ watchForFileChanges: false,
+ video: true,
+});
diff --git a/cypress/e2e/claim-portal-failure.cy.ts b/cypress/e2e/claim-portal-failure.cy.ts
new file mode 100644
index 00000000..e1305274
--- /dev/null
+++ b/cypress/e2e/claim-portal-failure.cy.ts
@@ -0,0 +1,186 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers";
+
+const beneficiary = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
+
+describe("Claims Portal Failures", () => {
+ describe("No connection to wallet provider", () => {
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+
+ setupIntercepts();
+ stubEthereum(beneficiary);
+
+ cy.visit(`/${claimUrl}`);
+ cy.wait(2000);
+ });
+
+ it("should handle no connected signer", () => {
+ /**
+ * This covers a user declining to connect their wallet
+ */
+ cy.get("#additionalDetails", { timeout: 15000 }).should("be.visible").invoke("click");
+
+ cy.get("button[id='make-claim']").should("be.visible").click();
+ cy.get("#invalidator").should("not.be.visible");
+ cy.get("#claim-loader").should("not.be.visible");
+ cy.get("#view-claim").should("not.be.visible").and("include.text", "View Claim");
+
+ cy.get("body").should("contain.text", "This reward is not for you");
+ });
+ });
+
+ describe("Failed transactions", () => {
+ const provider = new JsonRpcProvider("http://127.0.0.1:8545");
+ const signer = provider.getSigner();
+
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+
+ setupIntercepts();
+ stubEthereum(beneficiary, signer);
+
+ cy.visit(`/${claimUrl}`);
+ cy.wait(2000);
+ });
+
+ it("should handle feedback for a failed wallet provider transaction", () => {
+ cy.get("#additionalDetails", { timeout: 15000 }).should("be.visible").invoke("click");
+
+ cy.get("button[id='make-claim']").should("be.visible").click();
+ cy.get("#claim-loader").should("be.visible");
+ cy.get("#invalidator").should("not.be.visible");
+ // cy.get("#claim-loader").should("not.be.visible"); // gets stuck here
+ });
+ });
+});
+
+function setupIntercepts() {
+ cy.intercept("POST", "*", (req) => {
+ // return a 404 for rpc optimization meaning no successful RPC
+ // to return our balanceOf and allowance calls
+ if (req.body.method == "eth_getBlockByNumber") {
+ req.reply({
+ statusCode: 404,
+ body: {
+ jsonrpc: "2.0",
+ error: {
+ code: -32601,
+ message: "Method not found",
+ },
+ id: 1,
+ },
+ });
+ }
+
+ if (req.body.method == "eth_sendTransaction") {
+ req.reply({
+ statusCode: 404,
+ body: {
+ jsonrpc: "2.0",
+ id: 44,
+ result: "0x",
+ },
+ });
+ }
+
+ if (req.body.method == "eth_call") {
+ const selector = req.body.params[0].data.slice(0, 10);
+
+ // balanceOf
+ if (selector == "0x70a08231") {
+ req.reply({
+ statusCode: 200,
+ body: {
+ jsonrpc: "2.0",
+ id: 45,
+ result: "0x00000000000000000000000000000000000000000000478cf7610f95b9e70000",
+ },
+ });
+ } else if (selector == "0xdd62ed3e") {
+ // allowance
+
+ req.reply({
+ statusCode: 200,
+ body: {
+ jsonrpc: "2.0",
+ id: 46,
+ result: "0x0000000000000000000000000000000000c097ce7bc906e58377f59a8306ffff",
+ },
+ });
+ }
+ }
+ });
+
+ cy.intercept("POST", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("PATCH", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("GET", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+}
+
+function stubEthereum(address?: string, signer?: JsonRpcSigner) {
+ cy.on("window:before:load", (win) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ((win as any).ethereum = {
+ isMetaMask: true,
+ enable: cy.stub().resolves([address]),
+ request: cy.stub().callsFake(async (method) => providerFunctions(method)),
+ on: cy.stub().callsFake((event, cb) => {
+ if (event == "accountsChanged") {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (win as any).ethereum.onAccountsChanged = cb;
+ }
+ }),
+ autoRefreshOnNetworkChange: false,
+ chainId: "0x7a69",
+ selectedAddress: address,
+ requestAccounts: cy.stub().resolves([address]),
+ send: cy.stub().callsFake(async (method) => providerFunctions(method)),
+ }),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ signer ? ((win as any).signer = signer) : null;
+ });
+}
+
+function providerFunctions(method: string) {
+ switch (method) {
+ case "eth_requestAccounts":
+ return [beneficiary];
+ case "wallet_sendDomainMetadata":
+ return true;
+ case "wallet_addEthereumChain":
+ return true;
+ case "wallet_switchEthereumChain":
+ return true;
+ case "wallet_watchAsset":
+ return true;
+ case "eth_chainId":
+ return "0x7a69";
+ case "eth_accounts":
+ return [beneficiary];
+ case "eth_signTypedData_v4":
+ return "address";
+ case "eth_estimateGas":
+ return "0x00";
+ case "eth_sendTransaction":
+ return "0x";
+ case "eth_call":
+ return "0x";
+ }
+}
+
+// placed here due to length
+const claimUrl =
+ "?claim=W3sidHlwZSI6ImVyYzIwLXBlcm1pdCIsInBlcm1pdCI6eyJwZXJtaXR0ZWQiOnsidG9rZW4iOiIweGU5MUQxNTNFMGI0MTUxOEEyQ2U4RGQzRDc5NDRGYTg2MzQ2M2E5N2QiLCJhbW91bnQiOiIwIn0sIm5vbmNlIjoiNTM2ODA1OTA4ODcxOTE5NzkzMTM3NDczMTk1MTc3NTIyOTQ0ODQ1MTkwNDE4NjUxMzY5Mjk1Mjg3OTQyNjA5NzI3NDg5MzU2MDY1NTAiLCJkZWFkbGluZSI6IjExNTc5MjA4OTIzNzMxNjE5NTQyMzU3MDk4NTAwODY4NzkwNzg1MzI2OTk4NDY2NTY0MDU2NDAzOTQ1NzU4NDAwNzkxMzEyOTYzOTkzNSJ9LCJ0cmFuc2ZlckRldGFpbHMiOnsidG8iOiIweGYzOUZkNmU1MWFhZDg4RjZGNGNlNmFCODgyNzI3OWNmZkZiOTIyNjYiLCJyZXF1ZXN0ZWRBbW91bnQiOiIwIn0sIm93bmVyIjoiMHg3MDk5Nzk3MEM1MTgxMmRjM0EwMTBDN2QwMWI1MGUwZDE3ZGM3OUM4Iiwic2lnbmF0dXJlIjoiMHgzMTBkOGFmYWRkY2VlYTZmNWNhNGRkMmMzNGM5ZDBhNmVlMDQzYTJhMjExZDU4Y2E4ZDMxM2I4MmViZDc0YWU4MGJiYzc5ODE4Mjc2MmU1N2M3ODE2MTljZjlhYmE3Y2ZmYTJlZjJmNTBlYzk5ZThjMjY2YWEzMzA1NjdkZTI5MjFiIiwibmV0d29ya0lkIjozMTMzN30seyJ0eXBlIjoiZXJjMjAtcGVybWl0IiwicGVybWl0Ijp7InBlcm1pdHRlZCI6eyJ0b2tlbiI6IjB4ZTkxRDE1M0UwYjQxNTE4QTJDZThEZDNENzk0NEZhODYzNDYzYTk3ZCIsImFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwibm9uY2UiOiI2Nzk4MzU5OTA4NDY0NDc4MDExODU5ODU4ODIyOTc2NDQ5NTk4MzkwNzA2MDYxMTIzODM2Nzg3NzUyMzAxNjk0NzQ5MzcyNjY4NjU5OCIsImRlYWRsaW5lIjoiMTE1NzkyMDg5MjM3MzE2MTk1NDIzNTcwOTg1MDA4Njg3OTA3ODUzMjY5OTg0NjY1NjQwNTY0MDM5NDU3NTg0MDA3OTEzMTI5NjM5OTM1In0sInRyYW5zZmVyRGV0YWlscyI6eyJ0byI6IjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2NiIsInJlcXVlc3RlZEFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwib3duZXIiOiIweDcwOTk3OTcwQzUxODEyZGMzQTAxMEM3ZDAxYjUwZTBkMTdkYzc5QzgiLCJzaWduYXR1cmUiOiIweDMxMGQ4YWZhZGRjZWVhNmY1Y2E0ZGQyYzM0YzlkMGE2ZWUwNDNhMmEyMTFkNThjYThkMzEzYjgyZWJkNzRhZTgwYmJjNzk4MTgyNzYyZTU3Yzc4MTYxOWNmOWFiYTdjZmZhMmVmMmY1MGVjOTllOGMyNjZhYTMzMDU2N2RlMjkyMWIiLCJuZXR3b3JrSWQiOjMxMzM3fV0=";
diff --git a/cypress/e2e/claim-portal-non-web3.cy.ts b/cypress/e2e/claim-portal-non-web3.cy.ts
new file mode 100644
index 00000000..3b9da74c
--- /dev/null
+++ b/cypress/e2e/claim-portal-non-web3.cy.ts
@@ -0,0 +1,75 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+describe("Claims Portal Non-Web3", () => {
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+
+ setupIntercepts();
+
+ cy.visit(`/${claimUrl}`);
+ cy.wait(2000);
+ });
+
+ describe("No window.ethereum", () => {
+ it("Should toast and hide buttons in a non-web3 env", () => {
+ cy.get("#invalidator").should("not.be.visible");
+ cy.get("#claim-loader").should("not.be.visible");
+ cy.get("#view-claim").should("not.be.visible");
+
+ cy.get("body", { timeout: 3000 }).should("contain.text", "Please use a web3 enabled browser to collect this reward.");
+ });
+ });
+
+ describe("Mobile: No window.ethereum", () => {
+ beforeEach(() => {
+ cy.viewport("iphone-6");
+ cy.reload();
+ });
+
+ it("Should toast and hide buttons in a non-web3 env", () => {
+ cy.get("#invalidator").should("not.be.visible");
+ cy.get("#claim-loader").should("not.be.visible");
+ cy.get("#view-claim").should("not.be.visible");
+
+ cy.get("body", { timeout: 3000 }).should("contain.text", "Please use a web3 enabled browser to collect this reward.");
+ });
+ });
+});
+
+function setupIntercepts() {
+ cy.intercept("POST", "*", (req) => {
+ // return a 404 for rpc optimization meaning no successful RPC
+ // to return our balanceOf and allowance calls
+ if (req.body.method === "eth_getBlockByNumber") {
+ req.reply({
+ statusCode: 404,
+ body: {
+ jsonrpc: "2.0",
+ error: {
+ code: -32601,
+ message: "Method not found",
+ },
+ id: 1,
+ },
+ });
+ }
+ });
+
+ cy.intercept("POST", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("PATCH", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("GET", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+}
+
+// placed here due to length
+const claimUrl =
+ "?claim=W3sidHlwZSI6ImVyYzIwLXBlcm1pdCIsInBlcm1pdCI6eyJwZXJtaXR0ZWQiOnsidG9rZW4iOiIweGU5MUQxNTNFMGI0MTUxOEEyQ2U4RGQzRDc5NDRGYTg2MzQ2M2E5N2QiLCJhbW91bnQiOiIxMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6IjEwODc2OTM3ODM4MTQ4OTY1NTIxMDM2ODQ4NzgzNzgzMDA2MDU0MjAwMzcxOTM0NTY0MzYzMjQ5MDIzMTQ1MTcyOTczMTgzNDgwMTM5MiIsImRlYWRsaW5lIjoiMTE1NzkyMDg5MjM3MzE2MTk1NDIzNTcwOTg1MDA4Njg3OTA3ODUzMjY5OTg0NjY1NjQwNTY0MDM5NDU3NTg0MDA3OTEzMTI5NjM5OTM1In0sInRyYW5zZmVyRGV0YWlscyI6eyJ0byI6IjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2NiIsInJlcXVlc3RlZEFtb3VudCI6IjEwMDAwMDAwMDAwMDAwMDAwIn0sIm93bmVyIjoiMHg3MDk5Nzk3MEM1MTgxMmRjM0EwMTBDN2QwMWI1MGUwZDE3ZGM3OUM4Iiwic2lnbmF0dXJlIjoiMHg4YWZmYWU1ZTA5YTkyN2QwYjUzNDQ1M2Y4NTE5ZWVlZDE5MzY5MTBkZWFhOGY5YTA0OTM1ODQzNDMzNDA5NmExMTg5ZmVkM2MxNzgyZmU0ZGI5ZTNhMDg2NWVkYjc3ZDczYzliMDliOTgxMTBmN2Q0ZWEyY2Y5ZDBhM2Q1YjhjYzFjIiwibmV0d29ya0lkIjozMTMzN30seyJ0eXBlIjoiZXJjMjAtcGVybWl0IiwicGVybWl0Ijp7InBlcm1pdHRlZCI6eyJ0b2tlbiI6IjB4ZTkxRDE1M0UwYjQxNTE4QTJDZThEZDNENzk0NEZhODYzNDYzYTk3ZCIsImFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwibm9uY2UiOiI1NjQzNjc4ODI2MzUwOTQ3NTY2NzAwNzA4MDA5ODQ5MDM0MDE1OTExMzYxMjM5NTUyMTA3Mjk3NDkxNzcyNDA2Mzg0NDY2Mjc0NDEzMiIsImRlYWRsaW5lIjoiMTE1NzkyMDg5MjM3MzE2MTk1NDIzNTcwOTg1MDA4Njg3OTA3ODUzMjY5OTg0NjY1NjQwNTY0MDM5NDU3NTg0MDA3OTEzMTI5NjM5OTM1In0sInRyYW5zZmVyRGV0YWlscyI6eyJ0byI6IjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2NiIsInJlcXVlc3RlZEFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwib3duZXIiOiIweDcwOTk3OTcwQzUxODEyZGMzQTAxMEM3ZDAxYjUwZTBkMTdkYzc5QzgiLCJzaWduYXR1cmUiOiIweDhhZmZhZTVlMDlhOTI3ZDBiNTM0NDUzZjg1MTllZWVkMTkzNjkxMGRlYWE4ZjlhMDQ5MzU4NDM0MzM0MDk2YTExODlmZWQzYzE3ODJmZTRkYjllM2EwODY1ZWRiNzdkNzNjOWIwOWI5ODExMGY3ZDRlYTJjZjlkMGEzZDViOGNjMWMiLCJuZXR3b3JrSWQiOjMxMzM3fV0=";
diff --git a/cypress/e2e/claim-portal-success.cy.ts b/cypress/e2e/claim-portal-success.cy.ts
new file mode 100644
index 00000000..16f13255
--- /dev/null
+++ b/cypress/e2e/claim-portal-success.cy.ts
@@ -0,0 +1,155 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers";
+import { Wallet } from "ethers";
+
+const beneficiary = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // anvil
+const SENDER_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; // anvil
+
+describe("Claims Portal Success", () => {
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+ setupStubs();
+
+ setupIntercepts();
+
+ cy.visit(`/${claimUrl}`);
+ cy.wait(2000);
+ });
+ describe("Success", () => {
+ it("should successfully claim a permit", () => {
+ cy.get("#additionalDetails", { timeout: 15000 }).should("be.visible").invoke("click");
+
+ cy.get('table[data-make-claim="ok"]').should("exist").and("include.text", "337888.4 WXDAI");
+
+ cy.get("button[id='make-claim']").invoke("click");
+
+ cy.get("#invalidator").should("not.be.visible");
+
+ cy.get("#claim-loader").should("be.visible").as("loader");
+
+ cy.wait(5000); // required for the action to complete
+
+ cy.get("@loader").should("not.be.visible");
+
+ cy.get("#view-claim").should("be.visible").and("include.text", "View Claim");
+
+ // anvil confirms it instantly so there is two notifications
+ cy.get("body").should("contain.text", "Transaction sent");
+ cy.get("body").should("contain.text", "Claim Complete");
+ });
+ });
+
+ describe("Not meant for you", () => {
+ it("should fail to claim a permit not for meant for them", () => {
+ cy.visit(`/${notMeantForYouPermit}`).then(() => {
+ cy.wait(2000);
+ });
+ cy.get("#additionalDetails", { timeout: 15000 }).should("be.visible").invoke("click");
+
+ cy.get('table[data-make-claim="ok"]').should("exist");
+
+ cy.get("button[id='make-claim']").invoke("click");
+
+ cy.get("#invalidator").should("not.be.visible");
+
+ cy.get("#claim-loader").should("be.visible");
+
+ cy.get("#view-claim").should("not.be.visible");
+
+ cy.get("body").should("contain.text", "This reward is not for you");
+ });
+ });
+});
+
+function setupStubs() {
+ const provider = new JsonRpcProvider("http://localhost:8545");
+ const signer = provider.getSigner(beneficiary);
+ const wallet = new Wallet(SENDER_PRIVATE_KEY, provider);
+
+ stubEthereum(signer);
+
+ return { provider, signer, wallet };
+}
+
+function setupIntercepts() {
+ cy.intercept("POST", "*", (req) => {
+ // capturing the RPC optimization calls
+ if (req.body.method === "eth_getBlockByNumber") {
+ req.reply({
+ statusCode: 200,
+ body: cy.fixture("eth_getBlockByNumber.json"),
+ });
+ }
+ });
+
+ cy.intercept("POST", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("PATCH", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+ cy.intercept("GET", "https://wfzpewmlyiozupulbuur.supabase.co/rest/v1/*", {
+ statusCode: 200,
+ body: {},
+ });
+}
+
+function stubEthereum(signer: JsonRpcSigner) {
+ // Stubbing the ethereum object
+ cy.on("window:before:load", (win) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ((win as any).ethereum = {
+ isMetaMask: true,
+ enable: cy.stub().resolves(["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]),
+ request: cy.stub().callsFake(async (method) => providerFunctions(method)),
+ on: cy.stub().callsFake((event, cb) => {
+ if (event === "accountsChanged") {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (win as any).ethereum.onAccountsChanged = cb;
+ }
+ }),
+ autoRefreshOnNetworkChange: false,
+ chainId: "0x7a69",
+ selectedAddress: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ requestAccounts: cy.stub().resolves(["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]),
+ send: cy.stub().callsFake(async (method) => providerFunctions(method)),
+ getSigner: () => signer,
+ }),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ((win as any).signer = signer);
+ });
+}
+
+function providerFunctions(method: string) {
+ switch (method) {
+ case "eth_requestAccounts":
+ return ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"];
+ case "wallet_sendDomainMetadata":
+ return true;
+ case "wallet_addEthereumChain":
+ return true;
+ case "wallet_switchEthereumChain":
+ return true;
+ case "wallet_watchAsset":
+ return true;
+ case "eth_chainId":
+ return "0x7a69";
+ case "eth_accounts":
+ return ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"];
+ case "eth_signTypedData_v4":
+ return "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
+ case "eth_estimateGas":
+ return "0x7a69";
+ }
+}
+
+// placed here due to length
+const claimUrl =
+ "?claim=W3sidHlwZSI6ImVyYzIwLXBlcm1pdCIsInBlcm1pdCI6eyJwZXJtaXR0ZWQiOnsidG9rZW4iOiIweGU5MUQxNTNFMGI0MTUxOEEyQ2U4RGQzRDc5NDRGYTg2MzQ2M2E5N2QiLCJhbW91bnQiOiIxMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6IjEwODc2OTM3ODM4MTQ4OTY1NTIxMDM2ODQ4NzgzNzgzMDA2MDU0MjAwMzcxOTM0NTY0MzYzMjQ5MDIzMTQ1MTcyOTczMTgzNDgwMTM5MiIsImRlYWRsaW5lIjoiMTE1NzkyMDg5MjM3MzE2MTk1NDIzNTcwOTg1MDA4Njg3OTA3ODUzMjY5OTg0NjY1NjQwNTY0MDM5NDU3NTg0MDA3OTEzMTI5NjM5OTM1In0sInRyYW5zZmVyRGV0YWlscyI6eyJ0byI6IjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2NiIsInJlcXVlc3RlZEFtb3VudCI6IjEwMDAwMDAwMDAwMDAwMDAwIn0sIm93bmVyIjoiMHg3MDk5Nzk3MEM1MTgxMmRjM0EwMTBDN2QwMWI1MGUwZDE3ZGM3OUM4Iiwic2lnbmF0dXJlIjoiMHg4YWZmYWU1ZTA5YTkyN2QwYjUzNDQ1M2Y4NTE5ZWVlZDE5MzY5MTBkZWFhOGY5YTA0OTM1ODQzNDMzNDA5NmExMTg5ZmVkM2MxNzgyZmU0ZGI5ZTNhMDg2NWVkYjc3ZDczYzliMDliOTgxMTBmN2Q0ZWEyY2Y5ZDBhM2Q1YjhjYzFjIiwibmV0d29ya0lkIjozMTMzN30seyJ0eXBlIjoiZXJjMjAtcGVybWl0IiwicGVybWl0Ijp7InBlcm1pdHRlZCI6eyJ0b2tlbiI6IjB4ZTkxRDE1M0UwYjQxNTE4QTJDZThEZDNENzk0NEZhODYzNDYzYTk3ZCIsImFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwibm9uY2UiOiI1NjQzNjc4ODI2MzUwOTQ3NTY2NzAwNzA4MDA5ODQ5MDM0MDE1OTExMzYxMjM5NTUyMTA3Mjk3NDkxNzcyNDA2Mzg0NDY2Mjc0NDEzMiIsImRlYWRsaW5lIjoiMTE1NzkyMDg5MjM3MzE2MTk1NDIzNTcwOTg1MDA4Njg3OTA3ODUzMjY5OTg0NjY1NjQwNTY0MDM5NDU3NTg0MDA3OTEzMTI5NjM5OTM1In0sInRyYW5zZmVyRGV0YWlscyI6eyJ0byI6IjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2NiIsInJlcXVlc3RlZEFtb3VudCI6IjkwMDAwMDAwMDAwMDAwMDAwMDAifSwib3duZXIiOiIweDcwOTk3OTcwQzUxODEyZGMzQTAxMEM3ZDAxYjUwZTBkMTdkYzc5QzgiLCJzaWduYXR1cmUiOiIweDhhZmZhZTVlMDlhOTI3ZDBiNTM0NDUzZjg1MTllZWVkMTkzNjkxMGRlYWE4ZjlhMDQ5MzU4NDM0MzM0MDk2YTExODlmZWQzYzE3ODJmZTRkYjllM2EwODY1ZWRiNzdkNzNjOWIwOWI5ODExMGY3ZDRlYTJjZjlkMGEzZDViOGNjMWMiLCJuZXR3b3JrSWQiOjMxMzM3fV0=";
+
+const notMeantForYouPermit =
+ "?claim=W3sidHlwZSI6ImVyYzIwLXBlcm1pdCIsInBlcm1pdCI6eyJwZXJtaXR0ZWQiOnsidG9rZW4iOiIweGU5MUQxNTNFMGI0MTUxOEEyQ2U4RGQzRDc5NDRGYTg2MzQ2M2E5N2QiLCJhbW91bnQiOiIxMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6IjkxOTk3MjEyMjMyMTcxMDcyMTI5OTIwODIzNjMwOTY3ODE5ODgwNTcyNjcyMTc2ODcwNjU4MzE2Nzk4MjUxNzU4OTQ2MzQ1NDY2OTA1IiwiZGVhZGxpbmUiOiIxMTU3OTIwODkyMzczMTYxOTU0MjM1NzA5ODUwMDg2ODc5MDc4NTMyNjk5ODQ2NjU2NDA1NjQwMzk0NTc1ODQwMDc5MTMxMjk2Mzk5MzUifSwidHJhbnNmZXJEZXRhaWxzIjp7InRvIjoiMHhiYTEyMjIyMjIyMjI4ZDhiYTQ0NTk1OGE3NWEwNzA0ZDU2NmJmMmM4IiwicmVxdWVzdGVkQW1vdW50IjoiMTAwMDAwMDAwMDAwMDAwMDAifSwib3duZXIiOiIweDcwOTk3OTcwQzUxODEyZGMzQTAxMEM3ZDAxYjUwZTBkMTdkYzc5QzgiLCJzaWduYXR1cmUiOiIweDNiYzBjOTA5NzA1NmRhNmJkMzA4NmM4MGRiM2RmZDAzODNjNjgxN2FlZTAwMDExZDFlYTI3NzFkZWVlYjUxNjg1MWE3ZmYyY2UzNGUxNmI1ZjFkNTY1NGRmYzQ5MTk1YjQ4YmE5YmY1YmY0YTllMGRlOGY4ODc3YjBkMTY4NGRmMWMiLCJuZXR3b3JrSWQiOjMxMzM3fSx7InR5cGUiOiJlcmMyMC1wZXJtaXQiLCJwZXJtaXQiOnsicGVybWl0dGVkIjp7InRva2VuIjoiMHhlOTFEMTUzRTBiNDE1MThBMkNlOERkM0Q3OTQ0RmE4NjM0NjNhOTdkIiwiYW1vdW50IjoiOTAwMDAwMDAwMDAwMDAwMDAwMCJ9LCJub25jZSI6Ijc3MDE3MDM2NzU4NzI2MzU5MTc4ODExMjM0Njk4NTMxMjE2NjYwODc4NzU0NjMwMDc4NzAzMDY4NzA4NzM3MDEzNTYxODIxMDQwODkwIiwiZGVhZGxpbmUiOiIxMTU3OTIwODkyMzczMTYxOTU0MjM1NzA5ODUwMDg2ODc5MDc4NTMyNjk5ODQ2NjU2NDA1NjQwMzk0NTc1ODQwMDc5MTMxMjk2Mzk5MzUifSwidHJhbnNmZXJEZXRhaWxzIjp7InRvIjoiMHhiYTEyMjIyMjIyMjI4ZDhiYTQ0NTk1OGE3NWEwNzA0ZDU2NmJmMmM4IiwicmVxdWVzdGVkQW1vdW50IjoiOTAwMDAwMDAwMDAwMDAwMDAwMCJ9LCJvd25lciI6IjB4NzA5OTc5NzBDNTE4MTJkYzNBMDEwQzdkMDFiNTBlMGQxN2RjNzlDOCIsInNpZ25hdHVyZSI6IjB4M2JjMGM5MDk3MDU2ZGE2YmQzMDg2YzgwZGIzZGZkMDM4M2M2ODE3YWVlMDAwMTFkMWVhMjc3MWRlZWViNTE2ODUxYTdmZjJjZTM0ZTE2YjVmMWQ1NjU0ZGZjNDkxOTViNDhiYTliZjViZjRhOWUwZGU4Zjg4NzdiMGQxNjg0ZGYxYyIsIm5ldHdvcmtJZCI6MzEzMzd9XQ==";
diff --git a/cypress/fixtures/erc20-permits.json b/cypress/fixtures/erc20-permits.json
new file mode 100644
index 00000000..fb056520
--- /dev/null
+++ b/cypress/fixtures/erc20-permits.json
@@ -0,0 +1,38 @@
+[
+ {
+ "type": "erc20-permit",
+ "permit": {
+ "permitted": {
+ "token": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
+ "amount": "95255555000000000000"
+ },
+ "nonce": "36367598329506163629701178851688339747065676639093029479054969572911320530479",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
+ },
+ "transferDetails": {
+ "to": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ "requestedAmount": "95255555000000000000"
+ },
+ "owner": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "signature": "0x02fcd7e46df6f4e0437f8a309bd8dbcdbd66de04ed47f308db9548cf609909ce0b8e4d2e5031ff5daf959fc09cbfc95d606493d0ecd7ec0e9d6def4300a909f81b",
+ "networkId": 100
+ },
+ {
+ "type": "erc20-permit",
+ "permit": {
+ "permitted": {
+ "token": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
+ "amount": "95255555000000000000"
+ },
+ "nonce": "36367598329506163629701178851688339747065676639093029479054969572911320530479",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
+ },
+ "transferDetails": {
+ "to": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ "requestedAmount": "95255555000000000000"
+ },
+ "owner": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "signature": "0x02fcd7e46df6f4e0437f8a309bd8dbcdbd66de04ed47f308db9548cf609909ce0b8e4d2e5031ff5daf959fc09cbfc95d606493d0ecd7ec0e9d6def4300a909f81b",
+ "networkId": 100
+ }
+]
diff --git a/cypress/fixtures/erc721-permits.json b/cypress/fixtures/erc721-permits.json
new file mode 100644
index 00000000..4e581b8a
--- /dev/null
+++ b/cypress/fixtures/erc721-permits.json
@@ -0,0 +1,78 @@
+[
+ {
+ "type": "erc721-permit",
+ "permit": {
+ "permitted": {
+ "token": "0xAa1bfC0e51969415d64d6dE74f27CDa0587e645b",
+ "amount": "1"
+ },
+ "nonce": "313327",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
+ },
+ "transferDetails": {
+ "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "requestedAmount": "1"
+ },
+ "owner": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "signature": "0xfc56bee3c8f90cb0a5a528808f37883ac1c88e21b59647e6fab2640c34692a9636914cbc98b15b4f43d95d2c69c6832ce19e150c809cf14dc9f06e6025ee649f1b",
+ "networkId": 100,
+ "nftMetadata": {
+ "GITHUB_ORGANIZATION_NAME": "ubiquity",
+ "GITHUB_REPOSITORY_NAME": "pay.ubq.fi",
+ "GITHUB_ISSUE_ID": "1",
+ "GITHUB_USERNAME": "testing",
+ "GITHUB_CONTRIBUTION_TYPE": "issue"
+ },
+ "request": {
+ "beneficiary": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
+ "keys": [
+ "0x1c474488c03c83ad98714cfe3a60c752036f92ab8378227adfcb13585f115c5c",
+ "0x0480f34997cc2d3abc2dafd652b25652ba845458fc3e3692f28acbd4920a37ea",
+ "0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6",
+ "0x5f16f4c7f149ac4f9510d9cf8cf384038ad348b3bcdc01915f95de12df9d1b02",
+ "0xa33d0fabbfddf3db8e1458550edc0ab7e061990c85809d25341eab0885973d7d"
+ ],
+ "nonce": "313327",
+ "values": ["ubiquity", "pay.ubq.fi", "1", "testing", "issue"]
+ }
+ },
+ {
+ "type": "erc721-permit",
+ "permit": {
+ "permitted": {
+ "token": "0xAa1bfC0e51969415d64d6dE74f27CDa0587e645b",
+ "amount": "1"
+ },
+ "nonce": "3137",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
+ },
+ "transferDetails": {
+ "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "requestedAmount": "1"
+ },
+ "owner": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "signature": "0x49ef799bc3e859d8b850f58f696dd44a89c4209ec5677ce1aa6cb0b1312a3a1b14a594ef7af293efffeef0669c2817e7c4031cffe035ce084e34dd5a4d32ea5f1b",
+ "networkId": 100,
+ "nftMetadata": {
+ "GITHUB_ORGANIZATION_NAME": "ubiquity",
+ "GITHUB_REPOSITORY_NAME": "pay.ubq.fi",
+ "GITHUB_ISSUE_ID": "1",
+ "GITHUB_USERNAME": "testing",
+ "GITHUB_CONTRIBUTION_TYPE": "issue"
+ },
+ "request": {
+ "beneficiary": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "deadline": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
+ "keys": [
+ "0x1c474488c03c83ad98714cfe3a60c752036f92ab8378227adfcb13585f115c5c",
+ "0x0480f34997cc2d3abc2dafd652b25652ba845458fc3e3692f28acbd4920a37ea",
+ "0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6",
+ "0x5f16f4c7f149ac4f9510d9cf8cf384038ad348b3bcdc01915f95de12df9d1b02",
+ "0xa33d0fabbfddf3db8e1458550edc0ab7e061990c85809d25341eab0885973d7d"
+ ],
+ "nonce": "3137",
+ "values": ["ubiquity", "pay.ubq.fi", "1", "testing", "issue"]
+ }
+ }
+]
diff --git a/cypress/fixtures/get-block-number.json b/cypress/fixtures/get-block-number.json
new file mode 100644
index 00000000..a86ead3e
--- /dev/null
+++ b/cypress/fixtures/get-block-number.json
@@ -0,0 +1,96 @@
+{
+ "jsonrpc": "2.0",
+ "result": {
+ "author": "0x54e191b01aa9c1f61aa5c3bce8d00956f32d3e71",
+ "difficulty": "0x0",
+ "extraData": "0x4e65746865726d696e64",
+ "gasLimit": "0x1036640",
+ "gasUsed": "0x6763e5",
+ "hash": "0x407c72c5ae29ac6ffe6e3cefc592bf8d2bf4a8e0057ccfd1c4c7f11ab365977e",
+ "logsBloom": "0x640beb0d6578149cea2f950699258ce63d056392a4d0d2701834000a48381453bb508c109686c16aa69094201a0a45092c35a83549352240e42020b8560ff8e8027b89170c0713a4829dd68d009c805ada3edf8f9b3659464f0b4481408a6539006de405824712c52e5017a82d83ea100200ce70617cb003a5d397d00ae5044bb21ec5d0a152ad6168e463941a539082441dc04bfa5405c3031ab821b6d2565605e2c444a798ca0e5a82222b1723c0030340250187cc5905604527122707c500a00f951648950e600490a29601d3430224b87b03680a55d44080913ca64d6e3212c44645088c4194ade9576565a57c31f6005c1351783059a51969601c56b4c9",
+ "miner": "0x54e191b01aa9c1f61aa5c3bce8d00956f32d3e71",
+ "mixHash": "0x285fc6bec5ed8d335f2fe4f1e8526cfebf2f650de390d24a80d7a26353a86263",
+ "nonce": "0x0000000000000000",
+ "number": "0x1faad3c",
+ "parentHash": "0x2b0482bcb01362281ea62af8f424020c60b5b2d091ba3a91c2c5409facf3f4b5",
+ "receiptsRoot": "0x19b2d7960b3aa6d85bb89c796eed7e563f98879bb2923781514bc1a2e9afcab2",
+ "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ "size": "0x3dd5",
+ "stateRoot": "0x4bbbf4dab0b363cd3a469ee23f2ba34c04fb9531a7812f35d19a456b95ce2df3",
+ "totalDifficulty": "0x182cd9fffffffffffffffffffffffffea9528a2",
+ "timestamp": "0x66093cb8",
+ "baseFeePerGas": "0x49",
+ "transactions": [
+ "0x6dd7181abf5c33ef988a846e13c3cd079ae9070aacef2235885c95ecbf659dd2",
+ "0x5841c70fd3471baece987021f09b5c7fbb8eb5abd6b69a80be0a9e47a6bb2a7c",
+ "0xc0cb042f01e692db2ea8b1910220cdc6184f365f6e0dfe3938c225f556017d02",
+ "0xb51a6e7a02566b2ef0a85c688adf020c8a247e7d408bf7793c888c011eea0d8a",
+ "0x1bb4985d19175fc3431526c9ce01f423e47a49c674de58cad75950e343e90c69",
+ "0x9144a9e63da9787961a61ee26df25b3c468fc7da40e12e9b80d5c398e9d4728a",
+ "0xc176772b1f8280dd86239d435f4485c4d7041d61f5d1cf9787632c80775f38e8",
+ "0xccb751a6ec7f12c5213c0f3293573754c096d02f88599f9814fd285ec5c4769c",
+ "0x39b88eda15263cc35866775a2575f6aa537507e60e1e871ea9780fc304ac9200",
+ "0x8bf8a95ba856c20e5d0fc01d9b2625fb0b3b56c9773969903eb9d94b87bdfa5c",
+ "0x47509dd87254961d30e28941bffcf3d670a7b5e16076b06f38e636cf5246a61b",
+ "0xe7fd644dc604ecdaa81e1f956b98ad870f526c30fca6c6581afa2639b199d554",
+ "0x418a3b92316cf510ed8a015bd2d95c1989c504eea5952da98e6b0677ef30bbae"
+ ],
+ "transactionsRoot": "0xb4a1895451a4b3a9b7ff72f7226604289aa54043c22141d29d7e697311fe8f71",
+ "uncles": [],
+ "withdrawals": [
+ {
+ "index": "0x1e3b6bd",
+ "validatorIndex": "0x2eea4",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb710e7"
+ },
+ {
+ "index": "0x1e3b6be",
+ "validatorIndex": "0x2eea5",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb78afa"
+ },
+ {
+ "index": "0x1e3b6bf",
+ "validatorIndex": "0x2eea6",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb6f51c"
+ },
+ {
+ "index": "0x1e3b6c0",
+ "validatorIndex": "0x2eea7",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb6de93"
+ },
+ {
+ "index": "0x1e3b6c1",
+ "validatorIndex": "0x2eea8",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb7ab98"
+ },
+ {
+ "index": "0x1e3b6c2",
+ "validatorIndex": "0x2eea9",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb7a5f6"
+ },
+ {
+ "index": "0x1e3b6c3",
+ "validatorIndex": "0x2eeaa",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb6f80a"
+ },
+ {
+ "index": "0x1e3b6c4",
+ "validatorIndex": "0x2eeab",
+ "address": "0x9f03bfbdf1e026cedb7f606e740a0b3aa16044e8",
+ "amount": "0xb73e9b"
+ }
+ ],
+ "withdrawalsRoot": "0xa22437a0b85a24e7844bdacd756525c8868ff16f707fe55fcd7e714467cce022",
+ "blobGasUsed": "0x0",
+ "excessBlobGas": "0x0",
+ "parentBeaconBlockRoot": "0xe7d34f48290317240c3ef8f58427fa95f641bd3b56b405c1142301e62ab58e9f"
+ },
+ "id": 1
+}
diff --git a/cypress/scripts/anvil.ts b/cypress/scripts/anvil.ts
new file mode 100644
index 00000000..d8c22635
--- /dev/null
+++ b/cypress/scripts/anvil.ts
@@ -0,0 +1,80 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { spawn } from "child_process";
+
+const url = "http://localhost:8545";
+
+const anvil = spawn("anvil", ["--chain-id", "31337", "--fork-url", "https://gnosis.drpc.org", "--host", "127.0.0.1", "--port", "8545"], {
+ stdio: "inherit",
+});
+
+setTimeout(() => {
+ console.log(`\n\n Anvil setup complete \n\n`);
+}, 5000);
+
+// anvil --chain-id 31337 --fork-url https://eth.llamarpc.com --host 127.0.0.1 --port 8546
+
+spawn("cast", ["rpc", "--rpc-url", url, "anvil_impersonateAccount", "0xba12222222228d8ba445958a75a0704d566bf2c8"], {
+ stdio: "inherit",
+});
+spawn(
+ "cast",
+ [
+ "send",
+ "--rpc-url",
+ url,
+ "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
+ "--unlocked",
+ "--from",
+ "0xba12222222228d8ba445958a75a0704d566bf2c8",
+ "transfer(address,uint256)(bool)",
+ "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "337888400000000000000000",
+ ],
+ {
+ stdio: "inherit",
+ }
+);
+spawn(
+ "cast",
+ [
+ "send",
+ "--rpc-url",
+ url,
+ "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
+ "--unlocked",
+ "--from",
+ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ "approve(address,uint256)(bool)",
+ "0x000000000022D473030F116dDEE9F6B43aC78BA3",
+ "9999999999999991111111119999999999999999",
+ ],
+ {
+ stdio: "inherit",
+ }
+);
+spawn(
+ "cast",
+ [
+ "send",
+ "--rpc-url",
+ url,
+ "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
+ "--unlocked",
+ "--from",
+ "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "approve(address,uint256)(bool)",
+ "0x000000000022D473030F116dDEE9F6B43aC78BA3",
+ "999999999999999111119999999999999999",
+ ],
+ {
+ stdio: "inherit",
+ }
+);
+
+anvil.on("close", (code) => {
+ console.log(`Anvil exited with code ${code}`);
+});
+
+anvil.on("error", (err) => {
+ console.error("Failed to start Anvil", err);
+});
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
new file mode 100644
index 00000000..95857aea
--- /dev/null
+++ b/cypress/support/commands.ts
@@ -0,0 +1,37 @@
+///
+// ***********************************************
+// This example commands.ts shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+//
+// declare global {
+// namespace Cypress {
+// interface Chainable {
+// login(email: string, password: string): Chainable
+// drag(subject: string, options?: Partial): Chainable
+// dismiss(subject: string, options?: Partial): Chainable
+// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
+// }
+// }
+// }
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
new file mode 100644
index 00000000..6a173d6f
--- /dev/null
+++ b/cypress/support/e2e.ts
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/e2e.ts is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import "./commands";
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/package.json b/package.json
index e78109ca..67095103 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,12 @@
"format:prettier": "prettier --write .",
"format:cspell": "cspell **/*",
"prepare": "husky install",
- "postinstall": "git submodule update --init --recursive"
+ "postinstall": "git submodule update --init --recursive",
+ "test:anvil": "tsx cypress/scripts/anvil.ts",
+ "test:start": "yarn start",
+ "test:run": "cypress run",
+ "test:open": "cypress open",
+ "test:fund": "cast rpc --rpc-url http://127.0.0.1:8545 anvil_impersonateAccount 0xba12222222228d8ba445958a75a0704d566bf2c8 & cast send --rpc-url http://127.0.0.1:8545 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0xba12222222228d8ba445958a75a0704d566bf2c8 'transfer(address,uint256)(bool)' 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 337888400000000000000000 & cast send --rpc-url http://127.0.0.1:8545 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'approve(address,uint256)(bool)' 0x000000000022D473030F116dDEE9F6B43aC78BA3 9999999999999991111111119999999999999999 & cast send --rpc-url http://127.0.0.1:8545 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 'approve(address,uint256)(bool)' 0x000000000022D473030F116dDEE9F6B43aC78BA3 999999999999999111119999999999999999"
},
"keywords": [
"typescript",
@@ -40,6 +45,7 @@
"axios": "^1.6.7",
"dotenv": "^16.4.4",
"ethers": "^5.7.2",
+ "node-fetch": "^3.3.2",
"npm-run-all": "^4.1.5"
},
"devDependencies": {
@@ -53,6 +59,7 @@
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"cspell": "^8.3.2",
+ "cypress": "13.7.0",
"esbuild": "^0.20.0",
"eslint": "^8.56.0",
"eslint-plugin-sonarjs": "^0.24.0",
diff --git a/scripts/typescript/generate-erc20-permit-url.ts b/scripts/typescript/generate-erc20-permit-url.ts
index 36dc3a5f..fb23fec0 100644
--- a/scripts/typescript/generate-erc20-permit-url.ts
+++ b/scripts/typescript/generate-erc20-permit-url.ts
@@ -61,10 +61,10 @@ export async function generateERC20Permit() {
const permitTransferFromData = createPermitTransferFromData(process.env.AMOUNT_IN_ETH);
const signature = await signTypedData(myWallet, permitTransferFromData);
- // const permitTransferFromData2 = createPermitTransferFromData("9");
+ const permitTransferFromData2 = createPermitTransferFromData("9");
const sig = await signTypedData(myWallet, permitTransferFromData);
- const txData = [createTxData(myWallet, permitTransferFromData, signature), createTxData(myWallet, permitTransferFromData, sig)];
+ const txData = [createTxData(myWallet, permitTransferFromData, signature), createTxData(myWallet, permitTransferFromData2, sig)];
const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64");
diff --git a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts
index b7f885a6..b6654236 100644
--- a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts
+++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts
@@ -35,6 +35,7 @@ export async function readClaimDataFromUrl(app: AppState) {
} catch (e) {
toaster.create("error", `${e}`);
}
+
if (window.ethereum) {
try {
app.signer = await connectWallet();
diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts
index 81a29cb7..93167bb4 100644
--- a/static/scripts/rewards/web3/connect-wallet.ts
+++ b/static/scripts/rewards/web3/connect-wallet.ts
@@ -20,6 +20,10 @@ export async function connectWallet(): Promise {
return signer;
} catch (error: unknown) {
+ // For testing purposes
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (window.location.href.includes("localhost") && (window as any).signer) return (window as any).signer;
+
if (error instanceof Error) {
console.error(error);
if (error?.message?.includes("missing provider")) {
diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts
index 116140df..1a9a54c7 100644
--- a/static/scripts/rewards/web3/erc20-permit.ts
+++ b/static/scripts/rewards/web3/erc20-permit.ts
@@ -119,6 +119,8 @@ export function claimErc20PermitHandlerWrapper(app: AppState) {
const isPermitClaimable = await checkPermitClaimability(app);
if (!isPermitClaimable) return;
+ if (!app.signer) return;
+
const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, app.signer);
if (!permit2Contract) return;
@@ -177,9 +179,10 @@ async function checkPermitClaimable(app: AppState): Promise {
return false;
}
- let user: string;
+ let user: string | undefined;
try {
- user = (await app.signer.getAddress()).toLowerCase();
+ const address = await app.signer?.getAddress();
+ user = address?.toLowerCase();
} catch (error: unknown) {
console.error("Error in signer.getAddress: ", error);
return false;
@@ -197,8 +200,8 @@ async function checkPermitClaimable(app: AppState): Promise {
export async function checkRenderMakeClaimControl(app: AppState) {
try {
- const address = await app.signer.getAddress();
- const user = address.toLowerCase();
+ const address = await app.signer?.getAddress();
+ const user = address?.toLowerCase();
if (app.reward) {
const beneficiary = app.reward.beneficiary.toLowerCase();
@@ -216,8 +219,8 @@ export async function checkRenderMakeClaimControl(app: AppState) {
export async function checkRenderInvalidatePermitAdminControl(app: AppState) {
try {
- const address = await app.signer.getAddress();
- const user = address.toLowerCase();
+ const address = await app.signer?.getAddress();
+ const user = address?.toLowerCase();
if (app.reward) {
const owner = app.reward.owner.toLowerCase();
@@ -243,7 +246,9 @@ invalidateButton.addEventListener("click", async function invalidateButtonClickH
buttonController.hideInvalidator();
return;
}
- await invalidateNonce(app.signer, app.reward.nonce);
+
+ if (!app.signer) return;
+ await invalidateNonce(app.signer, app.reward.permit.nonce);
} catch (error: unknown) {
if (error instanceof Error) {
const e = error as unknown as MetaMaskError;
diff --git a/tsconfig.json b/tsconfig.json
index d22ebe77..18ac9fe6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,5 @@
{
- "include": ["src", "static", "build", "scripts/typescript", "globals.d.ts"],
+ "include": ["src", "static", "build", "scripts/typescript", "globals.d.ts", "cypress/**/*.ts", "cypress.config.ts"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
@@ -25,12 +25,12 @@
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
- "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
- // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ "types": ["cypress"] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */