From fc1f00eec9eceea513bd76ab702232df900bcaa9 Mon Sep 17 00:00:00 2001 From: mj221 Date: Sat, 17 Feb 2024 21:29:13 +1000 Subject: [PATCH] feat: refactored old dns resolver implementation --- .../app-extension/src/background/index.ts | 39 ++++ .../WebDomainResolver/CustomIpfsGateway.tsx | 60 ++++++ .../WebDomainResolver/SwitchIpfsGateway.tsx | 73 ++++++++ .../Preferences/WebDomainResolver/index.tsx | 120 ++++++++++++ .../Unlocked/Settings/Preferences/index.tsx | 3 + .../app-extension/src/dns-redirect-error.html | 38 ++++ packages/app-extension/src/dns-redirect.html | 108 +++++++++++ .../src/dns-redirects/constants.ts | 50 +++++ .../dns-redirects/helpers/domainResolver.ts | 32 ++++ .../src/dns-redirects/helpers/index.ts | 3 + .../src/dns-redirects/helpers/ipfsBuilder.ts | 171 ++++++++++++++++++ .../src/dns-redirects/helpers/tabHelper.ts | 62 +++++++ .../app-extension/src/dns-redirects/index.ts | 90 +++++++++ .../dns-redirects/networks/Ethereum/index.ts | 37 ++++ .../dns-redirects/networks/Solana/index.ts | 89 +++++++++ .../src/dns-redirects/networks/index.ts | 2 + packages/app-extension/src/manifest.json | 2 +- .../refactor/navigation/SettingsNavigator.tsx | 39 ++++ ...bDomainResolverIpfsGatewayCustomScreen.tsx | 27 +++ ...ncesWebDomainResolverIpfsGatewayScreen.tsx | 27 +++ .../PreferencesWebDomainResolverScreen.tsx | 27 +++ packages/app-extension/webpack.config.js | 1 + packages/background/src/backend/core.ts | 55 ++++++ packages/background/src/frontend/server-ui.ts | 53 ++++++ packages/common/src/constants.ts | 20 ++ packages/common/src/types.ts | 12 ++ packages/i18n/src/locales/en.json | 10 +- packages/i18n/src/locales/hi.json | 10 +- packages/i18n/src/locales/zh.json | 10 +- .../recoil/src/atoms/preferences/index.tsx | 24 ++- .../recoil/src/hooks/preferences/index.tsx | 12 ++ .../src/blockchain-configs/preferences.ts | 20 +- 32 files changed, 1315 insertions(+), 11 deletions(-) create mode 100644 packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/CustomIpfsGateway.tsx create mode 100644 packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/SwitchIpfsGateway.tsx create mode 100644 packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/index.tsx create mode 100644 packages/app-extension/src/dns-redirect-error.html create mode 100644 packages/app-extension/src/dns-redirect.html create mode 100644 packages/app-extension/src/dns-redirects/constants.ts create mode 100644 packages/app-extension/src/dns-redirects/helpers/domainResolver.ts create mode 100644 packages/app-extension/src/dns-redirects/helpers/index.ts create mode 100644 packages/app-extension/src/dns-redirects/helpers/ipfsBuilder.ts create mode 100644 packages/app-extension/src/dns-redirects/helpers/tabHelper.ts create mode 100644 packages/app-extension/src/dns-redirects/index.ts create mode 100644 packages/app-extension/src/dns-redirects/networks/Ethereum/index.ts create mode 100644 packages/app-extension/src/dns-redirects/networks/Solana/index.ts create mode 100644 packages/app-extension/src/dns-redirects/networks/index.ts create mode 100644 packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayCustomScreen.tsx create mode 100644 packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayScreen.tsx create mode 100644 packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverScreen.tsx diff --git a/packages/app-extension/src/background/index.ts b/packages/app-extension/src/background/index.ts index cdcc6df0da..d1a92fcb0a 100644 --- a/packages/app-extension/src/background/index.ts +++ b/packages/app-extension/src/background/index.ts @@ -1,5 +1,44 @@ import { start } from "@coral-xyz/background"; +import { supportedDomains, urlPatterns } from "../dns-redirects/constants"; +import { redirect } from "../dns-redirects/helpers/tabHelper"; + start({ isMobile: false, }); + +/** + * Resolves domain names in the form of URLs. + */ +chrome.webNavigation.onBeforeNavigate.addListener( + async (details) => { + await redirect(details.url); + }, + { + url: supportedDomains.map((domain) => { + return { urlMatches: `^[^:]+://[^/]+.${domain}/.*$` }; + }), + } +); + +/** + * Resolves domain names in the form of browser searches via Google, Bing, etc. + * DuckDuckGo has a unique search pattern and must be queried separately. + */ +chrome.webNavigation.onBeforeNavigate.addListener( + async (details) => { + const domainUrl = new URL(details.url).searchParams.get("q"); + if (domainUrl && domainUrl.indexOf(" ") < 0) await redirect(domainUrl); + }, + { + url: supportedDomains.flatMap((param) => + urlPatterns.map((pattern) => { + return { + urlMatches: pattern.includes("duckduckgo") + ? `${pattern}\\.${param}$` + : `${pattern}\\.${param}&.*$`, + }; + }) + ), + } +); diff --git a/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/CustomIpfsGateway.tsx b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/CustomIpfsGateway.tsx new file mode 100644 index 0000000000..203b958d2e --- /dev/null +++ b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/CustomIpfsGateway.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE } from "@coral-xyz/common"; +import { useTranslation } from "@coral-xyz/i18n"; +import { InputListItem, Inputs, PrimaryButton } from "@coral-xyz/react-common"; +import { useBackgroundClient } from "@coral-xyz/recoil"; + +import { setIPFSGateway } from "../../../../../dns-redirects/helpers"; + +export function PreferencesCustomIpfsGateway() { + const background = useBackgroundClient(); + const { t } = useTranslation(); + + const [gatewayUrl, setGatewayUrl] = useState(""); + const changeIpfsGateway = async () => { + try { + background + .request({ + method: UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE, + params: [gatewayUrl], + }) + .catch(console.error); + + await setIPFSGateway(gatewayUrl); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+
+ + { + setGatewayUrl(e.target.value); + }} + /> + +
+
+ +
+
+
+ ); +} diff --git a/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/SwitchIpfsGateway.tsx b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/SwitchIpfsGateway.tsx new file mode 100644 index 0000000000..003e939d91 --- /dev/null +++ b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/SwitchIpfsGateway.tsx @@ -0,0 +1,73 @@ +import { + DEFAULT_IPFS_GATEWAYS, + UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE, +} from "@coral-xyz/common"; +import { useTranslation } from "@coral-xyz/i18n"; +import { PushDetail } from "@coral-xyz/react-common"; +import { useBackgroundClient, useIpfsGateway } from "@coral-xyz/recoil"; +import { useNavigation } from "@react-navigation/native"; + +import { setIPFSGateway } from "../../../../../dns-redirects/helpers"; +import { Routes } from "../../../../../refactor/navigation/SettingsNavigator"; +import { SettingsList } from "../../../../common/Settings/List"; +import { Checkmark } from "../Blockchains/ConnectionSwitch"; + +interface MenuItems { + [key: string]: { + onClick: () => void; + detail?: React.ReactNode; + style?: React.CSSProperties; + classes?: any; + button?: boolean; + icon?: React.ReactNode; + label?: string; + }; +} +export function PreferencesIpfsGateway() { + const background = useBackgroundClient(); + const { t } = useTranslation(); + + const navigation = useNavigation(); + + const currentIpfsGatewayUrl = useIpfsGateway(); + const changeIpfsGateway = async (url: string) => { + try { + background + .request({ + method: UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE, + params: [url], + }) + .catch(console.error); + await setIPFSGateway(url); + } catch (err) { + console.error(err); + } + }; + + const menuItems = DEFAULT_IPFS_GATEWAYS.reduce((acc, gateway) => { + (acc as MenuItems)[gateway] = { + onClick: () => changeIpfsGateway(gateway), + detail: currentIpfsGatewayUrl === gateway && , + }; + return acc; + }, {}); + const customMenu: MenuItems = { + [t("custom")]: { + onClick: () => + navigation.push( + Routes.PreferencesWebDomainResolverIpfsGatewayCustomScreen + ), + detail: !DEFAULT_IPFS_GATEWAYS.includes(currentIpfsGatewayUrl) ? ( + <> + + + + ) : ( + + ), + }, + }; + Object.assign(menuItems, customMenu); + + return ; +} diff --git a/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/index.tsx b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/index.tsx new file mode 100644 index 0000000000..92f6f55c35 --- /dev/null +++ b/packages/app-extension/src/components/Unlocked/Settings/Preferences/WebDomainResolver/index.tsx @@ -0,0 +1,120 @@ +import { + Blockchain, + UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_UPDATE, +} from "@coral-xyz/common"; +import { useTranslation } from "@coral-xyz/i18n"; +import { + getBlockchainLogo, + useBackgroundClient, + useSupportedDnsNetwork, +} from "@coral-xyz/recoil"; +import { useNavigation } from "@react-navigation/native"; + +import { toggleSupportedNetworkResolution } from "../../../../../dns-redirects/helpers"; +import { Routes } from "../../../../../refactor/navigation/SettingsNavigator"; +import { SettingsList } from "../../../../common/Settings/List"; +import { ModeSwitch } from ".."; + +export const PreferencesDomainResolverContent: React.FC = () => { + const { t } = useTranslation(); + + const navigation = useNavigation(); + const resolverMenuItems = { + [t("ipfs_gateways")]: { + onClick: () => + navigation.push(Routes.PreferencesWebDomainResolverIpfsGatewayScreen), + }, + }; + + const background = useBackgroundClient(); + + const isSupportedNetwork = { + [Blockchain.SOLANA]: useSupportedDnsNetwork(Blockchain.SOLANA), + [Blockchain.ETHEREUM]: useSupportedDnsNetwork(Blockchain.ETHEREUM), + }; + + const toggleSupportedDNSResolutionNetworks = async ( + blockchain: Blockchain, + isEnabled: boolean + ) => { + try { + background + .request({ + method: UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_UPDATE, + params: [blockchain, isEnabled], + }) + .catch(console.error); + await toggleSupportedNetworkResolution(blockchain, isEnabled); + } catch (err) { + console.error(err); + } + }; + + const blockchainMenuItems: any = { + Solana: { + onClick: async () => { + await toggleSupportedDNSResolutionNetworks( + Blockchain.SOLANA, + !isSupportedNetwork[Blockchain.SOLANA] + ); + }, + icon: () => { + const blockchainLogo = getBlockchainLogo(Blockchain.SOLANA); + return ( + + ); + }, + detail: ( + + toggleSupportedDNSResolutionNetworks(Blockchain.SOLANA, enabled) + } + /> + ), + }, + Ethereum: { + onClick: async () => { + await toggleSupportedDNSResolutionNetworks( + Blockchain.ETHEREUM, + !isSupportedNetwork[Blockchain.ETHEREUM] + ); + }, + icon: () => { + const blockchainLogo = getBlockchainLogo(Blockchain.ETHEREUM); + return ( + + ); + }, + detail: ( + + toggleSupportedDNSResolutionNetworks(Blockchain.ETHEREUM, enabled) + } + /> + ), + }, + }; + + return ( +
+ + +
+ ); +}; diff --git a/packages/app-extension/src/components/Unlocked/Settings/Preferences/index.tsx b/packages/app-extension/src/components/Unlocked/Settings/Preferences/index.tsx index 31df80c374..3d460213ee 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/Preferences/index.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/Preferences/index.tsx @@ -90,6 +90,9 @@ export function Preferences() { [t("hidden_tokens")]: { onClick: () => navigation.push(Routes.PreferencesHiddenTokensScreen), }, + [t("web_domain_resolver")]: { + onClick: () => navigation.push(Routes.PreferencesWebDomainResolverScreen), + }, }; // if (BACKPACK_FEATURE_LIGHT_MODE) { diff --git a/packages/app-extension/src/dns-redirect-error.html b/packages/app-extension/src/dns-redirect-error.html new file mode 100644 index 0000000000..6ee339c98c --- /dev/null +++ b/packages/app-extension/src/dns-redirect-error.html @@ -0,0 +1,38 @@ +
+
+ Redirect Error +

