From 9a51650d6a3281782a411196a09ab93c61bce226 Mon Sep 17 00:00:00 2001 From: Diana Fulga Date: Tue, 2 Jul 2024 14:11:16 +0300 Subject: [PATCH] fix(background): ensure not to pay before interval, even on page reload (#349) * Prevent overpaying * Feedback * Fix lint * Implement feedback * Implement feedback * Implement feedback * Implement feedback * Implement feedback * Implement feedback * Fix lint --------- Co-authored-by: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> --- src/background/container.ts | 5 +- src/background/services/monetization.ts | 19 ++++++-- src/background/services/paymentSession.ts | 25 +++++++++- src/background/services/tabState.ts | 59 +++++++++++++++++++++++ src/background/utils.ts | 17 +++++-- 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/background/services/tabState.ts diff --git a/src/background/container.ts b/src/background/container.ts index b8a1d6f0..50720ca0 100644 --- a/src/background/container.ts +++ b/src/background/container.ts @@ -11,6 +11,7 @@ import { } from './services' import { createLogger, Logger } from '@/shared/logger' import { LOG_LEVEL } from '@/shared/defines' +import { TabState } from './services/tabState' interface Cradle { logger: Logger @@ -22,6 +23,7 @@ interface Cradle { monetizationService: MonetizationService tabEvents: TabEvents background: Background + tabState: TabState } export const configureContainer = () => { @@ -60,7 +62,8 @@ export const configureContainer = () => { .singleton() .inject(() => ({ logger: logger.getLogger('main') - })) + })), + tabState: asClass(TabState).singleton() }) return container diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index 1cc9f50d..73646cd5 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -8,10 +8,17 @@ import { } from '@/shared/messages' import { PaymentSession } from './paymentSession' import { emitToggleWM } from '../lib/messages' -import { computeRate, getCurrentActiveTab, getSender, getTabId } from '../utils' +import { + computeRate, + getCurrentActiveTab, + getSender, + getTabId, + removeQueryParams +} from '../utils' import { EventsService } from './events' import { ALLOWED_PROTOCOLS } from '@/shared/defines' import type { PopupStore } from '@/shared/types' +import { TabState } from './tabState' export class MonetizationService { private sessions: { @@ -23,7 +30,8 @@ export class MonetizationService { private openPaymentsService: OpenPaymentsService, private storage: StorageService, private browser: Browser, - private events: EventsService + private events: EventsService, + private tabState: TabState ) { this.sessions = {} this.registerEventListeners() @@ -51,7 +59,7 @@ export class MonetizationService { ) return } - const { tabId, frameId } = getSender(sender) + const { tabId, frameId, url, tab } = getSender(sender) if (this.sessions[tabId] == null) { this.sessions[tabId] = new Map() @@ -74,10 +82,13 @@ export class MonetizationService { receiver, connectedWallet, requestId, + tab, tabId, frameId, rate, - this.openPaymentsService + this.openPaymentsService, + this.tabState, + removeQueryParams(url!) ) sessions.set(requestId, session) diff --git a/src/background/services/paymentSession.ts b/src/background/services/paymentSession.ts index ff12e6ee..55acb2e7 100644 --- a/src/background/services/paymentSession.ts +++ b/src/background/services/paymentSession.ts @@ -9,6 +9,8 @@ import { OpenPaymentsClientError } from '@interledger/open-payments/dist/client' import { sendMonetizationEvent } from '../lib/messages' import { convert, sleep } from '@/shared/helpers' import { transformBalance } from '@/popup/lib/utils' +import { TabState } from './tabState' +import type { Tabs } from 'webextension-polyfill' const DEFAULT_INTERVAL_MS = 1000 const HOUR_MS = 3600 * 1000 @@ -24,10 +26,13 @@ export class PaymentSession { private receiver: WalletAddress, private sender: WalletAddress, private requestId: string, + private tab: Tabs.Tab, private tabId: number, private frameId: number, private rate: string, - private openPaymentsService: OpenPaymentsService + private openPaymentsService: OpenPaymentsService, + private tabState: TabState, + private url: string ) { this.adjustSessionAmount() } @@ -122,6 +127,14 @@ export class PaymentSession { let outgoingPayment: OutgoingPayment | undefined + const waitTime = this.tabState.getOverpayingWaitTime( + this.tab, + this.url, + this.receiver.id + ) + + await sleep(waitTime) + while (this.active) { try { outgoingPayment = await this.openPaymentsService.createOutgoingPayment({ @@ -172,6 +185,16 @@ export class PaymentSession { } }) + // TO DO: find a better source of truth for deciding if overpaying is applicable + if (this.intervalInMs > 1000) { + this.tabState.saveOverpaying( + this.tab, + this.url, + this.receiver.id, + this.intervalInMs + ) + } + await sleep(this.intervalInMs) } } diff --git a/src/background/services/tabState.ts b/src/background/services/tabState.ts new file mode 100644 index 00000000..5600ef6e --- /dev/null +++ b/src/background/services/tabState.ts @@ -0,0 +1,59 @@ +import { Tabs } from 'webextension-polyfill' + +type State = { + lastPaymentTimestamp: number + expiresAtTimestamp: number +} + +export class TabState { + private state = new WeakMap>() + + constructor() {} + + private getOverpayingStateKey(url: string, walletAddressId: string): string { + return `${url}:${walletAddressId}` + } + + getOverpayingWaitTime( + tab: Tabs.Tab, + url: string, + walletAddressId: string + ): number { + const key = this.getOverpayingStateKey(url, walletAddressId) + const state = this.state.get(tab)?.get(key) + const now = Date.now() + + if (state && state.expiresAtTimestamp > now) { + return state.expiresAtTimestamp - now + } + + return 0 + } + + saveOverpaying( + tab: Tabs.Tab, + url: string, + walletAddressId: string, + intervalInMs: number + ): void { + if (!intervalInMs) return + + const now = Date.now() + const expiresAtTimestamp = now + intervalInMs + + const key = this.getOverpayingStateKey(url, walletAddressId) + const state = this.state.get(tab)?.get(key) + + if (!state) { + const tabState = this.state.get(tab) || new Map() + tabState.set(key, { + expiresAtTimestamp: expiresAtTimestamp, + lastPaymentTimestamp: now + }) + this.state.set(tab, tabState) + } else { + state.expiresAtTimestamp = expiresAtTimestamp + state.lastPaymentTimestamp = now + } + } +} diff --git a/src/background/utils.ts b/src/background/utils.ts index d57370fd..07dd3dc0 100644 --- a/src/background/utils.ts +++ b/src/background/utils.ts @@ -1,5 +1,5 @@ -import { AmountValue, GrantDetails, WalletAmount } from '@/shared/types' -import { type Browser, Runtime } from 'webextension-polyfill' +import type { AmountValue, GrantDetails, WalletAmount } from '@/shared/types' +import type { Browser, Runtime, Tabs } from 'webextension-polyfill' import { DEFAULT_SCALE, EXCHANGE_RATES_URL } from './config' import { notNullOrUndef } from '@/shared/helpers' @@ -81,15 +81,26 @@ export const getTabId = (sender: Runtime.MessageSender): number => { return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab').id, 'tab.id') } +export const getTab = (sender: Runtime.MessageSender): Tabs.Tab => { + return notNullOrUndef(notNullOrUndef(sender.tab, 'sender.tab'), 'tab') +} + export const getSender = (sender: Runtime.MessageSender) => { const tabId = getTabId(sender) const frameId = notNullOrUndef(sender.frameId, 'sender.frameId') - return { tabId, frameId } + const tab = getTab(sender) + + return { tabId, frameId, url: sender.url, tab } } export const computeRate = (rate: string, sessionsCount: number) => (+rate / sessionsCount).toString() +export const removeQueryParams = (urlString: string) => { + const url = new URL(urlString) + return url.origin + url.pathname +} + export function computeBalance( grant?: GrantDetails | null, grantSpentAmount?: AmountValue | null