From 0485405418a77c3a2be8e77dd94222e0d2443b89 Mon Sep 17 00:00:00 2001 From: cong_wang Date: Wed, 14 Aug 2024 11:10:55 +0800 Subject: [PATCH] feat: Add new test cases, enhance domain name whitelist matching, and optimize content script code --- CHANGELOG.md | 9 ++ package.json | 9 +- src/common/components/ModalFundFlow/graph.ts | 13 ++- src/common/config/allowlist.ts | 5 +- src/common/constants/support.ts | 14 +-- src/common/types.ts | 2 +- src/common/utils/permission.ts | 22 ++-- src/content/arkham/index.tsx | 20 ++-- src/content/blockscout/index.tsx | 67 ++++++------ src/content/blocksec/index.tsx | 8 +- src/content/btc/index.tsx | 20 ++-- src/content/debank/index.tsx | 34 ++++--- src/content/etherscan/index.tsx | 102 ++++++++++--------- src/content/factory.ts | 53 ++++++++++ src/content/index.all_frames.ts | 16 +-- src/content/index.ts | 43 +------- src/content/jito/index.tsx | 35 +++---- src/content/jito/main.tsx | 67 ++++++++++++ src/content/merlin/index.tsx | 32 +++--- src/content/metasleuth/index.tsx | 24 +++-- src/content/opensea/index.tsx | 14 +-- src/content/scans/index.tsx | 96 ++++++++--------- src/content/solanaexpl/index.tsx | 20 ++-- src/content/solanafm/index.tsx | 20 ++-- src/content/solscan/index.tsx | 20 ++-- src/content/tronscan/index.tsx | 46 +++++---- src/tests/isMatchURL.test.ts | 98 ++++++++++++++++++ 27 files changed, 565 insertions(+), 344 deletions(-) create mode 100644 src/content/factory.ts create mode 100644 src/content/jito/main.tsx create mode 100644 src/tests/isMatchURL.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2530d9c..17a65cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### v5.3.0 + +- [test] Add new test cases to ensure the functionality of the code is thoroughly tested +- [feat] Link Solana Fundflow to the current blockchain explorer +- [feat] Enhance domain name whitelist matching to support parameter-level and multi-level subdomain URL matching rules +- [chore] Optimize content script code +- [update] Phalcon Explorer Polygon will no longer support simulate functionality +- [fix] Fix the issue of Phalcon Explorer button not working on Jito Explorer + ### v5.2.2 -[fix] Fix the issue of exporting CSV files causing character encoding problems diff --git a/package.json b/package.json index 2d1fe3a..38cd519 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metasuites", - "version": "5.2.2", + "version": "5.3.0", "repository": { "type": "git", "url": "https://github.com/blocksecteam/metasuites.git" @@ -22,9 +22,11 @@ "stylelint": "stylelint \"**/*.{css,scss,less}\"", "stylelint:fix": "stylelint \"**/*.{css,scss,less}\" --fix", "prepare": "husky install && husky set .husky/pre-commit \"npx lint-staged\" && husky set .husky/commit-msg 'npx --no -- commitlint --edit \"$1\"'", - "lint": "eslint 'src/**/*.{ts,tsx}'" + "lint": "eslint 'src/**/*.{ts,tsx}'", + "test": "vitest" }, "dependencies": { + "@remusao/url-match-patterns": "^1.2.0", "antd": "^5.11.2", "big.js": "^6.2.1", "blockies-ts": "^1.0.0", @@ -93,7 +95,8 @@ "terser": "^5.16.3", "tslib": "2.4.0", "typescript": "4.8.4", - "vite": "3.1.4" + "vite": "3.1.4", + "vitest": "^2.0.5" }, "license": "Apache-2.0" } diff --git a/src/common/components/ModalFundFlow/graph.ts b/src/common/components/ModalFundFlow/graph.ts index 5d18a9d..54d2a83 100644 --- a/src/common/components/ModalFundFlow/graph.ts +++ b/src/common/components/ModalFundFlow/graph.ts @@ -45,7 +45,18 @@ export const nodeHover = ( ele.clone(true) nodeAClone.classed('pointer', true).on('click', function () { - window.open(nodeData.url) + const isSolana = nodeData.chain === 'solana' + if (isSolana) { + try { + const url = new URL(nodeData.url) + url.hostname = window.location.hostname + window.open(url.href) + } catch (e) { + window.open(nodeData.url) + } + } else { + window.open(nodeData.url) + } }) nodeAClone.selectAll('text').each(function (_: any, index: number) { diff --git a/src/common/config/allowlist.ts b/src/common/config/allowlist.ts index 197bb05..a80a945 100644 --- a/src/common/config/allowlist.ts +++ b/src/common/config/allowlist.ts @@ -1,8 +1,5 @@ export default { - ETHERSCAN_V1_MATCHES: [ - '*://opbnb-testnet.bscscan.com/*', - '*://goerli-optimism.etherscan.io/*' - ], + ETHERSCAN_V1_MATCHES: ['*://opbnb-testnet.bscscan.com/*'], ETHERSCAN_V2_MATCHES: [ '*://ftmscan.com/*', '*://cn.etherscan.com/*', diff --git a/src/common/constants/support.ts b/src/common/constants/support.ts index 0472a96..3396f4c 100644 --- a/src/common/constants/support.ts +++ b/src/common/constants/support.ts @@ -205,17 +205,7 @@ export const EXT_SUPPORT_WEB_LIST: ExtSupportWebsite[] = [ chain: 'optimism', domains: ['optimistic.etherscan.io'], siteName: 'ETHERSCAN', - logo: 'https://assets.blocksec.com/image/1671777583236-2.png', - testNets: [ - { - name: 'Optimism Goerli', - chainID: 420, - chain: 'gor.optimism', - domains: ['goerli-optimism.etherscan.io'], - siteName: 'ETHERSCAN', - logo: 'https://assets.blocksec.com/image/1671777583236-2.png' - } - ] + logo: 'https://assets.blocksec.com/image/1671777583236-2.png' }, { name: 'Optimism (Blockscout)', @@ -503,7 +493,7 @@ export const PHALCON_SUPPORT_LIST = [ { pathname: 'polygon', chain: 'polygon', - supportSimulator: true + supportSimulator: false }, { pathname: 'bsc', diff --git a/src/common/types.ts b/src/common/types.ts index d54bcd1..9febab9 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -10,7 +10,7 @@ declare const ButtonTypes: [ 'secondary' ] -export type ButtonType = typeof ButtonTypes[number] +export type ButtonType = (typeof ButtonTypes)[number] export interface BaseComponent { style?: CSSProperties diff --git a/src/common/utils/permission.ts b/src/common/utils/permission.ts index 33ee0e9..1221f8b 100644 --- a/src/common/utils/permission.ts +++ b/src/common/utils/permission.ts @@ -1,3 +1,6 @@ +import { uniq } from 'lodash-es' +import URLMatchPattern from '@remusao/url-match-patterns' + import type { OptWebsite } from '@src/store' import { DEDAUB_SUPPORT_DIRECT_LIST, @@ -6,24 +9,11 @@ import { OPENCHAIN_SUPPORT_LIST, ETHERVM_SUPPORT_DIRECT_LIST } from '@common/constants' -import { uniq } from 'lodash-es' -// TODO: Exclude the effect of the link in the parameters export const isMatchURL = (url: string, patternList: string[]) => { - function toRegex(input: string) { - input = input.replace('*://', '(.*)://') - input = input.replace('*.', '(.*\\.|)') - input = input.replace('/*', '(.*)?') - return input - } - - try { - return patternList.some(pattern => { - return new RegExp(toRegex(pattern)).test(url) - }) - } catch (e) { - return false - } + return patternList.some(pattern => { + return URLMatchPattern(pattern, url) + }) } /** judge from supportWebList */ diff --git a/src/content/arkham/index.tsx b/src/content/arkham/index.tsx index 86098b4..a805ad5 100644 --- a/src/content/arkham/index.tsx +++ b/src/content/arkham/index.tsx @@ -2,15 +2,19 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initArkham = async () => { - execute() - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - execute() - sendResponse() - } - }) +export class ArkhamInitializer { + static matches = allowlist.ARKHAM_MATCHES + async init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/blockscout/index.tsx b/src/content/blockscout/index.tsx index d90c04a..ee9cb6d 100644 --- a/src/content/blockscout/index.tsx +++ b/src/content/blockscout/index.tsx @@ -3,49 +3,54 @@ import type { BlockscoutPageName } from '@common/constants' import { BLOCKSCOUT_PAGES } from '@common/constants' import { isAllowed, getChainSimpleName } from '@common/utils' import { store } from '@src/store' +import allowlist from '@common/config/allowlist' + import { initTxPageScript, initAddressPageScript } from './page-scripts' -import './styles/index' import { page } from './utils' +import './styles/index' -export const initBlockscout = async () => { - if (window.self !== window.top) { - return // This page is embedded in an iframe - } - /** get user options */ - const supportWebList = await store.get('supportWebList') +export class BlockscoutInitializer { + static matches = allowlist.BLOCKSCOUT_MATCHES + async init() { + if (window.self !== window.top) { + return // This page is embedded in an iframe + } + /** get user options */ + const supportWebList = await store.get('supportWebList') - /** check whether the script is allowed to run on the current page */ - const allowed = isAllowed(Object.values(supportWebList)) + /** check whether the script is allowed to run on the current page */ + const allowed = isAllowed(Object.values(supportWebList)) - /** get the necessary parameters required by the extension */ - const chain: string | undefined = getChainSimpleName() + /** get the necessary parameters required by the extension */ + const chain: string | undefined = getChainSimpleName() - if (!allowed || !chain) return + if (!allowed || !chain) return - page.runScript((pageName: string) => { - switch (pageName) { - case BLOCKSCOUT_PAGES.TX.name: { - const { pattern } = BLOCKSCOUT_PAGES[pageName as BlockscoutPageName] - const [, txHash] = window.location.pathname.match(pattern) || [] + page.runScript((pageName: string) => { + switch (pageName) { + case BLOCKSCOUT_PAGES.TX.name: { + const { pattern } = BLOCKSCOUT_PAGES[pageName as BlockscoutPageName] + const [, txHash] = window.location.pathname.match(pattern) || [] - if (!txHash) return + if (!txHash) return - const tab = new URL(window.location.href).searchParams.get('tab') + const tab = new URL(window.location.href).searchParams.get('tab') - if (!(tab === null || tab === 'index')) return + if (!(tab === null || tab === 'index')) return - initTxPageScript(chain, txHash) - break - } - case BLOCKSCOUT_PAGES.ADDRESS.name: { - const { pattern } = BLOCKSCOUT_PAGES[pageName as BlockscoutPageName] - const [, addressHash] = window.location.pathname.match(pattern) || [] + initTxPageScript(chain, txHash) + break + } + case BLOCKSCOUT_PAGES.ADDRESS.name: { + const { pattern } = BLOCKSCOUT_PAGES[pageName as BlockscoutPageName] + const [, addressHash] = window.location.pathname.match(pattern) || [] - if (!addressHash) return + if (!addressHash) return - initAddressPageScript(chain, addressHash) - break + initAddressPageScript(chain, addressHash) + break + } } - } - }) + }) + } } diff --git a/src/content/blocksec/index.tsx b/src/content/blocksec/index.tsx index daa2646..4a287d2 100644 --- a/src/content/blocksec/index.tsx +++ b/src/content/blocksec/index.tsx @@ -1,5 +1,9 @@ import { MSG_METADOCK_INSTALLED } from '@common/constants' +import allowlist from '@common/config/allowlist' -export const initBlockSec = () => { - window.postMessage(MSG_METADOCK_INSTALLED) +export class BlockSecInitializer { + static matches = allowlist.BLOCKSEC_MATCHES + init() { + window.postMessage(MSG_METADOCK_INSTALLED) + } } diff --git a/src/content/btc/index.tsx b/src/content/btc/index.tsx index a2fd16b..d9e3fd4 100644 --- a/src/content/btc/index.tsx +++ b/src/content/btc/index.tsx @@ -2,15 +2,19 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { EXECUTE_BTC_CONTENT_SCRIPT } from '@common/constants' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initBTCExplorer = () => { - execute() - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === EXECUTE_BTC_CONTENT_SCRIPT) { - execute() - sendResponse() - } - }) +export class BTCInitializer { + static matches = allowlist.BTC_EXPLORER_MATCHES + async init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === EXECUTE_BTC_CONTENT_SCRIPT) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/debank/index.tsx b/src/content/debank/index.tsx index 2fce940..a8a6a63 100644 --- a/src/content/debank/index.tsx +++ b/src/content/debank/index.tsx @@ -3,25 +3,29 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' import { store } from '@src/store' +import allowlist from '@common/config/allowlist' import { renderMainAddressLabel } from './feat-scripts' import { lazyLoad } from './helper' -export const initDebank = async () => { - execute() - async function execute() { - const { enablePrivateLabels } = await store.get('options') - // profile page - if (/^\/profile\/(0x[a-fA-F\d]{40})/i.test(window.location.pathname)) { - lazyLoad(() => { - if (enablePrivateLabels) renderMainAddressLabel() - }, 'div[class^="HeaderInfo_headerInfo"]') +export class DebankInitializer { + static matches = allowlist.DEBANK_MATCHES + async init() { + execute() + async function execute() { + const { enablePrivateLabels } = await store.get('options') + // profile page + if (/^\/profile\/(0x[a-fA-F\d]{40})/i.test(window.location.pathname)) { + lazyLoad(() => { + if (enablePrivateLabels) renderMainAddressLabel() + }, 'div[class^="HeaderInfo_headerInfo"]') + } } + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) } - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - execute() - sendResponse() - } - }) } diff --git a/src/content/etherscan/index.tsx b/src/content/etherscan/index.tsx index 689ec04..9a85db1 100644 --- a/src/content/etherscan/index.tsx +++ b/src/content/etherscan/index.tsx @@ -2,6 +2,7 @@ import '@common/styles/inject.common' import { store } from '@src/store' import { isAllowed, getChainSimpleName, getPageName } from '@common/utils' import { ETHERSCAN_PAGES } from '@common/constants' +import allowlist from '@common/config/allowlist' import { initAddressPageScript, @@ -18,59 +19,62 @@ import { initBlocksForkedPageScript } from './page-scripts' -export const initEtherscanV2 = async () => { - if (window.self !== window.top) { - return // This page is embedded in an iframe - } - /** get user options */ - const supportWebList = await store.get('supportWebList') +export class EtherscanV2Initializer { + static matches = allowlist.ETHERSCAN_V2_MATCHES + async init() { + if (window.self !== window.top) { + return // This page is embedded in an iframe + } + /** get user options */ + const supportWebList = await store.get('supportWebList') - /** check whether the script is allowed to run on the current page */ - const allowed = isAllowed(Object.values(supportWebList)) + /** check whether the script is allowed to run on the current page */ + const allowed = isAllowed(Object.values(supportWebList)) - /** get the necessary parameters required by the extension */ - const chain: string | undefined = getChainSimpleName() + /** get the necessary parameters required by the extension */ + const chain: string | undefined = getChainSimpleName() - if (!allowed || !chain) return + if (!allowed || !chain) return - const pageName = getPageName() + const pageName = getPageName() - switch (pageName) { - case ETHERSCAN_PAGES.ADDRESS.name: - initAddressPageScript(chain) - break - case ETHERSCAN_PAGES.TX.name: - initTxPageScript(chain) - break - case ETHERSCAN_PAGES.TXS.name: - initTxsPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCK.name: - initBlockPageScript(chain) - break - case ETHERSCAN_PAGES.ACCOUNTS.name: - initAccountsPageScript(chain) - break - case ETHERSCAN_PAGES.TOKEN.name: - initTokenPageScript(chain) - break - case ETHERSCAN_PAGES.TOKENTXNS.name: - initTokentxnsPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCKS.name: - initBlocksPageScript(chain) - break - case ETHERSCAN_PAGES.TOKEN_APPROVAL_CHECKER.name: - initTokenApprovalCheckerPageScript(chain) - break - case ETHERSCAN_PAGES.TXS_INTERNAL.name: - initTxsInternalPageScript(chain) - break - case ETHERSCAN_PAGES.NFT_TRANSFERS.name: - initNftTransfersPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCKS_FORKED.name: - initBlocksForkedPageScript(chain) - break + switch (pageName) { + case ETHERSCAN_PAGES.ADDRESS.name: + initAddressPageScript(chain) + break + case ETHERSCAN_PAGES.TX.name: + initTxPageScript(chain) + break + case ETHERSCAN_PAGES.TXS.name: + initTxsPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCK.name: + initBlockPageScript(chain) + break + case ETHERSCAN_PAGES.ACCOUNTS.name: + initAccountsPageScript(chain) + break + case ETHERSCAN_PAGES.TOKEN.name: + initTokenPageScript(chain) + break + case ETHERSCAN_PAGES.TOKENTXNS.name: + initTokentxnsPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCKS.name: + initBlocksPageScript(chain) + break + case ETHERSCAN_PAGES.TOKEN_APPROVAL_CHECKER.name: + initTokenApprovalCheckerPageScript(chain) + break + case ETHERSCAN_PAGES.TXS_INTERNAL.name: + initTxsInternalPageScript(chain) + break + case ETHERSCAN_PAGES.NFT_TRANSFERS.name: + initNftTransfersPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCKS_FORKED.name: + initBlocksForkedPageScript(chain) + break + } } } diff --git a/src/content/factory.ts b/src/content/factory.ts new file mode 100644 index 0000000..09b1b56 --- /dev/null +++ b/src/content/factory.ts @@ -0,0 +1,53 @@ +import { isMatchURL } from '@common/utils' +import { EtherscanV1Initializer } from '@src/content/scans' +import { EtherscanV2Initializer } from '@src/content/etherscan' +import { BlockscoutInitializer } from '@src/content/blockscout' +import { BTCInitializer } from '@src/content/btc' +import { BlockSecInitializer } from '@src/content/blocksec' +import { OpenseaInitializer } from '@src/content/opensea' +import { TronscanInitializer } from '@src/content/tronscan' +import { MerlinInitializer } from '@src/content/merlin' +import { MetaSleuthInitializer } from '@src/content/metasleuth' +import { SolscanInitializer } from '@src/content/solscan' +import { SolscanFMInitializer } from '@src/content/solanafm' +import { SolanaExplorerInitializer } from '@src/content/solanaexpl' +import { DebankInitializer } from '@src/content/debank' +import { ArkhamInitializer } from '@src/content/arkham' +import { JitoInitializer } from '@src/content/jito' + +export type Initializer = { + matches: string[] + new (): T +} + +const createInitializerFromMap = ( + url: string, + initializers: Initializer[] +) => { + for (const initializer of initializers) { + if (isMatchURL(url, initializer.matches)) { + return new initializer() + } + } + return null +} + +export const createInitializer = (url: string, all_frames = false) => { + const initializers = all_frames + ? [EtherscanV1Initializer, EtherscanV2Initializer, BlockscoutInitializer] + : [ + BTCInitializer, + BlockSecInitializer, + OpenseaInitializer, + TronscanInitializer, + MerlinInitializer, + MetaSleuthInitializer, + SolscanInitializer, + SolscanFMInitializer, + SolanaExplorerInitializer, + DebankInitializer, + ArkhamInitializer, + JitoInitializer + ] + return createInitializerFromMap(url, initializers) +} diff --git a/src/content/index.all_frames.ts b/src/content/index.all_frames.ts index 6f7c05a..322781d 100644 --- a/src/content/index.all_frames.ts +++ b/src/content/index.all_frames.ts @@ -1,15 +1,5 @@ -import { isMatchURL } from '@common/utils' -import allowlist from '@common/config/allowlist' -import { initEtherscanV2 } from '@src/content/etherscan' -import { initBlockscout } from '@src/content/blockscout' -import { initEtherscanV1 } from '@src/content/scans' +import { createInitializer } from './factory' const currentUrl = window.location.href - -if (isMatchURL(currentUrl, allowlist.ETHERSCAN_V1_MATCHES)) { - initEtherscanV1() -} else if (isMatchURL(currentUrl, allowlist.ETHERSCAN_V2_MATCHES)) { - initEtherscanV2() -} else if (isMatchURL(currentUrl, allowlist.BLOCKSCOUT_MATCHES)) { - initBlockscout() -} +const initializer = createInitializer(currentUrl, true) +initializer?.init() diff --git a/src/content/index.ts b/src/content/index.ts index 6130852..5c319c2 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,42 +1,5 @@ -import { isMatchURL } from '@common/utils' -import allowlist from '@common/config/allowlist' -import { initBlockSec } from '@src/content/blocksec' -import { initOpensea } from '@src/content/opensea' -import { initBTCExplorer } from '@src/content/btc' -import { initTronscan } from '@src/content/tronscan' -import { initMerlin } from '@src/content/merlin' -import { initSolscan } from '@src/content/solscan' -import { initSolscanFM } from '@src/content/solanafm' -import { initSolanaExplorer } from '@src/content/solanaexpl' -import { initMetaSleuth } from '@src/content/metasleuth' -import { initDebank } from '@src/content/debank' -import { initArkham } from '@src/content/arkham' -import { initJito } from '@src/content/jito' +import { createInitializer } from './factory' const currentUrl = window.location.href - -if (isMatchURL(currentUrl, allowlist.BTC_EXPLORER_MATCHES)) { - initBTCExplorer() -} else if (isMatchURL(currentUrl, allowlist.BLOCKSEC_MATCHES)) { - initBlockSec() -} else if (isMatchURL(currentUrl, allowlist.OPENSEA_MATCHES)) { - initOpensea() -} else if (isMatchURL(currentUrl, allowlist.TRONSCAN_MATCHES)) { - initTronscan() -} else if (isMatchURL(currentUrl, allowlist.MERLIN_SCAN_MATCHES)) { - initMerlin() -} else if (isMatchURL(currentUrl, allowlist.MS_MATCHES)) { - initMetaSleuth() -} else if (isMatchURL(currentUrl, allowlist.SOLSCAN_MATCHES)) { - initSolscan() -} else if (isMatchURL(currentUrl, allowlist.SOLANAFM_MATCHES)) { - initSolscanFM() -} else if (isMatchURL(currentUrl, allowlist.SOLANA_EXPLORER_MATCHES)) { - initSolanaExplorer() -} else if (isMatchURL(currentUrl, allowlist.DEBANK_MATCHES)) { - initDebank() -} else if (isMatchURL(currentUrl, allowlist.ARKHAM_MATCHES)) { - initArkham() -} else if (isMatchURL(currentUrl, allowlist.JITO_MATCHES)) { - initJito() -} +const initializer = createInitializer(currentUrl) +initializer?.init() diff --git a/src/content/jito/index.tsx b/src/content/jito/index.tsx index b4f684d..0b195f7 100644 --- a/src/content/jito/index.tsx +++ b/src/content/jito/index.tsx @@ -1,24 +1,19 @@ -import $ from 'jquery' -import { createRoot } from 'react-dom/client' +import browser from 'webextension-polyfill' -import { store } from '@src/store' +import allowlist from '@common/config/allowlist' +import { URL_UPDATED } from '@common/constants' -import { PhalconExplorerButton } from './components' +import execute from './main' -export const initJito = async () => { - const { quick2Parsers } = await store.get('options') - if (!quick2Parsers) return - const containers = $( - '#__next > div > div > div > div:nth-of-type(3) > main > main > div:nth-of-type(2) > div > div:nth-of-type(8) > div:nth-of-type(2) > div > a' - ) - containers.each(function () { - const container = $(this) - const txHash = container.find('div.break-all').text().trim() - console.log('txHash', txHash) - const rootEl = $( - '' - ) - container.find('> div > div:not(:hidden)').append(rootEl) - createRoot(rootEl[0]).render() - }) +export class JitoInitializer { + static matches = allowlist.JITO_MATCHES + async init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/jito/main.tsx b/src/content/jito/main.tsx new file mode 100644 index 0000000..73046ac --- /dev/null +++ b/src/content/jito/main.tsx @@ -0,0 +1,67 @@ +import { createRoot } from 'react-dom/client' +import $ from 'jquery' + +import '@common/styles/inject.common' +import { store } from '@src/store' +import { PATTERN_SOLANA_TX_HASH } from '@common/constants' + +import { PhalconExplorerButton } from './components' + +const execute = async () => { + const { quick2Parsers } = await store.get('options') + if (!quick2Parsers) return + + const maxRetries = 100 + const retryInterval = 500 + + let retryCount = 0 + const findContainers = () => { + const containers = $( + '#__next > div > div > div > div:nth-of-type(3) > main > main > div:nth-of-type(2) > div > div:nth-of-type(8) > div:nth-of-type(2) > div a' + ) + return containers.length > 0 ? containers : null + } + + const callback = () => { + const containers = findContainers() + if (containers) { + containers.each((index, element) => { + const container = $(element) + if (container.find('#phalcon-explorer-button').length > 0) return + const txHash = container.attr('href')?.split('/').pop() + if (txHash && PATTERN_SOLANA_TX_HASH.test(txHash)) { + const rootEl = $( + `` + ) + container.find('> div > div:not(:hidden)').append(rootEl) + createRoot(rootEl[0]).render( + + ) + } + }) + } else { + retryCount++ + if (retryCount < maxRetries) { + setTimeout(findContainersAndRender, retryInterval) + } else { + console.log('Maximum retries reached, giving up.') + } + } + } + + const findContainersAndRender = () => { + const containers = findContainers() + if (containers) { + callback() + } else { + retryCount++ + if (retryCount < maxRetries) { + setTimeout(findContainersAndRender, retryInterval) + } + } + } + + findContainersAndRender() +} + +export default execute diff --git a/src/content/merlin/index.tsx b/src/content/merlin/index.tsx index 427f66c..5d67f9b 100644 --- a/src/content/merlin/index.tsx +++ b/src/content/merlin/index.tsx @@ -2,27 +2,31 @@ import '@common/styles/inject.common' import { store } from '@src/store' import { isAllowed, getPageName } from '@common/utils' import { MERLINSCAN_PAGES } from '@common/constants' +import allowlist from '@common/config/allowlist' import { initTxPageScript } from './page-scripts' -export const initMerlin = async () => { - if (window.self !== window.top) { - return - } +export class MerlinInitializer { + static matches = allowlist.MERLIN_SCAN_MATCHES + async init() { + if (window.self !== window.top) { + return + } - /** get user options */ - const supportWebList = await store.get('supportWebList') + /** get user options */ + const supportWebList = await store.get('supportWebList') - /** check whether the script is allowed to run on the current page */ - const allowed = isAllowed(Object.values(supportWebList)) + /** check whether the script is allowed to run on the current page */ + const allowed = isAllowed(Object.values(supportWebList)) - if (!allowed) return + if (!allowed) return - const pageName = getPageName() + const pageName = getPageName() - switch (pageName) { - case MERLINSCAN_PAGES.TX.name: - initTxPageScript() - break + switch (pageName) { + case MERLINSCAN_PAGES.TX.name: + initTxPageScript() + break + } } } diff --git a/src/content/metasleuth/index.tsx b/src/content/metasleuth/index.tsx index 8d978c5..6ce7f71 100644 --- a/src/content/metasleuth/index.tsx +++ b/src/content/metasleuth/index.tsx @@ -1,15 +1,19 @@ import { store } from '@src/store' import { MSG_MS_SUBSCRIPTION_INFO } from '@common/constants' +import allowlist from '@common/config/allowlist' -export const initMetaSleuth = () => { - window.addEventListener('message', function (event) { - const message = event.data - if (message.type === MSG_MS_SUBSCRIPTION_INFO) { - const token = localStorage.getItem('blocksec_token') - if (token) { - store.set('token', token.replace(/^"(.*)"$/, '$1')) - window.location.assign(message.data.url) +export class MetaSleuthInitializer { + static matches = allowlist.MS_MATCHES + init() { + window.addEventListener('message', function (event) { + const message = event.data + if (message.type === MSG_MS_SUBSCRIPTION_INFO) { + const token = localStorage.getItem('blocksec_token') + if (token) { + store.set('token', token.replace(/^"(.*)"$/, '$1')) + window.location.assign(message.data.url) + } } - } - }) + }) + } } diff --git a/src/content/opensea/index.tsx b/src/content/opensea/index.tsx index 6d2f7ce..51e9b7a 100644 --- a/src/content/opensea/index.tsx +++ b/src/content/opensea/index.tsx @@ -4,6 +4,7 @@ import '@common/styles/inject.common' import { getPageName, isAllowed, insertScript } from '@common/utils' import { OPENSEA_PAGES, GraphqlEventIds } from '@common/constants' import { store } from '@src/store' +import allowlist from '@common/config/allowlist' import { initCollectionPageScript, @@ -88,10 +89,11 @@ const initObserver = () => { }) } -export const initOpensea = () => { - initObserver() - - runContentScript() - - insertScript('opensea-fetch-interceptor') +export class OpenseaInitializer { + static matches = allowlist.OPENSEA_MATCHES + init() { + initObserver() + runContentScript() + insertScript('opensea-fetch-interceptor') + } } diff --git a/src/content/scans/index.tsx b/src/content/scans/index.tsx index 6d29668..59af0d2 100644 --- a/src/content/scans/index.tsx +++ b/src/content/scans/index.tsx @@ -2,6 +2,7 @@ import '@common/styles/inject.common' import { store } from '@src/store' import { isAllowed, getChainSimpleName, getPageName } from '@common/utils' import { ETHERSCAN_PAGES } from '@common/constants' +import allowlist from '@common/config/allowlist' import { initAddressPageScript, @@ -17,56 +18,59 @@ import { initBlocksForkedPageScript } from './page-scripts' -export const initEtherscanV1 = async () => { - if (window.self !== window.top) { - return // This page is embedded in an iframe - } +export class EtherscanV1Initializer { + static matches = allowlist.ETHERSCAN_V1_MATCHES + async init() { + if (window.self !== window.top) { + return // This page is embedded in an iframe + } - /** get user options */ - const supportWebList = await store.get('supportWebList') + /** get user options */ + const supportWebList = await store.get('supportWebList') - /** check whether the script is allowed to run on the current page */ - const allowed = isAllowed(Object.values(supportWebList)) + /** check whether the script is allowed to run on the current page */ + const allowed = isAllowed(Object.values(supportWebList)) - /** get the necessary parameters required by the extension */ - const chain: string | undefined = getChainSimpleName() + /** get the necessary parameters required by the extension */ + const chain: string | undefined = getChainSimpleName() - if (!allowed || !chain) return + if (!allowed || !chain) return - const pageName = getPageName() - switch (pageName) { - case ETHERSCAN_PAGES.ADDRESS.name: - initAddressPageScript(chain) - break - case ETHERSCAN_PAGES.TX.name: - initTxPageScript(chain) - break - case ETHERSCAN_PAGES.TXS.name: - initTxsPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCK.name: - initBlockPageScript(chain) - break - case ETHERSCAN_PAGES.ACCOUNTS.name: - initAccountsPageScript(chain) - break - case ETHERSCAN_PAGES.TOKEN.name: - initTokenPageScript(chain) - break - case ETHERSCAN_PAGES.TOKENTXNS.name: - initTokentxnsPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCKS.name: - initBlocksPageScript(chain) - break - case ETHERSCAN_PAGES.TOKEN_APPROVAL_CHECKER.name: - initTokenApprovalCheckerPageScript(chain) - break - case ETHERSCAN_PAGES.TXS_INTERNAL.name: - initTxsInternalPageScript(chain) - break - case ETHERSCAN_PAGES.BLOCKS_FORKED.name: - initBlocksForkedPageScript(chain) - break + const pageName = getPageName() + switch (pageName) { + case ETHERSCAN_PAGES.ADDRESS.name: + initAddressPageScript(chain) + break + case ETHERSCAN_PAGES.TX.name: + initTxPageScript(chain) + break + case ETHERSCAN_PAGES.TXS.name: + initTxsPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCK.name: + initBlockPageScript(chain) + break + case ETHERSCAN_PAGES.ACCOUNTS.name: + initAccountsPageScript(chain) + break + case ETHERSCAN_PAGES.TOKEN.name: + initTokenPageScript(chain) + break + case ETHERSCAN_PAGES.TOKENTXNS.name: + initTokentxnsPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCKS.name: + initBlocksPageScript(chain) + break + case ETHERSCAN_PAGES.TOKEN_APPROVAL_CHECKER.name: + initTokenApprovalCheckerPageScript(chain) + break + case ETHERSCAN_PAGES.TXS_INTERNAL.name: + initTxsInternalPageScript(chain) + break + case ETHERSCAN_PAGES.BLOCKS_FORKED.name: + initBlocksForkedPageScript(chain) + break + } } } diff --git a/src/content/solanaexpl/index.tsx b/src/content/solanaexpl/index.tsx index 590b944..1880a29 100644 --- a/src/content/solanaexpl/index.tsx +++ b/src/content/solanaexpl/index.tsx @@ -2,15 +2,19 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initSolanaExplorer = async () => { - execute() - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - execute() - sendResponse() - } - }) +export class SolanaExplorerInitializer { + static matches = allowlist.SOLANA_EXPLORER_MATCHES + init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/solanafm/index.tsx b/src/content/solanafm/index.tsx index 0eaad3b..62d81e3 100644 --- a/src/content/solanafm/index.tsx +++ b/src/content/solanafm/index.tsx @@ -2,15 +2,19 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initSolscanFM = async () => { - execute() - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - execute() - sendResponse() - } - }) +export class SolscanFMInitializer { + static matches = allowlist.SOLANAFM_MATCHES + init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/solscan/index.tsx b/src/content/solscan/index.tsx index 5464fc6..66cd461 100644 --- a/src/content/solscan/index.tsx +++ b/src/content/solscan/index.tsx @@ -2,15 +2,19 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initSolscan = async () => { - execute() - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - execute() - sendResponse() - } - }) +export class SolscanInitializer { + static matches = allowlist.SOLSCAN_MATCHES + init() { + execute() + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + execute() + sendResponse() + } + }) + } } diff --git a/src/content/tronscan/index.tsx b/src/content/tronscan/index.tsx index 76d9b2c..e3a9473 100644 --- a/src/content/tronscan/index.tsx +++ b/src/content/tronscan/index.tsx @@ -3,31 +3,35 @@ import browser from 'webextension-polyfill' import '@common/styles/inject.common' import { URL_UPDATED } from '@common/constants' import { getPageName, isHashItemsSimilar } from '@common/utils' +import allowlist from '@common/config/allowlist' import execute from './main' -export const initTronscan = async () => { - execute() - let pageName = getPageName(window.location.hash.substring(1)) - let originURL = window.location.href - browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message === URL_UPDATED) { - const newPageName = getPageName(window.location.hash.substring(1)) - if (newPageName !== pageName) { - execute() - } else { - const isSimilar = isHashItemsSimilar( - new URL(originURL).hash, - window.location.hash, - 3 - ) - if (!isSimilar) { +export class TronscanInitializer { + static matches = allowlist.TRONSCAN_MATCHES + init() { + execute() + let pageName = getPageName(window.location.hash.substring(1)) + let originURL = window.location.href + browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message === URL_UPDATED) { + const newPageName = getPageName(window.location.hash.substring(1)) + if (newPageName !== pageName) { execute() + } else { + const isSimilar = isHashItemsSimilar( + new URL(originURL).hash, + window.location.hash, + 3 + ) + if (!isSimilar) { + execute() + } } + originURL = window.location.href + pageName = newPageName + sendResponse() } - originURL = window.location.href - pageName = newPageName - sendResponse() - } - }) + }) + } } diff --git a/src/tests/isMatchURL.test.ts b/src/tests/isMatchURL.test.ts new file mode 100644 index 0000000..71a8387 --- /dev/null +++ b/src/tests/isMatchURL.test.ts @@ -0,0 +1,98 @@ +import { expect, test } from 'vitest' +import { isMatchURL } from '@common/utils/permission' + +test('should match exact URL', () => { + const url = 'https://example.com/' + const patternList = ['https://example.com/'] + expect(isMatchURL(url, patternList)).toBe(true) +}) + +test('should not match different URL', () => { + const url = 'https://example.com/' + const patternList = ['https://example.net/'] + expect(isMatchURL(url, patternList)).toBe(false) +}) + +test('should match URL with wildcard', () => { + const url = 'https://example.com/path' + const patternList = ['https://example.com/*'] + expect(isMatchURL(url, patternList)).toBe(true) +}) + +test('should match URL with wildcard', () => { + const url = 'https://example.io/path?referer=https://example.com' + const patternList = ['https://example.com/*'] + expect(isMatchURL(url, patternList)).toBe(false) +}) + +test('should not match URL with different protocol', () => { + const url = 'http://example.com/' + const patternList = ['https://example.com/'] + expect(isMatchURL(url, patternList)).toBe(false) +}) + +test('should match URL with query parameters', () => { + const url = 'https://example.com/path?a=1&b=2' + const patternList = ['https://example.com/path?*'] + expect(isMatchURL(url, patternList)).toBe(true) +}) + +test('should match URL with query parameters', () => { + const url = + 'https://www-dev.metasleuth.io/result/solana/BNLtpXLqsjDGxzB1Mmcv3NmEiQhSSWFq8JViKrrrQ8Do?referer=https%3A%2F%2Fsolscan.io%2Faccount%2FBNLtpXLqsjDGxzB1Mmcv3NmEiQhSSWFq8JViKrrrQ8Do%23splTransfers&f=md&r=auth' + const patternList = ['*://*.metasleuth.io/*'] + expect(isMatchURL(url, patternList)).toBe(true) +}) + +test('should match URL with query parameters', () => { + const url = + 'https://solscan.io/account/BNLtpXLqsjDGxzB1Mmcv3NmEiQhSSWFq8JViKrrrQ8Do#splTransfers' + const patternList = [ + '*://ftmscan.com/*', + '*://cn.etherscan.com/*', + '*://goto.etherscan.com/*', + '*://sepolia.etherscan.io/*', + '*://goerli.etherscan.io/*', + '*://cn.etherscan.com/*', + '*://etherscan.io/*', + '*://bscscan.com/*', + '*://goto.bscscan.com/*', + '*://www.bscscan.com/*', + '*://testnet.bscscan.com/*', + '*://polygonscan.com/*', + '*://mumbai.polygonscan.com/*', + '*://*.moonscan.io/*', + '*://era.zksync.network/*', + '*://snowscan.xyz/*', + '*://optimistic.etherscan.io/*', + '*://*.bttcscan.com/*', + '*://*.celoscan.io/*', + '*://zkevm.polygonscan.com/*', + '*://*.lineascan.build/*', + '*://*.wemixscan.com/*', + '*://testnet.ftmscan.com/*', + '*://*.basescan.org/*', + '*://cronoscan.com/*', + '*://gnosisscan.io/*', + '*://*.arbiscan.io/*' + ] + expect(isMatchURL(url, patternList)).toBe(false) +}) + +test('should match URL with query parameters', () => { + const url = + 'https://etherscan.io/address1/0xde0336765d7549fb555883eb6c85e8862b4fdc41' + const patternList = [ + '*://cn.etherscan.com/*', + '*://cn.etherscan.com/*', + '*://etherscan.io/*' + ] + expect(isMatchURL(url, patternList)).toBe(true) +}) + +test('should match URL with query parameters', () => { + const url = + 'https://basescan.org/address/0x6afce80b5c75149289839f80f24ad2dffdd46cb5' + const patternList = ['*://*.basescan.org/*'] + expect(isMatchURL(url, patternList)).toBe(true) +})