Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 20 additions & 1 deletion src/background/services/paymentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import {
isOutOfBalanceError,
isTokenExpiredError,
} from '@/background/services/openPayments';
import { bigIntMax, convert, getNextSendableAmount } from '@/background/utils';
import {
bigIntMax,
convert,
convertWithExchangeRate,
getExchangeRatesMemoized,
getNextSendableAmount,
} from '@/background/utils';
import type {
MonetizationEventDetails,
MonetizationEventPayload,
Expand Down Expand Up @@ -144,6 +150,19 @@ export class PaymentSession {
}
}

if (isCrossCurrency) {
try {
const exchangeRates = await getExchangeRatesMemoized();
amountToSend = convertWithExchangeRate(
amountToSend,
this.receiver,
this.sender,
exchangeRates,
);
this.logger.debug('minSendAmount: via exchangeRate', amountToSend);
} catch {}
}

// This all will eventually get replaced by OpenPayments response update
// that includes a min rate that we can directly use.
await this.setIncomingPaymentUrl();
Expand Down
9 changes: 7 additions & 2 deletions src/background/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Browser, Runtime } from 'webextension-polyfill';
import { BACKGROUND_TO_POPUP_CONNECTION_NAME } from '@/shared/messages';
import { EXCHANGE_RATES_URL } from './config';
import { INTERNAL_PAGE_URL_PROTOCOLS, NEW_TAB_PAGES } from './constants';
import { notNullOrUndef } from '@/shared/helpers';
import { memoize, notNullOrUndef } from '@/shared/helpers';
import type { WalletAddress } from '@interledger/open-payments';

type OnConnectCallback = Parameters<
Expand Down Expand Up @@ -90,6 +90,11 @@ export const getExchangeRates = async (): Promise<ExchangeRates> => {
return rates;
};

export const getExchangeRatesMemoized = memoize(getExchangeRates, {
maxAge: 15 * 60 * 1000,
mechanism: 'stale-while-revalidate',
});

export const getExchangeRate = (
rates: ExchangeRates,
forAssetCode: string,
Expand Down Expand Up @@ -121,7 +126,7 @@ export const convertWithExchangeRate = <T extends AmountValue | bigint>(
const scaleDiff = from.assetScale - to.assetScale;
const scaledExchangeRate = exchangeRate * 10 ** scaleDiff;

const converted = BigInt(Math.ceil(Number(amount) / scaledExchangeRate));
const converted = BigInt(Math.round(Number(amount) / scaledExchangeRate));

return typeof amount === 'string'
? (converted.toString() as T)
Expand Down
20 changes: 20 additions & 0 deletions src/shared/helpers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ export function withResolvers<T>() {
return { resolve, reject, promise };
}

export function memoize<T extends () => Promise<unknown>>(
fn: T,
options: { maxAge: number; mechanism: 'stale-while-revalidate' | 'max-age' },
): () => ReturnType<T> {
const { maxAge, mechanism = 'max-age' } = options;
let result: ReturnType<T>;
let lastCall = 0;
return () => {
const lastResult = result;
if (Date.now() - lastCall > maxAge) {
lastCall = Date.now();
result = fn() as ReturnType<T>;
}
if (mechanism === 'stale-while-revalidate') {
return lastResult;
}
return result;
};
}

export const isOkState = (state: Storage['state']) => {
return Object.values(state).every((value) => value === false);
};
Expand Down