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

[ENG-3617] Update the rbf logic #354

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e609d51
[ENG-3617] Update the rbf logic
dhriaznov Jan 19, 2024
04a5eed
Add getLatestNonce and getRawTransaction methods for stacks txs
dhriaznov Jan 19, 2024
12e472b
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
dhriaznov Jan 23, 2024
861d531
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
dhriaznov Jan 24, 2024
574470e
Add the useRbfTransactionData hook
dhriaznov Jan 24, 2024
807945d
Export the useRbfTransactionData hook
dhriaznov Jan 24, 2024
3c82079
Update the useRbfTransactionData hook params
dhriaznov Jan 24, 2024
c203b32
Update the useRbfTransactionData hook params
dhriaznov Jan 24, 2024
920d935
Update the useRbfTransactionData hook logic
dhriaznov Jan 24, 2024
4ae3e3a
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
dhriaznov Jan 30, 2024
60e6d1c
Move some logic to the separate functions, add unit tests
dhriaznov Jan 31, 2024
3c807fd
Merge branch 'denys/eng-3617-speed-up-stacks-transactions-on-mobile' …
dhriaznov Jan 31, 2024
85f497d
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
dhriaznov Jan 31, 2024
7f02475
Fix the imports
dhriaznov Jan 31, 2024
30f5f88
Merge branch 'denys/eng-3617-speed-up-stacks-transactions-on-mobile' …
dhriaznov Jan 31, 2024
43fba6b
Move the rbf tests to the tests folder
dhriaznov Feb 1, 2024
7f9d3c8
Update the rbf logic syntax
dhriaznov Feb 1, 2024
2df8ba7
Remove the unnecessary prop
dhriaznov Feb 1, 2024
0a1dd77
Update the stx rbf unit tests
dhriaznov Feb 1, 2024
9354e55
Add react-query package, add separate react-query functions for the r…
dhriaznov Feb 1, 2024
b544424
Export the new hooks
dhriaznov Feb 1, 2024
945e9fa
Remove the useRbfTransactionData hook in favor of the separate react-…
dhriaznov Feb 1, 2024
fb5e1cf
Merge branch 'develop' into denys/eng-3617-speed-up-stacks-transactio…
dhriaznov Feb 5, 2024
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
1 change: 1 addition & 0 deletions hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './brc20';
export * from './inscriptions';
export * from './useRbfTransactionData';
176 changes: 176 additions & 0 deletions hooks/useRbfTransactionData/helpers.test.ts
teebszet marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import BigNumber from 'bignumber.js';
import { calculateStxData } from './helpers';
import { StxTransactionData, SettingsNetwork, AppInfo } from '../../types';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { StacksMocknet } from '@stacks/network';
import axios from 'axios';
import { microstacksToStx } from '../../currency';

vi.mock('axios');
vi.mock('@stacks/transactions', async () => {
const actual = await vi.importActual<any>('@stacks/transactions');
return {
...actual,
estimateTransaction: vi.fn().mockResolvedValue([
{ fee: 1000, fee_rate: 1000 },
{ fee: 2000, fee_rate: 2000 },
{ fee: 3000, fee_rate: 3000 },
]),
};
});

const lowFee = 1000;
const mediumFee = 2000;
const highFee = 3000;

const mockTransaction: StxTransactionData = {
txid: 'mock-txid',
amount: BigNumber(100),
seenTime: new Date(),
incoming: false,
txType: 'token_transfer',
txStatus: 'success',
blockHash: 'mock-blockhash',
blockHeight: 1,
burnBlockTime: 1,
burnBlockTimeIso: new Date(),
canonical: true,
fee: BigNumber(lowFee),
nonce: 1,
postConditionMode: 'mock-mode',
senderAddress: 'mock-senderaddress',
sponsored: false,
txIndex: 1,
txResults: 'mock-results',
};

const mockBtcNetwork: SettingsNetwork = {
type: 'Mainnet',
address: 'mock-address',
btcApiUrl: 'mock-btcApiUrl',
fallbackBtcApiUrl: 'mock-fallbackBtcApiUrl',
};

const mockStacksNetwork = new StacksMocknet();

