From 31f54c8cb8bf1474406328be7c5a26b5b5263a44 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 12 Oct 2023 16:25:30 +0200 Subject: [PATCH 1/2] fix(fee-randomization): fix fee range in PaymentConfirmModal (#655) --- src/components/PaymentConfirmModal.tsx | 22 ++++++++++++++-------- src/components/settings/FeeConfigModal.tsx | 2 +- src/i18n/locales/en/translation.json | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 3a7ac8f97..5b5f1520e 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -11,6 +11,13 @@ import { AmountSats } from '../libs/JmWalletApi' import { jarInitial } from './jars/Jar' import { isValidNumber } from '../utils' +const feeRange: (feeValues: FeeValues) => [number, number] = (feeValues) => { + const feeTargetInSatsPerVByte = feeValues.tx_fees! / 1_000 + const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte) + const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeValues.tx_fees_factor!) + return [minFeeSatsPerVByte, maxFeeSatsPerVByte] +} + const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) => { const { t } = useTranslation() @@ -24,24 +31,23 @@ const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) } else if (unit === 'blocks') { return t('send.confirm_send_modal.text_miner_fee_in_targeted_blocks', { count: feeConfigValues.tx_fees }) } else { - const feeTargetInSatsPerVByte = feeConfigValues.tx_fees! / 1_000 - if (feeConfigValues.tx_fees_factor === 0) { + const [minFeeSatsPerVByte, maxFeeSatsPerVByte] = feeRange(feeConfigValues) + const fractionDigits = 2 + + if (minFeeSatsPerVByte.toFixed(fractionDigits) === maxFeeSatsPerVByte.toFixed(fractionDigits)) { return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_exact', { - value: feeTargetInSatsPerVByte.toLocaleString(undefined, { + value: minFeeSatsPerVByte.toLocaleString(undefined, { maximumFractionDigits: Math.log10(1_000), }), }) } - const minFeeSatsPerVByte = Math.max(1, feeTargetInSatsPerVByte * (1 - feeConfigValues.tx_fees_factor!)) - const maxFeeSatsPerVByte = feeTargetInSatsPerVByte * (1 + feeConfigValues.tx_fees_factor!) - return t('send.confirm_send_modal.text_miner_fee_in_satspervbyte_randomized', { min: minFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, + maximumFractionDigits: fractionDigits, }), max: maxFeeSatsPerVByte.toLocaleString(undefined, { - maximumFractionDigits: 1, + maximumFractionDigits: fractionDigits, }), }) } diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index a1f6f37ad..41b1c0094 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -278,7 +278,7 @@ const FeeConfigForm = forwardRef( : '', })} - {t('settings.fees.description_tx_fees_factor')} + {t('settings.fees.description_tx_fees_factor_^0.9.10')} % diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 5f377b345..aeafa2573 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -239,7 +239,7 @@ "feedback_invalid_tx_fees_blocks": "Please provide a valid block target between {{ min }} and {{ max }}.", "feedback_invalid_tx_fees_satspervbyte": "Please provide a valid transaction fee in sats/vByte between {{ min }} and {{ max }}.", "label_tx_fees_factor": "Fee randomization", - "description_tx_fees_factor": "Random fees improve privacy. The percentage is to be understood as a +/- around the base fee. Example: If you set the base fee to 10 sats/vByte and the randomization to 30%, a value between 7 and 13 sats/vByte will be used. Default: 20%.", + "description_tx_fees_factor_^0.9.10": "Random fees improve privacy. The percentage is an upward randomization factor of the base fee. Example: If you set the base fee to 10 sats/vByte and the randomization to 30%, a value between 10 and 13 sats/vByte will be used. Default: 20%.", "feedback_invalid_tx_fees_factor": "Please provide a valid fee randomization value between {{ min }} and {{ max }}.", "title_max_cj_fee_settings": "Collaborator fees", "description_max_cj_fee_settings": "Collaborator fees relate to liquidity price and depend on market conditions. Total fees paid for each transaction depend on the amount of collaborators. Additional collaborators increase privacy, but also fees.", From c1d6bdfbc2ca3d9719f39e55315f35270a5988bb Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Thu, 12 Oct 2023 17:25:01 +0200 Subject: [PATCH 2/2] chore: debounce polling of session info (#675) --- src/context/ServiceInfoContext.tsx | 19 ++++++++++++------- src/context/WalletContext.tsx | 15 +++++++++------ src/context/WebsocketContext.jsx | 7 ++++--- src/utils.ts | 17 +++++++++++++++++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/context/ServiceInfoContext.tsx b/src/context/ServiceInfoContext.tsx index 36167719a..219fbf78e 100644 --- a/src/context/ServiceInfoContext.tsx +++ b/src/context/ServiceInfoContext.tsx @@ -14,7 +14,7 @@ import { useCurrentWallet, useClearCurrentWallet } from './WalletContext' import { useWebsocket } from './WebsocketContext' import { clearSession } from '../session' import { CJ_STATE_TAKER_RUNNING, CJ_STATE_MAKER_RUNNING } from '../constants/config' -import { toSemVer, UNKNOWN_VERSION } from '../utils' +import { noop, setIntervalDebounced, toSemVer, UNKNOWN_VERSION } from '../utils' import * as Api from '../libs/JmWalletApi' @@ -224,16 +224,21 @@ const ServiceInfoProvider = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { const abortCtrl = new AbortController() - const refreshSession = () => { - reloadServiceInfo({ signal: abortCtrl.signal }).catch((err) => { - if (abortCtrl.signal.aborted) return - console.error(err) - }) + const refreshSession = (): Promise => { + return reloadServiceInfo({ signal: abortCtrl.signal }) + .then(noop) + .catch((err) => { + if (!abortCtrl.signal.aborted) { + console.error(err) + } + }) } refreshSession() - const interval = setInterval(refreshSession, SESSION_REQUEST_INTERVAL) + let interval: NodeJS.Timer + setIntervalDebounced(refreshSession, SESSION_REQUEST_INTERVAL, (timerId) => (interval = timerId)) + return () => { clearInterval(interval) abortCtrl.abort() diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 19ce9aa67..5bfce6abc 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -5,7 +5,7 @@ import * as Api from '../libs/JmWalletApi' import { WalletBalanceSummary, toBalanceSummary } from './BalanceSummary' import { JM_API_AUTH_TOKEN_EXPIRY } from '../constants/config' import { isDevMode } from '../constants/debugFeatures' -import { walletDisplayName } from '../utils' +import { setIntervalDebounced, walletDisplayName } from '../utils' const API_AUTH_TOKEN_RENEW_INTERVAL: Milliseconds = isDevMode() ? 60 * 1_000 @@ -318,14 +318,14 @@ const WalletProvider = ({ children }: PropsWithChildren) => { const abortCtrl = new AbortController() - const renewToken = () => { + const renewToken = async () => { const session = getSession() if (!session?.auth?.refresh_token) { console.warn('Cannot renew auth token - no refresh_token available.') return } - Api.postToken( + return Api.postToken( { token: session.auth.token, signal: abortCtrl.signal }, { grant_type: 'refresh_token', @@ -341,12 +341,15 @@ const WalletProvider = ({ children }: PropsWithChildren) => { console.debug('Successfully renewed auth token.') }) .catch((err) => { - if (abortCtrl.signal.aborted) return - console.error(err) + if (!abortCtrl.signal.aborted) { + console.error(err) + } }) } - const interval = setInterval(renewToken, API_AUTH_TOKEN_RENEW_INTERVAL) + let interval: NodeJS.Timer + setIntervalDebounced(renewToken, API_AUTH_TOKEN_RENEW_INTERVAL, (timerId) => (interval = timerId)) + return () => { clearInterval(interval) abortCtrl.abort() diff --git a/src/context/WebsocketContext.jsx b/src/context/WebsocketContext.jsx index 7cb962935..6fd00628c 100644 --- a/src/context/WebsocketContext.jsx +++ b/src/context/WebsocketContext.jsx @@ -1,6 +1,8 @@ -import React, { createContext, useEffect, useState, useContext } from 'react' +import { createContext, useEffect, useState, useContext } from 'react' import { useCurrentWallet } from './WalletContext' +import { noop } from '../utils' +import { isDevMode } from '../constants/debugFeatures' const WEBSOCKET_RECONNECT_DELAY_STEP = 1_000 const WEBSOCKET_RECONNECT_MAX_DELAY = 10_000 @@ -23,8 +25,7 @@ const connectionRetryDelayLinear = (attempt = 0) => { // path that will be proxied to the backend server const WEBSOCKET_ENDPOINT_PATH = `${window.JM.PUBLIC_PATH}/jmws` -const NOOP = () => {} -const logToDebugConsoleInDevMode = process.env.NODE_ENV === 'development' ? console.debug : NOOP +const logToDebugConsoleInDevMode = isDevMode() ? console.debug : noop const createWebSocket = () => { const { protocol, host } = window.location diff --git a/src/utils.ts b/src/utils.ts index 0f834e035..c1134af0c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -94,3 +94,20 @@ export const toSemVer = (raw?: string): SemVer => { } export const scrollToTop = () => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + +export const noop = () => {} + +export const setIntervalDebounced = ( + callback: () => Promise, + delay: Milliseconds, + onTimerIdChanged: (timerId: NodeJS.Timer) => void, +) => { + ;(function loop() { + onTimerIdChanged( + setTimeout(async () => { + await callback() + loop() + }, delay), + ) + })() +}