diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 529109a..47547a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,71 +1,74 @@ -# Starknetkit Contribution Guide - -Thank you for investing your time in contributing to Starknetkit! - -We love pull requests and this guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved. - -If you want to contribute but don’t know what to do, take a look at issues labelled `good first issue`. - -## Getting started - -You can contribute to this repo in many ways: - -- Solve open issues -- Report bugs or feature requests -- Add new features such as new connectors -- Improve the documentation - -Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions: - -- Search for existing Issues and PRs before creating your own. -- If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error. -- Please use the same formatting in the code repository. You can configure your IDE to do it by using the prettier / linting config files included in each package. -- If applicable, please edit the README.md file to reflect the changes. - -### Issues - -Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created. - -#### Solve an issue - -Scan through our existing issues to find one that interests you. - -If a contributor is working on the issue, they will be assigned to the individual. If you find an issue to work on, you are welcome to assign it to yourself and open a PR with a fix for it. - -### Pull Requests - -#### Pull Request Process - -We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) - -1. Fork the repo -2. Clone the project -3. Create a new branch with a descriptive name -4. Commit your changes to the new branch -5. Push changes to your fork -6. Open a PR in our repository and tag one of the maintainers to review your PR - -Here are some tips for a high-quality pull request: - -- Create a title for the PR that accurately defines the work done. -- Structure the description neatly to make it easy to consume by the readers. For example, you can include bullet points and screenshots instead of having one large paragraph. -- Add the link to the issue if applicable. -- Have a good commit message that summarises the work done. - -Once you submit your PR: - -- We may ask questions, request additional information or ask for changes to be made before a PR can be merged. Please note that these are to make the PR clear for everyone involved and aims to create a frictionless interaction process. -- As you update your PR and apply changes, mark each conversation resolved. -- Once approved, your PR will be merged. - -#### Pull request targets -For the most common pull requests such as bug fixes, feature additions, documentation changes, etc., target the develop branch. - -### Other notes -- If you have commit access to the repository and want to make a big change or are unsure about something, make a new branch and open a pull request. -- We’re using Prettier to format code, so don’t worry much about code formatting. -- Don’t commit generated files, like minified JavaScript. -- Don’t change the version number or changelog. - -### Need help? -If you want to contribute but have any questions, concerns or doubts, feel free to ping maintainers. Ideally create a pull request with WIP (Work in progress) in its title and ask questions in the pull request description. +# Starknetkit Contribution Guide + +Thank you for investing your time in contributing to Starknetkit! + +We love pull requests and this guide aims to provide an overview of the contribution workflow to help us make the contribution process effective for everyone involved. + +If you want to contribute but don’t know what to do, take a look at issues labelled `good first issue`. + +## Getting started + +You can contribute to this repo in many ways: + +- Solve open issues +- Report bugs or feature requests +- Add new features such as new connectors +- Improve the documentation + +Contributions are made via Issues and Pull Requests (PRs). A few general guidelines for contributions: + +- Search for existing Issues and PRs before creating your own. +- If you're running into an error, please give context. Explain what you're trying to do and how to reproduce the error. +- Please use the same formatting in the code repository. You can configure your IDE to do it by using the prettier / linting config files included in each package. +- If applicable, please edit the README.md file to reflect the changes. + +### Issues + +Issues should be used to report problems, request a new feature, or discuss potential changes before a PR is created. + +#### Solve an issue + +Scan through our existing issues to find one that interests you. + +If a contributor is working on the issue, they will be assigned to the individual. If you find an issue to work on, you are welcome to assign it to yourself and open a PR with a fix for it. + +### Pull Requests + +#### Pull Request Process + +We follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) + +1. Fork the repo +2. Clone the project +3. Create a new branch with a descriptive name +4. Commit your changes to the new branch +5. Push changes to your fork +6. Open a PR in our repository and tag one of the maintainers to review your PR + +Here are some tips for a high-quality pull request: + +- Create a title for the PR that accurately defines the work done. +- Structure the description neatly to make it easy to consume by the readers. For example, you can include bullet points and screenshots instead of having one large paragraph. +- Add the link to the issue if applicable. +- Have a good commit message that summarises the work done. + +Once you submit your PR: + +- We may ask questions, request additional information or ask for changes to be made before a PR can be merged. Please note that these are to make the PR clear for everyone involved and aims to create a frictionless interaction process. +- As you update your PR and apply changes, mark each conversation resolved. +- Once approved, your PR will be merged. + +#### Pull request targets + +For the most common pull requests such as bug fixes, feature additions, documentation changes, etc., target the develop branch. + +### Other notes + +- If you have commit access to the repository and want to make a big change or are unsure about something, make a new branch and open a pull request. +- We’re using Prettier to format code, so don’t worry much about code formatting. +- Don’t commit generated files, like minified JavaScript. +- Don’t change the version number or changelog. + +### Need help? + +If you want to contribute but have any questions, concerns or doubts, feel free to ping maintainers. Ideally create a pull request with WIP (Work in progress) in its title and ask questions in the pull request description. diff --git a/src/connectors/webwallet/constants.ts b/src/connectors/webwallet/constants.ts index d209e53..c0357e0 100644 --- a/src/connectors/webwallet/constants.ts +++ b/src/connectors/webwallet/constants.ts @@ -14,3 +14,15 @@ 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.6" + +export const RPC_NODE_URL_MAINNET = + "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.6" diff --git a/src/connectors/webwallet/helpers/fetchAllowedDapps.ts b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts new file mode 100644 index 0000000..8d7a7c2 --- /dev/null +++ b/src/connectors/webwallet/helpers/fetchAllowedDapps.ts @@ -0,0 +1,59 @@ +import { constants } from "starknet" +import { MAINNET_WHITELIST_URL, TESTNET_WHITELIST_URL } from "../constants" + +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..44fdfe8 --- /dev/null +++ b/src/connectors/webwallet/helpers/openWebwallet.ts @@ -0,0 +1,83 @@ +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" +import { ProviderInterface } from "starknet" + +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, + undefined, + ) + } + + 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 ?? undefined }), + { modal, iframe }, + ) + return starknetWindowObject + } + } else { + const windowProxyClient = trpcProxyClient({}) + return await getWebWalletStarknetObject( + origin, + windowProxyClient, + undefined, + ) + } +} diff --git a/src/connectors/webwallet/index.ts b/src/connectors/webwallet/index.ts index 217341b..06099bd 100644 --- a/src/connectors/webwallet/index.ts +++ b/src/connectors/webwallet/index.ts @@ -9,7 +9,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" import { removeStarknetLastConnectedWallet } from "../../helpers/lastConnected" let _wallet: StarknetWindowObject | null = null @@ -177,9 +177,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 85beac5..3c7c2e4 100644 --- a/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts +++ b/src/connectors/webwallet/starknetWindowObject/getWebWalletStarknetObject.ts @@ -2,10 +2,24 @@ import type { CreateTRPCProxyClient } from "@trpc/client" 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) { @@ -22,5 +36,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/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 } +}