@@ -5,20 +5,23 @@ import { zeroAddress } from 'viem'
55
66import { RecurringTimeout } from '../../classes/recurringTimeout/recurringTimeout'
77import {
8+ PHISHING_ACTIVE_UPDATE_INTERVAL ,
89 PHISHING_FAILED_TO_GET_UPDATE_INTERVAL ,
9- PHISHING_UPDATE_INTERVAL
10+ PHISHING_INACTIVE_UPDATE_INTERVAL
1011} from '../../consts/intervals'
1112import { IAddressBookController } from '../../interfaces/addressBook'
1213import { IEventEmitterRegistryController } from '../../interfaces/eventEmitter'
1314import { Fetch } from '../../interfaces/fetch'
1415import { BlacklistedStatus , IPhishingController } from '../../interfaces/phishing'
1516import { IStorageController } from '../../interfaces/storage'
17+ import { IUiController } from '../../interfaces/ui'
1618import { getDappIdFromUrl } from '../../libs/dapps/helpers'
1719/* eslint-disable no-restricted-syntax */
1820import { fetchWithTimeout } from '../../utils/fetch'
1921import EventEmitter from '../eventEmitter/eventEmitter'
2022
2123const SCAMCHECKER_BASE_URL = 'https://cena.ambire.com/api/v3/scamchecker'
24+ const PHISHING_ACTIVE_VIEW_TYPES = new Set ( [ 'request-window' , 'popup' , 'tab' ] )
2225
2326export class PhishingController extends EventEmitter implements IPhishingController {
2427 #fetch: Fetch
@@ -27,10 +30,13 @@ export class PhishingController extends EventEmitter implements IPhishingControl
2730
2831 #addressBook: IAddressBookController
2932
33+ #ui: IUiController
34+
3035 #domains = new Set < string > ( )
3136
3237 #addresses = new Set < string > ( )
3338
39+ // Local versioning, used for requesting incremental phishing list updates.
3440 #version: number = 0
3541
3642 #updatedAt: number | null = null
@@ -43,6 +49,8 @@ export class PhishingController extends EventEmitter implements IPhishingControl
4349
4450 #shouldSyncDapps: boolean = false
4551
52+ #continuouslyUpdatePhishingPromise?: Promise < void >
53+
4654 get updatePhishingInterval ( ) {
4755 return this . #updatePhishingInterval
4856 }
@@ -62,25 +70,51 @@ export class PhishingController extends EventEmitter implements IPhishingControl
6270 eventEmitterRegistry,
6371 fetch,
6472 storage,
65- addressBook
73+ addressBook,
74+ ui
6675 } : {
6776 eventEmitterRegistry ?: IEventEmitterRegistryController
6877 fetch : Fetch
6978 storage : IStorageController
7079 addressBook : IAddressBookController
80+ ui : IUiController
7181 } ) {
7282 super ( eventEmitterRegistry )
7383
7484 this . #fetch = fetch
7585 this . #storage = storage
7686 this . #addressBook = addressBook
87+ this . #ui = ui
7788
7889 this . #updatePhishingInterval = new RecurringTimeout (
7990 async ( ) => this . continuouslyUpdatePhishing ( ) ,
80- PHISHING_UPDATE_INTERVAL ,
91+ PHISHING_INACTIVE_UPDATE_INTERVAL ,
8192 this . emitError . bind ( this )
8293 )
8394
95+ this . #ui. uiEvent . on ( 'addView' , ( view ) => {
96+ const isActiveViewType = PHISHING_ACTIVE_VIEW_TYPES . has ( view . type )
97+ const isAlreadyUsingActiveUpdateInterval =
98+ this . #updatePhishingInterval. currentTimeout === PHISHING_ACTIVE_UPDATE_INTERVAL
99+
100+ const shouldSwitchToActiveUpdateInterval =
101+ isActiveViewType && ! isAlreadyUsingActiveUpdateInterval
102+ if ( shouldSwitchToActiveUpdateInterval )
103+ this . #updatePhishingInterval. restart ( {
104+ timeout : PHISHING_ACTIVE_UPDATE_INTERVAL ,
105+ runImmediately : true
106+ } )
107+ } )
108+ this . #ui. uiEvent . on ( 'removeView' , ( ) => {
109+ const hasAtLeastOneActiveViewOpen = this . #ui. views . some ( ( view ) =>
110+ PHISHING_ACTIVE_VIEW_TYPES . has ( view . type )
111+ )
112+
113+ const shouldSwitchToInactiveUpdateInterval = ! hasAtLeastOneActiveViewOpen
114+ if ( shouldSwitchToInactiveUpdateInterval )
115+ this . #updatePhishingInterval. restart ( { timeout : PHISHING_INACTIVE_UPDATE_INTERVAL } )
116+ } )
117+
84118 // eslint-disable-next-line @typescript-eslint/no-floating-promises
85119 this . initialLoadPromise = this . #load( ) . finally ( ( ) => {
86120 this . initialLoadPromise = undefined
@@ -105,17 +139,56 @@ export class PhishingController extends EventEmitter implements IPhishingControl
105139 this . emitUpdate ( )
106140 }
107141
142+ /**
143+ * Wrapper around #continuouslyUpdatePhishing that:
144+ * 1) deduplicates concurrent triggers via a shared promise
145+ * 2) switches to the failed-retry interval when the fetch/update flow throws
146+ */
108147 async continuouslyUpdatePhishing ( ) {
109- await this . #continuouslyUpdatePhishing( ) . catch ( ( ) => {
110- this . updatePhishingInterval . updateTimeout ( { timeout : PHISHING_FAILED_TO_GET_UPDATE_INTERVAL } )
111- } )
148+ if ( this . #continuouslyUpdatePhishingPromise) {
149+ await this . #continuouslyUpdatePhishingPromise
150+
151+ return
152+ }
153+
154+ this . #continuouslyUpdatePhishingPromise = this . #continuouslyUpdatePhishing( )
155+ . catch ( ( err ) => {
156+ this . updatePhishingInterval . updateTimeout ( {
157+ timeout : PHISHING_FAILED_TO_GET_UPDATE_INTERVAL
158+ } )
159+ throw err
160+ } )
161+ . finally ( ( ) => {
162+ this . #continuouslyUpdatePhishingPromise = undefined
163+ } )
164+
165+ await this . #continuouslyUpdatePhishingPromise
112166 }
113167
114168 async #continuouslyUpdatePhishing( ) {
115169 // This prevents redundant requests to the relayer
116170 // when the extension reloads multiple times within a short period.
117- if ( this . #updatedAt && Date . now ( ) - this . #updatedAt < PHISHING_UPDATE_INTERVAL ) return
171+ const timeSinceLastUpdate = this . #updatedAt ? Date . now ( ) - this . #updatedAt : null
172+ if (
173+ this . #updatedAt &&
174+ timeSinceLastUpdate !== null &&
175+ timeSinceLastUpdate < this . updatePhishingInterval . currentTimeout
176+ ) {
177+ // NOTE: used for debugging only
178+ // console.log(
179+ // `[PhishingController] Skip update (sinceLastUpdate=${Math.floor(timeSinceLastUpdate / 1000)}s, timeout=${Math.floor(this.updatePhishingInterval.currentTimeout / 1000)}s)`
180+ // )
118181
182+ return
183+ }
184+
185+ // NOTE: used for debugging only
186+ // console.log(
187+ // `[PhishingController] Fetch update (version=${this.#version}, timeout=${Math.floor(this.updatePhishingInterval.currentTimeout / 1000)}s)`
188+ // )
189+
190+ // version=0 means no local snapshot yet -> fetch full data.
191+ // version>0 means we have a checkpoint -> fetch only the delta since that version.
119192 const res = await fetchWithTimeout (
120193 this . #fetch,
121194 this . #version
@@ -132,6 +205,7 @@ export class PhishingController extends EventEmitter implements IPhishingControl
132205 const phishing = await res . json ( )
133206
134207 if ( this . #version) {
208+ // Incremental update: apply add/remove operations on top of local sets.
135209 this . #version = phishing . toVersion || 0
136210 ; ( phishing . domains || [ ] ) . forEach (
137211 ( { op, domain } : { op : 'add' | 'remove' ; domain : string } ) => {
@@ -146,6 +220,7 @@ export class PhishingController extends EventEmitter implements IPhishingControl
146220 }
147221 )
148222 } else {
223+ // Initial/full update: replace local sets with the server snapshot.
149224 this . #version = phishing . version || 0
150225 this . #domains = new Set ( phishing . domains || [ ] )
151226 this . #addresses = new Set ( phishing . addresses || [ ] )
@@ -154,16 +229,24 @@ export class PhishingController extends EventEmitter implements IPhishingControl
154229 this . #shouldSyncDapps = true
155230 this . emitUpdate ( )
156231
232+ const updatedAt = Date . now ( )
233+ this . #updatedAt = updatedAt
234+
157235 await this . #storage. set ( 'phishing' , {
158236 version : this . #version,
159- updatedAt : Date . now ( ) ,
237+ updatedAt,
160238 domains : [ ...this . #domains] ,
161239 addresses : [ ...this . #addresses]
162240 } )
163241
164- if ( this . updatePhishingInterval . currentTimeout !== PHISHING_UPDATE_INTERVAL ) {
165- this . updatePhishingInterval . updateTimeout ( { timeout : PHISHING_UPDATE_INTERVAL } )
242+ if ( this . updatePhishingInterval . currentTimeout === PHISHING_FAILED_TO_GET_UPDATE_INTERVAL ) {
243+ this . updatePhishingInterval . updateTimeout ( { timeout : PHISHING_INACTIVE_UPDATE_INTERVAL } )
166244 }
245+
246+ // NOTE: used for debugging only
247+ // console.log(
248+ // `[PhishingController] Update applied (version=${this.#version}, domains=${this.#domains.size}, addresses=${this.#addresses.size})`
249+ // )
167250 }
168251
169252 /**
0 commit comments