Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: deduplicate token rotation requests #305

Merged
merged 8 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/background/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
import { createLogger, Logger } from '@/shared/logger'
import { LOG_LEVEL } from '@/shared/defines'
import { EventsService } from './services/events'
import { Deduplicator } from './services/deduplicator'

interface Cradle {
logger: Logger
browser: Browser
events: EventsService
deduplicator: Deduplicator
storage: StorageService
openPaymentsService: OpenPaymentsService
monetizationService: MonetizationService
Expand All @@ -33,6 +35,7 @@ export const configureContainer = () => {
logger: asValue(logger),
browser: asValue(browser),
events: asClass(EventsService).singleton(),
deduplicator: asClass(Deduplicator).singleton(),
storage: asClass(StorageService)
.singleton()
.inject(() => ({
Expand Down
67 changes: 67 additions & 0 deletions src/background/services/deduplicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
type AsyncFn<T> = (...args: any[]) => Promise<T>

interface CacheEntry {
promise: Promise<any>
}

interface DedupeOptions {
cacheFnArgs: boolean
}

export class Deduplicator {
private cache: Map<string, CacheEntry> = new Map()

constructor(private duration = 5000) {}

dedupe<T extends AsyncFn<any>>(
fn: T,
options: DedupeOptions = { cacheFnArgs: false }
): T {
return ((...args: Parameters<T>): ReturnType<T> => {
const key = this.generateCacheKey(fn, args, options.cacheFnArgs)
raducristianpopa marked this conversation as resolved.
Show resolved Hide resolved
const entry = this.cache.get(key)

if (entry) {
console.log('Deduping', fn.name)
return entry.promise as ReturnType<T>
}

const promise = fn(...args)
this.cache.set(key, { promise })

promise
.then((res) => {
this.cache.set(key, { promise: Promise.resolve(res) })
return res
})
.catch((err) => {
throw err
})
.finally(() => this.scheduleCacheClear(key))

console.log(promise)

return promise as ReturnType<T>
}) as unknown as T
}

private generateCacheKey<T>(
fn: AsyncFn<T>,
args: any[],
cacheFnArgs: boolean
): string {
let key = fn.name
if (cacheFnArgs) {
key += `_${JSON.stringify(args)}`
}
return key
}

private scheduleCacheClear(key: string): void {
setTimeout(() => {
const entry = this.cache.get(key)
console.log(Object.fromEntries(this.cache.entries()))
if (entry) this.cache.delete(key)
}, this.duration)
}
}
5 changes: 2 additions & 3 deletions src/background/services/monetization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,12 @@ export class MonetizationService {
tabId,
frameId,
rate,
this.openPaymentsService,
this.storage
this.openPaymentsService
)

this.sessions[tabId].set(requestId, session)

if (enabled === true) {
if (connected === true && enabled === true) {
void session.start()
}
}
Expand Down
135 changes: 116 additions & 19 deletions src/background/services/openPayments.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { WalletAmount } from 'shared/types'
import { AccessToken, WalletAmount } from 'shared/types'
import {
type AuthenticatedClient,
createAuthenticatedClient
} from '@interledger/open-payments/dist/client'
import {
isFinalizedGrant,
isPendingGrant,
OutgoingPayment,
Quote,
WalletAddress
} from '@interledger/open-payments/dist/types'
import * as ed from '@noble/ed25519'
Expand All @@ -29,6 +31,7 @@ import {
MAX_RATE_OF_PAY,
MIN_RATE_OF_PAY
} from '../config'
import { Deduplicator } from './deduplicator'

interface KeyInformation {
privateKey: string
Expand Down Expand Up @@ -76,22 +79,42 @@ interface CreateQuoteAndOutgoingPaymentGrantParams {
amount: WalletAmount
}

interface CreateQuoteParams {
walletAddress: WalletAddress
receiver: string
amount: string
}

interface CreateOutgoingPaymentParams {
walletAddress: WalletAddress
quoteId: string
}

export class OpenPaymentsService {
client?: AuthenticatedClient

private token: AccessToken

constructor(
private browser: Browser,
private storage: StorageService
private storage: StorageService,
private deduplicator: Deduplicator
) {
;(async () => {
const { connected, walletAddress } = await this.storage.get([
'connected',
'walletAddress'
])
if (connected === true && walletAddress) {
this.initClient(walletAddress.id)
}
})()
console.log(this.token)
void this.initialize()
}

private async initialize() {
const { token, connected, walletAddress } = await this.storage.get([
'connected',
'walletAddress',
'token'
])

if (connected === true && walletAddress && token) {
await this.initClient(walletAddress.id)
this.token = token
}
}

private async getPrivateKeyInformation(): Promise<KeyInformation> {
Expand Down Expand Up @@ -310,16 +333,19 @@ export class OpenPaymentsService {
throw new Error('Expected finalized grant. Received unfinalized grant.')
}

const token = {
value: continuation.access_token.value,
manage: continuation.access_token.manage
}

this.token = token
this.storage.set({
walletAddress,
rateOfPay,
minRateOfPay,
maxRateOfPay,
amount: transformedAmount,
token: {
value: continuation.access_token.value,
manage: continuation.access_token.manage
},
token,
grant: {
accessToken: continuation.continue.access_token.value,
continueUri: continuation.continue.uri
Expand Down Expand Up @@ -377,17 +403,13 @@ export class OpenPaymentsService {
return grant
}

// Fourth item- https://rafiki.dev/concepts/open-payments/grant-interaction/#endpoints
private async verifyInteractionHash({
clientNonce,
interactRef,
interactNonce,
hash,
authServer
}: VerifyInteractionHashParams): Promise<void> {
// Notice: The interaction hash is not correctly calculated within Rafiki at the momenet in certain scenarios.
// If at one point this will throw an error check the `grantEndpoint` value.
// `grantEndpoint` represents the route where grants are requested.
const grantEndpoint = new URL(authServer).origin + '/'
const data = new TextEncoder().encode(
`${clientNonce}\n${interactNonce}\n${interactRef}\n${grantEndpoint}`
Expand Down Expand Up @@ -437,6 +459,10 @@ export class OpenPaymentsService {
accessToken: grant.accessToken
})
await this.storage.clear()
this.token = {
value: '',
manage: ''
}
}
}

Expand All @@ -453,4 +479,75 @@ export class OpenPaymentsService {
keyId
})
}

async createQuote({
walletAddress,
receiver,
amount
}: CreateQuoteParams): Promise<Quote> {
console.log('createQuote token value', this.token)
const quote = await this.client!.quote.create(
{
accessToken: this.token.value,
url: walletAddress.resourceServer
},
{
method: 'ilp',
walletAddress: walletAddress.id,
receiver,
debitAmount: {
value: amount,
assetCode: walletAddress.assetCode,
assetScale: walletAddress.assetScale
}
}
)

return quote
}

async createOutgoingPayment({
walletAddress,
quoteId
}: CreateOutgoingPaymentParams): Promise<OutgoingPayment> {
return await this.client!.outgoingPayment.create(
{
accessToken: this.token.value,
url: walletAddress.resourceServer
},
{
quoteId,
walletAddress: walletAddress.id,
metadata: {
source: 'Web Monetization'
}
}
)
}

async rotateToken() {
const rotate = this.deduplicator.dedupe(this.client!.token.rotate, {
cacheFnArgs: false
})
const token = await rotate({
url: this.token.manage,
accessToken: this.token.value
})
console.log('rotateToken token', token)
const newToken = {
value: token.access_token.value,
manage: token.access_token.manage
}
await this.storage.set({
token: newToken
})
this.token = newToken

console.log(
'storage',
await this.storage.get(['token']),
'opservice',
this.token
)
}
}
Loading
Loading