Skip to content
16 changes: 16 additions & 0 deletions src/fixtures/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const upgradesInfo = {
apricotPhaselTime: '2020-12-05T05:00:00Z',
apricotPhase2Time: '2020-12-05T05:00:00Z',
apricotPhase3Time: '2020-12-05T05:00:00Z',
apricotPhase4Time: '2020-12-05T05:00:00Z',
apricotPhase4MinPChainHeight: 0,
apricotPhase5Time: '2020-12-05T05:00:00Z',
apricotPhasePre6Time: '2020-12-05T05:00:00Z',
apricotPhase6Time: '2020-12-05T05:00:00Z',
apricotPhasePost6Time: '2020-12-05T05:00:00Z',
banffTime: '2020-12-05T05:00:00Z',
cortinaTime: '2020-12-05T05:00:00Z',
cortinaXChainStopVertexID: '11111111111111111111111111111111LpoYY',
durangoTime: '2020-12-05T05:00:00Z',
etnaTime: '2020-12-05T05:00:00Z',
};
3 changes: 2 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export * from './getTransferableInputsByTx';
export * from './getTransferableOutputsByTx';
export * from './getUtxoInfo';
export * from './getBurnedAmountByTx';
export * from './validateBurnedAmount';
export * from './validateBurnedAmount/validateBurnedAmount';
export * from './isEtnaEnabled';
export { unpackWithManager, getManagerForVM, packTx } from './packTx';
8 changes: 8 additions & 0 deletions src/utils/isEtnaEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { GetUpgradesInfoResponse } from '../info/model';

export const isEtnaEnabled = (
upgradesInfo: GetUpgradesInfoResponse,
): boolean => {
const { etnaTime } = upgradesInfo;
return new Date(etnaTime) < new Date();
};
136 changes: 0 additions & 136 deletions src/utils/validateBurnedAmount.ts

This file was deleted.

100 changes: 100 additions & 0 deletions src/utils/validateBurnedAmount/validateBurnedAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Context } from '../../vms/context/model';
import type { Transaction, UnsignedTx } from '../../vms/common';
import type { EVMTx } from '../../serializable/evm';
import { isImportExportTx as isEvmImportExportTx } from '../../serializable/evm';
import { getBurnedAmountByTx } from '../getBurnedAmountByTx';
import type { AvaxTx } from '../../serializable/avax';
import { validateDynamicBurnedAmount } from './validateDynamicBurnedAmount';
import type { GetUpgradesInfoResponse } from '../../info/model';
import { isEtnaEnabled } from '../isEtnaEnabled';
import { validateStaticBurnedAmount } from './validateStaticBurnedAmount';
import { costCorethTx } from '../costs';
import { calculateFee } from '../../vms/pvm/txs/fee/calculator';

import {
isAddPermissionlessDelegatorTx,
isAddPermissionlessValidatorTx,
isAddSubnetValidatorTx,
isCreateChainTx,
isCreateSubnetTx,
isPvmBaseTx,
isExportTx as isPvmExportTx,
isImportTx as isPvmImportTx,
isRemoveSubnetValidatorTx,
isTransferSubnetOwnershipTx,
} from '../../serializable/pvm';

const _getBurnedAmount = (tx: Transaction, context: Context) => {
const burnedAmounts = getBurnedAmountByTx(tx as AvaxTx | EVMTx);
return burnedAmounts.get(context.avaxAssetID) ?? 0n;
};

// Check supported pvm transactions for Etna
// Todo: add isAvmBaseTx, isAvmExportTx and isAvmImportTx when avm dynmamic fee is implemented
const isEtnaSupported = (tx: Transaction) => {
return (
// isAvmBaseTx(tx) || // not implemented
// isAvmExportTx(tx) || // not implemented
// isAvmImportTx(tx) || // not implemented
isPvmBaseTx(tx) ||
isPvmExportTx(tx) ||
isPvmImportTx(tx) ||
isAddPermissionlessValidatorTx(tx) ||
isAddPermissionlessDelegatorTx(tx) ||
isAddSubnetValidatorTx(tx) ||
isCreateChainTx(tx) ||
isCreateSubnetTx(tx) ||
isRemoveSubnetValidatorTx(tx) ||
isTransferSubnetOwnershipTx(tx)
);
};