const mockAppInfo: AppInfo = {
stxSendTxMultiplier: 1,
poolStackingTxMultiplier: 1,
otherTxMultiplier: 1,
thresholdHighSatsFee: 1,
thresholdHighSatsPerByteRatio: 1,
thresholdHighStacksFee: 5000,
};

const mockStxAvailableBalance = '5000';

const mockResult = {
rbfTransaction: undefined,
rbfTxSummary: {
currentFee: microstacksToStx(BigNumber(lowFee)).toNumber(),
currentFeeRate: microstacksToStx(BigNumber(lowFee)).toNumber(),
minimumRbfFee: microstacksToStx(BigNumber(lowFee).multipliedBy(1.25)).toNumber(),
minimumRbfFeeRate: microstacksToStx(BigNumber(lowFee).multipliedBy(1.25)).toNumber(),
},
rbfRecommendedFees: {
high: {
enoughFunds: true,
fee: microstacksToStx(BigNumber(highFee)).toNumber(),
feeRate: microstacksToStx(BigNumber(highFee)).toNumber(),
},
medium: {
enoughFunds: true,
fee: microstacksToStx(BigNumber(mediumFee)).toNumber(),
feeRate: microstacksToStx(BigNumber(mediumFee)).toNumber(),
},
},
mempoolFees: {
fastestFee: microstacksToStx(BigNumber(highFee)).toNumber(),
halfHourFee: microstacksToStx(BigNumber(mediumFee)).toNumber(),
hourFee: microstacksToStx(BigNumber(lowFee)).toNumber(),
economyFee: microstacksToStx(BigNumber(lowFee)).toNumber(),
minimumFee: microstacksToStx(BigNumber(lowFee)).toNumber(),
},
};

const mockRawTx =
// eslint-disable-next-line max-len
'0x80800000000400483cd5c1c96119e132aa12b76df34f003c85f9af00000000000000240000000000021cd200006c47412a98c710eaaa276f93d9a77097616afe9ac8f48068bae96f8084c23ad26b4d6e77fce26b20be0faa86493e2fd8567e48283157973399ab5e283965b20e03020000000000051a5953622a9370e859e5a8e290ed38b1a885bf09df00000000000186a000000000000000000000000000000000000000000000000000000000000000000000';

describe('calculateStxData method', async () => {
beforeEach(() => {
vi.mocked(axios.get).mockResolvedValue({
data: {
raw_tx: mockRawTx,
},
});
});

it('should return the fee estimation fetched from the Stacks function', async () => {
const result = await calculateStxData(
mockTransaction,
mockBtcNetwork,
mockStacksNetwork,
mockAppInfo,
mockStxAvailableBalance,
);

expect(result).toBeDefined();
expect(result).toEqual(mockResult);
});

it('should return the fee estimation based on the current fee', async () => {
const fee = BigNumber(2000);
const tx = { ...mockTransaction, fee };
const result = await calculateStxData(tx, mockBtcNetwork, mockStacksNetwork, mockAppInfo, mockStxAvailableBalance);

expect(result).toBeDefined();
expect(result).toEqual({
...mockResult,
rbfRecommendedFees: {
highest: {
enoughFunds: true,
fee: microstacksToStx(fee.multipliedBy(1.5)).toNumber(),
feeRate: microstacksToStx(fee.multipliedBy(1.5)).toNumber(),
},
higher: {
enoughFunds: true,
fee: microstacksToStx(fee.multipliedBy(1.25)).toNumber(),
feeRate: microstacksToStx(fee.multipliedBy(1.25)).toNumber(),
},
},
rbfTxSummary: {
currentFee: microstacksToStx(fee).toNumber(),
currentFeeRate: microstacksToStx(fee).toNumber(),
minimumRbfFee: microstacksToStx(fee.multipliedBy(1.25)).toNumber(),
minimumRbfFeeRate: microstacksToStx(fee.multipliedBy(1.25)).toNumber(),
},
});
});

it('should return falsy value for the enoughFunds when the balance is not enough', async () => {
const availableBalance = '1';
const result = await calculateStxData(
mockTransaction,
mockBtcNetwork,
mockStacksNetwork,
mockAppInfo,
availableBalance,
);

expect(result).toBeDefined();
expect(result).toEqual({
...mockResult,
rbfRecommendedFees: {
high: {
...mockResult.rbfRecommendedFees.high,
enoughFunds: false,
},
medium: {
...mockResult.rbfRecommendedFees.medium,
enoughFunds: false,
},
},
});
});
});
121 changes: 121 additions & 0 deletions hooks/useRbfTransactionData/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { deserializeTransaction, estimateTransaction } from '@stacks/transactions';
import BigNumber from 'bignumber.js';
import { RbfRecommendedFees, getRawTransaction, rbf } from '../../transactions';
import {
AppInfo,
RecommendedFeeResponse,
SettingsNetwork,
BtcTransactionData,
StacksNetwork,
StacksTransaction,
StxTransactionData,
} from '../../types';
import { microstacksToStx } from '../../currency';

