diff --git a/package.json b/package.json index 831b881..013182b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "defillama-extension", "private": true, - "version": "0.0.4", + "version": "0.0.5", "type": "module", "description": "DefiLlama Extension", "displayName": "DefiLlama", diff --git a/public/manifest.json b/public/manifest.json index 21d8482..0d6c868 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "DefiLlama", - "version": "0.0.4", + "version": "0.0.5", "description": "DefiLlama Extension", "background": { "service_worker": "src/pages/background/index.js", diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index 1b346cd..2a447ef 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -7,259 +7,333 @@ import maxPain from "@assets/img/memes/max-pain-128.png"; import que from "@assets/img/memes/que-128.png"; import upOnly from "@assets/img/memes/up-only-128.png"; -import { Protocol, protocolsDb, allowedDomainsDb, blockedDomainsDb, fuzzyDomainsDb } from "../libs/db"; +import { + Protocol, + putAllowedDomainsDb, + putFuzzyDomainsDb, + putBlockedDomainsDb, + countAllowedDomainsDb, + countFuzzyDomainsDb, + countBlockedDomainsDb, +} from "@src/pages/libs/db"; import { PROTOCOLS_API, METAMASK_LIST_CONFIG_API, DEFILLAMA_DIRECTORY_API, PROTOCOL_TVL_THRESHOLD, TWITTER_CONFIG_API, -} from "../libs/constants"; -import { getStorage, setStorage } from "../libs/helpers"; -import { checkDomain } from "../libs/phishing-detector"; + DB_UPDATE_FREQUENCY, + DEFAULT_SETTINGS, + MessageType, +} from "@src/pages/libs/constants"; +import { getStorage, setStorage } from "@src/pages/libs/helpers"; +import { checkDomain } from "@src/pages/libs/phishing-detector"; -startupTasks(); +type DomainDbObj = { domain: string; updateId: string }; -async function getCurrentTab() { - const queryOptions = { active: true, currentWindow: true }; - const [tab] = await Browser.tabs.query(queryOptions); - return tab; -} +/* Background class that encapsulates all background operations and state */ +class Background { + updateDomainDbsRunning: boolean; + updateTwitterConfigRunning: boolean; + allowedDomainsTmpStorage: DomainDbObj[] | null; + blockedDomainsTmpStorage: DomainDbObj[] | null; + fuzzyDomainsTmpStorage: DomainDbObj[] | null; + constructor() { + // those variables are used to know if an update routine is currently running, to avoid launching another at the same time + this.updateDomainDbsRunning = false; + this.updateTwitterConfigRunning = false; + this.allowedDomainsTmpStorage = null; + this.blockedDomainsTmpStorage = null; + this.fuzzyDomainsTmpStorage = null; + // listeners to trigger the startup script + Browser.runtime.onInstalled.addListener(() => { + this.startupTasks(); + }); -async function handlePhishingCheck() { - const phishingDetector = await getStorage("local", "settings:phishingDetector", true); - if (!phishingDetector) { - Browser.action.setIcon({ path: cute }); - return; + Browser.runtime.onStartup.addListener(() => { + this.startupTasks(); + }); + + // listener for the alarms the startup script sets up + Browser.alarms.onAlarm.addListener(async (a) => { + switch (a.name) { + case "updateDomainDbs": + await this.updateDomainDbs(); + break; + case "updateTwitterConfig": + await this.updateTwitterConfig(); + break; + } + }); + + // listeners for tab update/activation to trigger phising check + Browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + await this.handlePhishingCheck(tabId); + }); + Browser.tabs.onActivated.addListener(async (activeInfo) => { + await this.handlePhishingCheck(activeInfo.tabId); + }); } - let isPhishing = false; - let isTrusted = false; - let reason = "Unknown website"; - const tab = await getCurrentTab(); - try { - const url = tab.url; - if (url.startsWith("https://metamask.github.io/phishing-warning")) { - // already captured and redirected to metamask phishing warning page - isPhishing = true; - reason = "Phishing detected by Metamask"; - } else { - const domain = new URL(url).hostname.replace("www.", ""); - const res = await checkDomain(domain); - console.log("checkDomain", res); - isPhishing = res.result; - if (isPhishing) { - switch (res.type) { - case "blocked": - reason = "Website is blacklisted"; - break; - case "fuzzy": - reason = `Website impersonating ${res.extra}`; - break; - default: - reason = "Suspicious website detected"; + async getCurrentTab() { + const queryOptions = { active: true, currentWindow: true }; + const [tab] = await Browser.tabs.query(queryOptions); + return tab; + } + + // checks if the current tab is a phising site + async handlePhishingCheck(tabId: number) { + const tab = await this.getCurrentTab(); + // This method can be triggered by site updates, and those can come from other tabs than the one being displayed + // (for example, Tradingview updates the title of the page with the current price, which happens very often) + // in that case (tabId !== tab.id) so the check is skipped + if (!tab || !tab.url || tabId !== tab.id) { + return; + } + console.log("phishing check"); + const phishingDetector = await getStorage("local", "settings:phishingDetector", DEFAULT_SETTINGS.PHISHING_DETECTOR); + if (!phishingDetector) { + Browser.action.setIcon({ path: cute }); + return; + } + + let isPhishing = false; + let isTrusted = false; + let reason = "Unknown website"; + + try { + const url = tab.url; + console.log("url", url); + if (url.startsWith("https://metamask.github.io/phishing-warning")) { + // already captured and redirected to metamask phishing warning page + isPhishing = true; + reason = "Phishing detected by Metamask"; + } else if (url.startsWith("chrome://")) { + // whitelist the new tab page + if (url === "chrome://newtab/") { + isTrusted = true; } + isPhishing = false; } else { - switch (res.type) { - case "allowed": - isTrusted = true; - reason = "Website is whitelisted"; - break; - default: - reason = "Unknown website"; + const domain = new URL(url).hostname.replace("www.", ""); + // transmit the temporary storage arrays to the checking function to use them if they are available + // those arrays are populated only when the dbs are being updated and are very slow to respond to queries + // since those arrays are there anyways, we use them to speed up the search process. + const res = await checkDomain( + domain, + this.allowedDomainsTmpStorage, + this.blockedDomainsTmpStorage, + this.fuzzyDomainsTmpStorage, + ); + console.log("checkDomain", res); + isPhishing = res.result; + if (isPhishing) { + switch (res.type) { + case "blocked": + reason = "Website is blacklisted"; + break; + case "fuzzy": + reason = `Website impersonating ${res.extra}`; + break; + default: + reason = "Suspicious website detected"; + } + } else { + switch (res.type) { + case "allowed": + isTrusted = true; + reason = "Website is whitelisted"; + break; + default: + reason = "Unknown website"; + } } } + } catch (error) { + console.log("handlePhishingCheck error", error); + isTrusted = false; + isPhishing = false; + reason = "Invalid URL"; } - } catch (error) { - console.log("handlePhishingCheck error", error); - isTrusted = false; - isPhishing = false; - reason = "Invalid URL"; - } - if (isTrusted) { - Browser.action.setIcon({ path: upOnly }); - Browser.action.setTitle({ title: reason }); - return; - } + if (isTrusted) { + Browser.action.setIcon({ path: upOnly }); + Browser.action.setTitle({ title: reason }); + return; + } - if (isPhishing) { - Browser.action.setIcon({ path: maxPain }); - Browser.action.setTitle({ title: reason }); - } else { - Browser.action.setIcon({ path: que }); - Browser.action.setTitle({ title: reason }); + if (isPhishing) { + Browser.action.setIcon({ path: maxPain }); + Browser.action.setTitle({ title: reason }); + } else { + Browser.action.setIcon({ path: que }); + Browser.action.setTitle({ title: reason }); + } } -} -export async function updateProtocolsDb() { - const raw = await fetch(PROTOCOLS_API).then((res) => res.json()); - const protocols = (raw["protocols"]?.map((x: any) => ({ - name: x.name, - url: x.url, - logo: x.logo, - category: x.category, - tvl: x.tvl, - })) ?? []) as Protocol[]; - if (protocols.length === 0) { - console.log("updateProtocolsDb", "no protocols found"); - return; - } - // empty db before updating - await protocolsDb.protocols.clear(); - const result = await protocolsDb.protocols.bulkPut(protocols); - console.log("updateProtocolsDb", result); -} + // method that fetches blocked/fuzzy/allowed domains lists from DefiLlama and Metamask, then puts them in the Dexie DB + async updateDomainDbs() { + if (this.updateDomainDbsRunning) { + return; + } + this.updateDomainDbsRunning = true; + try { + console.log("updateDomainDbs", "start"); + const rawProtocols = await fetch(PROTOCOLS_API).then((res) => res.json()); + const protocols = ( + (rawProtocols["protocols"]?.map((x: any) => ({ + name: x.name, + url: x.url, + logo: x.logo, + category: x.category, + tvl: x.tvl || 0, + })) ?? []) as Protocol[] + ) + // protocols with a TVL < to a certain threshold may have been added to DefiLlama, + // but could still be scams, so don't display them as safe yet + .filter((x) => x.tvl >= PROTOCOL_TVL_THRESHOLD); + const protocolDomains = protocols + .map((x) => { + try { + return new URL(x.url).hostname.replace("www.", ""); + } catch (error) { + console.log("updateDomainDbs domains mapping error", error); + return null; + } + }) + .filter((x) => x !== null); + const metamaskLists = (await fetch(METAMASK_LIST_CONFIG_API).then((res) => res.json())) as { + fuzzylist: string[]; + whitelist: string[]; + blacklist: string[]; + }; + const metamaskFuzzyDomains = metamaskLists.fuzzylist; + const metamaskAllowedDomains = metamaskLists.whitelist; + const metamaskBlockedDomains = metamaskLists.blacklist; + const rawDefillamaDirectory = (await fetch(DEFILLAMA_DIRECTORY_API).then((res) => res.json())) as { + version: number; + whitelist: string[]; + blacklist?: string[]; + fuzzylist?: string[]; + }; + const defillamaDomains = rawDefillamaDirectory.whitelist; + const defillamaBlockedDomains = rawDefillamaDirectory.blacklist ?? []; + const defillamaFuzzyDomains = rawDefillamaDirectory.fuzzylist ?? []; + const updateId = crypto.randomUUID(); + this.allowedDomainsTmpStorage = [metamaskAllowedDomains, protocolDomains, defillamaDomains] + .flat() + .map((x) => ({ domain: x, updateId })); + this.blockedDomainsTmpStorage = [metamaskBlockedDomains, defillamaBlockedDomains] + .flat() + .map((x) => ({ domain: x, updateId })); + this.fuzzyDomainsTmpStorage = [metamaskFuzzyDomains, protocolDomains, defillamaDomains, defillamaFuzzyDomains] + .flat() + .map((x) => ({ domain: x, updateId })); -export async function updateDomainDbs() { - console.log("updateDomainDbs", "start"); - const rawProtocols = await fetch(PROTOCOLS_API).then((res) => res.json()); - const protocols = ( - (rawProtocols["protocols"]?.map((x: any) => ({ - name: x.name, - url: x.url, - logo: x.logo, - category: x.category, - tvl: x.tvl || 0, - })) ?? []) as Protocol[] - ).filter((x) => x.tvl >= PROTOCOL_TVL_THRESHOLD); - const protocolDomains = protocols - .map((x) => { - try { - return new URL(x.url).hostname.replace("www.", ""); - } catch (error) { - console.log("updateDomainDbs", "error", error); - return null; + if (this.allowedDomainsTmpStorage.length === 0) { + console.log("allowedDomainsDb", "no allowed domains fetched, skipping update"); + } else { + try { + await putAllowedDomainsDb(this.allowedDomainsTmpStorage, updateId).then(async () => { + const count = await countAllowedDomainsDb(); + console.log("allowedDomainsDb", count); + }); + } catch (error) { + console.error("putAllowedDomainsDb error", error); + } } - }) - .filter((x) => x !== null) - .map((x) => ({ domain: x })); - const metamaskLists = (await fetch(METAMASK_LIST_CONFIG_API).then((res) => res.json())) as { - fuzzylist: string[]; - whitelist: string[]; - blacklist: string[]; - }; - const metamaskFuzzyDomains = metamaskLists.fuzzylist.map((x) => ({ domain: x })); - const metamaskAllowedDomains = metamaskLists.whitelist.map((x) => ({ domain: x })); - const metamaskBlockedDomains = metamaskLists.blacklist.map((x) => ({ domain: x })); - const rawDefillamaDirectory = (await fetch(DEFILLAMA_DIRECTORY_API).then((res) => res.json())) as { - version: number; - whitelist: string[]; - blacklist?: string[]; - fuzzylist?: string[]; - }; - const defillamaDomains = rawDefillamaDirectory.whitelist.map((x) => ({ domain: x })); - const defillamaBlockedDomains = rawDefillamaDirectory.blacklist?.map((x) => ({ domain: x })) ?? []; - const defillamaFuzzyDomains = rawDefillamaDirectory.fuzzylist?.map((x) => ({ domain: x })) ?? []; - const allowedDomains = [metamaskAllowedDomains, protocolDomains, defillamaDomains].flat(); - if (allowedDomains.length === 0) { - console.log("allowedDomainsDb", "no allowed domains fetched, skipping update"); - } else { - allowedDomainsDb.domains.clear(); - allowedDomainsDb.domains.bulkPut(allowedDomains); - console.log("allowedDomainsDb", await allowedDomainsDb.domains.count()); - } - const blockedDomains = [metamaskBlockedDomains, defillamaBlockedDomains].flat(); - if (blockedDomains.length === 0) { - console.log("blockedDomainsDb", "no blocked domains fetched, skipping update"); - } else { - blockedDomainsDb.domains.clear(); - blockedDomainsDb.domains.bulkPut(blockedDomains); - console.log("blockedDomainsDb", await blockedDomainsDb.domains.count()); - } - - const fuzzyDomains = [metamaskFuzzyDomains, protocolDomains, defillamaDomains, defillamaFuzzyDomains].flat(); - if (fuzzyDomains.length === 0) { - console.log("fuzzyDomainsDb", "no fuzzy domains fetched, skipping update"); - } else { - fuzzyDomainsDb.domains.clear(); - fuzzyDomainsDb.domains.bulkPut(fuzzyDomains); - console.log("fuzzyDomainsDb", await fuzzyDomainsDb.domains.count()); - } - - console.log("updateDomainDbs", "done"); -} + if (this.blockedDomainsTmpStorage.length === 0) { + console.log("blockedDomainsDb", "no blocked domains fetched, skipping update"); + } else { + try { + await putBlockedDomainsDb(this.blockedDomainsTmpStorage, updateId).then(async () => { + const count = await countBlockedDomainsDb(); + console.log("blockedDomainsDb", count); + }); + } catch (error) { + console.error("putBlockedDomainsDb error", error); + } + } -Browser.tabs.onUpdated.addListener(async () => { - console.log("onUpdated"); - await handlePhishingCheck(); -}); -Browser.tabs.onActivated.addListener(async () => { - console.log("onActivated"); - await handlePhishingCheck(); -}); + if (this.fuzzyDomainsTmpStorage.length === 0) { + console.log("fuzzyDomainsDb", "no fuzzy domains fetched, skipping update"); + } else { + try { + await putFuzzyDomainsDb(this.fuzzyDomainsTmpStorage, updateId).then(async () => { + const count = await countFuzzyDomainsDb(); + console.log("fuzzyDomainsDb", count); + }); + } catch (error) { + console.error("putFuzzyDomainsDb error", error); + } + } -function setupUpdateProtocolsDb() { - console.log("setupUpdateProtocolsDb"); - Browser.alarms.get("updateProtocolsDb").then((a) => { - if (!a) { - console.log("setupUpdateProtocolsDb", "create"); - updateProtocolsDb(); - Browser.alarms.create("updateProtocolsDb", { periodInMinutes: 1 }); // update once every 2 hours + console.log("updateDomainDbs", "done"); + } catch (error) { + console.error("updateDomainDbs error", error); } - }); -} + this.allowedDomainsTmpStorage = null; + this.blockedDomainsTmpStorage = null; + this.fuzzyDomainsTmpStorage = null; + this.updateDomainDbsRunning = false; + } -function setupUpdateDomainDbs() { - console.log("setupUpdateDomainDbs"); - Browser.alarms.get("updateDomainDbs").then((a) => { - if (!a) { - console.log("setupUpdateDomainDbs", "create"); - updateDomainDbs(); - Browser.alarms.create("updateDomainDbs", { periodInMinutes: 1 }); // update once every 2 hours + // method that fetches whitelisted/blacklisted twitter handles lists + async updateTwitterConfig() { + if (this.updateTwitterConfigRunning) { + return; } - }); -} - - -function setupUpdateTwitterConfig() { - console.log("setupUpdateTwitterConfig"); - Browser.alarms.get("updateTwitterConfig").then((a) => { - if (!a) { - console.log("setupUpdateTwitterConfig", "create"); - updateTwitterConfig(); - Browser.alarms.create("updateTwitterConfig", { periodInMinutes: 1 }); // update once every 2 hours + this.updateTwitterConfigRunning = true; + try { + const twitterConfig = await fetch(TWITTER_CONFIG_API).then((res) => res.json()); + setStorage("local", "twitterConfig", twitterConfig); + } catch (error) { + console.error("updateTwitterConfigDb error", error); } - }); -} - -export async function updateTwitterConfig() { - try { - const twitterConfig = await fetch(TWITTER_CONFIG_API).then((res) => res.json()); - setStorage("local", "twitterConfig", twitterConfig); - } catch (error) { - console.log("updateTwitterConfigDb", "error", error); + this.updateTwitterConfigRunning = false; } -} - -function startupTasks() { - console.log("startupTasks", "start"); - setupUpdateProtocolsDb(); - setupUpdateDomainDbs(); - setupUpdateTwitterConfig(); - Browser.action.setIcon({ path: cute }); - console.log("startupTasks", "done"); -} -Browser.runtime.onInstalled.addListener(() => { - startupTasks(); -}); + // initializing method that programs the domains DBs update routine to run every x hours, and runs it once if the alarms wasn't set before + // clear the alarm if it existed before, so if the config changed (alarm interval) it gets set to the new interval + setupUpdateDomainDbs() { + console.log("setupUpdateDomainDbs"); + Browser.alarms.get("updateDomainDbs").then((a) => { + if (!a) { + console.log("setupUpdateDomainDbs", "create"); + this.updateDomainDbs(); + } else { + console.log("setupUpdateDomainDbs", "recreate"); + Browser.alarms.clear("updateDomainDbs"); + } + Browser.alarms.create("updateDomainDbs", { periodInMinutes: DB_UPDATE_FREQUENCY }); // update once every 2 hours + }); + } -Browser.runtime.onStartup.addListener(() => { - startupTasks(); -}); + // initializing method that programs the twitter DB update routine to run every x hours, and runs it once if the alarms wasn't set before + // clear the alarm if it existed before, so if the config changed (alarm interval) it gets set to the new interval + setupUpdateTwitterConfig() { + console.log("setupUpdateTwitterConfig"); + Browser.alarms.get("updateTwitterConfig").then((a) => { + if (!a) { + console.log("setupUpdateTwitterConfig", "create"); + this.updateTwitterConfig(); + } else { + console.log("setupUpdateTwitterConfig", "recreate"); + Browser.alarms.clear("updateTwitterConfig"); + } + Browser.alarms.create("updateTwitterConfig", { periodInMinutes: DB_UPDATE_FREQUENCY }); // update once every 2 hours + }); + } -Browser.alarms.onAlarm.addListener(async (a) => { - switch (a.name) { - case "updateProtocolsDb": - await updateProtocolsDb(); - break; - case "updateDomainDbs": - await updateDomainDbs(); - break; - case "updateTwitterConfig": - await updateTwitterConfig(); - break; + // startup routine that sets up the databases and their alarms + startupTasks() { + console.log("startupTasks", "start"); + this.setupUpdateDomainDbs(); + this.setupUpdateTwitterConfig(); + Browser.action.setIcon({ path: cute }); + console.log("startupTasks", "done"); } -}); +} + +new Background(); diff --git a/src/pages/content/components/etherscanInjectPrice.tsx b/src/pages/content/components/etherscanInjectPrice.tsx index 9437860..db5072b 100644 --- a/src/pages/content/components/etherscanInjectPrice.tsx +++ b/src/pages/content/components/etherscanInjectPrice.tsx @@ -7,6 +7,7 @@ import { logImage, } from "@src/pages/libs/helpers"; import gib from "@src/assets/img/memes/gib-128.png"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; export type EtherscanAlikeExplorerConfig = { name: string; @@ -18,7 +19,7 @@ const SELECTOR_ERC20_TOKEN_INFO_PRICE = "#ContentPlaceHolder1_tr_tokeninfo > div const SELECTOR_ERC20_TOKEN_INFO_LINK = "#ContentPlaceHolder1_tr_tokeninfo > div > div.col-md-8 > a"; export async function injectPrice(config: EtherscanAlikeExplorerConfig) { - const priceInjector = await getStorage("local", "settings:priceInjector", true); + const priceInjector = await getStorage("local", "settings:priceInjector", DEFAULT_SETTINGS.PRICE_INJECTOR); if (!priceInjector) { return; } @@ -82,18 +83,15 @@ export async function injectPrice(config: EtherscanAlikeExplorerConfig) { let listItems = getERC20Items(); - const addressPriceMap = listItems.reduce( - (acc, item) => { - if (tokenHasPrice(item)) return acc; - - const url = new URL(item.href); - const address = url.pathname.split("/token/")[1]; - const prefixedAddress = config.chainPrefix + address; - acc[prefixedAddress] = address; - return acc; - }, - {} as Record, - ); + const addressPriceMap = listItems.reduce((acc, item) => { + if (tokenHasPrice(item)) return acc; + + const url = new URL(item.href); + const address = url.pathname.split("/token/")[1]; + const prefixedAddress = config.chainPrefix + address; + acc[prefixedAddress] = address; + return acc; + }, {} as Record); if (!Object.keys(addressPriceMap).length) return; let totalAmountTextNode = getElements(["a#availableBalanceDropdown", "button#dropdownMenuBalance"]); @@ -105,15 +103,12 @@ export async function injectPrice(config: EtherscanAlikeExplorerConfig) { const prices = await getBatchTokenPrices(Object.keys(addressPriceMap)); if (!Object.keys(prices).length) return; - const addressItemMap = getERC20Items().reduce( - (acc, item) => { - const url = new URL(item.href); - const address = url.pathname.split("/token/")[1]; - acc[address] = item; - return acc; - }, - {} as Record, - ); + const addressItemMap = getERC20Items().reduce((acc, item) => { + const url = new URL(item.href); + const address = url.pathname.split("/token/")[1]; + acc[address] = item; + return acc; + }, {} as Record); listItems = getERC20Items(); // refetch erc20 list for (const [address, { price, symbol }] of Object.entries(prices)) { diff --git a/src/pages/content/components/etherscanInjectTags.tsx b/src/pages/content/components/etherscanInjectTags.tsx index 4cbe03f..5449749 100644 --- a/src/pages/content/components/etherscanInjectTags.tsx +++ b/src/pages/content/components/etherscanInjectTags.tsx @@ -1,8 +1,9 @@ import { getStorage, getImageUrl, getAccountTagsV2, getTagIconUrl } from "@src/pages/libs/helpers"; import takeNote from "@src/assets/img/memes/take-note-128.png"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; export async function injectTags() { - const tagsInjector = await getStorage("local", "settings:tagsInjector", true); + const tagsInjector = await getStorage("local", "settings:tagsInjector", DEFAULT_SETTINGS.TAGS_INJECTOR); if (!tagsInjector) { return; } diff --git a/src/pages/content/components/new/injectPrice.tsx b/src/pages/content/components/new/injectPrice.tsx index 211d759..6f56659 100644 --- a/src/pages/content/components/new/injectPrice.tsx +++ b/src/pages/content/components/new/injectPrice.tsx @@ -7,9 +7,10 @@ import { logImage, } from "@src/pages/libs/helpers"; import gib from "@src/assets/img/memes/gib-128.png"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; export async function injectPrice() { - const priceInjector = await getStorage("local", "settings:priceInjector", true); + const priceInjector = await getStorage("local", "settings:priceInjector", DEFAULT_SETTINGS.PRICE_INJECTOR); if (!priceInjector) { return; } diff --git a/src/pages/content/components/new/injectTags.tsx b/src/pages/content/components/new/injectTags.tsx index c375eb8..4136b91 100644 --- a/src/pages/content/components/new/injectTags.tsx +++ b/src/pages/content/components/new/injectTags.tsx @@ -1,8 +1,9 @@ import { getStorage, getImageUrl, getAccountTagsV2, getTagIconUrl } from "@src/pages/libs/helpers"; import takeNote from "@src/assets/img/memes/take-note-128.png"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; export const injectTags = async () => { - const tagsInjector = await getStorage("local", "settings:tagsInjector", true); + const tagsInjector = await getStorage("local", "settings:tagsInjector", DEFAULT_SETTINGS.TAGS_INJECTOR); if (!tagsInjector) { return; } diff --git a/src/pages/content/components/twitter.tsx b/src/pages/content/components/twitter.tsx index 40b91cf..ace7e48 100644 --- a/src/pages/content/components/twitter.tsx +++ b/src/pages/content/components/twitter.tsx @@ -1,5 +1,6 @@ import { getStorage } from "@src/pages/libs/helpers"; import levenshtein from "fast-levenshtein"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; initPhishingHandleDetector(); const debouncedVerifyHandle = debounce(verifyHandle, 200); @@ -7,7 +8,11 @@ const debouncedVerifyHandle2 = debounce(verifyHandle, 2000); // maybe tweets tak const debouncedVerifyHandle3 = debounce(verifyHandle, 5000); // maybe tweets take some time to load if you scroll too fast async function initPhishingHandleDetector() { - const phishingHandleDetector = await getStorage("local", "settings:phishingHandleDetector", false); + const phishingHandleDetector = await getStorage( + "local", + "settings:phishingHandleDetector", + DEFAULT_SETTINGS.PHISHING_HANDLE_DETECTOR, + ); if (!phishingHandleDetector) return; verifyHandle(); @@ -84,8 +89,9 @@ function getTweetInfo(tweet: any) { if (!element) return 0; return +element.getAttribute("aria-label").split(" ")[0]; }; - let element = tweet.querySelectorAll('a[role="link"]') - if (element[0].innerText.endsWith("retweeted") || element[0].innerText.endsWith("reposted")) element = Array.from(element).slice(1); + let element = tweet.querySelectorAll('a[role="link"]'); + if (element[0].innerText.endsWith("retweeted") || element[0].innerText.endsWith("reposted")) + element = Array.from(element).slice(1); return { tweetHandle: (element[2] as any).innerText.replace("@", ""), displayName: (element[1] as any).innerText, diff --git a/src/pages/libs/constants.ts b/src/pages/libs/constants.ts index d85656c..e6c143c 100644 --- a/src/pages/libs/constants.ts +++ b/src/pages/libs/constants.ts @@ -77,3 +77,18 @@ export const EXPLORER_CHAIN_PREFIX_MAP: { [domain: string]: string } = { "gnosisscan.io": CHAIN_PREFIX.GNOSIS, "bobascan.com": CHAIN_PREFIX.BOBA, } as const; + +export const DEFAULT_SETTINGS = { + PRICE_INJECTOR: true, + TAGS_INJECTOR: true, + PHISHING_DETECTOR: true, + PHISHING_HANDLE_DETECTOR: false, +}; + +// Dexie write operations are divided into chunks to avoid blocking the DB for too long and/or make put operations too heavy. +export const DB_UPDATE_CHUNK_SIZE = 1000; +export const DB_UPDATE_FREQUENCY = 60 * 4; // 4 hours + +export enum MessageType { + ProtocolsQuery = "ProtocolsQuery", +} diff --git a/src/pages/libs/db.ts b/src/pages/libs/db.ts index a806d25..55a3b68 100644 --- a/src/pages/libs/db.ts +++ b/src/pages/libs/db.ts @@ -1,65 +1,129 @@ import Dexie, { Table } from "dexie"; +import { DB_UPDATE_CHUNK_SIZE } from "./constants"; export interface Protocol { + id: string; name: string; url: string; logo: string; category: string; + symbol?: string; tvl?: number; + updateId: string; } -export class ProtocolsDb extends Dexie { - protocols!: Table; - - constructor() { - super("ProtocolsDb"); - this.version(1).stores({ - protocols: "name, category", - }); - } -} - -export const protocolsDb = new ProtocolsDb(); - export interface Domain { domain: string; + updateId: string; } +// _____________ ALLOWED DOMAINS DB _____________ + export class AllowedDomainsDb extends Dexie { domains!: Table; constructor() { super("AllowedDomainsDb"); - this.version(1).stores({ - domains: "domain", - }); + this.version(2) + .stores({ + domains: "domain,updateId", + }) + .upgrade(() => { + return this.domains.clear(); + }); } } export const allowedDomainsDb = new AllowedDomainsDb(); +// method used to update AllowedDomainsDb +export const putAllowedDomainsDb = async (newDomains: Domain[], updateId: string): Promise => { + // we divide the update in chunks to avoid locking the DB + for (let index = 0; index < newDomains.length; index += DB_UPDATE_CHUNK_SIZE) { + const chunk = newDomains.slice(index, index + DB_UPDATE_CHUNK_SIZE); + // we do a bulkPut, which replaces existing items (by their primary key) or creates them + await allowedDomainsDb.domains.bulkPut(chunk); + } + // we then remove all entries that don't have this update's updateId + await allowedDomainsDb.domains.where("updateId").notEqual(updateId).delete(); +}; + +// method used to count the number of allowed domains in the DB +export const countAllowedDomainsDb = async () => { + const count = await allowedDomainsDb.domains.count(); + return count; +}; + +// _____________ FUZZY DOMAINS DB _____________ + export class FuzzyDomainsDb extends Dexie { domains!: Table; constructor() { super("FuzzyDomainsDb"); - this.version(1).stores({ - domains: "domain", - }); + this.version(2) + .stores({ + domains: "domain,updateId", + }) + .upgrade(() => { + return this.domains.clear(); + }); } } export const fuzzyDomainsDb = new FuzzyDomainsDb(); +// method used to update FuzzyDomainsDb +export const putFuzzyDomainsDb = async (newDomains: Domain[], updateId: string): Promise => { + // we divide the update in chunks to avoid locking the DB + for (let index = 0; index < newDomains.length; index += DB_UPDATE_CHUNK_SIZE) { + const chunk = newDomains.slice(index, index + DB_UPDATE_CHUNK_SIZE); + // we do a bulkPut, which replaces existing items (by their primary key) or creates them + await fuzzyDomainsDb.domains.bulkPut(chunk); + } + // we then remove all entries that don't have this update's updateId + await fuzzyDomainsDb.domains.where("updateId").notEqual(updateId).delete(); +}; + +// method used to count the number of fuzzy domains in the DB +export const countFuzzyDomainsDb = async () => { + const count = await fuzzyDomainsDb.domains.count(); + return count; +}; + +// _____________ BLOCKED DOMAINS DB _____________ + export class BlockedDomainsDb extends Dexie { domains!: Table; constructor() { super("BlockedDomainsDb"); - this.version(1).stores({ - domains: "domain", - }); + this.version(2) + .stores({ + domains: "domain,updateId", + }) + .upgrade(() => { + return this.domains.clear(); + }); } } export const blockedDomainsDb = new BlockedDomainsDb(); + +// method used to update BlockedDomainsDb +export const putBlockedDomainsDb = async (newDomains: Domain[], updateId: string): Promise => { + // we divide the update in chunks to avoid locking the DB + for (let index = 0; index < newDomains.length; index += DB_UPDATE_CHUNK_SIZE) { + const chunk = newDomains.slice(index, index + DB_UPDATE_CHUNK_SIZE); + // we do a bulkPut, which replaces existing items (by their primary key) or creates them + await blockedDomainsDb.domains.bulkPut(chunk); + } + // we then remove all entries that don't have this update's updateId + await blockedDomainsDb.domains.where("updateId").notEqual(updateId).delete(); +}; + +// method used to count the number of blocked domains in the DB +export const countBlockedDomainsDb = async () => { + const count = await blockedDomainsDb.domains.count(); + return count; +}; diff --git a/src/pages/libs/hooks.ts b/src/pages/libs/hooks.ts index 3971182..b5ec3fd 100644 --- a/src/pages/libs/hooks.ts +++ b/src/pages/libs/hooks.ts @@ -1,18 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; -import { Protocol, protocolsDb } from "./db"; import Browser from "webextension-polyfill"; -/** - * Protocols data synced with IndexedDB and updated every 4 hours using Dexie. - * - * @returns {Protocol[]} protocols - */ -export const useProtocols = (): Protocol[] => - useLiveQuery(async () => { - return await protocolsDb.protocols.toArray(); - }); - /** * State synced with local storage. Updates itself when local storage changes based on event listener. * diff --git a/src/pages/libs/phishing-detector.ts b/src/pages/libs/phishing-detector.ts index 20542b3..4f839eb 100644 --- a/src/pages/libs/phishing-detector.ts +++ b/src/pages/libs/phishing-detector.ts @@ -9,39 +9,66 @@ interface CheckDomainResult { extra?: string; } -export async function checkDomain(domain: string): Promise { +// check the domain against the various allow/block/fuzzy lists +// use the temporary storage arrays if they are available +export async function checkDomain( + domain: string, + allowedDomainsTmpStorage: { domain: string }[] | null, + blockedDomainsTmpStorage: { domain: string }[] | null, + fuzzyDomainsTmpStorage: { domain: string }[] | null, +): Promise { console.log("Checking domain", domain); const topLevelDomain = domain.split(".").slice(-2).join("."); - const isAllowed = - (await allowedDomainsDb.domains.get({ domain })) || - (await allowedDomainsDb.domains.get({ domain: topLevelDomain })); + let isAllowed = false; + if (allowedDomainsTmpStorage) { + isAllowed = allowedDomainsTmpStorage.some(({ domain: dmn }) => dmn === domain || dmn === topLevelDomain); + } else { + isAllowed = + !!(await allowedDomainsDb.domains.get({ domain })) || + !!(await allowedDomainsDb.domains.get({ domain: topLevelDomain })); + } if (isAllowed) { return { result: false, type: "allowed" }; } - - const isBlocked = - (await blockedDomainsDb.domains.get({ domain })) || - (await blockedDomainsDb.domains.get({ domain: topLevelDomain })); + let isBlocked = false; + if (blockedDomainsTmpStorage) { + isBlocked = blockedDomainsTmpStorage.some(({ domain: dmn }) => dmn === domain || dmn === topLevelDomain); + } else { + isBlocked = + !!(await blockedDomainsDb.domains.get({ domain })) || + !!(await blockedDomainsDb.domains.get({ domain: topLevelDomain })); + } if (isBlocked) { return { result: true, type: "blocked" }; } - const fuzzyDomains = fuzzyDomainsDb.domains.toCollection(); let fuzzyResult: CheckDomainResult; - await fuzzyDomains - .until((x) => { + if (fuzzyDomainsTmpStorage) { + fuzzyDomainsTmpStorage.some((x) => { const distance = levenshtein.get(x.domain, domain); const distanceTop = levenshtein.get(x.domain, topLevelDomain); const isMatched = distance <= DEFAULT_LEVENSHTEIN_TOLERANCE || distanceTop <= DEFAULT_LEVENSHTEIN_TOLERANCE; if (isMatched) { - console.log("fuzzy match", { domain, fuzzyDomain: x.domain, distance }); fuzzyResult = { result: true, type: "fuzzy", extra: x.domain }; return true; } - }, true) - .last(); + }); + } else { + const fuzzyDomains = fuzzyDomainsDb.domains.toCollection(); + await fuzzyDomains + .until((x) => { + const distance = levenshtein.get(x.domain, domain); + const distanceTop = levenshtein.get(x.domain, topLevelDomain); + const isMatched = distance <= DEFAULT_LEVENSHTEIN_TOLERANCE || distanceTop <= DEFAULT_LEVENSHTEIN_TOLERANCE; + if (isMatched) { + fuzzyResult = { result: true, type: "fuzzy", extra: x.domain }; + return true; + } + }, true) + .last(); + } if (fuzzyResult) { return fuzzyResult; } diff --git a/src/pages/popup/Popup.tsx b/src/pages/popup/Popup.tsx index 5082575..5402b2b 100644 --- a/src/pages/popup/Popup.tsx +++ b/src/pages/popup/Popup.tsx @@ -1,19 +1,32 @@ import cute from "@assets/img/memes/cute.gif"; import cuteStatic from "@assets/img/memes/cute-128.png"; import { Box, HStack, Icon, Image, Switch, Text, useColorModeValue, VStack, Link } from "@chakra-ui/react"; -import { useBrowserStorage } from "../libs/hooks"; +import { useBrowserStorage } from "@src/pages/libs/hooks"; +import { DEFAULT_SETTINGS } from "@src/pages/libs/constants"; import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa"; import Browser from "webextension-polyfill"; import packageJson from "../../../public/manifest.json"; const Popup = () => { - const [priceInjector, setPriceInjector] = useBrowserStorage("local", "settings:priceInjector", true); - const [tagsInjector, setTagsInjector] = useBrowserStorage("local", "settings:tagsInjector", true); - const [phishingDetector, setPhishingDetector] = useBrowserStorage("local", "settings:phishingDetector", true); + const [priceInjector, setPriceInjector] = useBrowserStorage( + "local", + "settings:priceInjector", + DEFAULT_SETTINGS.PRICE_INJECTOR, + ); + const [tagsInjector, setTagsInjector] = useBrowserStorage( + "local", + "settings:tagsInjector", + DEFAULT_SETTINGS.TAGS_INJECTOR, + ); + const [phishingDetector, setPhishingDetector] = useBrowserStorage( + "local", + "settings:phishingDetector", + DEFAULT_SETTINGS.PHISHING_DETECTOR, + ); const [phishingHandleDetector, setPhishingHandleDetector] = useBrowserStorage( "local", "settings:phishingHandleDetector", - false, + DEFAULT_SETTINGS.PHISHING_HANDLE_DETECTOR, ); return ( diff --git a/src/pages/popup/index.tsx b/src/pages/popup/index.tsx index 9a099c0..0395632 100644 --- a/src/pages/popup/index.tsx +++ b/src/pages/popup/index.tsx @@ -3,12 +3,11 @@ import ReactDOM from "react-dom/client"; import { ChakraProvider, ColorModeScript, extendTheme } from "@chakra-ui/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import Popup from "./Popup"; -import { protocolsDb } from "../libs/db"; const queryClient = new QueryClient(); const config = { - // initialColorMode: "dark", + initialColorMode: "system", useSystemColorMode: true, disableTransitionOnChange: false, }; @@ -22,12 +21,9 @@ ReactDOM.createRoot(rootElement).render( - + , ); - -//@ts-ignore -window.protocols = protocolsDb;