├── src ├── routes │ ├── +layout.ts │ ├── +page.svelte │ ├── +layout.svelte │ ├── settings │ │ └── +page.svelte │ ├── sites │ │ └── +page.svelte │ └── risk-score │ │ └── +page.svelte ├── lib │ ├── assets │ │ ├── logo.png │ │ ├── logo_lg.png │ │ ├── home.svg │ │ ├── globe.svg │ │ ├── power.svg │ │ ├── light-bulb.svg │ │ ├── gear.svg │ │ └── logo.svg │ ├── background.ts │ ├── content │ │ ├── message.ts │ │ ├── risk │ │ │ ├── utils.ts │ │ │ ├── tooltip.ts │ │ │ └── main.ts │ │ ├── utils.ts │ │ ├── content.styles.css │ │ └── index.ts │ ├── constants.ts │ ├── models │ │ ├── message.ts │ │ ├── site.ts │ │ └── statsData.ts │ ├── types.ts │ ├── utils.ts │ ├── stores.ts │ └── service │ │ └── SiteManagerService.ts ├── components │ ├── Line.svelte │ ├── TopBar.svelte │ ├── BottomNav.svelte │ ├── KnowYourRiskScore.svelte │ ├── Stats.svelte │ ├── CurrentSite.svelte │ └── HighlightingPreferences.svelte ├── app.d.ts ├── app.css └── app.html ├── static ├── logo.png ├── favicon.png ├── manifest_chrome.json ├── manifest_firefox.json └── logo.svg ├── postcss.config.cjs ├── tsconfig.json ├── vite.config.ts ├── svelte.config.js ├── README.md ├── webpack.config.cjs ├── package.json └── tailwind.config.js /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w5h10/wallet-highlighter-extension/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w5h10/wallet-highlighter-extension/HEAD/static/favicon.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w5h10/wallet-highlighter-extension/HEAD/src/lib/assets/logo.png -------------------------------------------------------------------------------- /src/lib/assets/logo_lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w5h10/wallet-highlighter-extension/HEAD/src/lib/assets/logo_lg.png -------------------------------------------------------------------------------- /src/components/Line.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 11 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/lib/background.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import { API_HOST } from './constants'; 3 | import { getUserId } from './content/utils'; 4 | const init = async () => { 5 | browser.runtime.setUninstallURL(`${API_HOST}/v1/uninstall?clientId=${await getUserId()}`); 6 | }; 7 | 8 | init(); 9 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter&display=swap'); 2 | 3 | .container { 4 | font-family: 'Inter', sans-serif; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: #000; 10 | } 11 | 12 | .button { 13 | @apply btn; 14 | @apply normal-case text-[0.65rem] font-normal min-h-fit h-[1.7rem]; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/content/message.ts: -------------------------------------------------------------------------------- 1 | import type { MessageType } from '$lib/constants'; 2 | import { Message } from '$lib/models/message'; 3 | 4 | export const sendMessage = async (type: MessageType) => { 5 | const tabs = await chrome.tabs.query({ currentWindow: true, active: true }); 6 | const activeTab = tabs[0]; 7 | 8 | console.log({ activeTab }); 9 | 10 | if (!activeTab.id) throw Error('INVALID TAB'); 11 | const message = new Message(type); 12 | const res = await chrome.tabs.sendMessage(activeTab.id, message.serialize()); 13 | return res?.value; 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import inject from '@rollup/plugin-inject'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | optimizeDeps: { 8 | esbuildOptions: { 9 | define: { 10 | global: 'globalThis' 11 | } 12 | } 13 | }, 14 | build: { 15 | rollupOptions: { 16 | plugins: [inject({ Buffer: ['Buffer', 'Buffer'] })] 17 | } 18 | }, 19 | resolve: { 20 | alias: { 21 | process: 'process/browser', 22 | stream: 'stream-browserify', 23 | zlib: 'browserify-zlib', 24 | util: 'util' 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | RERUN_SCAN = 'RERUN_SCAN' 3 | } 4 | 5 | export const STATS = 'STATS'; 6 | export const GET_STATS = 'GET_STATS'; 7 | 8 | export const LIST_TYPE = 'LIST_TYPE'; 9 | export enum ListOptionTypes { 10 | HIGHLIGHT_LISTED = 'HIGH_LIGHT_LISTED', 11 | EXCLUDED_LISTED = 'EXCLUEDED_LISTED' 12 | } 13 | 14 | export const USER_ID = 'USER_ID'; 15 | 16 | // Default return color HEX 17 | export const HIGH_RISK_COLOR = '#e74c3c'; 18 | export const MED_RISK_COLOR = '#f5b041'; 19 | export const LOW_RISK_COLOR = '#2ecc71'; 20 | 21 | export const API_HOST = 'https://api.wallethighlighter.com'; 22 | export const RISK_API_URL = `${API_HOST}/v1/analyze`; 23 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 25 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | %sveltekit.head% 14 | 20 | 21 | 22 |
%sveltekit.body%
23 | 24 | 25 | -------------------------------------------------------------------------------- /static/manifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.3", 3 | "manifest_version": 3, 4 | "name": "Wallet Highlighter - Crypto Fraud Detector", 5 | "description": "Crypto may be used for illegal activities and can be identified to protect users from losing money & connecting with threat actors.", 6 | "background": { 7 | "service_worker": "background.bundle.js" 8 | }, 9 | "action": { 10 | "default_popup": "index.html", 11 | "default_icon": "logo.png" 12 | }, 13 | "icons": { 14 | "128": "logo.png" 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [ 19 | "" 20 | ], 21 | "js": [ 22 | "contentScript.bundle.js" 23 | ], 24 | "css": [ 25 | "content.styles.css" 26 | ] 27 | } 28 | ], 29 | "permissions": [ 30 | "storage", 31 | "tabs" 32 | ] 33 | } -------------------------------------------------------------------------------- /static/manifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.3", 3 | "manifest_version": 3, 4 | "name": "Wallet Highlighter - Crypto Fraud Detector", 5 | "description": "Crypto may be used for illegal activities and can be identified to protect users from losing money & connecting with threat actors.", 6 | "background": { 7 | "scripts": [ 8 | "background.bundle.js", 9 | "browser-polyfill.js" 10 | ] 11 | }, 12 | "action": { 13 | "default_popup": "index.html", 14 | "default_icon": "logo.png" 15 | }, 16 | "icons": { 17 | "128": "logo.png" 18 | }, 19 | "content_scripts": [ 20 | { 21 | "matches": [ 22 | "" 23 | ], 24 | "js": [ 25 | "contentScript.bundle.js" 26 | ], 27 | "css": [ 28 | "content.styles.css" 29 | ] 30 | } 31 | ], 32 | "permissions": [ 33 | "storage", 34 | "tabs" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/components/TopBar.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 | logo 19 | Wallet Highlighter 20 |
21 |
25 | {#if $theme === 'wh-light'} 26 | 27 | {:else} 28 | 29 | {/if} 30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | // import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | import adapter from 'sveltekit-adapter-chrome-extension'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 8 | // for more information about preprocessors 9 | preprocess: vitePreprocess(), 10 | 11 | kit: { 12 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 13 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 14 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 15 | adapter: adapter({ 16 | // default options are shown 17 | pages: 'build', 18 | assets: 'build', 19 | fallback: null, 20 | precompress: false, 21 | manifest: 'manifest_chrome.json' 22 | }), 23 | prerender: { 24 | entries: ['*'] 25 | }, 26 | appDir: 'app' 27 | } 28 | }; 29 | 30 | export default config; 31 | -------------------------------------------------------------------------------- /src/lib/assets/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/models/message.ts: -------------------------------------------------------------------------------- 1 | import type { MessageType } from '$lib/constants'; 2 | 3 | export class Message { 4 | type: MessageType; 5 | value: boolean; 6 | attributes: { [key: string]: string }; 7 | 8 | private defaultBool = false; 9 | 10 | constructor(type: MessageType, value?: boolean, attributes?: { [key: string]: string }) { 11 | this.type = type; 12 | this.value = this.defaultBool; 13 | this.attributes = {}; 14 | if (attributes !== undefined) { 15 | this.attributes = attributes; 16 | } 17 | if (value !== undefined) { 18 | this.value = value; 19 | } 20 | } 21 | 22 | private toObject() { 23 | return { 24 | type: this.type, 25 | value: this.value, 26 | attributes: this.attributes 27 | }; 28 | } 29 | 30 | serialize() { 31 | return JSON.stringify(this.toObject()); 32 | } 33 | 34 | static fromSerialized(serialized: string) { 35 | const messageData: ReturnType = JSON.parse(serialized); 36 | 37 | const message: Message = new Message( 38 | messageData.type, 39 | messageData.value, 40 | messageData.attributes 41 | ); 42 | 43 | return message; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Wallet Highlighter risk actor types](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/a633266f-b2ed-4402-9d57-d0ad85714286) 2 | ![Untitled presentation](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/cd3fac39-f6da-4e8b-8f16-495d9e8e5ede) 3 | ![Wallet Highlighter risk actor types(1)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/89a72a29-a1f3-4e47-86d6-eeb48bdc16b6) 4 | ![Wallet Highlighter risk actor types(2)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/3dfd7566-5cd8-4992-844d-8746883cad12) 5 | ![Wallet Highlighter risk actor types(3)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/9691f739-3436-4302-b48f-d576f80aa455) 6 | ![Wallet Highlighter risk actor types(4)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/72f6d587-b2f3-4d06-9d4b-712091ad1d1b) 7 | ![Wallet Highlighter risk actor types(5)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/cb8ca1ee-e648-4a09-aa45-4447d9fef327) 8 | ![Wallet Highlighter risk actor types(6)](https://github.com/w5h10/wallet-highlighter-extension/assets/148644163/349e80a9-5df8-439f-bfec-67a8626a8db5) 9 | [**PRIVACY POLICY**]([url](https://www.wallethighlighter.com/en/privacy-policy)) 10 | -------------------------------------------------------------------------------- /src/lib/models/site.ts: -------------------------------------------------------------------------------- 1 | import type { RiskCounter } from '$lib/types'; 2 | 3 | export class Site { 4 | readonly siteId: string; 5 | readonly siteURL: string; 6 | readonly isExcluded: boolean; 7 | 8 | highRiskCount = 0; 9 | moderateRiskCount = 0; 10 | lowRiskCount = 0; 11 | 12 | constructor(siteId: string, siteURL: string, isExcluded: boolean) { 13 | this.siteId = siteId; 14 | this.siteURL = siteURL; 15 | this.isExcluded = isExcluded; 16 | } 17 | 18 | populateRisk(riskCount: RiskCounter) { 19 | this.highRiskCount = riskCount.high; 20 | this.moderateRiskCount = riskCount.medium; 21 | this.lowRiskCount = riskCount.low; 22 | } 23 | 24 | private toObject() { 25 | return { 26 | siteId: this.siteId, 27 | siteURL: this.siteURL, 28 | isExcluded: this.isExcluded, 29 | highRiskCount: this.highRiskCount, 30 | moderateRiskCount: this.moderateRiskCount, 31 | lowRiskCount: this.lowRiskCount 32 | }; 33 | } 34 | 35 | serialize() { 36 | return JSON.stringify(this.toObject()); 37 | } 38 | 39 | static fromSerialized(serialized: string) { 40 | const siteData: ReturnType = JSON.parse(serialized); 41 | 42 | const site: Site = new Site(siteData.siteId, siteData.siteURL, siteData.isExcluded); 43 | site.highRiskCount = siteData.highRiskCount; 44 | site.lowRiskCount = siteData.lowRiskCount; 45 | site.moderateRiskCount = siteData.moderateRiskCount; 46 | 47 | return site; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/assets/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/lib/content/risk/utils.ts: -------------------------------------------------------------------------------- 1 | export const colorMap = { 2 | red: "#e74c3c", //61-100 3 | orange: "#f5b041", //36-60 4 | green: "#2ecc71", //0-35 5 | null: null, //404 6 | }; 7 | 8 | 9 | export const getRiskColor = (score: number): string | null => { 10 | if (score === 404 || score === 30) { 11 | return colorMap.null; 12 | } 13 | if (score <= 35) { 14 | return colorMap.green; 15 | } 16 | if (score <= 60) { 17 | return colorMap.orange; 18 | } 19 | return colorMap.red; 20 | }; 21 | 22 | export const getRiskStatus = (score: number): string | null => { 23 | if (score === 404 || score === 30) { 24 | return 'N/A'; 25 | } 26 | if (score <= 35) { 27 | return 'Low'; 28 | } 29 | if (score <= 60) { 30 | return 'Medium'; 31 | } 32 | return 'High'; 33 | }; 34 | 35 | export const getRiskPercentage = (statusStr: string): number | null => { 36 | switch (statusStr.toUpperCase()) { 37 | // make case letter random 38 | case 'B': 39 | return 10; 40 | case 'C': 41 | return 20; 42 | case 'D': 43 | return 29; // 30 is considered null 44 | case 'E': 45 | return 40; 46 | case 'F': 47 | return 50; 48 | case 'G': 49 | return 60; 50 | case 'H': 51 | return 70; 52 | case 'I': 53 | return 80; 54 | case 'J': 55 | return 90; 56 | case 'K': 57 | return 100; 58 | default: 59 | return null; 60 | } 61 | }; 62 | 63 | export const roundToTwo = (num: number): number => { 64 | return Math.round((num + Number.EPSILON) * 100) / 100; 65 | }; 66 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | // declare namespace module {} 2 | export enum StoreName { 3 | excludedSites = 'excludedSites', 4 | includedSites = 'includedSites', 5 | highlightingPreference = 'highlightingPreference', 6 | theme = 'theme', 7 | pluginStatus = 'pluginStatus', 8 | userId = 'userId', 9 | siteInfo = 'siteInfo', 10 | statsData = 'stats' 11 | } 12 | export enum PluginStatus { 13 | Active = 'Active', 14 | Inactive = 'Inactive' 15 | } 16 | export enum HighlightingPreference { 17 | all = 'all', 18 | included = 'included' 19 | } 20 | 21 | export enum ThemePreference { 22 | Light = 'wh-light', 23 | Dark = 'wh-dark' 24 | } 25 | 26 | export interface RiskDetailsMapping { 27 | [k: string]: { 28 | color: string; 29 | detail: RiskDetailItem; 30 | } | null; 31 | } 32 | 33 | export interface RiskDetailItem { 34 | combinedRisk: string; 35 | fraudRisk: string; 36 | lendingRisk: string; 37 | reputationRisk: string; 38 | tags: Array; 39 | } 40 | 41 | export interface RiskTag { 42 | tag: string; 43 | desc: string; 44 | } 45 | 46 | export interface RiskCounter { 47 | high: number; 48 | low: number; 49 | medium: number; 50 | } 51 | 52 | export type Wallet = HTMLInputElement | HTMLSpanElement | null; 53 | 54 | export interface Wallets { 55 | [k: string]: Wallet[]; 56 | } 57 | export interface Obj { 58 | [k: string]: string | Obj; 59 | } 60 | export declare const Ext: any; 61 | 62 | export interface RiskApiResponse { 63 | colors: Record; 64 | details: Record; 65 | } 66 | 67 | export interface KnowRiskScoreResponseModel { 68 | status: 'OK' | 'ERROR'; 69 | error?: string; 70 | } 71 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | contentScript: './src/lib/content/index.ts', 8 | background: './src/lib/background.ts' 9 | }, 10 | output: { 11 | filename: '[name].bundle.js', 12 | path: path.resolve(__dirname, 'build'), 13 | sourceMapFilename: '[file].map[query]' 14 | }, 15 | target: 'web', 16 | resolve: { 17 | extensions: ['.js', '.ts'], 18 | alias: { 19 | $lib: path.resolve(__dirname, 'src/lib') 20 | } 21 | }, 22 | devtool: 'source-map', 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.ts$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: 'babel-loader', 30 | options: { 31 | presets: ['@babel/preset-env', '@babel/preset-typescript'] 32 | } 33 | } 34 | }, 35 | { 36 | // look for .css or .scss files 37 | test: /\.(css|scss)$/, 38 | // in the `src` directory 39 | use: [ 40 | { 41 | loader: 'style-loader' 42 | }, 43 | { 44 | loader: 'css-loader' 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new webpack.ProvidePlugin({ 52 | process: 'process/browser', 53 | Buffer: ['buffer', 'Buffer'] 54 | }), 55 | new CopyWebpackPlugin({ 56 | patterns: [ 57 | { 58 | from: 'src/lib/content/content.styles.css', 59 | to: path.join(__dirname, 'build'), 60 | force: true 61 | }, 62 | { 63 | from: 'src/lib/assets/**/*', 64 | to: path.join(__dirname, 'build', '[name][ext]'), 65 | force: true 66 | } 67 | ] 68 | }) 69 | ] 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/BottomNav.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 29 | 78 | -------------------------------------------------------------------------------- /src/components/KnowYourRiskScore.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | {#if $theme === ThemePreference.Dark} 11 | 12 | {:else} 13 | 14 | {/if} 15 |
16 | 17 |
18 | Time to Test your Risk Score 19 | 20 | Getting your score is easy. Just enter your wallet address and email 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 77 | -------------------------------------------------------------------------------- /src/lib/assets/power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/Stats.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
21 |
22 | 23 | {StatsData.humanReadableFormat(totalHighlighted)} 24 | 25 | Total wallets highlighted 26 |
27 |
28 |
29 |
30 | 31 | {highRiskPercentage}% 32 | 33 | High risk wallets 34 |
35 |
36 |
37 |
38 | 39 | {moderatePercentage}% 40 | 41 | Moderate risk wallets 42 |
43 |
44 |
45 | 46 | 88 | -------------------------------------------------------------------------------- /src/lib/assets/light-bulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 22 | 26 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /src/lib/models/statsData.ts: -------------------------------------------------------------------------------- 1 | import type { RiskCounter } from '$lib/types'; 2 | 3 | export class StatsData { 4 | highRiskPercentage: number; 5 | moderatePercentage: number; 6 | 7 | constructor(public totalHighlighted = 0, public totalHighRisk = 0, public totalModerateRisk = 0) { 8 | this.highRiskPercentage = 9 | totalHighlighted === 0 10 | ? 0 11 | : Math.round((this.totalHighRisk * 10000) / totalHighlighted) / 100; 12 | this.moderatePercentage = 13 | totalHighlighted === 0 14 | ? 0 15 | : Math.round((this.totalModerateRisk * 10000) / totalHighlighted) / 100; 16 | } 17 | 18 | private toObject() { 19 | return { 20 | totalHighlighted: this.totalHighlighted, 21 | totalHighRisk: this.totalHighRisk, 22 | totalModerateRisk: this.totalModerateRisk 23 | }; 24 | } 25 | 26 | serialize() { 27 | return JSON.stringify(this.toObject()); 28 | } 29 | 30 | static fromSerialized(serialized: string) { 31 | const statsData: ReturnType = JSON.parse(serialized); 32 | 33 | const stats: StatsData = new StatsData( 34 | statsData.totalHighlighted, 35 | statsData.totalHighRisk, 36 | statsData.totalModerateRisk 37 | ); 38 | 39 | return stats; 40 | } 41 | 42 | static initializeWithEmtpy(): StatsData { 43 | return new StatsData(); 44 | } 45 | 46 | public updateStatsCount(riskCount: RiskCounter) { 47 | const tmpTotal = this.totalHighlighted + riskCount.high + riskCount.low + riskCount.medium; 48 | const tmpTotalHigh = this.totalHighRisk + riskCount.high; 49 | const tmpTotalMod = this.totalModerateRisk + riskCount.medium; 50 | this.totalHighlighted = tmpTotal; 51 | this.totalHighRisk = tmpTotalHigh; 52 | this.totalModerateRisk = tmpTotalMod; 53 | } 54 | 55 | static humanReadableFormat(totalCount: number): string { 56 | const decPlaces = Math.pow(10, 2); 57 | 58 | // Enumerate number abbreviations 59 | const abbrev = ['k', 'm', 'b', 't']; 60 | let val = totalCount.toString(); 61 | for (let i = abbrev.length - 1; i >= 0; i--) { 62 | const size = Math.pow(10, (i + 1) * 3); 63 | if (size <= totalCount) { 64 | totalCount = Math.round((totalCount * decPlaces) / size) / decPlaces; 65 | val = totalCount.toString(); 66 | if (totalCount == 1000 && i < abbrev.length - 1) { 67 | totalCount = 1; 68 | i++; 69 | } 70 | val += abbrev[i]; 71 | break; 72 | } 73 | } 74 | return val; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-extension", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build:ui": "vite build", 8 | "build": "yarn build:ui && npx webpack --config webpack.config.cjs", 9 | "buildw": "yarn build --watch", 10 | "build:css": "npx tailwindcss -i ./src/app.css -o ./build/output.css --watch", 11 | "preview": "vite preview", 12 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 13 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 14 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 15 | "format": "prettier --plugin-search-dir . --write ." 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.22.9", 19 | "@babel/preset-env": "^7.22.9", 20 | "@babel/preset-typescript": "^7.22.5", 21 | "@crxjs/vite-plugin": "^1.0.14", 22 | "@sveltejs/adapter-auto": "^2.0.0", 23 | "@sveltejs/kit": "^1.20.4", 24 | "@types/webextension-polyfill": "^0.10.1", 25 | "@typescript-eslint/eslint-plugin": "^5.45.0", 26 | "@typescript-eslint/parser": "^5.45.0", 27 | "autoprefixer": "^10.4.14", 28 | "babel-loader": "^9.1.3", 29 | "copy-webpack-plugin": "^11.0.0", 30 | "daisyui": "^3.2.1", 31 | "eslint": "^8.28.0", 32 | "eslint-config-prettier": "^8.5.0", 33 | "eslint-plugin-svelte": "^2.30.0", 34 | "postcss": "^8.4.25", 35 | "prettier": "^2.8.0", 36 | "prettier-plugin-svelte": "^2.10.1", 37 | "svelte": "^4.0.0", 38 | "svelte-check": "^3.4.3", 39 | "sveltekit-adapter-chrome-extension": "^2.0.0", 40 | "tailwindcss": "^3.3.2", 41 | "ts-loader": "^9.4.4", 42 | "tslib": "^2.4.1", 43 | "typescript": "^5.1.6", 44 | "vite": "^4.3.6", 45 | "webextension-polyfill": "^0.10.0", 46 | "webpack": "^5.88.1", 47 | "webpack-cli": "^5.1.4" 48 | }, 49 | "type": "module", 50 | "dependencies": { 51 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3", 52 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2", 53 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 54 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 55 | "@iota/validators": "^1.0.0-beta.30", 56 | "@macfja/svelte-persistent-store": "^2.3.1", 57 | "@rollup/plugin-inject": "^5.0.3", 58 | "@sveltejs/adapter-static": "^2.0.2", 59 | "buffer": "^6.0.3", 60 | "node-stdlib-browser": "^1.2.0", 61 | "svelte-fa": "^3.0.4", 62 | "trezor-address-validator": "^0.4.3", 63 | "vite-plugin-node-polyfills": "^0.9.0", 64 | "vite-plugin-node-stdlib-browser": "^0.2.1" 65 | } 66 | } -------------------------------------------------------------------------------- /src/routes/settings/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 |
20 | Settings 21 | 22 |
23 | Plugin Status 24 | 25 |
30 | power 31 |
32 | Status : {$pluginStatus === PluginStatus.Active ? 'Active' : 'Inactive'} 35 | (You can change your status whenever needed) 36 |
37 | 38 |
39 | Change your theme 40 | 50 | 60 |
61 |
62 | 63 | 94 | -------------------------------------------------------------------------------- /src/lib/content/risk/tooltip.ts: -------------------------------------------------------------------------------- 1 | export const Tooltip: { 2 | tooltip: HTMLElement | undefined | null; 3 | target: HTMLElement | undefined | null; 4 | bindEvents: () => void; 5 | show: (ev: Event) => void; 6 | hide: () => void; 7 | } = { 8 | tooltip: null, 9 | target: null, 10 | bindEvents: function () { 11 | Tooltip.tooltip = document.getElementById('whe-tooltip'); 12 | const targets = document.querySelectorAll('[rel=whe-tooltip]'); 13 | for (let i = 0; i < targets.length; ++i) { 14 | targets[i].addEventListener('click', Tooltip.show); 15 | targets[i].addEventListener('mouseenter', Tooltip.show); 16 | targets[i].addEventListener('mouseleave', Tooltip.hide); 17 | } 18 | Tooltip.tooltip?.addEventListener('click', Tooltip.hide); 19 | window.addEventListener('resize', Tooltip.show); 20 | }, 21 | 22 | show: function (ev: Event) { 23 | Tooltip.hide(); 24 | Tooltip.target = ev.target as HTMLElement; 25 | if (!Tooltip.tooltip || !Tooltip.target) { 26 | return false; 27 | } 28 | const tip = Tooltip.target.getAttribute('whe-tooltip-text'); 29 | if (!tip || tip == '') { 30 | return false; 31 | } 32 | Tooltip.tooltip.className = ''; 33 | Tooltip.tooltip.innerHTML = tip; 34 | const targetDomRect = Tooltip.target.getBoundingClientRect(); 35 | const targetLeft = targetDomRect.left; 36 | const targetTop = targetDomRect.top + window.scrollY; 37 | if (window.innerWidth < Tooltip.tooltip.offsetWidth * 1.5) { 38 | Tooltip.tooltip.style.maxWidth = window.innerWidth / 2 + 'px'; 39 | } else { 40 | Tooltip.tooltip.style.maxWidth = 320 + 'px'; 41 | } 42 | 43 | // eslint-disable-next-line no-var 44 | var pos_left = targetLeft + Tooltip.target.offsetWidth / 2 - Tooltip.tooltip.offsetWidth / 2, 45 | pos_top = targetTop - Tooltip.tooltip.offsetHeight - 20; 46 | Tooltip.tooltip.className = ''; 47 | if (pos_left < 0) { 48 | pos_left = targetLeft + Tooltip.target.offsetWidth / 2 - 20; 49 | Tooltip.tooltip.className += ' whe-left'; 50 | } 51 | 52 | if (pos_left + Tooltip.tooltip.offsetWidth > window.innerWidth) { 53 | pos_left = targetLeft - Tooltip.tooltip.offsetWidth + Tooltip.target.offsetWidth / 2 + 20; 54 | Tooltip.tooltip.className += ' whe-right'; 55 | } 56 | 57 | if (pos_top < 0) { 58 | // eslint-disable-next-line no-var 59 | var pos_top = targetTop + Tooltip.target.offsetHeight; 60 | Tooltip.tooltip.className += ' whe-top'; 61 | } 62 | 63 | Tooltip.tooltip.style.left = pos_left + 'px'; 64 | Tooltip.tooltip.style.top = pos_top + 'px'; 65 | 66 | Tooltip.tooltip.className += ' whe-show'; 67 | }, 68 | hide: function () { 69 | if (!Tooltip.tooltip) { 70 | return; 71 | } 72 | Tooltip.tooltip.className = Tooltip.tooltip.className.replace('whe-show', ''); 73 | } 74 | }; 75 | 76 | export const initTooltip = () => { 77 | document.body.insertAdjacentHTML('afterbegin', '
'); 78 | }; 79 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import IOTA from '@iota/validators'; 2 | import WAValidator from 'trezor-address-validator'; 3 | import { getUserId } from './content/utils'; 4 | import { API_HOST } from './constants'; 5 | import type { KnowRiskScoreResponseModel } from './types'; 6 | 7 | class RiskScoreError extends Error {} 8 | const coins = [ 9 | // "AUR", 10 | // "BKX", 11 | // "BVC", 12 | // "BIO", 13 | 'BTC', 14 | 'BCH', 15 | // "BTG", 16 | // "BTCP", 17 | // "BTCZ", 18 | // "CLO", 19 | // "DASH", 20 | // "DCR", 21 | // "DGB", 22 | 'DOGE', 23 | 'ETH', 24 | // "ETC", 25 | // "ETZ", 26 | // "FRC", 27 | // "GRLC", 28 | // "HUSH", 29 | // "KMD", 30 | 'LTC', 31 | // "MEC", 32 | 'XMR', 33 | 'NMC', 34 | // "NANO", 35 | // "NEO", 36 | // "GAS", 37 | 'PPC', 38 | // "XPM", 39 | // "PTS", 40 | // "QTUM", 41 | // "XRB", 42 | // "XRP", 43 | // "SNG", 44 | // "VTC", 45 | // "VOT", 46 | // "ZEC", 47 | // "ZCL", 48 | // "ZEN", 49 | 'XTZ' 50 | ]; 51 | 52 | const isValidIotaAddress = (address: string) => 53 | IOTA.isAddress(address) || new RegExp('^(iota1|iot1|io1)[a-zA-Z0-9]{25,}').test(address); 54 | 55 | export const validateWallet = (wallet: string): boolean => { 56 | if (!wallet) { 57 | return false; 58 | } 59 | 60 | if (isValidIotaAddress(wallet)) { 61 | return true; 62 | } 63 | 64 | return ( 65 | WAValidator.validate(wallet.trim()) || 66 | coins.some((coin) => WAValidator.validate(wallet.trim(), coin)) 67 | ); 68 | }; 69 | 70 | export const validateEmail = (email: string): boolean => { 71 | return new RegExp(/[a-z0-9]+@[a-z]+\.[a-z]{2,3}/).test(email); 72 | }; 73 | 74 | export const sendApiRequest = async (email: string, wallet: string): Promise => { 75 | if (!email || !wallet) { 76 | throw new RiskScoreError('missing email or wallet address'); 77 | } 78 | 79 | const userID = await getUserId(); 80 | console.log({ userID }); 81 | 82 | let result = null; 83 | try { 84 | const response = await fetch(`${API_HOST}/v1/knowriskscore`, { 85 | method: 'POST', 86 | body: JSON.stringify({ email, wallet, userID, source: 'WEB_EXTENSION' }), 87 | headers: { 88 | 'Content-Type': 'application/json' 89 | } 90 | }); 91 | result = (await response.json()) as KnowRiskScoreResponseModel; 92 | } catch (e) { 93 | throw new RiskScoreError( 94 | 'An Error occured while connecting to server. Please check your connection.' 95 | ); 96 | } 97 | if (result.status.toUpperCase() === 'ERROR') { 98 | throw new RiskScoreError(result.error ?? 'An error has occured'); 99 | } 100 | }; 101 | 102 | export const getScamReportHash = async (address: string, ticker: string, websiteUrl: string) => { 103 | const hashable = [address, ticker, websiteUrl].join('_'); 104 | const hashBuffer = await crypto.subtle.digest('SHA-256', Buffer.from(hashable)); 105 | const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array 106 | return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string 107 | }; 108 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | fontFamily: { 6 | sans: 'Inter' 7 | }, 8 | extend: {} 9 | }, 10 | plugins: [require('daisyui')], 11 | daisyui: { 12 | themes: [ 13 | 'dark', 14 | { 15 | 'wh-light': { 16 | primary: '#000', 17 | 'primary-focus': '#000', 18 | 'primary-content': '#fff', 19 | 20 | secondary: '#cacaca', 21 | 'secondary-focus': '', 22 | 'secondary-content': '', 23 | 24 | accent: '#4277FF', 25 | 'accent-focus': '', 26 | 'accent-content': '', 27 | 28 | neutral: '#fff', 29 | 'neutral-focus': '', 30 | 'neutral-content': '#000', 31 | 32 | 'base-100': '#000', 33 | 'base-200': '#f0eff1', 34 | 'base-300': '#FFFFFF14', 35 | 'base-content': '#A5A5A5', 36 | 37 | info: '#075985', 38 | 'info-content': '#075985', 39 | 40 | success: '#28CF6B', 41 | 'success-content': '#28CF6B', 42 | 43 | warning: '#FF7B69', 44 | 'warning-content': '#FF7B69', 45 | 46 | error: '#DF3333', 47 | 'error-content': '#DF3333', 48 | 49 | '.contrast': { 50 | 'background-color': '#F9F9F9', 51 | color: '#000', 52 | 'border-color': '#F5F2FA' 53 | }, 54 | '.btn-primary': { 55 | 'border-color': '#5F1DCC', 56 | 'background-color': '#000' 57 | }, 58 | '.button[disabled]': { 59 | 'border-color': '#A9A9A9', 60 | 'background-color': '#A9A9A9', 61 | color: '#FFF' 62 | } 63 | }, 64 | 'wh-dark': { 65 | primary: '#853BFF', 66 | 'primary-focus': '', 67 | 'primary-content': '#fff', 68 | 69 | secondary: '#cacaca', 70 | 'secondary-focus': '', 71 | 'secondary-content': '', 72 | 73 | accent: '#4277FF', 74 | 'accent-focus': '', 75 | 'accent-content': '', 76 | 77 | neutral: '#141414', // The actual dark/light background 78 | 'neutral-focus': '', 79 | 'neutral-content': '#fff', 80 | 81 | 'base-100': '#fff', 82 | 'base-200': '#242424', 83 | 'base-300': '#1a1a1a', 84 | 'base-content': '#A5A5A5', 85 | 86 | info: '#075985', 87 | 'info-content': '#075985', 88 | 89 | success: '#28CF6B', 90 | 'success-content': '#28CF6B', 91 | 92 | warning: '#FF7B69', 93 | 'warning-content': '#FF7B69', 94 | 95 | error: '#DF3333', 96 | 'error-content': '#DF3333', 97 | 98 | '.contrast': { 99 | 'background-color': '#1A1A1A', 100 | color: '#fff', 101 | 'border-color': '#242424' 102 | }, 103 | '.btn-primary': { 104 | 'border-color': '#5F1DCC', 105 | 'background-color': '#000' 106 | }, 107 | '.btn-primary:hover': { 108 | 'border-color': '#5F1DCC', 109 | 'background-color': '#853BFF' 110 | }, 111 | '.button[disabled]': { 112 | 'border-color': '#3c3c3c', 113 | 'background-color': '#3c3c3c', 114 | color: '#838383' 115 | } 116 | } 117 | } 118 | ] 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { get, writable } from 'svelte/store'; 2 | import { HighlightingPreference, PluginStatus, StoreName, ThemePreference } from '$lib/types'; 3 | 4 | const getStore = (name: StoreName, defaultValue: T) => { 5 | const store = writable(defaultValue); 6 | const customStore = { 7 | set: (value: T) => { 8 | store.set(value); 9 | chrome.storage.local.set({ [name]: value }).catch(console.error); 10 | }, 11 | update: (cb: (value: T) => T) => { 12 | const newVal = cb(get(store)); 13 | store.set(newVal); 14 | chrome.storage.local.set({ [name]: newVal }).catch(console.error); 15 | }, 16 | subscribe: store.subscribe 17 | }; 18 | 19 | return customStore; 20 | }; 21 | 22 | export const excludedSites = getStore(StoreName.excludedSites, [] as string[]); 23 | export const includedSites = getStore(StoreName.includedSites, [] as string[]); 24 | export const highlightingPreference = getStore( 25 | StoreName.highlightingPreference, 26 | HighlightingPreference.all 27 | ); 28 | export const theme = getStore(StoreName.theme, ThemePreference.Dark); 29 | export const pluginStatus = getStore(StoreName.pluginStatus, PluginStatus.Active); 30 | export const userId = getStore(StoreName.userId, ''); 31 | export const siteInfo = getStore(StoreName.siteInfo, [] as string[]); 32 | export const statsData = getStore(StoreName.statsData, '{}'); 33 | 34 | const storeConfig = [ 35 | { name: StoreName.excludedSites, store: excludedSites, defaultValue: [] as string[] }, 36 | { name: StoreName.includedSites, store: includedSites, defaultValue: [] as string[] }, 37 | { 38 | name: StoreName.highlightingPreference, 39 | store: highlightingPreference, 40 | defaultValue: HighlightingPreference.all 41 | }, 42 | { name: StoreName.theme, store: theme, defaultValue: ThemePreference.Light }, 43 | { name: StoreName.pluginStatus, store: pluginStatus, defaultValue: PluginStatus.Active }, 44 | { name: StoreName.userId, store: userId, defaultValue: '' }, 45 | { name: StoreName.siteInfo, store: siteInfo, defaultValue: [] as string[] }, 46 | { name: StoreName.statsData, store: statsData, defaultValue: '{}' } 47 | ]; 48 | 49 | export const loadStores = async () => { 50 | for (const { name, store, defaultValue } of storeConfig) { 51 | const entry = await chrome.storage.local.get(name); 52 | const value = entry[name] !== undefined ? entry[name] : defaultValue; 53 | store.set(value); 54 | } 55 | }; 56 | 57 | export const getStoreValueFromLocalStorage = async ( 58 | storeName: StoreName, 59 | defaultValue: unknown 60 | ): Promise => { 61 | const entry = await chrome.storage.local.get(storeName); 62 | const value: unknown = entry[storeName] !== undefined ? entry[storeName] : undefined; 63 | const storeConfigEntry = storeConfig.find(({ name }) => storeName === name); 64 | if (storeConfigEntry === undefined) { 65 | throw Error('Store not found in local storage'); 66 | } 67 | storeConfigEntry.store.set(value); 68 | return value ?? defaultValue; 69 | }; 70 | 71 | export const setStoreValueToLocalStorage = async ( 72 | storeName: StoreName, 73 | value: unknown 74 | ): Promise => { 75 | const storeConfigEntry = storeConfig.find(({ name }) => storeName === name); 76 | if (storeConfigEntry === undefined) { 77 | throw Error('Store not found in local storage'); 78 | } 79 | storeConfigEntry.store.set(value); 80 | try { 81 | await chrome.storage.local.set({ [storeName]: value }); 82 | return true; 83 | } catch (err) { 84 | console.error(err); 85 | return false; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/lib/assets/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/CurrentSite.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 | Current Site 56 | {currentSite} 57 |
58 | 66 | 74 |
75 | {#if inExcludedSites || inIncludedSites} 76 | 77 | This site is in the 78 | {#if inIncludedSites} 79 | included 80 | {:else if inExcludedSites} 81 | excluded 82 | {/if} 83 | list. 84 | Remove 89 | 90 | {/if} 91 |
92 | 93 | 142 | -------------------------------------------------------------------------------- /src/routes/sites/+page.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | 46 |
47 | Sites 48 | 60 |
61 | light-bulb 62 | What is {activeTab === Tab.Included ? 'Included' : 'Excluded'} websites 65 |
66 | The list of of websites from which wallet addresses will be highlighted when the preference is 68 | set to {activeTab === Tab.Included ? 'Included sites' : 'All sites except excluded'} 70 | {#if activeTab === Tab.Included} 71 |
72 | {#each $includedSites as site} 73 |
74 |
75 | 76 | {site} 77 |
78 | await removeFromIncluded(site)}>Remove 82 |
83 | {:else} 84 | No websites in the included list so far. 85 | {/each} 86 |
87 | {:else if activeTab === Tab.Excluded} 88 |
89 | {#each $excludedSites as site} 90 |
91 |
92 | 93 | {site} 94 |
95 | await removeFromExcluded(site)}>Remove 99 |
100 | {:else} 101 | No websites in the excluded list so far. 102 | {/each} 103 |
104 | {/if} 105 |
106 | 107 | 133 | -------------------------------------------------------------------------------- /src/lib/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/routes/risk-score/+page.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 | 53 | 54 |
55 | {#if $theme === ThemePreference.Dark} 56 | 57 | {:else} 58 | 59 | {/if} 60 |
61 |
62 | Know your Risk Score 63 | 64 | Getting your score is easy. Just enter your wallet address and email 65 | 66 |
67 | 68 |
69 |
70 | Wallet Address 71 | 72 |
73 |
74 | Email 75 | 76 |
77 | 83 |
84 | {statusMessage} 87 |
88 |
89 | 90 | 175 | -------------------------------------------------------------------------------- /src/components/HighlightingPreferences.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | Highlighting Preference 30 |
31 | {preference == HighlightingPreference.all 33 | ? 'All Sites except excluded' 34 | : 'Only Included Sites'} 36 | modal.showModal()} on:keydown={() => modal.showModal()}> 37 | Change 38 | 39 |
40 | 41 |
42 | 51 | 52 | 76 |
77 |
78 |
79 | 80 | 179 | -------------------------------------------------------------------------------- /src/lib/content/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HIGH_RISK_COLOR, 3 | LOW_RISK_COLOR, 4 | MED_RISK_COLOR, 5 | RISK_API_URL, 6 | MessageType 7 | } from '$lib/constants'; 8 | import WAValidator from 'trezor-address-validator'; 9 | import { isAddress as isIotaAddress } from '@iota/validators'; 10 | import { 11 | StoreName, 12 | type RiskApiResponse, 13 | type RiskCounter, 14 | type RiskDetailsMapping 15 | } from '$lib/types'; 16 | import { getStoreValueFromLocalStorage } from '$lib/stores'; 17 | import { Message } from '$lib/models/message'; 18 | // window.Buffer = {}; 19 | const coins = [ 20 | // "AUR", 21 | // "BKX", 22 | // "BVC", 23 | // "BIO", 24 | 'BTC', 25 | 'BCH', 26 | // "BTG", 27 | // "BTCP", 28 | // "BTCZ", 29 | // "CLO", 30 | // "DASH", 31 | // "DCR", 32 | // "DGB", 33 | 'DOGE', 34 | 'ETH', 35 | // "ETC", 36 | // "ETZ", 37 | // "FRC", 38 | // "GRLC", 39 | // "HUSH", 40 | // "KMD", 41 | 'LTC', 42 | // "MEC", 43 | 'XMR', 44 | 'NMC', 45 | // "NANO", 46 | // "NEO", 47 | // "GAS", 48 | 'PPC', 49 | // "XPM", 50 | // "PTS", 51 | // "QTUM", 52 | // "XRB", 53 | // "XRP", 54 | // "SNG", 55 | // "VTC", 56 | // "VOT", 57 | // "ZEC", 58 | // "ZCL", 59 | // "ZEN", 60 | 'XTZ' 61 | ]; 62 | 63 | export const validateWallet = (wallet: string): boolean => { 64 | if (!wallet) { 65 | return false; 66 | } 67 | 68 | if (isValidIotaAddress(wallet)) { 69 | return true; 70 | } 71 | 72 | return ( 73 | WAValidator.validate(wallet.trim()) || 74 | coins.some((coin) => WAValidator.validate(wallet.trim(), coin)) 75 | ); 76 | }; 77 | 78 | const isValidIotaAddress = (address: string) => 79 | isIotaAddress(address) || new RegExp('^(iota1|iot1|io1)[a-zA-Z0-9]{25,}').test(address); 80 | 81 | export async function* getRiskDetails( 82 | wallets: string[] 83 | ): AsyncGenerator { 84 | const domainHashed = await getDomainHashed(); 85 | 86 | for (let i = 0; i < wallets.length; i += 100) { 87 | const walletsSlice = wallets.slice(i, i + 100); 88 | const apiRespose = await getRiskDetailsFromApi(domainHashed, walletsSlice); 89 | if (!apiRespose) { 90 | continue; 91 | } 92 | const result: RiskDetailsMapping = {}; 93 | Object.keys(apiRespose.colors).forEach((wallet) => { 94 | const color = apiRespose.colors[wallet]; 95 | const detail = apiRespose.details[wallet]; 96 | 97 | if (color != null && detail != null) { 98 | result[wallet] = { 99 | color, 100 | detail 101 | }; 102 | } else { 103 | result[wallet] = null; 104 | } 105 | }); 106 | yield result; 107 | } 108 | } 109 | 110 | const getDomainHashed = async () => { 111 | const domain = window.location.origin; 112 | const encoder = new TextEncoder(); 113 | 114 | const domainUint8 = encoder.encode(domain); // encode as (utf-8) Uint8Array 115 | const hashBuffer = await crypto.subtle.digest('SHA-256', domainUint8); // hash the message 116 | const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array 117 | const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string 118 | return hashHex; 119 | }; 120 | 121 | const getRiskDetailsFromApi = async (domainHash: string, wallets: string[]) => { 122 | if (!domainHash || !wallets || !wallets.length) { 123 | return null; 124 | } 125 | 126 | try { 127 | const siteUrl = window.location.href; 128 | const userID = await getUserId(); 129 | if (userID === undefined) { 130 | throw Error('userID cannot be undefined'); 131 | } 132 | const response = await fetch(RISK_API_URL, { 133 | method: 'POST', 134 | body: JSON.stringify({ domainHash, wallets, siteUrl, userID }), 135 | headers: { 136 | 'Content-Type': 'application/json' 137 | } 138 | }); 139 | return (await response.json()) as RiskApiResponse; 140 | } catch (e) { 141 | console.error('Error getting colors', e); 142 | } 143 | return null; 144 | }; 145 | 146 | export const populateRiskCount = (highlightColor: string, riskLevelCount: RiskCounter) => { 147 | switch (highlightColor) { 148 | case HIGH_RISK_COLOR: 149 | riskLevelCount.high += 1; 150 | break; 151 | case MED_RISK_COLOR: 152 | riskLevelCount.medium += 1; 153 | break; 154 | case LOW_RISK_COLOR: 155 | riskLevelCount.low += 1; 156 | break; 157 | default: 158 | break; 159 | } 160 | return riskLevelCount; 161 | }; 162 | 163 | export const clearCounter = (riskLevelCount: RiskCounter) => { 164 | riskLevelCount = { high: 0, medium: 0, low: 0 }; 165 | return riskLevelCount; 166 | }; 167 | 168 | const getNewUserId = (): string => { 169 | const randomPool = new Uint8Array(32); 170 | crypto.getRandomValues(randomPool); 171 | let hex = ''; 172 | for (let i = 0; i < randomPool.length; ++i) { 173 | hex += randomPool[i].toString(16); 174 | } 175 | return hex; 176 | }; 177 | 178 | export const getUserId = async (): Promise => { 179 | const stored = (await getStoreValueFromLocalStorage(StoreName.userId, getNewUserId())) as string; 180 | if (stored === undefined || stored.length === 0) { 181 | return getNewUserId(); 182 | } 183 | return stored; 184 | }; 185 | 186 | export const sendMessage = async (type: MessageType) => { 187 | const tabs = await chrome.tabs.query({ currentWindow: true, active: true }); 188 | const activeTab = tabs[0]; 189 | 190 | if (!activeTab.id) throw Error('INVALID TAB'); 191 | const message = new Message(type); 192 | const res = await chrome.tabs.sendMessage(activeTab.id, message.serialize()); 193 | return res?.value; 194 | }; 195 | -------------------------------------------------------------------------------- /src/lib/service/SiteManagerService.ts: -------------------------------------------------------------------------------- 1 | import { Site } from '$lib/models/site'; 2 | import { StatsData } from '$lib/models/statsData'; 3 | import { StoreName, type RiskCounter } from '$lib/types'; 4 | import { getStoreValueFromLocalStorage, setStoreValueToLocalStorage } from '$lib/stores'; 5 | 6 | export abstract class SiteManagerService { 7 | private static siteData: { [url: string]: Site } = {}; 8 | public static total = 0; 9 | public static totalHighPercent = 0.0; 10 | public static totalMedPercent = 0.0; 11 | static excluded_list: string[]; 12 | static highlight_list: string[]; 13 | 14 | public static async addSite( 15 | siteId: string, 16 | siteUrl: string, 17 | isExcluded: boolean, 18 | riskLevelCount: RiskCounter 19 | ) { 20 | siteUrl = this.processURL(siteUrl); 21 | const allSites = await this.load(); 22 | this.populateList(allSites.value); 23 | const tmpSite = new Site(siteId, siteUrl, isExcluded); 24 | tmpSite.populateRisk(riskLevelCount); 25 | this.siteData[siteUrl] = tmpSite; 26 | this.save(); 27 | return Promise.resolve({ value: this.getAllSitesFromSiteData() }); 28 | } 29 | 30 | static processURL(siteUrl: string): string { 31 | const url = new URL(siteUrl); 32 | return url.hostname; 33 | } 34 | 35 | public static async deleteSite(siteURL: string): Promise> { 36 | const allSites = await this.load(); 37 | this.populateList(allSites.value); 38 | delete this.siteData[siteURL]; 39 | this.save(); 40 | return Promise.resolve({ value: this.getAllSitesFromSiteData() }); 41 | } 42 | 43 | public static async updateSite( 44 | siteURL: string, 45 | riskLevelCount: RiskCounter 46 | ): Promise> { 47 | siteURL = this.processURL(siteURL); 48 | const allSites = await this.load(); 49 | this.populateList(allSites.value); 50 | if (siteURL in this.siteData) { 51 | const tmpSite = this.siteData[siteURL]; 52 | if (this.checkForChange(riskLevelCount, tmpSite)) { 53 | tmpSite.populateRisk(riskLevelCount); 54 | this.siteData[siteURL] = tmpSite; 55 | this.save(); 56 | } 57 | } 58 | return Promise.resolve({ value: this.getAllSitesFromSiteData() }); 59 | } 60 | static checkForChange(riskLevelCount: RiskCounter, tmpSite: Site) { 61 | if ( 62 | riskLevelCount.high !== tmpSite.highRiskCount || 63 | riskLevelCount.medium !== tmpSite.moderateRiskCount || 64 | riskLevelCount.low !== tmpSite.lowRiskCount 65 | ) { 66 | return true; 67 | } else { 68 | return false; 69 | } 70 | } 71 | 72 | private static getAllSitesFromSiteData(): any { 73 | const allSites: Site[] = []; 74 | for (const key in SiteManagerService.siteData) { 75 | const value: Site = SiteManagerService.siteData[key]; 76 | allSites.push(value); 77 | } 78 | return allSites; 79 | } 80 | 81 | private static async save(): Promise { 82 | const serializedData: string[] = []; 83 | if (SiteManagerService.siteData != null) { 84 | for (const key in SiteManagerService.siteData) { 85 | const value: Site = SiteManagerService.siteData[key]; 86 | serializedData.push(value.serialize()); 87 | } 88 | return setStoreValueToLocalStorage(StoreName.siteInfo, serializedData); 89 | } else { 90 | console.error('Save called for null data'); 91 | } 92 | return false; 93 | } 94 | 95 | public static async load() { 96 | const serializedData: string[] = (await getStoreValueFromLocalStorage( 97 | StoreName.siteInfo, 98 | [] 99 | )) as string[]; 100 | const allSites: Site[] = []; 101 | for (let i = 0; i < serializedData.length; i++) { 102 | const siteData: Site = Site.fromSerialized(serializedData[i] ?? '{}'); 103 | allSites.push(siteData); 104 | } 105 | return Promise.resolve({ value: allSites }); 106 | } 107 | 108 | private static populateList(allSites: Site[]) { 109 | const tmpSiteData: { [siteId: string]: Site } = {}; 110 | const tmpHighlight_list: string[] = []; 111 | const tmpExcluded_list: string[] = []; 112 | allSites.forEach((site) => { 113 | tmpSiteData[site.siteURL] = site; 114 | if (site.isExcluded) { 115 | tmpHighlight_list.push(site.siteURL); 116 | } else { 117 | tmpExcluded_list.push(site.siteURL); 118 | } 119 | }); 120 | this.siteData = tmpSiteData; 121 | this.excluded_list = tmpExcluded_list; 122 | this.highlight_list = tmpHighlight_list; 123 | } 124 | 125 | public static getStats(rawStats: string) { 126 | const stats: StatsData = StatsData.fromSerialized(rawStats); 127 | const total = stats.totalHighlighted; 128 | const high = stats.totalHighRisk; 129 | const med = stats.totalModerateRisk; 130 | 131 | return { 132 | totalCount: total, 133 | highPercent: total === 0 ? 0 : ((high / total) * 100).toFixed(0), 134 | medPercent: total === 0 ? 0 : ((med / total) * 100).toFixed(0) 135 | }; 136 | } 137 | 138 | public static getSiteByUrl(url: string, allSites: Site[]): Site | undefined { 139 | this.populateList(allSites); 140 | if (url in this.siteData) { 141 | return this.siteData[url]; 142 | } 143 | return undefined; 144 | } 145 | 146 | public static async updateStats(riskLevelCount: RiskCounter) { 147 | if (!(riskLevelCount.high === 0 && riskLevelCount.medium === 0 && riskLevelCount.low === 0)) { 148 | const serializedData: string = (await getStoreValueFromLocalStorage( 149 | StoreName.statsData, 150 | '{}' 151 | )) as string; 152 | const stats: StatsData = StatsData.fromSerialized(serializedData); 153 | stats.updateStatsCount(riskLevelCount); 154 | await setStoreValueToLocalStorage(StoreName.statsData, stats.serialize()); 155 | } 156 | } 157 | 158 | static checkIfDefaultExcluded(currentSite: string): boolean { 159 | return [ 160 | 'https://api.wallethighlighter.com', 161 | ].includes(currentSite); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/lib/content/content.styles.css: -------------------------------------------------------------------------------- 1 | .whe-view-details { 2 | position: absolute; 3 | display: block; 4 | width: max-content; 5 | height: max-content; 6 | padding-right: 20px; 7 | transform: translateY(-6px); 8 | z-index: 9999; 9 | cursor: pointer; 10 | } 11 | 12 | .whe-highlighted { 13 | border-radius: 4px; 14 | } 15 | 16 | .whe-details-button { 17 | background-color: white; 18 | color: gray; 19 | text-decoration: none; 20 | border-radius: 15px; 21 | height: auto; 22 | max-height: 22px; 23 | 24 | display: inline-flex; 25 | align-items: center; 26 | 27 | overflow: hidden; 28 | width: auto; 29 | max-width: 22px; 30 | -webkit-transition: all 0.5s; 31 | transition: all 0.5s ease; 32 | 33 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 34 | } 35 | 36 | .whe-details-icon { 37 | opacity: 1; 38 | padding: 0px; 39 | display: flex; 40 | align-items: center; 41 | transition: opacity 0.2s ease; 42 | } 43 | 44 | .whe-details-button:hover { 45 | max-width: 400px; 46 | max-height: 400px; 47 | border-radius: 10px; 48 | overflow-x: scroll; 49 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 50 | } 51 | .whe-report-scam-message { 52 | font-size: 0.8rem; 53 | } 54 | 55 | .whe-report-scam-button { 56 | border: none; 57 | background: transparent; 58 | color: #4277ff; 59 | border-bottom: 0.1rem solid; 60 | padding: 0; 61 | line-height: 1.1rem; 62 | font-size: 0.8rem; 63 | } 64 | 65 | .whe-details-button:hover .whe-details-icon { 66 | opacity: 0; 67 | } 68 | 69 | .whe-details-button:hover .whe-details-element { 70 | opacity: 1; 71 | } 72 | 73 | .whe-details-element { 74 | white-space: nowrap; 75 | transition: all 0.25s ease 0.25s; 76 | padding: 15px 15px 15px 0px; 77 | opacity: 0; 78 | } 79 | 80 | .whe-details-element td { 81 | text-align: center; 82 | vertical-align: middle; 83 | padding: 8px; 84 | font-size: 0.9em; 85 | } 86 | 87 | .whe-risk-card span:nth-of-type(2) { 88 | text-align: center; 89 | vertical-align: middle; 90 | font-size: 1.1em; 91 | font-weight: bold; 92 | } 93 | 94 | .whe-risk-card span:nth-of-type(1) { 95 | font-size: 0.9em; 96 | } 97 | 98 | .whe-risk-card { 99 | padding: 8px; 100 | display: flex; 101 | flex-direction: column; 102 | align-items: center; 103 | border-radius: 10px; 104 | } 105 | 106 | .whe-badge { 107 | color: #77838f; 108 | background-color: rgba(119, 131, 143, 0.1); 109 | padding: 5px; 110 | text-align: center; 111 | border-radius: 10px; 112 | font-size: 0.9em; 113 | } 114 | 115 | .whe-badge:hover { 116 | color: #fff; 117 | background-color: #77838f; 118 | } 119 | 120 | .whe-risk-tags { 121 | padding-bottom: 5px; 122 | } 123 | 124 | .whe-risk-tags .whe-badge { 125 | margin-right: 5px; 126 | } 127 | 128 | #whe-risk-tags-container-id { 129 | overflow: scroll; 130 | padding-right: 8px; 131 | padding-top: 8px; 132 | padding-bottom: 8px; 133 | } 134 | 135 | /*-------- toolTip ------------*/ 136 | #whe-tooltip { 137 | opacity: 0; 138 | text-align: center; 139 | color: #fff; 140 | background: rgba(0, 0, 0, 0.6); 141 | position: absolute; 142 | z-index: 99999; 143 | padding: 15px; 144 | pointer-events: none; 145 | border-radius: 5px; 146 | -moz-border-radius: 5px; 147 | -webkit-border-radius: 5px; 148 | -o-border-radius: 5px; 149 | -ms-border-radius: 5px; 150 | transition: all 0.25s ease-in-out; 151 | -moz-transition: all 0.25s ease-in-out; 152 | -webkit-transition: all 0.25s ease-in-out; 153 | -o-transition: all 0.25s ease-in-out; 154 | -ms-transition: all 0.25s ease-in-out; 155 | } 156 | #whe-tooltip.whe-top { 157 | margin-top: 20px; 158 | } 159 | #whe-tooltip.whe-show { 160 | opacity: 1; 161 | margin-top: 10px; 162 | pointer-events: all; 163 | } 164 | #whe-tooltip.whe-show.whe-top { 165 | margin-top: 10px; 166 | } 167 | #whe-tooltip:after { 168 | width: 0; 169 | height: 0; 170 | border-left: 10px solid transparent; 171 | border-right: 10px solid transparent; 172 | border-top: 10px solid rgba(0, 0, 0, 0.6); 173 | content: ''; 174 | position: absolute; 175 | left: 50%; 176 | bottom: -10px; 177 | margin-left: -10px; 178 | } 179 | #whe-tooltip.whe-top:after { 180 | border-top-color: transparent; 181 | border-bottom: 10px solid rgba(0, 0, 0, 0.6); 182 | top: -20px; 183 | bottom: auto; 184 | } 185 | #whe-tooltip.whe-left:after { 186 | left: 10px; 187 | margin: 0; 188 | } 189 | #whe-tooltip.whe-right:after { 190 | right: 10px; 191 | left: auto; 192 | margin: 0; 193 | } 194 | 195 | /*----------- guage ------------*/ 196 | .whe-guage-mask { 197 | position: relative; 198 | overflow: hidden; 199 | display: block; 200 | width: 50px; 201 | height: 25px; 202 | margin: 5px; 203 | } 204 | 205 | .whe-guage-semi-circle { 206 | position: relative; 207 | display: block; 208 | width: 50px; 209 | height: 25px; 210 | border-radius: 50% 50% 50% 50%/100% 100% 0% 0%; 211 | } 212 | .whe-guage-semi-circle::before { 213 | content: ''; 214 | position: absolute; 215 | bottom: 0; 216 | left: 50%; 217 | z-index: 2; 218 | display: block; 219 | width: 35px; 220 | height: 17.5px; 221 | margin-left: -17.5px; 222 | background: #fff; 223 | border-radius: 50% 50% 50% 50%/100% 100% 0% 0%; 224 | } 225 | 226 | .whe-guage-semi-circle--mask { 227 | position: absolute; 228 | top: 0; 229 | left: 0; 230 | width: 50px; 231 | height: 50px; 232 | background: transparent; 233 | transform: rotate(120deg) translate3d(0, 0, 0); 234 | transform-origin: center center; 235 | backface-visibility: hidden; 236 | transition: all 0.3s ease-in-out; 237 | } 238 | .whe-guage-semi-circle--mask::before { 239 | content: ''; 240 | position: absolute; 241 | top: 0; 242 | left: 0%; 243 | z-index: 2; 244 | display: block; 245 | width: 50.5px; 246 | height: 25.5px; 247 | margin: -0.5px 0 0 -0.5px; 248 | background: #f2f2f2; 249 | border-radius: 50% 50% 50% 50%/100% 100% 0% 0%; 250 | } 251 | -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 34 | 44 | 48 | 54 | 61 | 62 | 64 | 72 | 76 | 82 | 85 | 88 | 92 | 96 | 101 | 107 | 108 | 116 | 119 | 123 | 124 | 132 | 135 | 139 | 140 | 147 | 150 | 154 | 158 | 159 | 166 | 170 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/lib/content/risk/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getRiskColor, 3 | getRiskPercentage, 4 | getRiskStatus, 5 | roundToTwo 6 | } from '$lib/content/risk/utils'; 7 | import type { RiskDetailItem, RiskTag } from '$lib/types'; 8 | 9 | export const getRiskDetailUI = (riskDetail: RiskDetailItem, reportStatus = false): string => { 10 | return ` 11 | 12 |
13 | ${getIconSvg()} 14 |
15 |
16 | ${getRiskTagContent(riskDetail.tags)} 17 |
18 | Anything fishy about this address? 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 |
24 | 25 | ${getIndividualRiskScoreGuage(riskDetail.combinedRisk)} 26 | Combined Risk 27 | 28 | 30 | 31 | ${getIndividualRiskScoreGuage(riskDetail.fraudRisk)} 32 | Fraud Risk 33 | 34 |
38 | 39 | ${getIndividualRiskScoreGuage(riskDetail.lendingRisk)} 40 | Lending Risk 41 | 42 | 44 | 45 | ${getIndividualRiskScoreGuage(riskDetail.reputationRisk)} 46 | Reputation Risk 47 | 48 |
51 |
52 |
53 |
`; 54 | }; 55 | 56 | const getRiskTagContent = (riskTags: RiskTag[]): string => { 57 | return ` 58 |
59 |
60 | ${riskTags 61 | .map((rt) => { 62 | return ` 63 | 67 | ${rt.tag} 68 | `; 69 | }) 70 | .join(' ')} 71 |
72 |
`; 73 | }; 74 | 75 | const getIndividualRiskScoreGuage = (scoreStatus: string): string => { 76 | const score = getRiskPercentage(scoreStatus) ?? 404; 77 | const riskColour = getRiskColor(roundToTwo(score)); 78 | return ` 79 |
80 |
82 |
84 |
85 | 87 | ${getRiskStatus(score)} 88 | `; 89 | }; 90 | 91 | const getIconSvg = (): string => { 92 | return ` 93 | 106 | 125 | 135 | 139 | 145 | 152 | 153 | 155 | 163 | 167 | 173 | 176 | 179 | 183 | 187 | 192 | 198 | 199 | 207 | 210 | 214 | 215 | 223 | 226 | 230 | 231 | 238 | 241 | 245 | 249 | 250 | 257 | 261 | 265 | 266 | 267 | 268 | `; 269 | }; 270 | -------------------------------------------------------------------------------- /src/lib/content/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearCounter, 3 | getRiskDetails, 4 | validateWallet, 5 | populateRiskCount, 6 | getUserId 7 | } from '$lib/content/utils'; 8 | import { SiteManagerService } from '$lib/service/SiteManagerService'; 9 | import { API_HOST, HIGH_RISK_COLOR } from '$lib/constants'; 10 | import { initTooltip, Tooltip } from '$lib/content/risk/tooltip'; 11 | import { getRiskDetailUI } from '$lib/content/risk/main'; 12 | import { 13 | StoreName, 14 | type RiskCounter, 15 | type RiskDetailsMapping, 16 | type Wallet, 17 | type Wallets, 18 | PluginStatus, 19 | HighlightingPreference 20 | } from '$lib/types'; 21 | import { getStoreValueFromLocalStorage } from '$lib/stores'; 22 | import browser from 'webextension-polyfill'; 23 | import { getScamReportHash } from '$lib/utils'; 24 | 25 | let scanning = false; 26 | const wallets: Wallets = {}; 27 | const riskDetails: RiskDetailsMapping = {}; 28 | let referredElement: HTMLElement | null = null; 29 | let openedAddr: string | null = null; 30 | 31 | /** 32 | * Single scan of the page 33 | */ 34 | async function scan(_highlightOnlyHigh: boolean) { 35 | if (scanning) { 36 | return; 37 | } 38 | 39 | scanning = true; 40 | scanInputs(); 41 | scanText(); 42 | 43 | try { 44 | await highlightWallets(_highlightOnlyHigh); 45 | await SiteManagerService.updateSite(window.location.href, riskLevelCount); 46 | } catch (error) { 47 | console.error(error); 48 | } 49 | 50 | scanning = false; 51 | } 52 | 53 | let lastInputs: HTMLInputElement[] = []; 54 | let riskLevelCount: RiskCounter = { high: 0, medium: 0, low: 0 }; 55 | 56 | const scanInputs = () => { 57 | const inputs = [...document.getElementsByTagName('input')]; 58 | 59 | const inputsDifference = inputs.filter((input) => !lastInputs.includes(input)); 60 | 61 | inputsDifference.forEach((input) => { 62 | if (!input || !input.value || input.value.length < MINIMUM_WALLET_LENGTH) { 63 | return; 64 | } 65 | 66 | const inputValue: string = input.value.trim(); 67 | 68 | if (inputValue && validateWallet(inputValue)) { 69 | const nodesByWallet = wallets[inputValue] || []; 70 | wallets[inputValue] = [...nodesByWallet, input]; 71 | } 72 | }); 73 | 74 | lastInputs = inputs; 75 | }; 76 | 77 | function deepText(node: HTMLElement | ChildNode | null) { 78 | let A: { node: ChildNode; parent: HTMLElement | null }[] = []; 79 | if (node) { 80 | node = node.firstChild; 81 | while (node != null) { 82 | if (node.nodeType == 3) A[A.length] = { node, parent: node.parentNode as HTMLElement | null }; 83 | else if (node.nodeType == 1 && (node as HTMLElement).tagName.toLowerCase() === 'iframe') { 84 | const iframeElement = node as HTMLIFrameElement; 85 | if (!iframeElement) { 86 | continue; 87 | } 88 | iframeElement.onload = () => { 89 | runScrapper(); 90 | }; 91 | const iframeDoc = iframeElement.contentDocument; 92 | if (iframeDoc) { 93 | const iframeNodes = deepText(iframeDoc.body); 94 | A = A.concat(iframeNodes); 95 | } 96 | } else { 97 | A = A.concat(deepText(node)); 98 | } 99 | node = node.nextSibling; 100 | } 101 | } 102 | 103 | return A; 104 | } 105 | const HIGHLIGHTED_CLASSNAME = 'whe-highlighted'; 106 | const VIEW_DETAILS_CLASSNAME = 'whe-view-details'; 107 | const ADDR_ATTRIBUTE = 'data-whe-address'; 108 | const MINIMUM_WALLET_LENGTH = 26; 109 | 110 | const scanText = () => { 111 | const textNodes = deepText(document.body); 112 | 113 | const ElementAddressMapping: { 114 | element: HTMLElement; 115 | addressArr: string[]; 116 | }[] = []; 117 | textNodes.forEach(({ node, parent }) => { 118 | if (!node || !node.textContent || !parent) { 119 | return; 120 | } 121 | 122 | const pieces = node.textContent 123 | .split(/[^\d\w]/g) 124 | .map((x) => x.trim()) 125 | .filter((x) => x.length >= MINIMUM_WALLET_LENGTH) 126 | // filter duplicates and empty strings 127 | .filter((value, index, self) => self.indexOf(value) === index && value); 128 | 129 | pieces.forEach((x) => { 130 | const text = x.trim(); 131 | if (validateWallet(text) && parent) { 132 | const index = ElementAddressMapping.findIndex((i) => i.element === parent); 133 | if (index > -1) { 134 | if (!ElementAddressMapping[index].addressArr.includes(text)) { 135 | ElementAddressMapping[index].addressArr.push(text); 136 | } 137 | } else { 138 | ElementAddressMapping.push({ element: parent, addressArr: [text] }); 139 | } 140 | } 141 | }); 142 | }); 143 | 144 | ElementAddressMapping.forEach((addressItem) => { 145 | const parent = addressItem.element; 146 | if (!parent || parent.closest(`.${HIGHLIGHTED_CLASSNAME}`)) return; 147 | let parentInnerText = parent.innerText; 148 | const parentTextContent = parent.textContent; 149 | addressItem.addressArr.forEach((addressTxt) => { 150 | if (addressTxt && addressTxt.trim().length === parentTextContent?.trim().length) { 151 | if (!parent.closest(`.${HIGHLIGHTED_CLASSNAME}`)) { 152 | parent.classList.add(HIGHLIGHTED_CLASSNAME); 153 | parent.setAttribute('whe-address', addressTxt); 154 | } 155 | } else { 156 | parentInnerText = parentInnerText.replaceAll( 157 | addressTxt, 158 | `${addressTxt}` 159 | ); 160 | } 161 | }); 162 | 163 | let walletNodes: HTMLElement[] = [parent]; 164 | 165 | if (!parent.closest(`.${HIGHLIGHTED_CLASSNAME}`)) { 166 | parent.innerHTML = parentInnerText; 167 | 168 | const nodes = parent.querySelectorAll(`span.${HIGHLIGHTED_CLASSNAME}`); 169 | walletNodes = Array.from(nodes).map((node) => node as HTMLElement); 170 | } 171 | 172 | if (!walletNodes || walletNodes.length === 0) return; 173 | 174 | walletNodes.forEach((walletNode) => { 175 | const addressTxt = walletNode.getAttribute 176 | ? walletNode.getAttribute('whe-address') 177 | : null; 178 | if (!addressTxt) return; 179 | const nodesByWallet = wallets[addressTxt] || []; 180 | if (!nodesByWallet.includes(walletNode)) { 181 | wallets[addressTxt] = [...nodesByWallet, walletNode]; 182 | } 183 | }); 184 | }); 185 | }; 186 | 187 | const clearDetailsBoxes = () => { 188 | document.querySelectorAll(`.${VIEW_DETAILS_CLASSNAME}`).forEach((el) => el.remove()); 189 | }; 190 | 191 | const highlightWallet = (node: Wallet, color: string, address: string) => { 192 | if (!color || !node) { 193 | return; 194 | } 195 | 196 | node.style.background = color; 197 | node.style.color = '#000'; 198 | if (!node.classList.contains(HIGHLIGHTED_CLASSNAME)) { 199 | node.classList.add(HIGHLIGHTED_CLASSNAME); 200 | } 201 | node.setAttribute(ADDR_ATTRIBUTE, address); 202 | }; 203 | 204 | const highlightWallets = async (_highlightOnlyHigh: boolean) => { 205 | if (!wallets) { 206 | return; 207 | } 208 | 209 | let keys = Object.keys(wallets); 210 | if (!keys.length) { 211 | return; 212 | } 213 | 214 | console.debug(`highlighting wallet `); 215 | 216 | // clear counters 217 | riskLevelCount = clearCounter(riskLevelCount); 218 | 219 | // highlight already fetched wallets 220 | Object.keys(riskDetails).forEach((addr) => { 221 | highlightAddr(addr, _highlightOnlyHigh); 222 | }); 223 | 224 | // fetch and highlight new wallets from api if any 225 | keys = keys.filter((key) => !Object.keys(riskDetails).includes(key)); 226 | if (keys.length > 0) { 227 | const newRiskDataGenerator = getRiskDetails(keys); 228 | for await (const newRiskData of newRiskDataGenerator) { 229 | Object.keys(newRiskData).forEach((newAddr) => { 230 | riskDetails[newAddr] = newRiskData[newAddr]; 231 | highlightAddr(newAddr, _highlightOnlyHigh); 232 | }); 233 | } 234 | } 235 | }; 236 | 237 | const highlightAddr = (walletAddr: string, _highlightOnlyHigh: boolean) => { 238 | const highlightColor = riskDetails[walletAddr]?.color; 239 | if (highlightColor == null) { 240 | return; 241 | } 242 | 243 | riskLevelCount = populateRiskCount(highlightColor, riskLevelCount); 244 | 245 | wallets[walletAddr].forEach((node) => { 246 | if ((_highlightOnlyHigh && highlightColor === HIGH_RISK_COLOR) || !_highlightOnlyHigh) { 247 | highlightWallet(node, highlightColor, walletAddr); 248 | } 249 | }); 250 | }; 251 | 252 | const unhighlight = () => { 253 | const highlightedElements = [...document.getElementsByClassName(HIGHLIGHTED_CLASSNAME)]; 254 | 255 | highlightedElements.forEach((element) => { 256 | element.removeAttribute('style'); 257 | element.removeAttribute(ADDR_ATTRIBUTE); 258 | }); 259 | }; 260 | 261 | const shouldHighlightSite = async (currentSite: string): Promise => { 262 | const isExtRun = 263 | (await getStoreValueFromLocalStorage(StoreName.pluginStatus, PluginStatus.Active)) === 264 | PluginStatus.Active; 265 | if (!isExtRun) return false; 266 | 267 | // const { value: allSites } = await SiteManagerService.load(); 268 | const highlightingPreference = (await getStoreValueFromLocalStorage( 269 | StoreName.highlightingPreference, 270 | HighlightingPreference.all 271 | )) as HighlightingPreference; 272 | 273 | // const site = SiteManagerService.getSiteByUrl(currentSite, allSites); 274 | // if (site !== undefined) { 275 | // return isSiteRun; 276 | // } 277 | 278 | const excludedSites: string[] = (await getStoreValueFromLocalStorage( 279 | StoreName.excludedSites, 280 | [] 281 | )) as string[]; 282 | if (excludedSites.includes(currentSite)) { 283 | return false; 284 | } 285 | if (highlightingPreference == HighlightingPreference.included) { 286 | const includedSites: string[] = (await getStoreValueFromLocalStorage( 287 | StoreName.includedSites, 288 | [] 289 | )) as string[]; 290 | return includedSites.includes(currentSite); 291 | } 292 | return true; 293 | }; 294 | 295 | /** 296 | * Runs the scan on the page if available 297 | */ 298 | const runScrapper = async () => { 299 | const onlyHighlightHighRisk = false; // TODO 300 | const currentSite = SiteManagerService.processURL(window.location.href); 301 | if (SiteManagerService.checkIfDefaultExcluded(currentSite)) { 302 | return; 303 | } 304 | 305 | const highlightSite: boolean = await shouldHighlightSite(currentSite); 306 | 307 | unhighlight(); 308 | if (!highlightSite) { 309 | return; 310 | } 311 | await scan(onlyHighlightHighRisk); 312 | }; 313 | 314 | /** 315 | * Case: when user visit site first time - highlight should work (if extension turned on) 316 | */ 317 | window.onload = myMain; 318 | 319 | function myMain() { 320 | initTooltip(); 321 | runScrapper().then(() => { 322 | SiteManagerService.updateStats(riskLevelCount); 323 | }); 324 | } 325 | /** 326 | * Observe the page for url changes 327 | */ 328 | const config = { subtree: true, childList: true }; 329 | 330 | let prevUrl = window.location.href; 331 | const observer = new MutationObserver(function (mutations) { 332 | if (window.location.href !== prevUrl) { 333 | prevUrl = window.location.href; 334 | runScrapper(); 335 | } else if ( 336 | // check if iframe changed 337 | mutations.some( 338 | (i) => 339 | i.type === 'childList' && 340 | [...i.target.childNodes].some((j) => j.nodeName.toLowerCase() === 'iframe') 341 | ) 342 | ) { 343 | runScrapper(); 344 | } 345 | }); 346 | 347 | observer.observe(document, config); 348 | 349 | const saveReportedAddress = async (address: string, ticker: string, websiteUrl: string) => { 350 | const hashHex = await getScamReportHash(address, ticker, websiteUrl); 351 | console.log({ hashHex }); 352 | 353 | await chrome.storage.local.set({ [hashHex]: true }); 354 | }; 355 | 356 | const isAlreadyReported = async ( 357 | address: string, 358 | ticker: string, 359 | websiteUrl: string 360 | ): Promise => { 361 | const hashHex = await getScamReportHash(address, ticker, websiteUrl); 362 | const entry = await chrome.storage.local.get(hashHex); 363 | return entry[hashHex] !== undefined; 364 | }; 365 | 366 | const report = async (address: string, ticker: string, websiteUrl: string) => { 367 | const userId = await getUserId(); 368 | return fetch(`${API_HOST}/v1/report-scam`, { 369 | method: 'POST', 370 | body: JSON.stringify({ address, ticker, clientId: userId, websiteUrl }), 371 | headers: { 372 | 'Content-Type': 'application/json' 373 | } 374 | }); 375 | }; 376 | 377 | const loadReportScamButton = async (address: string) => { 378 | const currentUrl = window.location.href; 379 | if (await isAlreadyReported(address, 'ETH', currentUrl)) { 380 | // TODO: handle the ticker 381 | return; 382 | } 383 | const reportScamButton = document.getElementById('report-scam'); 384 | if (reportScamButton != null) { 385 | reportScamButton.onclick = async () => { 386 | reportScamButton.disabled = true; 387 | reportScamButton.textContent = 'Reporting...'; 388 | try { 389 | const response = await report(address, 'ETH', currentUrl); // TODO: handle the ticker 390 | if (response.status === 200) { 391 | await saveReportedAddress(address, 'ETH', currentUrl); 392 | reportScamButton.textContent = 'Reported as Scam'; 393 | } else { 394 | reportScamButton.textContent = 'Failed to report'; 395 | } 396 | } catch (err) { 397 | console.error(err); 398 | reportScamButton.textContent = 'Failed to report'; 399 | } 400 | }; 401 | } 402 | }; 403 | 404 | /** 405 | * Event listeners 406 | */ 407 | browser.runtime.onMessage.addListener(async (__: string, _) => { 408 | // const msg: Message = Message.fromSerialized(msgData); 409 | await runScrapper(); 410 | }); 411 | 412 | /** 413 | * Add Risk UI 414 | */ 415 | initTooltip(); 416 | document.addEventListener( 417 | 'mousemove', 418 | async (event) => { 419 | const target = event?.target as HTMLElement; 420 | if (!target || !target?.getAttribute) { 421 | clearDetailsBoxes(); 422 | return; 423 | } 424 | // mouse on this element 425 | if (target.closest(`.${VIEW_DETAILS_CLASSNAME}`)) return; 426 | if (target.closest('#whe-tooltip')) return; 427 | 428 | const address = target.getAttribute(ADDR_ATTRIBUTE); 429 | 430 | // mouse on different element 431 | if (openedAddr && (!address || referredElement !== target)) { 432 | openedAddr = null; 433 | referredElement = null; 434 | clearDetailsBoxes(); 435 | return; 436 | } 437 | 438 | if (!address) return; 439 | 440 | // mouse on same element 441 | if (openedAddr === address && referredElement === target) return; 442 | 443 | // mouse moved from one addr to another element with addr 444 | if (openedAddr && address !== openedAddr) clearDetailsBoxes(); 445 | 446 | // if address has no info then omit 447 | const riskData = riskDetails[address]?.detail; 448 | if (riskData == null) return; 449 | 450 | openedAddr = address; 451 | referredElement = target; 452 | const domRect = referredElement.getBoundingClientRect(); 453 | const top = domRect.top + window.scrollY; 454 | const left = domRect.left - 35 >= 0 ? domRect.left - 35 : 0; 455 | const scamReportStatus = await isAlreadyReported(address, 'ETH', window.location.href); // handle ticker 456 | document.body.insertAdjacentHTML( 457 | 'beforeend', 458 | ` 465 | ${getRiskDetailUI(riskData, scamReportStatus)} 466 | ` 467 | ); 468 | document 469 | .getElementById('whe-risk-tags-container-id') 470 | ?.style.setProperty( 471 | 'max-width', 472 | `${document.getElementById('whe-risk-details-table-id')?.offsetWidth}px`, 473 | 'important' 474 | ); 475 | await loadReportScamButton(address); 476 | Tooltip.bindEvents(); 477 | }, 478 | { passive: true } 479 | ); 480 | --------------------------------------------------------------------------------