@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
255 lines (213 loc) • 8.11 kB
text/typescript
import cvsApi from "@ledgerhq/live-countervalues/api/index";
import BigNumber from "bignumber.js";
import {
DEFAULT_GAS_LIMIT,
DEFAULT_GAS_PRICE_TINYBARS,
DEFAULT_TINYBAR_FEE,
ESTIMATED_FEE_SAFETY_RATE,
ESTIMATED_GAS_SAFETY_RATE,
HEDERA_OPERATION_TYPES,
HEDERA_TRANSACTION_MODES,
TINYBAR_SCALE,
} from "../constants";
import { apiClient } from "../network/api";
import { getMockedAccount } from "../test/fixtures/account.fixture";
import { getMockedERC20TokenCurrency } from "../test/fixtures/currency.fixture";
import { estimateFees } from "./estimateFees";
import { getCurrencyToUSDRate, toEVMAddress } from "./utils";
jest.mock("@ledgerhq/live-countervalues/api/index");
jest.mock("../network/api");
describe("getEstimatedFees", () => {
const mockedAccount = getMockedAccount();
const mockedTokenCurrencyERC20 = getMockedERC20TokenCurrency();
const senderAddress = "0.0.12345";
const recipientAddress = "0.0.67890";
beforeEach(() => {
jest.clearAllMocks();
// reset LRU cache to make sure all tests receive correct mocks from mockedFetchLatest
getCurrencyToUSDRate.clear(mockedAccount.currency.ticker);
});
it("returns estimated fee based on USD rate for CryptoTransfer", async () => {
const usdRate = 1; // 1 HBAR = 1 USD
(cvsApi.fetchLatest as jest.Mock).mockResolvedValueOnce([usdRate]);
const result = await estimateFees({
currency: mockedAccount.currency,
operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
});
const baseFeeTinybar = 0.0001 * 10 ** TINYBAR_SCALE;
const expectedTinybars = new BigNumber(baseFeeTinybar)
.div(usdRate)
.integerValue(BigNumber.ROUND_CEIL)
.multipliedBy(ESTIMATED_FEE_SAFETY_RATE);
expect(result).toMatchObject({
tinybars: expectedTinybars,
});
});
it("returns estimated fee based on USD rate for TokenTransfer", async () => {
const usdRate = 0.5; // 1 HBAR = 0.5 USD
(cvsApi.fetchLatest as jest.Mock).mockResolvedValueOnce([usdRate]);
const result = await estimateFees({
currency: mockedAccount.currency,
operationType: HEDERA_OPERATION_TYPES.TokenTransfer,
});
const baseFeeTinybar = 0.001 * 10 ** TINYBAR_SCALE;
const expectedTinybars = new BigNumber(baseFeeTinybar)
.div(usdRate)
.integerValue(BigNumber.ROUND_CEIL)
.multipliedBy(ESTIMATED_FEE_SAFETY_RATE);
expect(result).toMatchObject({
tinybars: expectedTinybars,
});
});
it("returns estimated fee based on USD rate for TokenAssociate", async () => {
const usdRate = 2; // 1 HBAR = 2 USD
(cvsApi.fetchLatest as jest.Mock).mockResolvedValueOnce([usdRate]);
const result = await estimateFees({
currency: mockedAccount.currency,
operationType: HEDERA_OPERATION_TYPES.TokenAssociate,
});
const baseFeeTinybar = 0.05 * 10 ** TINYBAR_SCALE;
const expectedTinybars = new BigNumber(baseFeeTinybar)
.div(usdRate)
.integerValue(BigNumber.ROUND_CEIL)
.multipliedBy(ESTIMATED_FEE_SAFETY_RATE);
expect(result).toMatchObject({
tinybars: expectedTinybars,
});
});
it("returns estimated fee based on gas for ContractCall", async () => {
const estimatedGasLimit = new BigNumber(50000);
const gasPriceTinybars = new BigNumber(900);
const transferAmount = BigInt(1000000);
(apiClient.getAccount as jest.Mock).mockImplementation(address => ({
address,
evm_address: "0x0000000000000000000000000000000000012345",
}));
(apiClient.getNetworkFees as jest.Mock).mockResolvedValueOnce({
fees: [
{
transaction_type: "ContractCall",
gas: gasPriceTinybars.toNumber(),
},
],
});
(apiClient.estimateContractCallGas as jest.Mock).mockResolvedValueOnce(estimatedGasLimit);
const result = await estimateFees({
operationType: HEDERA_OPERATION_TYPES.ContractCall,
txIntent: {
intentType: "transaction",
type: HEDERA_TRANSACTION_MODES.Send,
sender: senderAddress,
recipient: recipientAddress,
amount: transferAmount,
asset: {
type: "erc20",
assetReference: mockedTokenCurrencyERC20.contractAddress,
},
},
});
const expectedGas = estimatedGasLimit
.multipliedBy(ESTIMATED_GAS_SAFETY_RATE)
.integerValue(BigNumber.ROUND_CEIL);
const expectedTinybars = expectedGas
.multipliedBy(gasPriceTinybars)
.integerValue(BigNumber.ROUND_CEIL);
expect(apiClient.getAccount).toHaveBeenCalledTimes(2);
expect(apiClient.getNetworkFees).toHaveBeenCalledTimes(1);
expect(apiClient.estimateContractCallGas).toHaveBeenCalledTimes(1);
expect(apiClient.estimateContractCallGas).toHaveBeenCalledWith(
await toEVMAddress(senderAddress),
await toEVMAddress(recipientAddress),
mockedTokenCurrencyERC20.contractAddress,
transferAmount,
);
expect(result).toMatchObject({
tinybars: expectedTinybars,
gas: expectedGas,
});
});
it("falls back to default gas values when getNetworkFees fail", async () => {
const transferAmount = BigInt(1000000);
(apiClient.getNetworkFees as jest.Mock).mockRejectedValueOnce(new Error("Network error"));
const result = await estimateFees({
operationType: HEDERA_OPERATION_TYPES.ContractCall,
txIntent: {
intentType: "transaction",
type: HEDERA_TRANSACTION_MODES.Send,
sender: senderAddress,
recipient: recipientAddress,
amount: transferAmount,
asset: {
type: "erc20",
assetReference: mockedTokenCurrencyERC20.contractAddress,
},
},
});
const expectedGas = DEFAULT_GAS_LIMIT;
const expectedTinybars = new BigNumber(expectedGas)
.multipliedBy(DEFAULT_GAS_PRICE_TINYBARS)
.integerValue(BigNumber.ROUND_CEIL);
expect(result).toMatchObject({
tinybars: expectedTinybars,
gas: expectedGas,
});
});
it("falls back to default gas values when estimateContractCallGas fail", async () => {
const transferAmount = BigInt(1000000);
(apiClient.getNetworkFees as jest.Mock).mockResolvedValueOnce({
fees: [],
});
(apiClient.estimateContractCallGas as jest.Mock).mockRejectedValueOnce(
new Error("Network error"),
);
const result = await estimateFees({
operationType: HEDERA_OPERATION_TYPES.ContractCall,
txIntent: {
intentType: "transaction",
type: HEDERA_TRANSACTION_MODES.Send,
sender: senderAddress,
recipient: recipientAddress,
amount: transferAmount,
asset: {
type: "erc20",
assetReference: mockedTokenCurrencyERC20.contractAddress,
},
},
});
const expectedGas = DEFAULT_GAS_LIMIT;
const expectedTinybars = new BigNumber(expectedGas)
.multipliedBy(DEFAULT_GAS_PRICE_TINYBARS)
.integerValue(BigNumber.ROUND_CEIL);
expect(result).toMatchObject({
tinybars: expectedTinybars,
gas: expectedGas,
});
});
it("falls back to default estimate when cvs api returns null", async () => {
const usdRate = null;
(cvsApi.fetchLatest as jest.Mock).mockResolvedValueOnce([usdRate]);
const result = await estimateFees({
currency: mockedAccount.currency,
operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
});
const expectedTinybars = new BigNumber(DEFAULT_TINYBAR_FEE).multipliedBy(
ESTIMATED_FEE_SAFETY_RATE,
);
expect(result).toMatchObject({
tinybars: expectedTinybars,
});
});
it("falls back to default estimate on cvs api failure", async () => {
(cvsApi.fetchLatest as jest.Mock).mockRejectedValueOnce(new Error("Network error"));
const result = await estimateFees({
currency: mockedAccount.currency,
operationType: HEDERA_OPERATION_TYPES.CryptoTransfer,
});
const expectedTinybars = new BigNumber(DEFAULT_TINYBAR_FEE).multipliedBy(
ESTIMATED_FEE_SAFETY_RATE,
);
expect(result).toMatchObject({
tinybars: expectedTinybars,
});
});
});