Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet provider management #242

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ config();

export default defineConfig({
e2e: {
setupNodeEvents() {},
setupNodeEvents() { },
baseUrl: "http://localhost:8080",
experimentalStudio: true,
},
Expand Down
70 changes: 60 additions & 10 deletions cypress/e2e/claim-portal-non-web3.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ describe("Claims Portal Non-Web3", () => {

setupIntercepts();

cy.visit(`/${claimUrl}`);
cy.wait(2000);
});

describe("No window.ethereum", () => {
it("Should toast and hide buttons in a non-web3 env", () => {
cy.visit(`/${claimUrl}`)
cy.wait(2000);

cy.get("#invalidator").should("not.be.visible");
cy.get("#claim-loader").should("not.be.visible");
cy.get("#view-claim").should("not.be.visible");
Expand All @@ -22,22 +23,71 @@ describe("Claims Portal Non-Web3", () => {
});

describe("Mobile: No window.ethereum", () => {
beforeEach(() => {
cy.viewport("iphone-6");
cy.reload();
const userAgents = [
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 WebView MetaMaskMobile",
"Mozilla/5.0 (Android; Mobile; rv:89.0) Gecko/89.0 Firefox/89.0 WebView MetaMaskMobile",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.14977",
"Mozilla/5.0 (Linux; Android 10; SM-A505FN) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36 WebView MetaMaskMobile",
"Mozilla/5.0 (Linux; U; Android 8.1.0; en-us; Redmi Note 5 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/68.0.3440.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 11; Pixel 4 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36"
]

it("UserAgent 0", () => {
testUserAgent(userAgents[0]);
});

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");
it("UserAgent 1", () => {
testUserAgent(userAgents[1]);
});

it("UserAgent 2", () => {
testUserAgent(userAgents[2]);
});

it("UserAgent 3", () => {
testUserAgent(userAgents[3]);
});

it("UserAgent 4", () => {
testUserAgent(userAgents[4]);
});

it("UserAgent 5", () => {
testUserAgent(userAgents[5]);
});

cy.get("body", { timeout: 3000 }).should("contain.text", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward");
it("UserAgent 6", () => {
testUserAgent(userAgents[6]);
});

it("UserAgent 7", () => {
testUserAgent(userAgents[7]);
});
});
});

function testUserAgent(userAgent: string) {
cy.visit(`/${claimUrl}`, {
onBeforeLoad: (win) => {
Object.defineProperty(win.navigator, 'userAgent', {
value: userAgent,
configurable: true
});
},
});

cy.wait(2000);
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 mobile-friendly Web3 browser such as MetaMask 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,4 @@
"@commitlint/config-conventional"
]
}
}
}
4 changes: 2 additions & 2 deletions static/scripts/rewards/cirip/query-reverse-ens.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { AppState } from "../app-state";
import { app } from "../app-state";
import { useRpcHandler } from "../web3/use-rpc-handler";
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
import { reverseEnsInterface } from "./ens-lookup";

export async function queryReverseEns(address: string, networkID: number) {
// Try to get the ENS name from localStorage
const cachedEnsName = localStorage.getItem(address);
const endpoint = (await useRpcHandler({ networkId: networkID } as AppState)).connection.url;
const endpoint = app.provider?.connection.url || (await useRpcHandler(app)).connection.url;

if (!endpoint) {
console.error("ENS lookup failed: No endpoint found for network ID", networkID);
Expand Down
1 change: 0 additions & 1 deletion static/scripts/rewards/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { grid } from "./the-grid";

displayCommitHash(); // @DEV: display commit hash in footer
grid(document.getElementById("grid") as HTMLElement, gridLoadedCallback); // @DEV: display grid background

readClaimDataFromUrl(app).catch(console.error); // @DEV: read claim data from URL

declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export async function readClaimDataFromUrl(app: AppState) {
try {
app.provider = await useRpcHandler(app);
} catch (e) {
toaster.create("error", `e`);
if (e instanceof Error) {
toaster.create("error", e.message);
} else {
toaster.create("error", JSON.stringify(e));
}
}

try {
Expand Down
148 changes: 145 additions & 3 deletions static/scripts/rewards/web3/connect-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { JsonRpcSigner } from "@ethersproject/providers";
import { ethers } from "ethers";
import { buttonController, toaster } from "../toaster";
import { app } from "../app-state";
import { useHandler } from "../web3/use-rpc-handler";

function _mobileCheck(a: string) {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) {
return true;
}
}

function mobileCheck() {
return _mobileCheck(navigator.userAgent || navigator.vendor || (window as any).opera);
}

export async function connectWallet(): Promise<JsonRpcSigner | null> {
try {
Expand All @@ -18,20 +30,150 @@ export async function connectWallet(): Promise<JsonRpcSigner | null> {
return null;
}

const isOkay = await stressTestWalletRpc(wallet);

if (!isOkay) {
if (mobileCheck()) {
toaster.create("info", `In case of network issues, please change your in-wallet RPC to the one below...`, 15000);
} else {
// Their wallet provider will auto-prompt due to the call succeeding
toaster.create("error", "We have detected potential issues with your in-wallet RPC. Accept the request to replace it with a more reliable one.");
}
await addFastestHandlerNetwork(wallet);
}

return signer;
} catch (error: unknown) {
return connectErrorHandler(error);
}
}

async function addFastestHandlerNetwork(wallet: ethers.providers.Web3Provider) {
const networkId = app.networkId ?? (await wallet.getNetwork()).chainId;
const handler = useHandler(networkId);
let provider = await handler.getFastestRpcProvider();
const appUrl = app.provider?.connection?.url;

const latencies = handler.getLatencies();
const latenciesArray = Object.entries(latencies).map(([url, latency]) => ({ url, latency }) as { url: string; latency: number });
const sorted = latenciesArray.sort((a, b) => a.latency - b.latency);

let toSuggest = sorted[0];

let isOkay = false;

for await (const { url } of sorted) {
const _url = url.split("__")[1];
if (_url !== appUrl) {
provider = new ethers.providers.JsonRpcProvider(_url);

isOkay = await stressTestWalletRpc(provider);

if (isOkay) {
toSuggest = { url: _url, latency: latencies[url] };
break;
}
}
}

if (!isOkay) {
toaster.create("error", "We failed to find a more reliable RPC for you. Please try again later if you have network issues.");
return;
}

try {
await addHandlerSuggested(wallet, toSuggest.url);
} catch (error) {
toaster.create("info", `${toSuggest.url}`, Infinity);
}
}

async function addHandlerSuggested(provider: ethers.providers.Web3Provider, url: string) {
const symbol = app.networkId === 1 ? "ETH" : "XDAI";
const altSymbol = app.networkId === 1 ? "eth" : "xdai";
const altSymbol2 = app.networkId === 1 ? "Eth" : "xDai";

if (mobileCheck()) {
/**
* https://github.com/MetaMask/metamask-mobile/issues/9519
*
* Until this is resolved it is not possible for us to add a network on mobile
* so we'll show a toast suggesting they do it manually
*/

toaster.create("info", `${url}`, Infinity);
return;
}

// It will not work unless the symbols match, so we try them all
for (const _symbol of [symbol, altSymbol, altSymbol2]) {
// this does not work on mobile yet
await addProvider(provider, url, _symbol, app.networkId);
}
}

async function addProvider(provider: ethers.providers.Web3Provider, url: string, symbol: string, chainId: number | null) {
const _chainId = chainId || (await provider.getNetwork()).chainId;
try {
await provider.send("wallet_addEthereumChain", [
{
chainId: `0x${_chainId.toString(16)}`,
chainName: _chainId === 1 ? "Ethereum" : "Gnosis",
nativeCurrency: {
name: _chainId === 1 ? "ETH" : "XDAI",
symbol,
decimals: 18,
},
rpcUrls: [url],
blockExplorerUrls: [`https://${_chainId === 1 ? "etherscan" : "gnosisscan"}.io`],
},
]);
} catch {
console.error("Failed to add network");
}
}

async function stressTestWalletRpc(provider: ethers.providers.Web3Provider) {
const success: Promise<string | boolean>[] = [];

for (let i = 0; i < 6; i++) {
success.push(testNonceBitmapEthCall(provider));
}

// if the test takes too long, we'll just assume it's not working
const timeoutPromise = new Promise<[false]>((resolve) => {
setTimeout(() => {
resolve([false]);
}, 7000);
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
});

const results = await Promise.race([Promise.all(success), timeoutPromise]);

return results.filter((s) => s === "0x" + "00".repeat(32)).length > 5 && results.filter((s) => s === false).length < 1;
}

async function testNonceBitmapEthCall(provider: ethers.providers.Web3Provider) {
try {
return await provider.send("eth_call", [
{
to: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
// input works for desktop, needs to be data for mobile
data: "0x4fe02b44000000000000000000000000d9530f3fbbea11bed01dc09e79318f2f20223716001fd097bcb5a1759ce02c0a671386a0bbbfa8216559e5855698a9d4de4cddea",
},
"latest",
]);
} catch {
// if the call fails, we'll assume the RPC is not working
}
}

function connectErrorHandler(error: unknown) {
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) {
if (mobileCheck()) {
toaster.create("warning", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward", Infinity);
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
} else if (!window.ethereum) {
toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity);
Expand All @@ -41,7 +183,7 @@ function connectErrorHandler(error: unknown) {
toaster.create("error", error.message);
}
} else {
toaster.create("error", "An unknown error occurred.");
toaster.create("error", "An unknown error occurred" + JSON.stringify(error));
}

if (window.location.href.includes("localhost")) {
Expand Down
4 changes: 3 additions & 1 deletion static/scripts/rewards/web3/erc20-permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ async function waitForTransaction(tx: TransactionResponse) {

export function claimErc20PermitHandlerWrapper(app: AppState) {
return async function claimErc20PermitHandler() {
const signer = await connectWallet();
const signer = await connectWallet(); // we are re-testing the in-wallet rpc at this point
if (!signer) {
buttonController.hideAll();
toaster.create("error", `Please connect your wallet to claim this reward.`);
return;
}

app.signer = signer; // update this here to be sure it's set if it wasn't before

buttonController.hideMakeClaim();
buttonController.showLoader();

Expand Down
5 changes: 2 additions & 3 deletions static/scripts/rewards/web3/use-rpc-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { RPCHandler } from "@ubiquity-dao/rpc-handler";
import { AppState } from "../app-state";
import { ethers } from "ethers";

export async function useHandler(networkId: number) {
export function useHandler(networkId: number) {
const config = {
networkId: networkId,
autoStorage: true,
Expand All @@ -29,5 +28,5 @@ export async function useRpcHandler(app: AppState) {
if (!url) {
throw new Error("Provider URL not set");
}
return new ethers.providers.JsonRpcProvider(provider.connection.url);
return provider;
}
Loading
Loading