export type RbfData = {
rbfTransaction?: InstanceType<typeof rbf.RbfTransaction>;
rbfTxSummary?: {
currentFee: number;
currentFeeRate: number;
minimumRbfFee: number;
minimumRbfFeeRate: number;
};
rbfRecommendedFees?: RbfRecommendedFees;
mempoolFees?: RecommendedFeeResponse;
isLoading?: boolean;
errorCode?: 'SOMETHING_WENT_WRONG';
};

export const isBtcTransaction = (
transaction: BtcTransactionData | StxTransactionData,
): transaction is BtcTransactionData => transaction?.txType === 'bitcoin';

export const constructRecommendedFees = (
lowerName: keyof RbfRecommendedFees,
lowerFeeRate: number,
higherName: keyof RbfRecommendedFees,
higherFeeRate: number,
stxAvailableBalance: string,
): RbfRecommendedFees => {
const bigNumLowerFee = BigNumber(lowerFeeRate);
const bigNumHigherFee = BigNumber(higherFeeRate);

return {
[lowerName]: {
enoughFunds: bigNumLowerFee.lte(BigNumber(stxAvailableBalance)),
feeRate: microstacksToStx(bigNumLowerFee).toNumber(),
fee: microstacksToStx(bigNumLowerFee).toNumber(),
},
[higherName]: {
enoughFunds: bigNumHigherFee.lte(BigNumber(stxAvailableBalance)),
feeRate: microstacksToStx(bigNumHigherFee).toNumber(),
fee: microstacksToStx(bigNumHigherFee).toNumber(),
},
};
};

export const sortFees = (fees: RbfRecommendedFees) =>
Object.fromEntries(
Object.entries(fees).sort((a, b) => {
const priorityOrder = ['highest', 'higher', 'high', 'medium'];
return priorityOrder.indexOf(a[0]) - priorityOrder.indexOf(b[0]);
}),
);

export const calculateStxData = async (
transaction: StxTransactionData,
btcNetwork: SettingsNetwork,
stacksNetwork: StacksNetwork,
appInfo: AppInfo | null,
stxAvailableBalance: string,
): Promise<RbfData> => {
const { fee } = transaction;
const txRaw: string = await getRawTransaction(transaction.txid, btcNetwork);
const unsignedTx: StacksTransaction = deserializeTransaction(txRaw);

const [slow, medium, high] = await estimateTransaction(unsignedTx.payload, undefined, stacksNetwork);

let feePresets: RbfRecommendedFees = {};
let mediumFee = medium.fee;
let highFee = high.fee;
const higherFee = fee.multipliedBy(1.25).toNumber();
const highestFee = fee.multipliedBy(1.5).toNumber();

if (appInfo?.thresholdHighStacksFee) {
if (high.fee > appInfo.thresholdHighStacksFee) {
// adding a fee cap
highFee = appInfo.thresholdHighStacksFee * 1.5;
mediumFee = appInfo.thresholdHighStacksFee;
}
}

let minimumFee = fee.multipliedBy(1.25).toNumber();
teebszet marked this conversation as resolved.
Show resolved Hide resolved
if (!Number.isSafeInteger(minimumFee)) {
// round up the fee to the nearest integer
minimumFee = Math.ceil(minimumFee);
}

if (fee.lt(BigNumber(mediumFee))) {
feePresets = constructRecommendedFees('medium', mediumFee, 'high', highFee, stxAvailableBalance);
} else {
feePresets = constructRecommendedFees('higher', higherFee, 'highest', highestFee, stxAvailableBalance);
}
teebszet marked this conversation as resolved.
Show resolved Hide resolved
teebszet marked this conversation as resolved.
Show resolved Hide resolved

return {
rbfTransaction: undefined,
teebszet marked this conversation as resolved.
Show resolved Hide resolved
rbfTxSummary: {
currentFee: microstacksToStx(fee).toNumber(),
currentFeeRate: microstacksToStx(fee).toNumber(),
minimumRbfFee: microstacksToStx(BigNumber(minimumFee)).toNumber(),
minimumRbfFeeRate: microstacksToStx(BigNumber(minimumFee)).toNumber(),
},
rbfRecommendedFees: sortFees(feePresets),
mempoolFees: {
fastestFee: microstacksToStx(BigNumber(high.fee)).toNumber(),
halfHourFee: microstacksToStx(BigNumber(medium.fee)).toNumber(),
hourFee: microstacksToStx(BigNumber(slow.fee)).toNumber(),
economyFee: microstacksToStx(BigNumber(slow.fee)).toNumber(),
minimumFee: microstacksToStx(BigNumber(slow.fee)).toNumber(),
fedeerbes marked this conversation as resolved.
Show resolved Hide resolved
},
};
};
96 changes: 96 additions & 0 deletions hooks/useRbfTransactionData/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useCallback, useEffect, useState } from 'react';
import { rbf } from '../../transactions';
import { Account, AppInfo, BtcTransactionData, SettingsNetwork, StacksNetwork, StxTransactionData } from '../../types';
import { RbfData, calculateStxData, isBtcTransaction, sortFees } from './helpers';
import { BitcoinEsploraApiProvider, mempoolApi } from '../../api';

