diff --git a/packages/coinjoin/README.md b/packages/coinjoin/README.md index 1c70a0adf6..7f5c0a9203 100644 --- a/packages/coinjoin/README.md +++ b/packages/coinjoin/README.md @@ -11,7 +11,7 @@ const client = new CoinjoinClient(settings); const status = await client.enable(); ``` -Once enabled it periodically sync with coordinator `/status` using random proxy (TOR) identities and time frequency until `client.disable();` is called. +Once enabled it periodically syncs with coordinator `/status` using random proxy (TOR) identities and time frequency until `client.disable();` is called. Status changes are emitted as `status` event: @@ -20,8 +20,9 @@ client.on('status', event => {}); { rounds: Round[]; // current list of rounds - changes: Round[]: // list of changed rounds since recent update - feeRatesMedians: Array<{ timeFrame: string; medianFeeRate: number; }>, // timeFrame format: "0d 0h 0m 0s" - coordinatorFeeRate: action.status.coordinatorFeeRate, // current coordinatorFeeRate + changed: Round[]: // list of changed rounds since recent update + maxMingFee: number // max mining fee resulting from recommended fee rate median + coordinatorFeeRate: CoordinationFeeRate // current rate and plebsDontPayThreshold + allowedInputAmounts: AllowedRange; // min and max allowed input value } ``` diff --git a/packages/coinjoin/src/constants.ts b/packages/coinjoin/src/constants.ts index 90e21e9eaa..04a9ec7ad9 100644 --- a/packages/coinjoin/src/constants.ts +++ b/packages/coinjoin/src/constants.ts @@ -40,6 +40,7 @@ export const PLEBS_DONT_PAY_THRESHOLD = 1000000; export const COORDINATOR_FEE_RATE = 0.003; export const MIN_ALLOWED_AMOUNT = 5000; export const MAX_ALLOWED_AMOUNT = 134375000000; +export const MAX_MINING_FEE = 1; // affiliation flag: // - sent coordinator/ready-to-sign request **only** when Alice pays coordination fee diff --git a/packages/coinjoin/src/types/client.ts b/packages/coinjoin/src/types/client.ts index 5dad351716..12cfb8ab41 100644 --- a/packages/coinjoin/src/types/client.ts +++ b/packages/coinjoin/src/types/client.ts @@ -5,7 +5,7 @@ import { CoinjoinRequestEvent, CoinjoinRoundEvent } from './round'; export interface CoinjoinStatusEvent { rounds: Round[]; changed: Round[]; - feeRatesMedians: { fast: number; recommended: number }; + maxMiningFee: number; coordinationFeeRate: CoordinationFeeRate; allowedInputAmounts: AllowedRange; } diff --git a/packages/coinjoin/src/utils/roundUtils.ts b/packages/coinjoin/src/utils/roundUtils.ts index 609dfe5ac8..cd55477568 100644 --- a/packages/coinjoin/src/utils/roundUtils.ts +++ b/packages/coinjoin/src/utils/roundUtils.ts @@ -1,5 +1,6 @@ import { COORDINATOR_FEE_RATE, + MAX_MINING_FEE, MAX_ALLOWED_AMOUNT, MIN_ALLOWED_AMOUNT, PLEBS_DONT_PAY_THRESHOLD, @@ -158,36 +159,26 @@ const getDataFromRounds = (rounds: Round[]) => { }; }; -/** - * Transform from coordinator format to coinjoinReducer format `CoinjoinClientFeeRatesMedians` - * array => object { name: value-in-vBytes } - */ -export const transformFeeRatesMedians = (medians: CoinjoinStatus['coinJoinFeeRateMedians']) => { - const [fast, recommended] = medians.map(m => m.medianFeeRate); - // convert from kvBytes (kilo virtual bytes) to vBytes (how the value is displayed in UI) - const kvB2vB = (v: number) => (v ? Math.round(v / 1000) : 1); - - return { - fast: kvB2vB(fast) * 2, // NOTE: this calculation will be smarter once have enough data - recommended: kvB2vB(recommended), - }; -}; - /** * Transform from coordinator format to coinjoinReducer format `CoinjoinClientInstance` * - coordinatorFeeRate: multiply the amount registered for coinjoin by this value to get the total fee - * - feeRatesMedians: array => object with values in kvBytes + * - maxMiningFee: array => value in kvBytes */ export const transformStatus = ({ coinJoinFeeRateMedians, roundStates: rounds, }: CoinjoinStatus) => { - const feeRatesMedians = transformFeeRatesMedians(coinJoinFeeRateMedians); const { allowedInputAmounts, coordinationFeeRate } = getDataFromRounds(rounds); + // coinJoinFeeRateMedians include an array of medians per day, week and month - we take the second (week) median as the recommended fee rate + const recommendedMedian = coinJoinFeeRateMedians[1]; + // the value is converted from kvBytes (kilo virtual bytes) to vBytes (how the value is displayed in UI) + const maxMiningFee = recommendedMedian + ? Math.round(coinJoinFeeRateMedians[1].medianFeeRate / 1000) + : MAX_MINING_FEE; return { rounds, - feeRatesMedians, + maxMiningFee, coordinationFeeRate, allowedInputAmounts, }; diff --git a/packages/coinjoin/tests/fixtures/round.fixture.ts b/packages/coinjoin/tests/fixtures/round.fixture.ts index c005bd1fc0..38c51d8244 100644 --- a/packages/coinjoin/tests/fixtures/round.fixture.ts +++ b/packages/coinjoin/tests/fixtures/round.fixture.ts @@ -136,13 +136,8 @@ export const createCoinjoinRound = ( return round; }; -export const FEE_RATE_RESULTS = { - fast: 258, - recommended: 129, -}; - export const STATUS_TRANSFORMED = { - feeRatesMedians: FEE_RATE_RESULTS, + maxMiningFee: 129, allowedInputAmounts: { max: 134375000000, min: 5000, diff --git a/packages/coinjoin/tests/utils/roundUtils.test.ts b/packages/coinjoin/tests/utils/roundUtils.test.ts index c98401872d..b47849be6d 100644 --- a/packages/coinjoin/tests/utils/roundUtils.test.ts +++ b/packages/coinjoin/tests/utils/roundUtils.test.ts @@ -2,17 +2,10 @@ import { getCommitmentData, readTimeSpan, estimatePhaseDeadline, - transformFeeRatesMedians, transformStatus, } from '../../src/utils/roundUtils'; import { ROUND_REGISTRATION_END_OFFSET } from '../../src/constants'; -import { - DEFAULT_ROUND, - FEE_RATE_MEDIANS, - FEE_RATE_RESULTS, - STATUS_EVENT, - STATUS_TRANSFORMED, -} from '../fixtures/round.fixture'; +import { DEFAULT_ROUND, STATUS_EVENT, STATUS_TRANSFORMED } from '../fixtures/round.fixture'; describe('roundUtils', () => { it('getCommitmentData', () => { @@ -89,19 +82,11 @@ describe('roundUtils', () => { ); }); - describe('transformFeeRatesMedians', () => { - it('transform correctly', () => { - const feeRatesMedians = transformFeeRatesMedians(FEE_RATE_MEDIANS); - - expect(feeRatesMedians).toEqual(FEE_RATE_RESULTS); - }); - }); - describe('transformStatus', () => { it('transform correctly', () => { - const feeRatesMedians = transformStatus(STATUS_EVENT); + const status = transformStatus(STATUS_EVENT); - expect(feeRatesMedians).toEqual(STATUS_TRANSFORMED); + expect(status).toEqual(STATUS_TRANSFORMED); }); }); }); diff --git a/packages/suite/src/actions/wallet/__tests__/coinjoinAccountActions.test.ts b/packages/suite/src/actions/wallet/__tests__/coinjoinAccountActions.test.ts index becf2fe046..75e4aea221 100644 --- a/packages/suite/src/actions/wallet/__tests__/coinjoinAccountActions.test.ts +++ b/packages/suite/src/actions/wallet/__tests__/coinjoinAccountActions.test.ts @@ -22,7 +22,7 @@ jest.mock('@suite/services/coinjoin/coinjoinService', () => { enable: jest.fn(() => Promise.resolve({ rounds: [{ id: '00', phase: 0 }], - feeRatesMedians: [], + maxMiningFee: 0, coordinatorFeeRate: 0.003, allowedInputAmounts: { min: 5000, max: 134375000000 }, }), diff --git a/packages/suite/src/reducers/wallet/coinjoinReducer.ts b/packages/suite/src/reducers/wallet/coinjoinReducer.ts index d61a434f55..5be393c64d 100644 --- a/packages/suite/src/reducers/wallet/coinjoinReducer.ts +++ b/packages/suite/src/reducers/wallet/coinjoinReducer.ts @@ -27,7 +27,7 @@ import { AccountsRootState, selectAccountByKey } from '@suite-common/wallet-core export interface CoinjoinClientInstance extends Pick< CoinjoinStatusEvent, - 'coordinationFeeRate' | 'allowedInputAmounts' | 'feeRatesMedians' + 'coordinationFeeRate' | 'allowedInputAmounts' | 'maxMiningFee' > { rounds: { id: string; phase: RoundPhase }[]; // store only slice of Round in reducer. may be extended in the future status: 'loading' | 'loaded'; @@ -552,9 +552,8 @@ export const selectMinAllowedInputWithFee = memoizeWithArgs( const status = coinjoinClient || DEFAULT_CLIENT_STATUS; const minAllowedInput = status.allowedInputAmounts.min; const txSize = getInputSize('Taproot') + getOutputSize('Taproot'); - const recommendedFeeRate = status.feeRatesMedians.recommended; - return minAllowedInput + txSize * recommendedFeeRate; + return minAllowedInput + txSize * status.maxMiningFee; }, ); diff --git a/packages/suite/src/services/coinjoin/config.ts b/packages/suite/src/services/coinjoin/config.ts index 346e34c145..fc45181b42 100644 --- a/packages/suite/src/services/coinjoin/config.ts +++ b/packages/suite/src/services/coinjoin/config.ts @@ -3,6 +3,7 @@ import { COORDINATOR_FEE_RATE, MIN_ALLOWED_AMOUNT, MAX_ALLOWED_AMOUNT, + MAX_MINING_FEE, } from '@trezor/coinjoin/src/constants'; import type { CoinjoinBackendSettings, CoinjoinClientSettings } from '@trezor/coinjoin'; import type { PartialRecord } from '@trezor/type-utils'; @@ -142,7 +143,7 @@ export const DEFAULT_TARGET_ANONYMITY = 10; export const DEFAULT_CLIENT_STATUS = { rounds: [], - feeRatesMedians: { fast: 0, recommended: 0 }, + maxMiningFee: MAX_MINING_FEE, coordinationFeeRate: { rate: COORDINATOR_FEE_RATE, plebsDontPayThreshold: PLEBS_DONT_PAY_THRESHOLD, diff --git a/packages/suite/src/utils/wallet/coinjoinUtils.ts b/packages/suite/src/utils/wallet/coinjoinUtils.ts index 14591d14d5..9ff7b5696d 100644 --- a/packages/suite/src/utils/wallet/coinjoinUtils.ts +++ b/packages/suite/src/utils/wallet/coinjoinUtils.ts @@ -111,12 +111,12 @@ export const calculateAnonymityProgress = ({ export const transformCoinjoinStatus = ({ coordinationFeeRate, - feeRatesMedians, + maxMiningFee, allowedInputAmounts, rounds, }: CoinjoinStatusEvent) => ({ coordinationFeeRate, - feeRatesMedians, + maxMiningFee, allowedInputAmounts, rounds: rounds.map(({ id, phase }) => ({ id, phase })), }); diff --git a/packages/suite/src/views/wallet/anonymize/components/CoinjoinConfirmation.tsx b/packages/suite/src/views/wallet/anonymize/components/CoinjoinConfirmation.tsx index 3ddb0d0561..1414b4cc26 100644 --- a/packages/suite/src/views/wallet/anonymize/components/CoinjoinConfirmation.tsx +++ b/packages/suite/src/views/wallet/anonymize/components/CoinjoinConfirmation.tsx @@ -162,7 +162,7 @@ export const CoinjoinConfirmation = ({ account }: CoinjoinConfirmationProps) => dispatch( startCoinjoinSession(account, { maxCoordinatorFeeRate: coordinatorData.coordinationFeeRate.rate, - maxFeePerKvbyte: coordinatorData.feeRatesMedians.recommended * 1000, // transform to kvB + maxFeePerKvbyte: coordinatorData.maxMiningFee * 1000, // transform to kvB maxRounds, skipRounds: RECOMMENDED_SKIP_ROUNDS, targetAnonymity,