/**
* Validate burned amount for avalanche transactions
*
* @param unsignedTx: unsigned transaction
* @param burnedAmount: burned amount in nAVAX
* @param baseFee
** c-chain: fetched from the network and converted into nAvax (https://docs.avax.network/quickstart/transaction-fees#c-chain-fees)
** x/p-chain: pvm dynamic fee caculator, https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/fee/dynamic_calculator.go
* @param feeTolerance: tolerance percentage range where the burned amount is considered valid. e.g.: with FeeTolerance = 20% -> (expectedFee <= burnedAmount <= expectedFee * 1.2)
* @return {boolean} isValid: : true if the burned amount is valid, false otherwise.
* @return {bigint} txFee: burned amount in nAVAX
*/
export const validateBurnedAmount = ({
unsignedTx,
context,
upgradesInfo,
burnedAmount,
baseFee,
feeTolerance,
}: {
unsignedTx: UnsignedTx;
context: Context;
upgradesInfo?: GetUpgradesInfoResponse;
burnedAmount?: bigint;
baseFee: bigint;
feeTolerance: number;
}): { isValid: boolean; txFee: bigint } => {
const tx = unsignedTx.getTx();
const burned = burnedAmount ?? _getBurnedAmount(tx, context);

if (
isEvmImportExportTx(tx) ||
(upgradesInfo && isEtnaEnabled(upgradesInfo) && isEtnaSupported(tx))
) {
const feeAmount = isEvmImportExportTx(tx)
? baseFee * costCorethTx(unsignedTx)
: calculateFee(tx, context.platformFeeConfig.weights, baseFee);
return validateDynamicBurnedAmount({
burnedAmount: burned,
feeAmount,
feeTolerance,
});
}
return validateStaticBurnedAmount({
unsignedTx,
context,
burnedAmount: burned,
});
};
68 changes: 68 additions & 0 deletions src/utils/validateBurnedAmount/validateDynamicBurnedAmount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { validateDynamicBurnedAmount } from './validateDynamicBurnedAmount';

describe('validateDynamicBurnedAmount', () => {
it('throws an expected error if feeTolerance is less than 1', () => {
expect(() =>
validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower,
feeAmount: 280750n,
feeTolerance: 0.5,
}),
).toThrowError('feeTolerance must be [1,100]');
});
it('throws an expected error if feeTolerance is greater than 100', () => {
expect(() =>
validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower,
feeAmount: 280750n,
feeTolerance: 101,
}),
).toThrowError('feeTolerance must be [1,100]');
});

it('returns false if burned amount is over the tolerance range', () => {
const resultHigher = validateDynamicBurnedAmount({
burnedAmount: (280750n * 151n) / 100n, // 51% higher
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultHigher).toStrictEqual({
isValid: false,
txFee: (280750n * 151n) / 100n,
});
});

it('returns false if burned amount is below the tolerance range', () => {
const resultLower = validateDynamicBurnedAmount({
burnedAmount: (280750n * 49n) / 100n, // 51% lower
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultLower).toStrictEqual({
isValid: false,
txFee: (280750n * 49n) / 100n,
});
});
it('returns true if burned amount is within the min tolerance range', () => {
const resultLower = validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultLower).toStrictEqual({
isValid: true,
txFee: (280750n * 75n) / 100n,
});
});
it('returns true if burned amount is within the max tolerance range', () => {
const resultHigher = validateDynamicBurnedAmount({
burnedAmount: (280750n * 125n) / 100n, // 25% higher
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultHigher).toStrictEqual({
isValid: true,
txFee: (280750n * 125n) / 100n,
});
});
});
32 changes: 32 additions & 0 deletions src/utils/validateBurnedAmount/validateDynamicBurnedAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Validate dynamic burned amount for avalanche c/p transactions
*
* @param burnedAmount: burned amount in nAVAX
* @param feeAmount: fee
* @param feeTolerance: tolerance percentage range where the burned amount is considered valid. e.g.: with FeeTolerance = 20% -> (expectedFee <= burnedAmount <= expectedFee * 1.2)
* @return {boolean} isValid: : true if the burned amount is valid, false otherwise.
* @return {bigint} txFee: burned amount in nAVAX
*/
export const validateDynamicBurnedAmount = ({
burnedAmount,
feeAmount,
feeTolerance,
}: {
burnedAmount: bigint;
feeAmount: bigint;
feeTolerance: number;
}): { isValid: boolean; txFee: bigint } => {
const feeToleranceInt = Math.floor(feeTolerance);

if (feeToleranceInt < 1 || feeToleranceInt > 100) {
throw new Error('feeTolerance must be [1,100]');
}

const min = (feeAmount * (100n - BigInt(feeToleranceInt))) / 100n;
const max = (feeAmount * (100n + BigInt(feeToleranceInt))) / 100n;

return {
isValid: burnedAmount >= min && burnedAmount <= max,
txFee: burnedAmount,
};
};
Loading
Loading