Skip to content

Commit

Permalink
Merge pull request #184 from secretkeylabs/mahmoud/eng-5128-stxcity-t…
Browse files Browse the repository at this point in the history
…oken-fails-on-estimate-fees-need-a-fallback-for

Fallback to mempool fees if no estimate
  • Loading branch information
m-aboelenein authored Sep 16, 2024
2 parents 748e61b + adbbab9 commit 04f443d
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 5 deletions.
12 changes: 12 additions & 0 deletions api/stacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
parseMempoolStxTransactionsData,
parseStxTransactionData,
} from './helper';
import { MempoolFeePriorities } from '@stacks/stacks-blockchain-api-types';

// TODO: these methods needs to be refactored
// reference https://github.com/secretkeylabs/xverse-core/pull/217/files#r1298242728
Expand Down Expand Up @@ -480,3 +481,14 @@ export async function fetchCoinMetaData(contract: string, network: StacksNetwork
return undefined;
}
}

export const getMempoolFeePriorities = async (network: StacksNetwork): Promise<MempoolFeePriorities> => {
const apiUrl = `${network.coreApiUrl}/extended/v2/mempool/fees`;
const response = await axios.get<MempoolFeePriorities>(apiUrl);
return response.data;
};

export interface FeeEstimation {
fee: number;
fee_rate?: number;
}
8 changes: 4 additions & 4 deletions hooks/transactions/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deserializeTransaction, estimateTransaction } from '@stacks/transactions';
import { deserializeTransaction } from '@stacks/transactions';
import BigNumber from 'bignumber.js';
import { RbfRecommendedFees, getRawTransaction, rbf } from '../../transactions';
import { RbfRecommendedFees, estimateStacksTransactionWithFallback, getRawTransaction, rbf } from '../../transactions';
import {
AppInfo,
RecommendedFeeResponse,
Expand Down Expand Up @@ -66,7 +66,7 @@ export const calculateStxRbfData = async (
fee: BigNumber,
feeEstimations: {
fee: number;
fee_rate: number;
fee_rate?: number;
}[],
appInfo: AppInfo | null,
stxAvailableBalance: string,
Expand Down Expand Up @@ -114,7 +114,7 @@ export const fetchStxRbfData = async (
const { fee } = transaction;
const txRaw: string = await getRawTransaction(transaction.txid, btcNetwork);
const unsignedTx: StacksTransaction = deserializeTransaction(txRaw);
const feeEstimations = await estimateTransaction(unsignedTx.payload, undefined, stacksNetwork);
const feeEstimations = await estimateStacksTransactionWithFallback(unsignedTx, stacksNetwork);

return calculateStxRbfData(fee, feeEstimations, appInfo, stxAvailableBalance);
};
169 changes: 169 additions & 0 deletions tests/transactions/stx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as StacksTransactions from '@stacks/transactions';
import { StacksNetwork } from '@stacks/network';
import * as TransactionUtils from '../../transactions';
import * as APIUtils from '../../api';

const mockTransaction = {
payload: {
payloadType: StacksTransactions.PayloadType.ContractCall,
},
} as StacksTransactions.StacksTransaction;

vi.mock('@stacks/transactions');

vi.mock('../../api');

const mockNetwork = {} as StacksNetwork;

describe('estimateStacksTransactionWithFallback', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return estimated fees when estimation is successful', async () => {
vi.spyOn(StacksTransactions, 'estimateTransactionByteLength').mockReturnValue(100);
vi.spyOn(StacksTransactions, 'estimateTransaction').mockImplementation(async () => [
{ fee: 100, fee_rate: 1 },
{ fee: 200, fee_rate: 1 },
{ fee: 300, fee_rate: 1 },
]);

const result = await TransactionUtils.estimateStacksTransactionWithFallback(mockTransaction, mockNetwork);
expect(result).toEqual([
{ fee: 100, fee_rate: 1 },
{ fee: 200, fee_rate: 1 },
{ fee: 300, fee_rate: 1 },
]);
expect(StacksTransactions.estimateTransactionByteLength).toHaveBeenCalledWith(mockTransaction);
expect(StacksTransactions.estimateTransaction).toHaveBeenCalledWith(mockTransaction.payload, 100, mockNetwork);
});

it('should return mempool fees for ContractCall when estimation fails', async () => {
vi.spyOn(StacksTransactions, 'estimateTransaction').mockRejectedValueOnce(new Error('NoEstimateAvailable'));
vi.spyOn(APIUtils, 'getMempoolFeePriorities').mockResolvedValueOnce({
contract_call: {
low_priority: 46,
medium_priority: 120,
high_priority: 222,
no_priority: 0,
},
all: {
low_priority: 40,
medium_priority: 90,
high_priority: 200,
no_priority: 0,
},
smart_contract: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
token_transfer: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
});

const result = await TransactionUtils.estimateStacksTransactionWithFallback(mockTransaction, mockNetwork);
expect(result).toEqual([{ fee: 46 }, { fee: 120 }, { fee: 222 }]);
expect(StacksTransactions.estimateTransactionByteLength).toHaveBeenCalledWith(mockTransaction);
expect(StacksTransactions.estimateTransaction).toHaveBeenCalledWith(mockTransaction.payload, 100, mockNetwork);
expect(APIUtils.getMempoolFeePriorities).toHaveBeenCalledWith(mockNetwork);
});

it('should return mempool fees for TokenTransfer when estimation fails', async () => {
const mockTransactionTokenTransfer = {
payload: {
payloadType: StacksTransactions.PayloadType.TokenTransfer,
},
} as StacksTransactions.StacksTransaction;
vi.spyOn(StacksTransactions, 'estimateTransaction').mockRejectedValueOnce(new Error('NoEstimateAvailable'));
vi.spyOn(APIUtils, 'getMempoolFeePriorities').mockResolvedValueOnce({
contract_call: {
low_priority: 46,
medium_priority: 120,
high_priority: 222,
no_priority: 0,
},
all: {
low_priority: 40,
medium_priority: 90,
high_priority: 200,
no_priority: 0,
},
smart_contract: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
token_transfer: {
low_priority: 55,
medium_priority: 200,
high_priority: 300,
no_priority: 0,
},
});

const result = await TransactionUtils.estimateStacksTransactionWithFallback(
mockTransactionTokenTransfer,
mockNetwork,
);
expect(result).toEqual([{ fee: 55 }, { fee: 200 }, { fee: 300 }]);
expect(StacksTransactions.estimateTransactionByteLength).toHaveBeenCalledWith(mockTransactionTokenTransfer);
expect(StacksTransactions.estimateTransaction).toHaveBeenCalledWith(
mockTransactionTokenTransfer.payload,
100,
mockNetwork,
);
expect(APIUtils.getMempoolFeePriorities).toHaveBeenCalledWith(mockNetwork);
});

it('should return general mempool fees when specific ones are not available', async () => {
const mockTransactionTenure = {
payload: {
payloadType: StacksTransactions.PayloadType.TenureChange,
},
} as StacksTransactions.StacksTransaction;
vi.spyOn(StacksTransactions, 'estimateTransaction').mockRejectedValueOnce(new Error('NoEstimateAvailable'));
vi.spyOn(APIUtils, 'getMempoolFeePriorities').mockResolvedValueOnce({
contract_call: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
all: {
low_priority: 40,
medium_priority: 90,
high_priority: 200,
no_priority: 0,
},
smart_contract: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
token_transfer: {
low_priority: 50,
medium_priority: 100,
high_priority: 150,
no_priority: 0,
},
});

const result = await TransactionUtils.estimateStacksTransactionWithFallback(mockTransactionTenure, mockNetwork);
expect(result).toEqual([{ fee: 40 }, { fee: 90 }, { fee: 200 }]);
expect(StacksTransactions.estimateTransactionByteLength).toHaveBeenCalledWith(mockTransactionTenure);
expect(StacksTransactions.estimateTransaction).toHaveBeenCalledWith(
mockTransactionTenure.payload,
100,
mockNetwork,
);
expect(APIUtils.getMempoolFeePriorities).toHaveBeenCalledWith(mockNetwork);
});
});
62 changes: 61 additions & 1 deletion transactions/stx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
estimateContractDeploy,
estimateContractFunctionCall,
estimateTransaction,
estimateTransactionByteLength,
estimateTransfer,
getNonce as fetchNewNonce,
hexToCV,
Expand All @@ -56,6 +57,8 @@ import {
import { getStxAddressKeyChain } from '../wallet/index';
import { capStxFeeAtThreshold, getNewNonce, makeFungiblePostCondition, makeNonFungiblePostCondition } from './helper';
import axios from 'axios';
import { MempoolFeePriorities } from '@stacks/stacks-blockchain-api-types';
import { FeeEstimation, getMempoolFeePriorities } from '../api';

export interface StacksRecipient {
address: string;
Expand Down Expand Up @@ -276,6 +279,63 @@ export async function estimateContractCallFees(
});
}

