From c3b77ca08ca84a9a594d527ef50fe1751b1773c9 Mon Sep 17 00:00:00 2001 From: bluecco Date: Fri, 1 Dec 2023 15:02:29 +0100 Subject: [PATCH 1/5] feat: add iframes logic and allowed dapps list for argent webwallet --- .../webwallet/helpers/fetchAllowedDapps.ts | 63 ++++++++++++++++ .../webwallet/helpers/openWebwallet.ts | 74 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/connectors/webwallet/helpers/fetchAllowedDapps.ts create mode 100644 src/connectors/webwallet/helpers/openWebwallet.ts diff --git a/src/connectors/webwallet/helpers/fetchAllowedDapps.ts b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts new file mode 100644 index 0000000..a1ee840 --- /dev/null +++ b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts @@ -0,0 +1,63 @@ +import { constants } from "starknet" + +const TESTNET_WHITELIST_URL = + "https://static.hydrogen.argent47.net/webwallet/iframe_whitelist_testnet.json" +const MAINNET_WHITELIST_URL = + "https://static.argent.xyz/webwallet/iframe_whitelist_mainnet.json" + +const CACHE_NAME = "allowed-dapps" + +type AllowedDappsJsonResponse = { + allowedDapps: string[] +} + +export const fetchAllowedDapps = async ( + network: constants.NetworkName, +): Promise => { + const url = + network === constants.NetworkName.SN_MAIN + ? MAINNET_WHITELIST_URL + : TESTNET_WHITELIST_URL + try { + const cache = await caches.open(CACHE_NAME) + const cachedResponse = await cache.match(url) + + if (cachedResponse) { + const cachedTimestamp = parseInt( + cachedResponse.headers.get("X-Cache-Timestamp"), + 10, + ) + const currentTimestamp = new Date().getTime() + const timeDiff = currentTimestamp - cachedTimestamp + const hoursDiff = timeDiff / (1000 * 60 * 60) + + if (hoursDiff < 24) { + return cachedResponse.json() + } + } + + const response = await fetch(url) + const clonedHeaders = new Headers(response.headers) + + // Store the current timestamp in a custom header + clonedHeaders.set("X-Cache-Timestamp", new Date().getTime().toString()) + + // Read JSON data from the original response + const responseData = await response.json() + + // Create a new response with the cloned headers and original JSON data + const responseToCache = new Response(JSON.stringify(responseData), { + status: response.status, + statusText: response.statusText, + headers: clonedHeaders, + }) + + // Open the cache and add the response to it + const updatedCache = await caches.open(CACHE_NAME) + await updatedCache.put(url, responseToCache) + + return responseData + } catch (error) { + throw new Error(error) + } +} diff --git a/src/connectors/webwallet/helpers/openWebwallet.ts b/src/connectors/webwallet/helpers/openWebwallet.ts new file mode 100644 index 0000000..f2200c0 --- /dev/null +++ b/src/connectors/webwallet/helpers/openWebwallet.ts @@ -0,0 +1,74 @@ +import { createModal } from "../starknetWindowObject/wormhole" +import { getWebWalletStarknetObject } from "../starknetWindowObject/getWebWalletStarknetObject" +import { trpcProxyClient } from "./trpc" +import type { StarknetWindowObject } from "get-starknet-core" +import { mapTargetUrlToNetworkId } from "../../../helpers/mapTargetUrlToNetworkId" +import { fetchAllowedDapps } from "./fetchAllowedDapps" + +const checkIncognitoChrome = async (isChrome: boolean) => { + return new Promise((resolve) => { + if (!isChrome) { + return resolve(false) + } + try { + const webkitTemporaryStorage = (navigator as any).webkitTemporaryStorage + webkitTemporaryStorage.queryUsageAndQuota( + (_: unknown, quota: number) => { + resolve( + Math.round(quota / (1024 * 1024)) < + Math.round( + ((performance as any)?.memory?.jsHeapSizeLimit ?? 1073741824) / + (1024 * 1024), + ) * + 2, + ) + }, + () => resolve(false), + ) + } catch { + resolve(false) + } + }) +} + +export const openWebwallet = async ( + origin: string, +): Promise => { + const { userAgent } = navigator + const isChrome = Boolean( + navigator.vendor && + navigator.vendor.indexOf("Google") === 0 && + (navigator as any).brave === undefined && + !userAgent.match(/Edg/) && + !userAgent.match(/OPR/), + ) + + const isChromeIncognito = await checkIncognitoChrome(isChrome) + + // if not chrome or is chrome incognito + // use the popup mode and avoid checking allowed dapps for iframes + if (!isChrome || isChromeIncognito) { + const windowProxyClient = trpcProxyClient({}) + return await getWebWalletStarknetObject(origin, windowProxyClient) + } + + const network = mapTargetUrlToNetworkId(origin) + const { allowedDapps } = await fetchAllowedDapps(network) + + if (allowedDapps.includes(window.location.hostname)) { + const { iframe, modal } = await createModal(origin, false) + const windowProxyClient = trpcProxyClient({}) + const isConnected = await windowProxyClient.authorize.mutate() + if (isConnected) { + const starknetWindowObject = await getWebWalletStarknetObject( + origin, + trpcProxyClient({ iframe: iframe.contentWindow }), + { modal, iframe }, + ) + return starknetWindowObject + } + } else { + const windowProxyClient = trpcProxyClient({}) + return await getWebWalletStarknetObject(origin, windowProxyClient) + } +} From a439ab0c19ffa38258cf451d0165f3e2ea7dc0eb Mon Sep 17 00:00:00 2001 From: bluecco Date: Fri, 1 Dec 2023 15:03:23 +0100 Subject: [PATCH 2/5] feat: add iframe style and create method for argent webwallet --- .../starknetWindowObject/wormhole.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/connectors/webwallet/starknetWindowObject/wormhole.ts diff --git a/src/connectors/webwallet/starknetWindowObject/wormhole.ts b/src/connectors/webwallet/starknetWindowObject/wormhole.ts new file mode 100644 index 0000000..2564177 --- /dev/null +++ b/src/connectors/webwallet/starknetWindowObject/wormhole.ts @@ -0,0 +1,82 @@ +const applyModalStyle = (iframe: HTMLIFrameElement) => { + // middle of the screen + iframe.style.position = "fixed" + iframe.style.top = "50%" + iframe.style.left = "50%" + iframe.style.transform = "translate(-50%, -50%)" + iframe.style.width = "380px" + iframe.style.height = "420px" + iframe.style.border = "none" + + // round corners + iframe.style.borderRadius = "40px" + // box shadow + iframe.style.boxShadow = "0px 4px 20px rgba(0, 0, 0, 0.5)" + + const background = document.createElement("div") + background.style.display = "none" + background.style.position = "fixed" + background.style.top = "0" + background.style.left = "0" + background.style.right = "0" + background.style.bottom = "0" + background.style.backgroundColor = "rgba(0, 0, 0, 0.5)" + background.style.zIndex = "99999" + ;(background.style as any).backdropFilter = "blur(4px)" + + background.appendChild(iframe) + + return background +} + +export const showModal = (modal: HTMLDivElement) => { + modal.style.display = "block" +} + +export const hideModal = (modal: HTMLDivElement) => { + modal.style.display = "none" +} + +export const setIframeHeight = (modal: HTMLIFrameElement, height: number) => { + modal.style.height = `min(${height || 420}px, 100%)` +} + +export const createModal = async (targetUrl: string, shouldShow: boolean) => { + // make sure target url has always /iframes/comms as the path + const url = new URL(targetUrl) + url.pathname = "/iframes/comms" + targetUrl = url.toString() + + const iframe = document.createElement("iframe") + iframe.src = targetUrl + ;(iframe as any).loading = "eager" + iframe.sandbox.add( + "allow-scripts", + "allow-same-origin", + "allow-forms", + "allow-top-navigation", + "allow-popups", + ) + iframe.allow = "clipboard-write" + + const modal = applyModalStyle(iframe) + modal.style.display = shouldShow ? "block" : "none" + + // append the modal to the body + window.document.body.appendChild(modal) + + // wait for the iframe to load + await new Promise((resolve, reject) => { + const pid = setTimeout( + () => reject(new Error("Timeout while loading an iframe")), + 20000, + ) + + iframe.addEventListener("load", async () => { + clearTimeout(pid) + resolve() + }) + }) + + return { iframe, modal } +} From 8069d3999514afded8ecf2fea72306a6619ffcc1 Mon Sep 17 00:00:00 2001 From: bluecco Date: Fri, 1 Dec 2023 15:03:54 +0100 Subject: [PATCH 3/5] feat: update argent webwallet connector logic to implement iframes on chrome --- src/connectors/webwallet/index.ts | 8 ++-- .../getWebWalletStarknetObject.ts | 40 +++++++++++++++++-- src/helpers/mapTargetUrlToNetworkId.ts | 31 ++++++++++++-- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/connectors/webwallet/index.ts b/src/connectors/webwallet/index.ts index 18ddfb8..1619bc6 100644 --- a/src/connectors/webwallet/index.ts +++ b/src/connectors/webwallet/index.ts @@ -8,7 +8,7 @@ import { type ConnectorData, type ConnectorIcons, } from "../connector" -import { setPopupOptions, trpcProxyClient } from "./helpers/trpc" +import { setPopupOptions } from "./helpers/trpc" import { ConnectorNotConnectedError, @@ -17,7 +17,7 @@ import { UserRejectedRequestError, } from "../../errors" import { DEFAULT_WEBWALLET_ICON, DEFAULT_WEBWALLET_URL } from "./constants" -import { getWebWalletStarknetObject } from "./starknetWindowObject/getWebWalletStarknetObject" +import { openWebwallet } from "./helpers/openWebwallet" let _wallet: StarknetWindowObject | null = null @@ -167,9 +167,9 @@ export class WebWalletConnector extends Connector { origin, location: "/interstitialLogin", }) - const wallet = await getWebWalletStarknetObject(origin, trpcProxyClient({})) - _wallet = wallet ?? null + _wallet = await openWebwallet(origin) + this._wallet = _wallet } } diff --git a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts index ffca03d..4a3c651 100644 --- a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts +++ b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts @@ -1,22 +1,36 @@ import type { CreateTRPCProxyClient } from "@trpc/client" -import { SequencerProvider } from "starknet" +import { RpcProvider } from "starknet" -import { mapTargetUrlToNetworkId } from "../../../helpers/mapTargetUrlToNetworkId" +import { mapTargetUrlToNodeUrl } from "../../../helpers/mapTargetUrlToNetworkId" import type { AppRouter } from "../helpers/trpc" import type { WebWalletStarknetWindowObject } from "./argentStarknetWindowObject" import { getArgentStarknetWindowObject } from "./argentStarknetWindowObject" +import { hideModal, setIframeHeight, showModal } from "./wormhole" + +type IframeProps = { + modal: HTMLDivElement + iframe: HTMLIFrameElement +} + +type ModalEvents = + | { + action: "show" | "hide" + visible: boolean + } + | { action: "updateHeight"; height: number } export const getWebWalletStarknetObject = async ( target: string, proxyLink: CreateTRPCProxyClient, + iframeProps?: IframeProps, ): Promise => { const globalWindow = typeof window !== "undefined" ? window : undefined if (!globalWindow) { throw new Error("window is not defined") } - const network = mapTargetUrlToNetworkId(target) - const defaultProvider = new SequencerProvider({ network }) + const nodeUrl = mapTargetUrlToNodeUrl(target) + const defaultProvider = new RpcProvider({ nodeUrl }) const starknetWindowObject = getArgentStarknetWindowObject( { host: globalWindow.location.origin, @@ -29,5 +43,23 @@ export const getWebWalletStarknetObject = async ( proxyLink, ) + if (iframeProps) { + const { iframe, modal } = iframeProps + proxyLink.updateModal.subscribe(undefined, { + onData(modalEvent: ModalEvents) { + switch (modalEvent.action) { + case "show": + showModal(modal) + break + case "hide": + hideModal(modal) + break + case "updateHeight": + setIframeHeight(iframe, modalEvent.height) + } + }, + }) + } + return starknetWindowObject } diff --git a/src/helpers/mapTargetUrlToNetworkId.ts b/src/helpers/mapTargetUrlToNetworkId.ts index 0f949df..9c2f6ba 100644 --- a/src/helpers/mapTargetUrlToNetworkId.ts +++ b/src/helpers/mapTargetUrlToNetworkId.ts @@ -17,9 +17,6 @@ export function mapTargetUrlToNetworkId(target: string): constants.NetworkName { if (origin.includes("staging")) { return Network.SN_MAIN } - if (origin.includes("dev")) { - return Network.SN_GOERLI2 - } if (origin.includes("argent.xyz")) { return Network.SN_MAIN } @@ -30,3 +27,31 @@ export function mapTargetUrlToNetworkId(target: string): constants.NetworkName { } return Network.SN_MAIN } + +const RPC_NODE_URL_TESTNET = + "https://api.hydrogen.argent47.net/v1/starknet/goerli/rpc/v0.5" +const RPC_NODE_URL_MAINNET = + "https://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.5" + +export function mapTargetUrlToNodeUrl(target: string): string { + try { + const { origin } = new URL(target) + if (origin.includes("localhost") || origin.includes("127.0.0.1")) { + return RPC_NODE_URL_TESTNET + } + if (origin.includes("hydrogen")) { + return RPC_NODE_URL_TESTNET + } + if (origin.includes("staging")) { + return RPC_NODE_URL_MAINNET + } + if (origin.includes("argent.xyz")) { + return RPC_NODE_URL_MAINNET + } + } catch (e) { + console.warn( + "Could not determine rpc nodeUrl from target URL, defaulting to mainnet", + ) + } + return RPC_NODE_URL_MAINNET +} From 35838d2473b5e7bab825e306d1285eda08185e3f Mon Sep 17 00:00:00 2001 From: bluecco Date: Thu, 7 Dec 2023 10:41:37 +0100 Subject: [PATCH 4/5] chore: refactor constants --- src/connectors/webwallet/constants.ts | 11 ++++++++ .../webwallet/helpers/fetchAllowedDapps.ts | 6 +--- .../helpers/mapTargetUrlToNodeUrl.ts | 24 ++++++++++++++++ .../getWebWalletStarknetObject.ts | 2 +- src/helpers/mapTargetUrlToNetworkId.ts | 28 ------------------- 5 files changed, 37 insertions(+), 34 deletions(-) create mode 100644 src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts diff --git a/src/connectors/webwallet/constants.ts b/src/connectors/webwallet/constants.ts index d209e53..0868c26 100644 --- a/src/connectors/webwallet/constants.ts +++ b/src/connectors/webwallet/constants.ts @@ -14,3 +14,14 @@ export const DEFAULT_WEBWALLET_ICON = ` ` + +export const TESTNET_WHITELIST_URL = + "https://static.hydrogen.argent47.net/webwallet/iframe_whitelist_testnet.json" + +export const MAINNET_WHITELIST_URL = + "https://static.argent.xyz/webwallet/iframe_whitelist_mainnet.json" + +export const RPC_NODE_URL_TESTNET = + "https://api.hydrogen.argent47.net/v1/starknet/goerli/rpc/v0.5" +export const RPC_NODE_URL_MAINNET = + "https://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.5" diff --git a/src/connectors/webwallet/helpers/fetchAllowedDapps.ts b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts index a1ee840..8d7a7c2 100644 --- a/src/connectors/webwallet/helpers/fetchAllowedDapps.ts +++ b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts @@ -1,9 +1,5 @@ import { constants } from "starknet" - -const TESTNET_WHITELIST_URL = - "https://static.hydrogen.argent47.net/webwallet/iframe_whitelist_testnet.json" -const MAINNET_WHITELIST_URL = - "https://static.argent.xyz/webwallet/iframe_whitelist_mainnet.json" +import { MAINNET_WHITELIST_URL, TESTNET_WHITELIST_URL } from "../constants" const CACHE_NAME = "allowed-dapps" diff --git a/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts b/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts new file mode 100644 index 0000000..d3f83de --- /dev/null +++ b/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts @@ -0,0 +1,24 @@ +import { RPC_NODE_URL_MAINNET, RPC_NODE_URL_TESTNET } from "../constants" + +export function mapTargetUrlToNodeUrl(target: string): string { + try { + const { origin } = new URL(target) + if (origin.includes("localhost") || origin.includes("127.0.0.1")) { + return RPC_NODE_URL_TESTNET + } + if (origin.includes("hydrogen")) { + return RPC_NODE_URL_TESTNET + } + if (origin.includes("staging")) { + return RPC_NODE_URL_MAINNET + } + if (origin.includes("argent.xyz")) { + return RPC_NODE_URL_MAINNET + } + } catch (e) { + console.warn( + "Could not determine rpc nodeUrl from target URL, defaulting to mainnet", + ) + } + return RPC_NODE_URL_MAINNET +} diff --git a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts index 4a3c651..a42e84e 100644 --- a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts +++ b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts @@ -1,7 +1,7 @@ import type { CreateTRPCProxyClient } from "@trpc/client" import { RpcProvider } from "starknet" -import { mapTargetUrlToNodeUrl } from "../../../helpers/mapTargetUrlToNetworkId" +import { mapTargetUrlToNodeUrl } from "../helpers/mapTargetUrlToNodeUrl" import type { AppRouter } from "../helpers/trpc" import type { WebWalletStarknetWindowObject } from "./argentStarknetWindowObject" import { getArgentStarknetWindowObject } from "./argentStarknetWindowObject" diff --git a/src/helpers/mapTargetUrlToNetworkId.ts b/src/helpers/mapTargetUrlToNetworkId.ts index 9c2f6ba..15ae934 100644 --- a/src/helpers/mapTargetUrlToNetworkId.ts +++ b/src/helpers/mapTargetUrlToNetworkId.ts @@ -27,31 +27,3 @@ export function mapTargetUrlToNetworkId(target: string): constants.NetworkName { } return Network.SN_MAIN } - -const RPC_NODE_URL_TESTNET = - "https://api.hydrogen.argent47.net/v1/starknet/goerli/rpc/v0.5" -const RPC_NODE_URL_MAINNET = - "https://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.5" - -export function mapTargetUrlToNodeUrl(target: string): string { - try { - const { origin } = new URL(target) - if (origin.includes("localhost") || origin.includes("127.0.0.1")) { - return RPC_NODE_URL_TESTNET - } - if (origin.includes("hydrogen")) { - return RPC_NODE_URL_TESTNET - } - if (origin.includes("staging")) { - return RPC_NODE_URL_MAINNET - } - if (origin.includes("argent.xyz")) { - return RPC_NODE_URL_MAINNET - } - } catch (e) { - console.warn( - "Could not determine rpc nodeUrl from target URL, defaulting to mainnet", - ) - } - return RPC_NODE_URL_MAINNET -} From 3faeba5c542570719a7e7145eb027d93c323593d Mon Sep 17 00:00:00 2001 From: bluecco Date: Thu, 7 Dec 2023 15:57:44 +0100 Subject: [PATCH 5/5] chore: remove rpc provider --- src/connectors/webwallet/constants.ts | 5 ---- .../helpers/mapTargetUrlToNodeUrl.ts | 24 ------------------- .../getWebWalletStarknetObject.ts | 8 +++---- 3 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts diff --git a/src/connectors/webwallet/constants.ts b/src/connectors/webwallet/constants.ts index 0868c26..6844631 100644 --- a/src/connectors/webwallet/constants.ts +++ b/src/connectors/webwallet/constants.ts @@ -20,8 +20,3 @@ export const TESTNET_WHITELIST_URL = export const MAINNET_WHITELIST_URL = "https://static.argent.xyz/webwallet/iframe_whitelist_mainnet.json" - -export const RPC_NODE_URL_TESTNET = - "https://api.hydrogen.argent47.net/v1/starknet/goerli/rpc/v0.5" -export const RPC_NODE_URL_MAINNET = - "https://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.5" diff --git a/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts b/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts deleted file mode 100644 index d3f83de..0000000 --- a/src/connectors/webwallet/helpers/mapTargetUrlToNodeUrl.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RPC_NODE_URL_MAINNET, RPC_NODE_URL_TESTNET } from "../constants" - -export function mapTargetUrlToNodeUrl(target: string): string { - try { - const { origin } = new URL(target) - if (origin.includes("localhost") || origin.includes("127.0.0.1")) { - return RPC_NODE_URL_TESTNET - } - if (origin.includes("hydrogen")) { - return RPC_NODE_URL_TESTNET - } - if (origin.includes("staging")) { - return RPC_NODE_URL_MAINNET - } - if (origin.includes("argent.xyz")) { - return RPC_NODE_URL_MAINNET - } - } catch (e) { - console.warn( - "Could not determine rpc nodeUrl from target URL, defaulting to mainnet", - ) - } - return RPC_NODE_URL_MAINNET -} diff --git a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts index a42e84e..538a6a9 100644 --- a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts +++ b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts @@ -1,11 +1,11 @@ import type { CreateTRPCProxyClient } from "@trpc/client" -import { RpcProvider } from "starknet" +import { SequencerProvider } from "starknet" -import { mapTargetUrlToNodeUrl } from "../helpers/mapTargetUrlToNodeUrl" import type { AppRouter } from "../helpers/trpc" import type { WebWalletStarknetWindowObject } from "./argentStarknetWindowObject" import { getArgentStarknetWindowObject } from "./argentStarknetWindowObject" import { hideModal, setIframeHeight, showModal } from "./wormhole" +import { mapTargetUrlToNetworkId } from "../../../helpers/mapTargetUrlToNetworkId" type IframeProps = { modal: HTMLDivElement @@ -29,8 +29,8 @@ export const getWebWalletStarknetObject = async ( throw new Error("window is not defined") } - const nodeUrl = mapTargetUrlToNodeUrl(target) - const defaultProvider = new RpcProvider({ nodeUrl }) + const network = mapTargetUrlToNetworkId(target) + const defaultProvider = new SequencerProvider({ network }) const starknetWindowObject = getArgentStarknetWindowObject( { host: globalWindow.location.origin,