Skip to content

Commit

Permalink
refactor(connect): separate bitcoin and misc fee levels
Browse files Browse the repository at this point in the history
  • Loading branch information
enjojoy committed Feb 21, 2025
1 parent 918dc5b commit 6c7361a
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 60 deletions.
2 changes: 2 additions & 0 deletions packages/blockchain-link-types/src/responses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Block, MempoolTransactionNotification } from './blockbook';
import { Eip1559Fees } from './blockbook-api';
import type {
AccountBalanceHistory,
AccountInfo,
Expand Down Expand Up @@ -98,6 +99,7 @@ export interface EstimateFee {
feePerUnit: string;
feePerTx?: string;
feeLimit?: string;
eip1559?: Eip1559Fees;
}[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { BigNumber } from '@trezor/utils/src/bigNumber';

import { Blockchain } from '../../backend/BlockchainLink';
import type { CoinInfo, FeeLevel } from '../../types';

import type { BitcoinNetworkInfo, FeeLevel } from '../../types';
import { findBlocksForFee } from '../common/MiscFees';
type Blocks = Array<string | undefined>;

const convertFeeRate = (fee: string, minFee: number) => {
Expand Down Expand Up @@ -56,56 +56,20 @@ const findLowest = (blocks: Blocks) =>
.reverse()
.find(item => typeof item === 'string');

const findBlocksForFee = (feePerUnit: string, blocks: Blocks) => {
const bn = new BigNumber(feePerUnit);
// find first occurrence of value lower or equal than requested
const lower = blocks.find(b => typeof b === 'string' && bn.gte(b));
if (!lower) return -1;

// if not found get latest know value
return blocks.indexOf(lower);
};

export class FeeLevels {
coinInfo: CoinInfo;
export class BitcoinFeeLevels {
coinInfo: BitcoinNetworkInfo;

levels: FeeLevel[];
longTermFeeRate?: string; // long term fee rate is used by @trezor/utxo-lib composeTx module

blocks: Blocks = [];

constructor(coinInfo: CoinInfo) {
constructor(coinInfo: BitcoinNetworkInfo) {
this.coinInfo = coinInfo;
this.levels = coinInfo.defaultFees;
}

async loadMisc(blockchain: Blockchain) {
try {
const [response] = await blockchain.estimateFee({ blocks: [1] });
// misc coins should have only one FeeLevel (normal)
this.levels[0] = {
...this.levels[0],
...response,
// validate `feePerUnit` from the backend
// should be lower than `coinInfo.maxFee` and higher than `coinInfo.minFee`
// xrp sends values from 1 to very high number occasionally
// see: https://github.com/trezor/trezor-suite/blob/develop/packages/blockchain-link/src/workers/ripple/index.ts#L316
feePerUnit: Math.min(
this.coinInfo.maxFee,
Math.max(this.coinInfo.minFee, parseInt(response.feePerUnit, 10)),
).toString(),
};
} catch {
// silent
}

return this.levels;
}

async load(blockchain: Blockchain) {
if (this.coinInfo.type !== 'bitcoin') return this.loadMisc(blockchain);
// only for bitcoin-like

let blocks = fillGap(0, 1, 10);
if (this.levels.length > 1) {
// multiple levels
Expand Down Expand Up @@ -135,7 +99,10 @@ export class FeeLevels {
try {
const response = await blockchain.estimateFee({ blocks });
response.forEach(({ feePerUnit }, index) => {
this.blocks[blocks[index]] = convertFeeRate(feePerUnit, this.coinInfo.minFee);
this.blocks[blocks[index]] = convertFeeRate(
feePerUnit || '0',
this.coinInfo.minFee,
);
});

this.levels.forEach(level => {
Expand All @@ -154,7 +121,7 @@ export class FeeLevels {
return this.levels;
}

updateCustomFee(feePerUnit: string) {
updateBitcoinCustomFee(feePerUnit: string) {
// remove "custom" level from list
this.levels = this.levels.filter(l => l.label !== 'custom');
// recreate "custom" level
Expand Down
12 changes: 6 additions & 6 deletions packages/connect/src/api/bitcoin/TransactionComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { BigNumber } from '@trezor/utils/src/bigNumber';
import { ComposeOutput, TransactionInputOutputSortingStrategy, composeTx } from '@trezor/utxo-lib';

import { FeeLevels } from './Fees';
import { BitcoinFeeLevels } from './BitcoinFees';
import { Blockchain } from '../../backend/BlockchainLink';
import type { BitcoinNetworkInfo, DiscoveryAccount, SelectFeeLevel } from '../../types';
import type {
Expand Down Expand Up @@ -36,7 +36,7 @@ export class TransactionComposer {

sortingStrategy: TransactionInputOutputSortingStrategy;

feeLevels: FeeLevels;
feeLevels: BitcoinFeeLevels;

composed: { [key: string]: ComposeResult } = {};

Expand All @@ -47,7 +47,7 @@ export class TransactionComposer {
this.blockHeight = 0;
this.baseFee = options.baseFee || 0;
this.sortingStrategy = options.sortingStrategy;
this.feeLevels = new FeeLevels(options.coinInfo);
this.feeLevels = new BitcoinFeeLevels(options.coinInfo);

// map to @trezor/utxo-lib/compose format
const { addresses } = options.account;
Expand Down Expand Up @@ -101,7 +101,7 @@ export class TransactionComposer {

const tx = this.compose(lastFee.toString());
if (tx.type === 'final') {
this.feeLevels.updateCustomFee(lastFee.toString());
this.feeLevels.updateBitcoinCustomFee(lastFee.toString());
this.composed.custom = tx;

return true;
Expand All @@ -118,9 +118,9 @@ export class TransactionComposer {
const tx = this.compose(fee);
this.composed.custom = tx;
if (tx.type === 'final') {
this.feeLevels.updateCustomFee(tx.feePerByte);
this.feeLevels.updateBitcoinCustomFee(tx.feePerByte);
} else {
this.feeLevels.updateCustomFee(fee);
this.feeLevels.updateBitcoinCustomFee(fee);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import coinsJSON from '@trezor/connect-common/files/coins.json';

import { initBlockchain } from '../../../backend/BlockchainLink';
import { getBitcoinNetwork, parseCoinsJson } from '../../../data/coinInfo';
import { FeeLevels } from '../Fees';
import { BitcoinFeeLevels } from '../BitcoinFees';

describe('api/bitcoin/Fees', () => {
// load coin definitions
Expand All @@ -29,15 +29,15 @@ describe('api/bitcoin/Fees', () => {
});

const backend = await initBlockchain(coinInfo, () => {});
const feeLevels = new FeeLevels(coinInfo);
const feeLevels = new BitcoinFeeLevels(coinInfo);

expect(feeLevels.levels.length).toEqual(4); // Bitcoin has 4 defined levels in coins.json
expect(feeLevels.levels.map(l => l.feePerUnit)).toEqual(
coinInfo.defaultFees.map(l => l.feePerUnit),
); // preloaded values from coins.json

const smartFeeLevels = await feeLevels.load(backend);
expect(smartFeeLevels.map(l => l.feePerUnit)).toEqual(['10', '10', '8.86', '4.69']);
expect(smartFeeLevels?.map(l => l.feePerUnit)).toEqual(['10', '10', '8.86', '4.69']);

backend.disconnect();
spy.mockClear();
Expand All @@ -63,10 +63,10 @@ describe('api/bitcoin/Fees', () => {
});

const backend = await initBlockchain(coinInfo, () => {});
const feeLevels = new FeeLevels(coinInfo);
const feeLevels = new BitcoinFeeLevels(coinInfo);

const smartFeeLevels = await feeLevels.load(backend);
expect(smartFeeLevels.map(l => l.feePerUnit)).toEqual(['10', '8.86', '3.18', '1']);
expect(smartFeeLevels?.map(l => l.feePerUnit)).toEqual(['10', '8.86', '3.18', '1']);

backend.disconnect();
spy.mockClear();
Expand All @@ -92,15 +92,15 @@ describe('api/bitcoin/Fees', () => {
});

const backend = await initBlockchain(coinInfo, () => {});
const feeLevels = new FeeLevels(coinInfo);
const feeLevels = new BitcoinFeeLevels(coinInfo);

expect(feeLevels.levels.length).toEqual(1); // Testnet has 1 defined levels in coins.json
expect(feeLevels.levels.map(l => l.feePerUnit)).toEqual(
coinInfo.defaultFees.map(l => l.feePerUnit),
); // preloaded values from coins.json

const smartFeeLevels = await feeLevels.load(backend);
expect(smartFeeLevels.map(l => l.feePerUnit)).toEqual(['9.24']);
expect(smartFeeLevels?.map(l => l.feePerUnit)).toEqual(['9.24']);

backend.disconnect();
spy.mockClear();
Expand Down
2 changes: 1 addition & 1 deletion packages/connect/src/api/bitcoin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './createPendingTx';
export * from './enhanceSignTx';
export * from './Fees';
export * from './BitcoinFees';
export * from './inputs';
export * from './outputs';
export * from './refTx';
Expand Down
8 changes: 6 additions & 2 deletions packages/connect/src/api/blockchainEstimateFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { ERRORS } from '../constants';
import { AbstractMethod, MethodReturnType, Payload } from '../core/AbstractMethod';
import { FeeLevels } from './bitcoin/Fees';
import { validateParams } from './common/paramsValidator';
import { initBlockchain, isBackendSupported } from '../backend/BlockchainLink';
import { getCoinInfo } from '../data/coinInfo';
import type { CoinInfo } from '../types';
import { BitcoinFeeLevels } from './bitcoin/BitcoinFees';
import { MiscFeeLevels } from './common/MiscFees';

type Params = {
coinInfo: CoinInfo;
Expand Down Expand Up @@ -73,7 +74,10 @@ export default class BlockchainEstimateFee extends AbstractMethod<'blockchainEst
levels: [],
};
if (request && request.feeLevels) {
const fees = new FeeLevels(coinInfo);
const fees =
coinInfo.type === 'bitcoin'
? new BitcoinFeeLevels(coinInfo)
: new MiscFeeLevels(coinInfo);
if (request.feeLevels === 'smart') {
const backend = await initBlockchain(coinInfo, this.postMessage, identity);
await fees.load(backend);
Expand Down
68 changes: 68 additions & 0 deletions packages/connect/src/api/common/MiscFees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/tx/Fees.js

import { BigNumber } from '@trezor/utils/src/bigNumber';

import { Blockchain } from '../../backend/BlockchainLink';
import type { CoinInfo, FeeLevel } from '../../types';

type Blocks = Array<string | undefined>;

export const findBlocksForFee = (feePerUnit: string, blocks: Blocks) => {
const bn = new BigNumber(feePerUnit);
// find first occurrence of value lower or equal than requested
const lower = blocks.find(b => typeof b === 'string' && bn.gte(b));
if (!lower) return -1;

// if not found get latest know value
return blocks.indexOf(lower);
};

export class MiscFeeLevels {
coinInfo: CoinInfo;

levels: FeeLevel[];
longTermFeeRate?: string; // long term fee rate is used by @trezor/utxo-lib composeTx module

blocks: Blocks = [];

constructor(coinInfo: CoinInfo) {
this.coinInfo = coinInfo;
this.levels = coinInfo.defaultFees;
}

async load(blockchain: Blockchain) {
try {
const [response] = await blockchain.estimateFee({ blocks: [1] });

//misc coins should have only one FeeLevel (normal)
this.levels[0] = {
...this.levels[0],
...response,
// validate `feePerUnit` from the backend
// should be lower than `coinInfo.maxFee` and higher than `coinInfo.minFee`
// xrp sends values from 1 to very high number occasionally
// see: https://github.com/trezor/trezor-suite/blob/develop/packages/blockchain-link/src/workers/ripple/index.ts#L316
feePerUnit: Math.min(
this.coinInfo.maxFee,
Math.max(this.coinInfo.minFee, parseInt(response.feePerUnit ?? '0', 10)),
).toString(),
};
} catch {
// silent
}

return this.levels;
}

updateCustomFee(feePerUnit: string) {
// remove "custom" level from list
this.levels = this.levels.filter(l => l.label !== 'custom');
// recreate "custom" level
const blocks = findBlocksForFee(feePerUnit, this.blocks);
this.levels.push({
label: 'custom',
feePerUnit,
blocks,
});
}
}
Loading

0 comments on commit 6c7361a

Please sign in to comment.