const useRbfTransactionData = ({
account,
transaction,
stacksNetwork,
btcNetwork,
esploraProvider,
stxAvailableBalance,
appInfo,
isLedgerAccount,
}: {
account: Account | null;
transaction?: BtcTransactionData | StxTransactionData;
stacksNetwork: StacksNetwork;
btcNetwork: SettingsNetwork;
esploraProvider: BitcoinEsploraApiProvider;
stxAvailableBalance: string;
appInfo: AppInfo | null;
isLedgerAccount: boolean;
}): RbfData => {
const [isLoading, setIsLoading] = useState(true);
const [rbfData, setRbfData] = useState<RbfData>({});
const [errorCode, setErrorCode] = useState<'SOMETHING_WENT_WRONG' | undefined>();

const fetchStxData = useCallback(async () => {
if (!transaction || isBtcTransaction(transaction)) {
return;
}
try {
setIsLoading(true);
const calculatedData = await calculateStxData(
transaction,
btcNetwork,
stacksNetwork,
appInfo,
stxAvailableBalance,
);
setRbfData(calculatedData);
} catch (err: any) {
setErrorCode('SOMETHING_WENT_WRONG');
} finally {
setIsLoading(false);
}
}, [transaction, btcNetwork, stacksNetwork, appInfo, stxAvailableBalance]);
teebszet marked this conversation as resolved.
Show resolved Hide resolved

const fetchRbfData = useCallback(async () => {
if (!account || !transaction) {
return;
}

if (!isBtcTransaction(transaction)) {
return fetchStxData();
}

try {
setIsLoading(true);

const rbfTx = new rbf.RbfTransaction(transaction, {
...account,
accountType: account.accountType || 'software',
accountId: isLedgerAccount && account.deviceAccountIndex ? account.deviceAccountIndex : account.id,
network: btcNetwork.type,
esploraProvider,
});

const mempoolFees = await mempoolApi.getRecommendedFees(btcNetwork.type);
const rbfRecommendedFeesResponse = await rbfTx.getRbfRecommendedFees(mempoolFees);

const rbfTransactionSummary = await rbf.getRbfTransactionSummary(esploraProvider, transaction.txid);

setRbfData({
rbfTransaction: rbfTx,
rbfTxSummary: rbfTransactionSummary,
rbfRecommendedFees: sortFees(rbfRecommendedFeesResponse),
mempoolFees,
});
} catch (err: any) {
setErrorCode('SOMETHING_WENT_WRONG');
} finally {
setIsLoading(false);
}
}, [account, transaction, btcNetwork.type]);

useEffect(() => {
fetchRbfData();
}, [fetchRbfData]);

return { ...rbfData, isLoading, errorCode };
};

export default useRbfTransactionData;
Loading
Loading