404

+ Your domain was unavailable. +
+
+ diff --git a/packages/app-extension/src/dns-redirect.html b/packages/app-extension/src/dns-redirect.html new file mode 100644 index 0000000000..0fec2d5506 --- /dev/null +++ b/packages/app-extension/src/dns-redirect.html @@ -0,0 +1,108 @@ + + + + + + Backpack DNS Resolver + + + + + + +
+ Redirecting to +
+
+ + + + +
+ + + \ No newline at end of file diff --git a/packages/app-extension/src/dns-redirects/constants.ts b/packages/app-extension/src/dns-redirects/constants.ts new file mode 100644 index 0000000000..3714014325 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/constants.ts @@ -0,0 +1,50 @@ +import { Blockchain } from "@coral-xyz/common"; + +// TLD constants +export const ETH_TLD = "eth"; +export const SOL_TLD = "sol"; + +export const TldToNetworkMapping = { + [ETH_TLD]: Blockchain.ETHEREUM, + [SOL_TLD]: Blockchain.SOLANA, +}; + +export const supportedDomains: string[] = [SOL_TLD, ETH_TLD]; + +// Prefix constants to be queried in the domain content +export const ipnsOrIpfsPrefix = ["ipns=", "ipfs=", "ipfs://", "ipns://"]; +export const arweavePrefix = "arw://"; +export const shadowDrivePrefix = "shdw://"; + +// Custom prefixes to resolve. Add your custom prefix here. +export const customPrefixes: Record = { + arweave: arweavePrefix, + shadowDrive: shadowDrivePrefix, +}; + +export const allPrefixes: string[] = [ + ...ipnsOrIpfsPrefix, + ...Object.values(customPrefixes), +]; + +export const ipAddressRegex = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + +export enum PREFIX { + IPFS = "/ipfs/", + IPNS = "/ipns/", +} + +// Url regex patterns for most popular search engines: Google, Bing and DuckDuckGo. +export const urlPatterns: string[] = [ + "^[^:]+://www\\.google(\\.[a-z]{2,3}){1,2}/search\\?q=.*", + "^[^:]+://www\\.bing\\.com/search\\?q=.*", + "^[^:]+://duckduckgo\\.com/\\?q=.*", +]; + +// Re-exporting for convenience +export { DEFAULT_IPFS_GATEWAYS } from "@coral-xyz/common"; +export { + DEFAULT_GATEWAY, + DEFAULT_SOLANA_CLUSTER, +} from "@coral-xyz/secure-background/legacyCommon"; diff --git a/packages/app-extension/src/dns-redirects/helpers/domainResolver.ts b/packages/app-extension/src/dns-redirects/helpers/domainResolver.ts new file mode 100644 index 0000000000..ca8f0f247f --- /dev/null +++ b/packages/app-extension/src/dns-redirects/helpers/domainResolver.ts @@ -0,0 +1,32 @@ +import { ETH_TLD, SOL_TLD, supportedDomains } from "../constants"; +import { handleDomainETH, handleDomainSOL } from "../networks"; + +// Declare types +type DomainHandler = ( + nameServicePathAndSearch: string, + fullDomainString: string +) => Promise; + +type ResolveDomainNameType = { + [K in typeof ETH_TLD | typeof SOL_TLD]: DomainHandler; +}; + +// Implementation + +// Add-on domain name resolution methods here +/** + * Domain resolution methods. + * Able to add more domain resolution methods. + */ +export const ResolveDomainName: ResolveDomainNameType = { + [ETH_TLD]: handleDomainETH, + [SOL_TLD]: handleDomainSOL, + // ... [MONAD_TLD] : handleDomainMONAD, +}; + +// Verifies that the top level domain (e.g. sol, eth) is supported +export const isSupportedTLD = ( + tld: string +): tld is keyof typeof ResolveDomainName => { + return supportedDomains.includes(tld); +}; diff --git a/packages/app-extension/src/dns-redirects/helpers/index.ts b/packages/app-extension/src/dns-redirects/helpers/index.ts new file mode 100644 index 0000000000..0c082c0a92 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./domainResolver"; +export * from "./ipfsBuilder"; +export * from "./tabHelper"; diff --git a/packages/app-extension/src/dns-redirects/helpers/ipfsBuilder.ts b/packages/app-extension/src/dns-redirects/helpers/ipfsBuilder.ts new file mode 100644 index 0000000000..4b12b224db --- /dev/null +++ b/packages/app-extension/src/dns-redirects/helpers/ipfsBuilder.ts @@ -0,0 +1,171 @@ +// Modified implementation from https://github.com/zhvng/sns-resolver. Thank @zhvng! + +import { + allPrefixes, + arweavePrefix, + DEFAULT_GATEWAY, + ipAddressRegex, + PREFIX, + shadowDrivePrefix, +} from "../constants"; + +import { addHttps } from "./tabHelper"; + +// ---------------------------------------------------------------- +// Handles IPFS Gateway Fetching and Resolution Toggle +interface GatewayData { + IPFSGateway?: string; +} +/** + * Retreive user preferred IPFS gateway from local storage + * + * @returns {Promise} Stored IPFS gateway url + */ + +export async function getIPFSGateway() { + const data: GatewayData = await new Promise((resolve, reject) => { + chrome.storage.local.get("IPFSGateway", (data) => + data ? resolve(data) : reject() + ); + }); + + if ("IPFSGateway" in data) { + return data["IPFSGateway"]; + } else { + await setIPFSGateway(DEFAULT_GATEWAY); + return DEFAULT_GATEWAY; + } +} + +/** + * Set user's preferred IPFS gateway to a new gateway url. + * Retrieve url from the `IPFSGateways` object or use a custom url if needed + * @param {string} gateway New IPFS Gateway url + */ +export async function setIPFSGateway(gateway: string) { + await new Promise((resolve) => { + chrome.storage.local.set({ IPFSGateway: gateway }, () => resolve()); + }); +} + +export const toggleSupportedNetworkResolution = async ( + network: string, + enabled: boolean +) => { + await new Promise((resolve) => { + chrome.storage.local.set({ [`${network}-domain`]: enabled }, () => + resolve() + ); + }); +}; + +export interface SupportedWebDNSNetworkResolutionData { + [key: string]: boolean; +} +export const getSupportedNetworkResolution = async ( + network: string +): Promise => { + const data = await new Promise( + (resolve, reject) => { + chrome.storage.local.get(`${network}-domain`, (data) => + data ? resolve(data) : reject() + ); + } + ); + + if (`${network}-domain` in data) { + return data[`${network}-domain`]; + } else { + await toggleSupportedNetworkResolution(network, false); + return false; + } +}; + +// ---------------------------------------------------------------- +// Url Builder Helpers + +/** + * Build IPFS url from cid and path + * + * @param {string} cid IPFS cid to access with url + * @param {string} path path of the url (containing any search params) + * @param {PREFIX} prefix prefix of the url + */ +export const buildIpfsOrIpnsUrl = async ( + cid: string, + path: string, + prefix: PREFIX +) => { + const gatewayUrl = await getIPFSGateway(); + return addHttps(gatewayUrl + prefix + cid + path); +}; + +/** + * Checks if URL prefix starts with IPFS or IPNS + * @param data Domain content + * @param prefixes Defaults to specified IPFS and IPNS + * @returns Returns the prefix it starts with else null + */ +export const checkUrlPrefix = ( + data: string, + prefixes: string[] = allPrefixes +) => { + for (let prefix of prefixes) { + if (data.startsWith(prefix)) { + return prefix; + } + } + return null; +}; + +/** + * Constructs a url depending on the type of url and prefix + * @param data Domain content + * @param nameServicePathAndSearch Path of the url + */ +export const redirectToIpfs = async ( + data: string, + nameServicePathAndSearch: string +) => { + const urlPrefix = checkUrlPrefix(data); + let url = ""; + if (urlPrefix) { + const content = data.slice(urlPrefix.length); + url = await getUrlByPrefix(urlPrefix, content, nameServicePathAndSearch); + } else if (data.match(ipAddressRegex)) { + url = "http://" + data + nameServicePathAndSearch; + } else { + url = addHttps(data + nameServicePathAndSearch); + } + let response = await fetch(url); + if (response.status != 200) { + throw new Error("invalid url"); + } + window.location.href = url; +}; + +/** + * Helper function to get the URL based on the prefix + * @param prefix The prefix of the URL + * @param content The content identifier or hash + * @param pathAndSearch The path and search query of the URL + * @returns The formatted URL + */ +export const getUrlByPrefix = async ( + prefix: string, + content: string, + pathAndSearch: string +): Promise => { + if (prefix.includes("ipfs") || prefix.includes("ipns")) { + return await buildIpfsOrIpnsUrl( + content, + pathAndSearch, + prefix.includes("ipfs") ? PREFIX.IPFS : PREFIX.IPNS + ); + } else if (prefix === arweavePrefix) { + return `https://arweave.net/${content}${pathAndSearch}`; + } else if (prefix === shadowDrivePrefix) { + return `https://shdw-drive.genesysgo.net/${content}${pathAndSearch}`; + } + return ""; +}; diff --git a/packages/app-extension/src/dns-redirects/helpers/tabHelper.ts b/packages/app-extension/src/dns-redirects/helpers/tabHelper.ts new file mode 100644 index 0000000000..8a45006c9c --- /dev/null +++ b/packages/app-extension/src/dns-redirects/helpers/tabHelper.ts @@ -0,0 +1,62 @@ +import { TldToNetworkMapping } from "../constants"; +import { extractDomainParts } from ".."; + +import { getSupportedNetworkResolution } from "./ipfsBuilder"; + +/** + * Check if the network resolution is enabled for the given domain. + * @param urlString - Parsed url string which consists of the TLD and its subdomains. + */ +const checkIfNetworkResolutionEnabled = async ( + urlString: string +): Promise => { + const { currentTLD } = extractDomainParts(new URL(addHttps(urlString))); + // Check if the domain resolution is enabled + if (currentTLD in TldToNetworkMapping) { + const blockchain = + TldToNetworkMapping[currentTLD as keyof typeof TldToNetworkMapping]; + return getSupportedNetworkResolution(blockchain); + } + + return false; +}; + +export const redirect = async (urlString: string) => { + if (await checkIfNetworkResolutionEnabled(urlString)) { + const tab: chrome.tabs.Tab = await getCurrentTab(); + + if (tab !== undefined && urlString !== undefined) { + await chrome.tabs.update(tab.id || chrome.tabs.TAB_ID_NONE, { + url: `./dns-redirect.html?domain=${urlString}`, + }); + } + } +}; + +export const getCurrentTab = (): Promise => { + return new Promise((resolve, reject) => { + try { + chrome.tabs.query( + { active: true, windowId: chrome.windows.WINDOW_ID_CURRENT }, + function (tabs) { + resolve(tabs[0]); + } + ); + } catch (e) { + reject(e); + } + }); +}; + +/** + * Append the `https://` scheme to the beginning of a url if it does not have it. + * + * @param {string} url to add the scheme to + * @returns {string} url with a scheme + */ +export const addHttps = (url: string): string => { + if (!url.match(/^(?:f|ht)tps?:\/\//)) { + url = "https://" + url; + } + return url; +}; diff --git a/packages/app-extension/src/dns-redirects/index.ts b/packages/app-extension/src/dns-redirects/index.ts new file mode 100644 index 0000000000..6a906227e2 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/index.ts @@ -0,0 +1,90 @@ +import { isSupportedTLD, ResolveDomainName } from "./helpers/domainResolver"; +import { addHttps } from "./helpers/tabHelper"; + +// Define the return type for the extractDomainParts function +interface DomainParts { + /** + * An array of the domain parts, split by the period character. E.g. ["blog", "mjlee", "sol"] + */ + hostNameArray: string[]; + /** + * The user's level domain. E.g. "mjlee" + */ + nameServiceDomain: string; + /** + * The most Top level domain. E.g. "sol" + */ + currentTLD: string; +} + +/** + * A function to extract domain parts from a URL object. + * @param url - A URL object to extract domain parts from. + * @returns {DomainParts} - An object containing the domain parts. + * + * For example, given a URL like `blog.mjlee.sol`: + * - hostNameArray is an array ["blog", "mjlee", "sol"] + * - nameServiceDomain is the user's level domain, in this case, "mjlee" + * - currentTLD is the most top level domain, in this case, "sol" + */ +export const extractDomainParts = (url: URL): DomainParts => { + const hostNameArray = url.hostname.split("."); + const nameServiceDomain = hostNameArray[hostNameArray.length - 2]; + const currentTLD = hostNameArray[hostNameArray.length - 1]; + return { hostNameArray, nameServiceDomain, currentTLD }; +}; + +/** + * Main function that handles domain resolution and redirecting. + * This function extracts the domain from the URL, checks if the domain TLD is supported, + * updates the domain display text, and calls the relevant ResolveDomainName function. + * If an error occurs during domain resolution, it redirects the user to the error page. + */ +const main: () => Promise = async () => { + if (typeof window === "undefined") { + return; + } + // Extract the domainUrl search parameter + const domain = new URL(window.location.href).searchParams.get("domain"); + if (!domain) { + return; + } + + // Reformat URL if required, for parsing and extracting information we need. + const urlParsed = new URL(addHttps(domain)); + const { hostNameArray, nameServiceDomain, currentTLD } = + extractDomainParts(urlParsed); + + // Exit early if the domain is not supported + if (!isSupportedTLD(currentTLD)) { + return; + } + + // Concatenate the domain parts to form the full domain + const leadingDomain = hostNameArray.slice(0, -2).join("."); + const fullDomainString = + (leadingDomain ? leadingDomain + "." : "") + + nameServiceDomain + + "." + + currentTLD; + + // Concatenate the name service path and search values + const nameServicePathAndSearch = urlParsed.pathname + urlParsed.search; + + // Update the domain display text + const displayText = document.getElementById("domainDisplay"); + if (displayText) displayText.textContent = fullDomainString; + + try { + // Call the relevant ResolveDomainName function for the current TLD + await ResolveDomainName[currentTLD]( + nameServicePathAndSearch, + fullDomainString + ); + } catch (err) { + console.log(err); + window.location.href = "./dns-redirect-error.html"; + } +}; + +void main(); diff --git a/packages/app-extension/src/dns-redirects/networks/Ethereum/index.ts b/packages/app-extension/src/dns-redirects/networks/Ethereum/index.ts new file mode 100644 index 0000000000..d2f65f1e72 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/networks/Ethereum/index.ts @@ -0,0 +1,37 @@ +import { EthereumConnectionUrl } from "@coral-xyz/secure-background/src/blockchain-configs/ethereum/connection-url"; +import { ethers } from "ethers"; + +import { redirectToIpfs } from "../../helpers/"; + +const getContentFromAccount = async ( + nameServiceDomain: string +): Promise => { + const provider = new ethers.providers.JsonRpcProvider( + EthereumConnectionUrl.MAINNET + ); + const resolver = await provider.getResolver(nameServiceDomain); + + const data = await resolver?.getContentHash(); + if (!data) { + return; + } + return data; +}; + +/** + * Resolves Ethereum Domain + * @param nameServiceDomain User's TLD + * @param hostNameArray Array of domains which includes Domains and Subdomains + * @param nameServicePathAndSearch Path of the url + * @param domainFull Full concatenated domain + */ +export const handleDomainETH = async ( + nameServicePathAndSearch: string, + domainFull: string +) => { + const data = await getContentFromAccount(domainFull); + if (!data) { + throw new Error("invalid domain content"); + } + await redirectToIpfs(data, nameServicePathAndSearch); +}; diff --git a/packages/app-extension/src/dns-redirects/networks/Solana/index.ts b/packages/app-extension/src/dns-redirects/networks/Solana/index.ts new file mode 100644 index 0000000000..8426075719 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/networks/Solana/index.ts @@ -0,0 +1,89 @@ +import type { NameRegistryState } from "@bonfida/spl-name-service"; +import { + getArweaveRecord, + getDomainKeySync, + getIpfsRecord, + getShdwRecord, + getUrlRecord, +} from "@bonfida/spl-name-service"; +import * as web3 from "@solana/web3.js"; + +import { DEFAULT_SOLANA_CLUSTER } from "../../constants"; +import { redirectToIpfs } from "../../helpers"; + +// Helper function to fetch record content and handle errors +const fetchRecordContent = async ( + recordPromise: ( + connection: web3.Connection, + domain: string + ) => Promise, + connection: web3.Connection, + domainFull: string +): Promise => { + try { + const record = await recordPromise(connection, domainFull); + return record.data?.toString("ascii"); + } catch (error: unknown) { + // If the record is not found, it errors and should return undefined + console.error(error); + return undefined; + } +}; + +// Fetches domain content +const fetchDomainContent = async ( + connection: web3.Connection, + domainFull: string +): Promise => { + const nameAccount = await connection.getAccountInfo( + getDomainKeySync(domainFull).pubkey, + "processed" + ); + + return nameAccount?.data.toString("ascii").slice(96).replace(/\0/g, ""); +}; + +// Main function to fetch content from different records in priority order +const getContentFromAccount = async ( + domainFull: string, + apiUrl: string = DEFAULT_SOLANA_CLUSTER +): Promise => { + const connection = new web3.Connection(apiUrl); + + // Create an array of promises for fetching content from different records with chronological priority + // Prioritizes url, ipfs, shdw, ar records in this order, and checks domain content as well if none of them resolves + const fetchPromises = [ + fetchRecordContent(getUrlRecord, connection, domainFull), + fetchRecordContent(getIpfsRecord, connection, domainFull), + fetchRecordContent(getShdwRecord, connection, domainFull), + fetchRecordContent(getArweaveRecord, connection, domainFull), + fetchDomainContent(connection, domainFull), + ]; + + // Use Promise.allSettled to wait for all promises to be settled (fulfilled or rejected) + const settledResults = await Promise.allSettled(fetchPromises); + + // Find the first fulfilled promise with a valid result (i.e., the first successful record fetched) + const firstFulfilled = settledResults.find( + (result) => result.status === "fulfilled" && result.value !== undefined + ) as PromiseFulfilledResult; + + return firstFulfilled.value; +}; + +// Function to handle the domain, fetch content, and redirect to IPFS +export const handleDomainSOL = async ( + nameServicePathAndSearch: string, + domainFull: string +) => { + // Fetch content from the domain + const data = await getContentFromAccount(domainFull); + + // If no data is found, throw an error + if (!data) { + throw new Error("Invalid domain content"); + } + + // Redirect to IPFS using the fetched data + await redirectToIpfs(data, nameServicePathAndSearch); +}; diff --git a/packages/app-extension/src/dns-redirects/networks/index.ts b/packages/app-extension/src/dns-redirects/networks/index.ts new file mode 100644 index 0000000000..a0c7ce2d94 --- /dev/null +++ b/packages/app-extension/src/dns-redirects/networks/index.ts @@ -0,0 +1,2 @@ +export * from "./Ethereum"; +export * from "./Solana"; diff --git a/packages/app-extension/src/manifest.json b/packages/app-extension/src/manifest.json index d1fc7b2051..e69fcddc0e 100644 --- a/packages/app-extension/src/manifest.json +++ b/packages/app-extension/src/manifest.json @@ -34,5 +34,5 @@ "192": "anchor.png", "512": "anchor.png" }, - "permissions": ["alarms", "storage", "background"] + "permissions": ["alarms", "storage", "background", "webNavigation", "tabs"] } diff --git a/packages/app-extension/src/refactor/navigation/SettingsNavigator.tsx b/packages/app-extension/src/refactor/navigation/SettingsNavigator.tsx index 568f049bc6..192c2a5e75 100644 --- a/packages/app-extension/src/refactor/navigation/SettingsNavigator.tsx +++ b/packages/app-extension/src/refactor/navigation/SettingsNavigator.tsx @@ -16,6 +16,9 @@ import { PreferencesHiddenTokensScreen } from "../screens/Unlocked/Settings/Pref import { PreferencesLanguageScreen } from "../screens/Unlocked/Settings/PreferencesLanguageScreen"; import { PreferencesScreen } from "../screens/Unlocked/Settings/PreferencesScreen"; import { PreferencesTrustedSitesScreen } from "../screens/Unlocked/Settings/PreferencesTrustedSitesScreen"; +import { PreferencesWebDomainResolverIpfsGatewayCustomScreen } from "../screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayCustomScreen"; +import { PreferencesWebDomainResolverIpfsGatewayScreen } from "../screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayScreen"; +import { PreferencesWebDomainResolverScreen } from "../screens/Unlocked/Settings/PreferencesWebDomainResolverScreen"; import { SettingsScreen } from "../screens/Unlocked/Settings/SettingsScreen"; import { WalletAddBackpackRecoveryPhraseScreen } from "../screens/Unlocked/Settings/WalletAddBackpackRecoveryPhraseScreen"; import { WalletAddBlockchainSelectScreen } from "../screens/Unlocked/Settings/WalletAddBlockchainSelectScreen"; @@ -75,6 +78,9 @@ export enum Routes { PreferencesBlockchainCommitmentScreen = "PreferencesBlockchainCommitment", PreferencesBlockchainExplorerScreen = "PreferencesBlockchainExplorerer", PreferencesBlockchainRpcConnectionCustomScreen = "PreferencesBlockchainRpcConnectionCustomScreen", + PreferencesWebDomainResolverScreen = "PreferencesWebDomainResolverScreen", + PreferencesWebDomainResolverIpfsGatewayScreen = "PreferencesWebDomainResolverIpfsGatewayScreen", + PreferencesWebDomainResolverIpfsGatewayCustomScreen = "PreferencesWebDomainResolverIpfsCustomScreen", AboutScreen = "AboutScreen", } @@ -142,6 +148,9 @@ type SettingsScreenStackNavigatorParamList = { [Routes.PreferencesTrustedSitesScreen]: undefined; [Routes.PreferencesLanguageScreen]: undefined; [Routes.PreferencesHiddenTokensScreen]: undefined; + [Routes.PreferencesWebDomainResolverScreen]: undefined; + [Routes.PreferencesWebDomainResolverIpfsGatewayScreen]: undefined; + [Routes.PreferencesWebDomainResolverIpfsGatewayCustomScreen]: undefined; [Routes.PreferencesBlockchainScreen]: { blockchain: Blockchain; }; @@ -461,6 +470,36 @@ export function SettingsNavigator({ }; }} /> + { + return { + title: t("web_domain_resolver"), + ...maybeCloseButton(false, navigation), + }; + }} + /> + { + return { + title: t("ipfs_gateway"), + ...maybeCloseButton(false, navigation), + }; + }} + /> + { + return { + title: t("custom_ipfs_gateway"), + ...maybeCloseButton(false, navigation), + }; + }} + /> +) { + return ( + }> + + + ); +} + +// TODO: Inherit from other screens? +function Loading() { + return null; +} + +function Container( + _props: SettingsScreenProps +) { + return ; +} diff --git a/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayScreen.tsx b/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayScreen.tsx new file mode 100644 index 0000000000..59515f59fe --- /dev/null +++ b/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverIpfsGatewayScreen.tsx @@ -0,0 +1,27 @@ +import { PreferencesIpfsGateway } from "../../../../components/Unlocked/Settings/Preferences/WebDomainResolver/SwitchIpfsGateway"; +import { ScreenContainer } from "../../../components/ScreenContainer"; +import type { + Routes, + SettingsScreenProps, +} from "../../../navigation/SettingsNavigator"; + +export function PreferencesWebDomainResolverIpfsGatewayScreen( + props: SettingsScreenProps +) { + return ( + }> + + + ); +} + +// TODO: Inherit from other screens? +function Loading() { + return null; +} + +function Container( + _props: SettingsScreenProps +) { + return ; +} diff --git a/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverScreen.tsx b/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverScreen.tsx new file mode 100644 index 0000000000..c439323e9e --- /dev/null +++ b/packages/app-extension/src/refactor/screens/Unlocked/Settings/PreferencesWebDomainResolverScreen.tsx @@ -0,0 +1,27 @@ +import { PreferencesDomainResolverContent } from "../../../../components/Unlocked/Settings/Preferences/WebDomainResolver"; +import { ScreenContainer } from "../../../components/ScreenContainer"; +import type { + Routes, + SettingsScreenProps, +} from "../../../navigation/SettingsNavigator"; + +export function PreferencesWebDomainResolverScreen( + props: SettingsScreenProps +) { + return ( + }> + + + ); +} + +// TODO: Inherit from other screens?? +function Loading() { + return null; +} + +function Container( + _props: SettingsScreenProps +) { + return ; +} diff --git a/packages/app-extension/webpack.config.js b/packages/app-extension/webpack.config.js index 93f0ebe5ee..09d1cf8541 100644 --- a/packages/app-extension/webpack.config.js +++ b/packages/app-extension/webpack.config.js @@ -190,6 +190,7 @@ const options = { options: "./src/options/index.tsx", permissions: "./src/permissions/index.tsx", popup: "./src/index.tsx", + dnsRedirects: "./src/dns-redirects/index.ts", quickStart: "./src/quickStart.ts", contentScript: "./src/contentScript/index.ts", // injected: "../provider-injection/dist/browser/index.js", diff --git a/packages/background/src/backend/core.ts b/packages/background/src/backend/core.ts index dfb8ccbed7..51ecdb11ac 100644 --- a/packages/background/src/backend/core.ts +++ b/packages/background/src/backend/core.ts @@ -642,6 +642,61 @@ export class Backend { return SUCCESS_RESPONSE; } + async domainContentIPFSGatewayRead(uuid: string): Promise { + const data = await secureStore.getWalletDataForUser(uuid); + return data.webDnsResolutionGateway.ipfsGateway; + } + + async domainContentIPFSGatewayUpdate(ipfsGateway: string): Promise { + const uuid = (await this.keyringStore.activeUserKeyring()).uuid; + const data = await secureStore.getWalletDataForUser(uuid!); + + await secureStore.setWalletDataForUser(uuid!, { + ...data, + webDnsResolutionGateway: { + ...data.webDnsResolutionGateway, + ipfsGateway, + }, + }); + await this.notificationsClient.userUpdated(); + + return SUCCESS_RESPONSE; + } + + async supportedWebDNSNetworkRead( + uuid: string, + blockchain: Blockchain + ): Promise { + const data = await secureStore.getWalletDataForUser(uuid); + const supportedNetworks = + data.webDnsResolutionGateway.supportedWebDNSNetwork; + + return blockchain in supportedNetworks + ? supportedNetworks[blockchain] + : false; + } + + async supportedWebDNSNetworkUpdate( + blockchain: Blockchain, + isEnabled: boolean + ): Promise { + const uuid = (await this.keyringStore.activeUserKeyring()).uuid; + const data = await secureStore.getWalletDataForUser(uuid!); + + await secureStore.setWalletDataForUser(uuid!, { + ...data, + webDnsResolutionGateway: { + ...data.webDnsResolutionGateway, + supportedWebDNSNetwork: { + ...data.webDnsResolutionGateway.supportedWebDNSNetwork, + [blockchain]: isEnabled, + }, + }, + }); + await this.notificationsClient.userUpdated(); + return SUCCESS_RESPONSE; + } + async setFeatureGates(gates: FEATURE_GATES_MAP) { await legacyStore.setFeatureGates(gates); this.events.emit(BACKEND_EVENT, { diff --git a/packages/background/src/frontend/server-ui.ts b/packages/background/src/frontend/server-ui.ts index 5ad70ef081..0518138ff5 100644 --- a/packages/background/src/frontend/server-ui.ts +++ b/packages/background/src/frontend/server-ui.ts @@ -85,6 +85,10 @@ import { UI_RPC_METHOD_SETTINGS_DEVELOPER_MODE_READ, UI_RPC_METHOD_SETTINGS_DEVELOPER_MODE_UPDATE, UI_RPC_METHOD_SETTINGS_LOCK_FULL_SCREEN_UPDATE, + UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_READ, + UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE, + UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_READ, + UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_UPDATE, UI_RPC_METHOD_TOGGLE_SHOW_ALL_COLLECTIBLES, UI_RPC_METHOD_USER_READ, withContextPort, @@ -176,6 +180,18 @@ async function handle( return await handleApprovedOriginsUpdate(ctx, params[0]); case UI_RPC_METHOD_APPROVED_ORIGINS_DELETE: return await handleApprovedOriginsDelete(ctx, params[0]); + case UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_READ: + return await handleDomainContentIPFSGatewayRead(ctx, params[0]); + case UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE: + return await handleDomainContentIPFSGatewayUpdate(ctx, params[0]); + case UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_READ: + return await handleSupportedWebDNSNetworkRead(ctx, params[0], params[1]); + case UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_UPDATE: + return await handleSupportedWebDNSNetworkUpdate( + ctx, + params[0], + params[1] + ); case UI_RPC_METHOD_SET_FEATURE_GATES: return await handleSetFeatureGates(ctx, params[0]); case UI_RPC_METHOD_GET_FEATURE_GATES: @@ -590,3 +606,40 @@ async function handleToggleShowAllCollectibles( const resp = await ctx.backend.toggleShowAllCollectibles(); return [resp]; } + +async function handleDomainContentIPFSGatewayRead( + ctx: Context, + uuid: string +): Promise> { + const resp = await ctx.backend.domainContentIPFSGatewayRead(uuid); + return [resp]; +} + +async function handleDomainContentIPFSGatewayUpdate( + ctx: Context, + ipfsGateway: string +): Promise> { + const resp = await ctx.backend.domainContentIPFSGatewayUpdate(ipfsGateway); + return [resp]; +} + +async function handleSupportedWebDNSNetworkRead( + ctx: Context, + uuid: string, + blockchain: Blockchain +): Promise> { + const resp = await ctx.backend.supportedWebDNSNetworkRead(uuid, blockchain); + return [resp]; +} + +async function handleSupportedWebDNSNetworkUpdate( + ctx: Context, + blockchain: Blockchain, + isEnabled: boolean +): Promise> { + const resp = await ctx.backend.supportedWebDNSNetworkUpdate( + blockchain, + isEnabled + ); + return [resp]; +} diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index eb79d90eee..120b3086df 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -166,6 +166,14 @@ export const UI_RPC_METHOD_SETTINGS_AGGREGATE_WALLETS_UPDATE = "ui-rpc-method-settings-aggregate-wallet-update"; export const UI_RPC_METHOD_TRY_TO_SIGN_MESSAGE = "ui-rpc-method-try-to-sign-message"; +export const UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_READ = + "ui-rpc-method-settings-domain-content-ipfs-gateway-read"; +export const UI_RPC_METHOD_SETTINGS_DOMAIN_CONTENT_IPFS_GATEWAY_UPDATE = + "ui-rpc-method-settings-domain-content-ipfs-gateway-update"; +export const UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_READ = + "ui-rpc-method-settings-domain-resolution-networks-read"; +export const UI_RPC_METHOD_SETTINGS_DOMAIN_RESOLUTION_NETWORKS_UPDATE = + "ui-rpc-method-settings-domain-resolution-networks-update"; export const UI_RPC_METHOD_USER_READ = "ui-rpc-method-user-read"; export const UI_RPC_METHOD_ALL_USERS_READ = "ui-rpc-method-all-users-read"; export const UI_RPC_METHOD_PREFERENCES_READ = "ui-rpc-method-references-read"; @@ -595,3 +603,15 @@ export const BACKPACK_TEAM = [ ]; export const MOBILE_USER_PASSWORD_KEY = "user-password"; + +// Default IPFS gateways used to resolve web domains +export const DEFAULT_IPFS_GATEWAYS = [ + "dweb.link", + "infura-ipfs.io", + "cf-ipfs.com", + "astyanax.io", + "ipfs.io", + "cloudflare-ipfs.com", + "gateway.pinata.cloud", + "4everland.io", +]; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index a84e2b7a90..6c86cae0dc 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -195,6 +195,7 @@ export type Preferences = { lockedCollections?: string[]; hiddenTokenAddresses?: Record; showAllCollectibles?: boolean; + webDnsResolutionGateway: WebDnsResolutionGateway; } & DeprecatedWalletDataDoNotUse; export type AutolockSettingsOption = "never" | "onClose" | undefined; @@ -253,3 +254,14 @@ export type Sender = { windowId: number; }; }; + +// Object to store enabled/disabled web dns resolution for supported networks. +export type SupportedWebDnsResolutionNetwork = { + [network in Blockchain]: boolean; +}; + +// Stores the IPFS gateway pairing +export type WebDnsResolutionGateway = { + ipfsGateway: string; + supportedWebDNSNetwork: SupportedWebDnsResolutionNetwork; +}; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 04d05d009c..20fe226f39 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -736,5 +736,13 @@ "your_withdrawal_addresses": "Your Withdrawal Addresses", "youre_all_good": "You're all good!", "youre_verified": "You're verified!", - "zip": "Zip Code" + "zip": "Zip Code", + "ipfs_gateways": "IPFS Gateway", + "custom": "Custom", + "gateway": "Gateway", + "gateway_url": "Gateway URL", + "switch": "Switch", + "ipfs_gateway": "IPFS Gateway", + "custom_ipfs_gateway": "Custom IPFS Gateway", + "web_domain_resolver": "Web Domain Resolver" } diff --git a/packages/i18n/src/locales/hi.json b/packages/i18n/src/locales/hi.json index 989995fcc2..eae09c01f9 100644 --- a/packages/i18n/src/locales/hi.json +++ b/packages/i18n/src/locales/hi.json @@ -229,5 +229,13 @@ }, "you_pay": "आप भुगतान करते हैं", "your_account": "आपका खाता", - "your_addresses": "आपके पते" + "your_addresses": "आपके पते", + "ipfs_gateways": "IPFS गेटवे", + "custom": "कस्टम", + "gateway": "गेटवे", + "gateway_url": "गेटवे URL", + "switch": "स्विच", + "ipfs_gateway": "IPFS गेटवे", + "custom_ipfs_gateway": "कस्टम IPFS गेटवे", + "web_domain_resolver": "वेब डोमेन रिज़ॉल्वर" } diff --git a/packages/i18n/src/locales/zh.json b/packages/i18n/src/locales/zh.json index 2baa9ab393..2f00edf5d7 100644 --- a/packages/i18n/src/locales/zh.json +++ b/packages/i18n/src/locales/zh.json @@ -374,5 +374,13 @@ "you_pay": "你支付", "your_account": "你的账户", "your_addresses": "你的地址", - "zip": "邮政编码" + "zip": "邮政编码", + "ipfs_gateways": "IPFS网关", + "custom": "自定义", + "gateway": "网关", + "gateway_url": "网关URL", + "switch": "切换", + "ipfs_gateway": "IPFS网关", + "custom_ipfs_gateway": "自定义IPFS网关", + "web_domain_resolver": "网域解析器" } diff --git a/packages/recoil/src/atoms/preferences/index.tsx b/packages/recoil/src/atoms/preferences/index.tsx index 40a84164f9..148895c38f 100644 --- a/packages/recoil/src/atoms/preferences/index.tsx +++ b/packages/recoil/src/atoms/preferences/index.tsx @@ -2,8 +2,13 @@ import type { AutolockSettings, Blockchain, Preferences, + SupportedWebDnsResolutionNetwork, } from "@coral-xyz/common"; -import { DEFAULT_AUTO_LOCK_INTERVAL_SECS } from "@coral-xyz/secure-background/legacyCommon"; +import { + DEFAULT_AUTO_LOCK_INTERVAL_SECS, + DEFAULT_ENABLED_WEB_DNS_RESOLUTION_NETWORKS, + DEFAULT_GATEWAY, +} from "@coral-xyz/secure-background/legacyCommon"; import type { User } from "@coral-xyz/secure-background/types"; import type { Commitment } from "@solana/web3.js"; import { atom, selector, selectorFamily } from "recoil"; @@ -227,4 +232,21 @@ export const allBlockchainConnectionUrls = atom<{ }), }); +export const domainContentIpfsGateway = selector({ + key: "domainContentIpfsGateway", + get: async ({ get }) => { + const p = get(preferences); + return p.webDnsResolutionGateway?.ipfsGateway; + }, +}); + +export const enabledDNSResolverNetworks = + selector({ + key: "enabledDNSResolverNetworks", + get: async ({ get }) => { + const p = get(preferences); + return p.webDnsResolutionGateway?.supportedWebDNSNetwork; + }, + }); + export * from "./xnft-preferences"; diff --git a/packages/recoil/src/hooks/preferences/index.tsx b/packages/recoil/src/hooks/preferences/index.tsx index 5ea6bd53b6..2728fdd63d 100644 --- a/packages/recoil/src/hooks/preferences/index.tsx +++ b/packages/recoil/src/hooks/preferences/index.tsx @@ -1,3 +1,4 @@ +import type { Blockchain } from "@coral-xyz/common"; import type { User } from "@coral-xyz/secure-background/types"; import { useRecoilValue } from "recoil"; @@ -42,3 +43,14 @@ export function useAllUsers(): User[] { export function useAllUsersNullable(): User[] | null { return useRecoilValue(atoms.allUsersNullable); } + +export function useIpfsGateway(): string { + return useRecoilValue(atoms.domainContentIpfsGateway)!; +} + +export function useSupportedDnsNetwork(blockchain: Blockchain): boolean { + const supportedNetworks = useRecoilValue(atoms.enabledDNSResolverNetworks); + return blockchain in supportedNetworks + ? supportedNetworks[blockchain] + : false; +} diff --git a/packages/secure-background/src/blockchain-configs/preferences.ts b/packages/secure-background/src/blockchain-configs/preferences.ts index 3436f1a1ed..a04b430428 100644 --- a/packages/secure-background/src/blockchain-configs/preferences.ts +++ b/packages/secure-background/src/blockchain-configs/preferences.ts @@ -1,15 +1,19 @@ -import type { - Blockchain, - BlockchainPreferences, - Preferences, -} from "@coral-xyz/common"; - +import type { BlockchainPreferences, Preferences } from "@coral-xyz/common"; +import { Blockchain, DEFAULT_IPFS_GATEWAYS } from "@coral-xyz/common"; import { getEnabledBlockchainConfigs } from "./blockchains"; export const DEFAULT_DARK_MODE = false; export const DEFAULT_DEVELOPER_MODE = false; export const DEFAULT_AGGREGATE_WALLETS = false; export const DEFAULT_AUTO_LOCK_INTERVAL_SECS = 10 * 60; +export const DEFAULT_GATEWAY = DEFAULT_IPFS_GATEWAYS[0]; +export const DEFAULT_ENABLED_WEB_DNS_RESOLUTION_NETWORKS = Object.values( + Blockchain +).reduce((acc, blockchain) => { + // disable all by default + acc[blockchain] = false; + return acc; +}, {} as Record); export function defaultPreferences(): Preferences { const blockchains = Object.fromEntries( @@ -32,5 +36,9 @@ export function defaultPreferences(): Preferences { blockchains: { ...blockchains, }, + webDnsResolutionGateway: { + ipfsGateway: DEFAULT_GATEWAY, + supportedWebDNSNetwork: DEFAULT_ENABLED_WEB_DNS_RESOLUTION_NETWORKS, + }, } as Preferences; }