From aab3f0c3891257ee74c59b6b7fafaaf7d8dc08a4 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:58:38 +0100 Subject: [PATCH 1/4] feat: better mobile feedback --- .../read-claim-data-from-url.ts | 28 +++++++++------ static/scripts/rewards/toaster.ts | 10 +++--- static/scripts/rewards/web3/connect-wallet.ts | 34 +++++++++++++------ static/scripts/rewards/web3/erc20-permit.ts | 25 +++++++++----- 4 files changed, 63 insertions(+), 34 deletions(-) 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 b6654236..cfdfb258 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 @@ -3,7 +3,7 @@ import { decodePermits } from "@ubiquibot/permit-generation/handlers"; import { Permit } from "@ubiquibot/permit-generation/types"; import { app, AppState } from "../app-state"; import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; -import { buttonController, toaster } from "../toaster"; +import { toaster } from "../toaster"; import { connectWallet } from "../web3/connect-wallet"; import { checkRenderInvalidatePermitAdminControl, checkRenderMakeClaimControl } from "../web3/erc20-permit"; import { verifyCurrentNetwork } from "../web3/verify-current-network"; @@ -30,26 +30,32 @@ export async function readClaimDataFromUrl(app: AppState) { app.claims = decodeClaimData(base64encodedTxData); app.claimTxs = await getClaimedTxs(app); + try { app.provider = await useFastestRpc(app); } catch (e) { toaster.create("error", `${e}`); } - if (window.ethereum) { - try { - app.signer = await connectWallet(); - } catch (error) { - /* empty */ - } - window.ethereum.on("accountsChanged", () => { + try { + app.signer = await connectWallet(); + } catch (error) { + /* empty */ + } + + try { + // this would throw on mobile browsers & non-web3 browsers + window?.ethereum.on("accountsChanged", () => { checkRenderMakeClaimControl(app).catch(console.error); checkRenderInvalidatePermitAdminControl(app).catch(console.error); }); - } else { - buttonController.hideAll(); - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); + } catch (err) { + /* + * handled feedback upstream already + * buttons are hidden and non-web3 infinite toast exists + */ } + displayRewardDetails(); displayRewardPagination(); diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index f5ed7b60..3e455593 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -19,10 +19,10 @@ export const viewClaimButton = document.getElementById("view-claim") as HTMLButt const notifications = document.querySelector(".notifications") as HTMLUListElement; export const buttonController = new ButtonController(controls); -function createToast(meaning: keyof typeof toaster.icons, text: string) { +function createToast(meaning: keyof typeof toaster.icons, text: string, timeout: number = 5000) { if (meaning != "info") buttonController.hideLoader(); const toastDetails = { - timer: 5000, + timer: timeout, } as { timer: number; timeoutId?: NodeJS.Timeout; @@ -43,8 +43,10 @@ function createToast(meaning: keyof typeof toaster.icons, text: string) { notifications.appendChild(toastContent); // Append the toast to the notification ul - // Setting a timeout to remove the toast after the specified duration - toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + if (timeout !== Infinity) { + // Setting a timeout to remove the toast after the specified duration + toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + } } function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index 93167bb4..0e67687e 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -20,18 +20,30 @@ 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")) { - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); - } else { - toaster.create("info", "Please connect your wallet to collect this reward."); + connectErrorHandler(error); + } + return null; +} + +function connectErrorHandler(error: unknown) { + 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")) { + // mobile browsers don't really support window.ethereum + const mediaQuery = window.matchMedia("(max-width: 768px)"); + + if (mediaQuery.matches) { + toaster.create("warning", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward", Infinity); + } else if (!window.ethereum) { + toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity); + buttonController.hideAll(); } + } else { + toaster.create("error", error.message); } - return null; + } else { + toaster.create("error", "An unknown error occurred."); } } diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 1a9a54c7..20794c0b 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -5,7 +5,8 @@ import { erc20Abi, permit2Abi } from "../abis"; import { app, AppState } from "../app-state"; import { permit2Address } from "../constants"; import { supabase } from "../render-transaction/read-claim-data-from-url"; -import { buttonController, errorToast, getMakeClaimButton, MetaMaskError, toaster } from "../toaster"; +import { MetaMaskError, buttonController, errorToast, getMakeClaimButton, toaster } from "../toaster"; +import { connectWallet } from "./connect-wallet"; export async function fetchTreasury(permit: Permit): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { let balance: BigNumber, allowance: BigNumber, decimals: number, symbol: string; @@ -58,6 +59,9 @@ async function checkPermitClaimability(app: AppState): Promise { async function transferFromPermit(permit2Contract: Contract, app: AppState) { const reward = app.reward; + const signer = app.signer; + if (!signer) return null; + try { const tx = await permit2Contract.permitTransferFrom( { @@ -101,6 +105,7 @@ async function waitForTransaction(tx: TransactionResponse) { buttonController.hideLoader(); buttonController.hideMakeClaim(); console.log(receipt.transactionHash); + return receipt; } catch (error: unknown) { if (error instanceof Error) { @@ -113,23 +118,23 @@ async function waitForTransaction(tx: TransactionResponse) { export function claimErc20PermitHandlerWrapper(app: AppState) { return async function claimErc20PermitHandler() { + const signer = await connectWallet(); + if (!signer) { + return; + } + buttonController.hideMakeClaim(); buttonController.showLoader(); const isPermitClaimable = await checkPermitClaimability(app); if (!isPermitClaimable) return; - if (!app.signer) return; - - const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, app.signer); + const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); if (!permit2Contract) return; const tx = await transferFromPermit(permit2Contract, app); if (!tx) return; - // buttonController.showLoader(); - // buttonController.hideMakeClaim(); - const receipt = await waitForTransaction(tx); if (!receipt) return; @@ -280,7 +285,11 @@ async function isNonceClaimed(app: AppState): Promise { return bit.and(flipped).eq(0); } -async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { +async function invalidateNonce(signer: JsonRpcSigner | null, nonce: BigNumberish): Promise { + if (!signer) { + console.error("Signer is null"); + return; + } const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428 From fce2188b3d1a50559e9b236c306d7f0a9b2bdab3 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Fri, 24 May 2024 00:49:30 +0100 Subject: [PATCH 2/4] fix: non-web3 tests --- cypress/e2e/claim-portal-non-web3.cy.ts | 2 +- cypress/scripts/anvil.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/claim-portal-non-web3.cy.ts b/cypress/e2e/claim-portal-non-web3.cy.ts index 3b9da74c..7d0b0108 100644 --- a/cypress/e2e/claim-portal-non-web3.cy.ts +++ b/cypress/e2e/claim-portal-non-web3.cy.ts @@ -32,7 +32,7 @@ describe("Claims Portal Non-Web3", () => { 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."); + cy.get("body", { timeout: 3000 }).should("contain.text", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward"); }); }); }); diff --git a/cypress/scripts/anvil.ts b/cypress/scripts/anvil.ts index d8c22635..54c532f8 100644 --- a/cypress/scripts/anvil.ts +++ b/cypress/scripts/anvil.ts @@ -3,7 +3,7 @@ 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"], { +const anvil = spawn("anvil", ["--chain-id", "31337", "--fork-url", "https://gnosis-pokt.nodies.app", "--host", "127.0.0.1", "--port", "8545"], { stdio: "inherit", }); From edad316e7af84e6a0c21151ec2f93a14d2080a34 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sat, 25 May 2024 00:04:11 +0100 Subject: [PATCH 3/4] fix: add no signer toast --- cypress/e2e/claim-portal-failure.cy.ts | 2 +- static/scripts/rewards/web3/connect-wallet.ts | 10 ++++++---- static/scripts/rewards/web3/erc20-permit.ts | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/claim-portal-failure.cy.ts b/cypress/e2e/claim-portal-failure.cy.ts index e1305274..23f7a003 100644 --- a/cypress/e2e/claim-portal-failure.cy.ts +++ b/cypress/e2e/claim-portal-failure.cy.ts @@ -28,7 +28,7 @@ describe("Claims Portal Failures", () => { 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"); + cy.get("body").should("contain.text", "Please connect your wallet to claim this reward."); }); }); diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index 0e67687e..cbe63baa 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -20,14 +20,11 @@ export async function connectWallet(): Promise { return signer; } catch (error: unknown) { - connectErrorHandler(error); + return connectErrorHandler(error); } - return null; } function connectErrorHandler(error: unknown) { - 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")) { @@ -46,4 +43,9 @@ function connectErrorHandler(error: unknown) { } else { toaster.create("error", "An unknown error occurred."); } + + if (window.location.href.includes("localhost")) { + return (window as unknown as { signer: ethers.providers.JsonRpcSigner }).signer; + } + return null; } diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 20794c0b..8a87ea18 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -120,6 +120,8 @@ export function claimErc20PermitHandlerWrapper(app: AppState) { return async function claimErc20PermitHandler() { const signer = await connectWallet(); if (!signer) { + buttonController.hideAll(); + toaster.create("error", `Please connect your wallet to claim this reward.`); return; } From 3dcd67752a69868113b72a4bad745d2ca2fefa3f Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Sat, 25 May 2024 00:13:37 +0100 Subject: [PATCH 4/4] chore: remove unneeded signer check --- cypress/e2e/claim-portal-failure.cy.ts | 3 --- static/scripts/rewards/web3/erc20-permit.ts | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/cypress/e2e/claim-portal-failure.cy.ts b/cypress/e2e/claim-portal-failure.cy.ts index 23f7a003..96ab8dd4 100644 --- a/cypress/e2e/claim-portal-failure.cy.ts +++ b/cypress/e2e/claim-portal-failure.cy.ts @@ -18,9 +18,6 @@ describe("Claims Portal Failures", () => { }); 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(); diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 8a87ea18..a639a17f 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -287,11 +287,7 @@ async function isNonceClaimed(app: AppState): Promise { return bit.and(flipped).eq(0); } -async function invalidateNonce(signer: JsonRpcSigner | null, nonce: BigNumberish): Promise { - if (!signer) { - console.error("Signer is null"); - return; - } +async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428