const getFallbackFees = (
transaction: StacksTransaction,
mempoolFees: MempoolFeePriorities,
): [FeeEstimation, FeeEstimation, FeeEstimation] => {
if (!transaction || !transaction.payload) {
throw new Error('Invalid transaction object');
}

if (!mempoolFees) {
throw new Error('Invalid mempool fees object');
}

const { payloadType } = transaction.payload;

if (payloadType === PayloadType.ContractCall && mempoolFees.contract_call) {
return [
{ fee: mempoolFees.contract_call.low_priority },
{ fee: mempoolFees.contract_call.medium_priority },
{ fee: mempoolFees.contract_call.high_priority },
];
} else if (payloadType === PayloadType.TokenTransfer && mempoolFees.token_transfer) {
return [
{ fee: mempoolFees.token_transfer.low_priority },
{ fee: mempoolFees.token_transfer.medium_priority },
{ fee: mempoolFees.token_transfer.high_priority },
];
}
return [
{ fee: mempoolFees.all.low_priority },
{ fee: mempoolFees.all.medium_priority },
{ fee: mempoolFees.all.high_priority },
];
};

/**
* Estimates the fee using {@link getMempoolFeePriorities} as a fallback if
* {@link estimateTransaction} does not get an estimation due to the
* {NoEstimateAvailableError} error.
*/
export const estimateStacksTransactionWithFallback = async (
transaction: StacksTransaction,
network: StacksNetwork,
): Promise<[FeeEstimation, FeeEstimation, FeeEstimation]> => {
try {
const estimatedLen = estimateTransactionByteLength(transaction);
const [slower, regular, faster] = await estimateTransaction(transaction.payload, estimatedLen, network);
return [slower, regular, faster];
} catch (error) {
const err = error.toString();
if (!err.includes('NoEstimateAvailable')) {
throw error;
}
const mempoolFees = await getMempoolFeePriorities(network);
return getFallbackFees(transaction, mempoolFees);
}
};

/**
* generate fungible token transfer or nft transfer transaction
* @param amount
Expand Down Expand Up @@ -343,7 +403,7 @@ export async function generateUnsignedTransaction(unsginedTx: UnsignedStacksTran
};
unsignedTx = await generateUnsignedContractCall(unsignedContractCallParam);

const [slower, regular, faster] = await estimateTransaction(unsignedTx.payload, undefined, network);
const [slower, regular, faster] = await estimateStacksTransactionWithFallback(unsignedTx, network);
unsignedTx.setFee(regular.fee);

// bump nonce by number of pending transactions
Expand Down

0 comments on commit 04f443d

Please sign in to comment.