Skip to content

Commit

Permalink
Merge pull request #242 from ubq-testing/inwallet-rpc-swap
Browse files Browse the repository at this point in the history
wallet provider management
  • Loading branch information
Keyrxng authored Jun 26, 2024
2 parents 9dbb0c6 + 6f30edb commit bf5ca41
Show file tree
Hide file tree
Showing 9 changed files with 866 additions and 405 deletions.
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";
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
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 @@ -20,20 +32,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);
});

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);
} else if (!window.ethereum) {
toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity);
Expand All @@ -43,7 +185,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 @@ -123,13 +123,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

0 comments on commit bf5ca41

Please sign in to comment.