diff --git a/src/fixtures/info.ts b/src/fixtures/info.ts new file mode 100644 index 000000000..149856066 --- /dev/null +++ b/src/fixtures/info.ts @@ -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', +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index b36f58155..67266936e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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'; diff --git a/src/utils/isEtnaEnabled.ts b/src/utils/isEtnaEnabled.ts new file mode 100644 index 000000000..3762b98dc --- /dev/null +++ b/src/utils/isEtnaEnabled.ts @@ -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(); +}; diff --git a/src/utils/validateBurnedAmount.ts b/src/utils/validateBurnedAmount.ts deleted file mode 100644 index f162e6e55..000000000 --- a/src/utils/validateBurnedAmount.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { Context } from '../vms/context/model'; -import { - isAddDelegatorTx, - isAddPermissionlessDelegatorTx, - isAddPermissionlessValidatorTx, - isAddSubnetValidatorTx, - isAddValidatorTx, - isCreateChainTx, - isCreateSubnetTx, - isPvmBaseTx, - isExportTx as isPvmExportTx, - isImportTx as isPvmImportTx, - isRemoveSubnetValidatorTx, - isTransferSubnetOwnershipTx, - isTransformSubnetTx, -} from '../serializable/pvm'; -import type { Transaction, UnsignedTx } from '../vms/common'; -import type { EVMTx } from '../serializable/evm'; -import { isImportExportTx as isEvmImportExportTx } from '../serializable/evm'; -import { costCorethTx } from './costs'; -import { - isAvmBaseTx, - isExportTx as isAvmExportTx, - isImportTx as isAvmImportTx, -} from '../serializable/avm'; -import { getBurnedAmountByTx } from './getBurnedAmountByTx'; -import type { AvaxTx } from '../serializable/avax'; -import { PrimaryNetworkID } from '../constants/networkIDs'; - -const _getBurnedAmount = (tx: Transaction, context: Context) => { - const burnedAmounts = getBurnedAmountByTx(tx as AvaxTx | EVMTx); - return burnedAmounts.get(context.avaxAssetID) ?? 0n; -}; - -export const validateBurnedAmount = ({ - unsignedTx, - context, - burnedAmount, - evmBaseFee, - evmFeeTolerance, -}: { - unsignedTx: UnsignedTx; - context: Context; - burnedAmount?: bigint; - evmBaseFee?: bigint; // fetched from the network and converted into nAvax (https://docs.avax.network/quickstart/transaction-fees#c-chain-fees) - evmFeeTolerance?: number; // tolerance percentage range where the burned amount is considered valid. e.g.: with evmFeeTolerance = 20% -> (evmBaseFee * 0.8 <= burnedAmount <= evmBaseFee * 1.2) -}): { isValid: boolean; txFee: bigint } => { - const tx = unsignedTx.getTx(); - const burned = burnedAmount ?? _getBurnedAmount(tx, context); - - const validate = (expectedAmount: bigint) => ({ - isValid: burned === expectedAmount, - txFee: expectedAmount, - }); - - if (isEvmImportExportTx(tx)) { - if (!evmBaseFee || !evmFeeTolerance) { - throw new Error('missing evm fee data'); - } - - const feeToleranceInt = Math.floor(evmFeeTolerance); - - if (feeToleranceInt < 1 || feeToleranceInt > 100) { - throw new Error('evmFeeTolerance must be [1,100]'); - } - - const feeAmount = evmBaseFee * costCorethTx(unsignedTx); - const min = (feeAmount * (100n - BigInt(feeToleranceInt))) / 100n; - const max = (feeAmount * (100n + BigInt(feeToleranceInt))) / 100n; - - return { - isValid: burned >= min && burned <= max, - txFee: burned, - }; - } - - if (isAddValidatorTx(tx)) { - return validate(context.addPrimaryNetworkValidatorFee); - } - - if (isAddDelegatorTx(tx)) { - return validate(context.addPrimaryNetworkDelegatorFee); - } - - if (isCreateSubnetTx(tx)) { - return validate(context.createSubnetTxFee); - } - - if (isCreateChainTx(tx)) { - return validate(context.createBlockchainTxFee); - } - - if (isAddSubnetValidatorTx(tx)) { - return validate(context.addSubnetValidatorFee); - } - - if (isTransformSubnetTx(tx)) { - return validate(context.transformSubnetTxFee); - } - - if (isAddPermissionlessValidatorTx(tx)) { - const isPrimarySubnet = - tx.subnetValidator.subnetId.toString() === PrimaryNetworkID.toString(); - - return validate( - isPrimarySubnet - ? context.addPrimaryNetworkValidatorFee - : context.addSubnetValidatorFee, - ); - } - - if (isAddPermissionlessDelegatorTx(tx)) { - const isPrimarySubnet = - tx.subnetValidator.subnetId.toString() === PrimaryNetworkID.toString(); - return validate( - isPrimarySubnet - ? context.addPrimaryNetworkDelegatorFee - : context.addSubnetDelegatorFee, - ); - } - - if ( - isAvmBaseTx(tx) || - isPvmBaseTx(tx) || - isAvmExportTx(tx) || - isAvmImportTx(tx) || - isPvmExportTx(tx) || - isPvmImportTx(tx) || - isRemoveSubnetValidatorTx(tx) || - isTransferSubnetOwnershipTx(tx) - ) { - return validate(context.baseTxFee); - } - - throw new Error(`tx type is not supported`); -}; diff --git a/src/utils/validateBurnedAmount/validateBurnedAmount.ts b/src/utils/validateBurnedAmount/validateBurnedAmount.ts new file mode 100644 index 000000000..ed62cbb68 --- /dev/null +++ b/src/utils/validateBurnedAmount/validateBurnedAmount.ts @@ -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, + }); +}; diff --git a/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.test.ts b/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.test.ts new file mode 100644 index 000000000..12b32cf5e --- /dev/null +++ b/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.test.ts @@ -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, + }); + }); +}); diff --git a/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.ts b/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.ts new file mode 100644 index 000000000..75353c5c0 --- /dev/null +++ b/src/utils/validateBurnedAmount/validateDynamicBurnedAmount.ts @@ -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, + }; +}; diff --git a/src/utils/validateBurnedAmount.test.ts b/src/utils/validateBurnedAmount/validateStaticBurnedAmount.test.ts similarity index 54% rename from src/utils/validateBurnedAmount.test.ts rename to src/utils/validateBurnedAmount/validateStaticBurnedAmount.test.ts index 21b68eb75..c4cfd9b80 100644 --- a/src/utils/validateBurnedAmount.test.ts +++ b/src/utils/validateBurnedAmount/validateStaticBurnedAmount.test.ts @@ -1,16 +1,15 @@ -import { testAddress1, testAddress2, testEthAddress1 } from '../fixtures/vms'; -import { testContext } from '../fixtures/context'; -import { newExportTxFromBaseFee, newImportTxFromBaseFee } from '../vms/evm'; -import { Utxo } from '../serializable/avax/utxo'; -import { utxoId } from '../fixtures/avax'; -import { Address, Id } from '../serializable/fxs/common'; -import { OutputOwners, TransferOutput } from '../serializable/fxs/secp256k1'; -import { BigIntPr, Int } from '../serializable/primitives'; +import { testAddress1, testAddress2 } from '../../fixtures/vms'; +import { testContext } from '../../fixtures/context'; +import { Utxo } from '../../serializable/avax/utxo'; +import { utxoId } from '../../fixtures/avax'; +import { Address, Id } from '../../serializable/fxs/common'; +import { OutputOwners, TransferOutput } from '../../serializable/fxs/secp256k1'; +import { BigIntPr, Int } from '../../serializable/primitives'; import { newBaseTx as avmBaseTx, newExportTx as avmExportTx, newImportTx as avmImportTx, -} from '../vms/avm'; +} from '../../vms/avm'; import { newBaseTx as pvmBaseTx, newExportTx as pvmExportTx, @@ -25,13 +24,16 @@ import { newAddPermissionlessDelegatorTx, newRemoveSubnetValidatorTx, newTransferSubnetOwnershipTx, -} from '../vms/pvm'; -import { TransferableOutput } from '../serializable'; -import { nodeId } from '../fixtures/common'; -import { validateBurnedAmount } from './validateBurnedAmount'; -import { testSubnetId } from '../fixtures/transactions'; -import { PrimaryNetworkID } from '../constants/networkIDs'; -import { blsPublicKeyBytes, blsSignatureBytes } from '../fixtures/primitives'; +} from '../../vms/pvm'; +import { TransferableOutput } from '../../serializable'; +import { nodeId } from '../../fixtures/common'; +import { testSubnetId } from '../../fixtures/transactions'; +import { PrimaryNetworkID } from '../../constants/networkIDs'; +import { + blsPublicKeyBytes, + blsSignatureBytes, +} from '../../fixtures/primitives'; +import { validateStaticBurnedAmount } from './validateStaticBurnedAmount'; const utxoMock = new Utxo( utxoId(), @@ -54,200 +56,7 @@ const outputMock = new TransferableOutput( ), ); -describe('validateBurnedAmount', () => { - describe('missing burned amount', () => { - it('calculates the burned amount', () => { - const unsignedTx = avmBaseTx( - testContext, - [testAddress1], - [utxoMock], - [outputMock], - ); - - const result = validateBurnedAmount({ - unsignedTx, - context: testContext, - }); - - expect(result).toStrictEqual({ - isValid: true, - txFee: testContext.baseTxFee, - }); - }); - }); - - describe('export from C', () => { - const unsignedTx = newExportTxFromBaseFee( - testContext, - 25n, - 1000000000n, - 'X', - testEthAddress1, - [testAddress1], - 1n, - ); - - it('throws if fee data is missing', () => { - expect(() => - validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower - }), - ).toThrowError('missing evm fee data'); - - expect(() => - validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower - evmBaseFee: 25n, - }), - ).toThrowError('missing evm fee data'); - }); - - it('throws if evmFeeTolerance is incorrect', () => { - expect(() => - validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower, - evmBaseFee: 25n, - evmFeeTolerance: 0.5, - }), - ).toThrowError('evmFeeTolerance must be [1,100]'); - - expect(() => - validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower, - evmBaseFee: 25n, - evmFeeTolerance: 101, - }), - ).toThrowError('evmFeeTolerance must be [1,100]'); - }); - - it('returns true if burned amount is in the tolerance range', () => { - const resultLower = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - const resultHigher = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 125n) / 100n, // 25% higher - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - expect(resultLower).toStrictEqual({ - isValid: true, - txFee: (280750n * 75n) / 100n, - }); - expect(resultHigher).toStrictEqual({ - isValid: true, - txFee: (280750n * 125n) / 100n, - }); - }); - - it('returns false if burned amount is not in the tolerance range', () => { - const resultLower = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 49n) / 100n, // 51% lower - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - const resultHigher = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 151n) / 100n, // 51% higher - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - expect(resultLower).toStrictEqual({ - isValid: false, - txFee: (280750n * 49n) / 100n, - }); - expect(resultHigher).toStrictEqual({ - isValid: false, - txFee: (280750n * 151n) / 100n, - }); - }); - }); - - describe('import to C', () => { - const unsignedTx = newImportTxFromBaseFee( - testContext, - testEthAddress1, - [testAddress1], - [utxoMock], - 'X', - 25n, - ); - - it('returns true if burned amount is in the tolerance range', () => { - const resultLower = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 75n) / 100n, // 25% lower - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - const resultHigher = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 125n) / 100n, // 25% higher - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - expect(resultLower).toStrictEqual({ - isValid: true, - txFee: (280750n * 75n) / 100n, - }); - expect(resultHigher).toStrictEqual({ - isValid: true, - txFee: (280750n * 125n) / 100n, - }); - }); - - it('returns false if burned amount is not in the tolerance range', () => { - const resultLower = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 49n) / 100n, // 51% lower - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - const resultHigher = validateBurnedAmount({ - unsignedTx, - context: testContext, - burnedAmount: (280750n * 151n) / 100n, // 51% higher - evmBaseFee: 25n, - evmFeeTolerance: 50.9, - }); - - expect(resultLower).toStrictEqual({ - isValid: false, - txFee: (280750n * 49n) / 100n, - }); - expect(resultHigher).toStrictEqual({ - isValid: false, - txFee: (280750n * 151n) / 100n, - }); - }); - }); - +describe('validateStaticBurnedAmount', () => { const testData = [ { name: 'base tx on X', @@ -514,7 +323,7 @@ describe('validateBurnedAmount', () => { describe.each(testData)('$name', ({ unsignedTx, correctBurnedAmount }) => { it('returns true if burned amount is correct', () => { - const result = validateBurnedAmount({ + const result = validateStaticBurnedAmount({ unsignedTx, context: testContext, burnedAmount: correctBurnedAmount, @@ -527,7 +336,7 @@ describe('validateBurnedAmount', () => { }); it('returns false if burned amount is not correct', () => { - const result = validateBurnedAmount({ + const result = validateStaticBurnedAmount({ unsignedTx, context: testContext, burnedAmount: correctBurnedAmount - 1n, diff --git a/src/utils/validateBurnedAmount/validateStaticBurnedAmount.ts b/src/utils/validateBurnedAmount/validateStaticBurnedAmount.ts new file mode 100644 index 000000000..ce272ed86 --- /dev/null +++ b/src/utils/validateBurnedAmount/validateStaticBurnedAmount.ts @@ -0,0 +1,111 @@ +import type { Context } from '../../vms/context/model'; +import { + isAddDelegatorTx, + isAddPermissionlessDelegatorTx, + isAddPermissionlessValidatorTx, + isAddSubnetValidatorTx, + isAddValidatorTx, + isCreateChainTx, + isCreateSubnetTx, + isPvmBaseTx, + isExportTx as isPvmExportTx, + isImportTx as isPvmImportTx, + isRemoveSubnetValidatorTx, + isTransferSubnetOwnershipTx, + isTransformSubnetTx, +} from '../../serializable/pvm'; +import type { UnsignedTx } from '../../vms/common'; +import { + isAvmBaseTx, + isExportTx as isAvmExportTx, + isImportTx as isAvmImportTx, +} from '../../serializable/avm'; +import { PrimaryNetworkID } from '../../constants/networkIDs'; + +/** + * Validate static burned amount for avalanche x/p transactions + * + * @param unsignedTx: unsigned transaction + * @param context + * @param burnedAmount: burned amount in nAVAX + * @return {boolean} isValid: : true if the burned amount is valid, false otherwise. + * @return {bigint} txFee: burned amount in nAVAX + */ +export const validateStaticBurnedAmount = ({ + unsignedTx, + context, + burnedAmount, +}: { + unsignedTx: UnsignedTx; + context: Context; + burnedAmount: bigint; +}): { isValid: boolean; txFee: bigint } => { + const tx = unsignedTx.getTx(); + + if (isAddValidatorTx(tx)) { + return validate(burnedAmount, context.addPrimaryNetworkValidatorFee); + } + + if (isAddDelegatorTx(tx)) { + return validate(burnedAmount, context.addPrimaryNetworkDelegatorFee); + } + + if (isCreateSubnetTx(tx)) { + return validate(burnedAmount, context.createSubnetTxFee); + } + + if (isCreateChainTx(tx)) { + return validate(burnedAmount, context.createBlockchainTxFee); + } + + if (isAddSubnetValidatorTx(tx)) { + return validate(burnedAmount, context.addSubnetValidatorFee); + } + + if (isTransformSubnetTx(tx)) { + return validate(burnedAmount, context.transformSubnetTxFee); + } + + if (isAddPermissionlessValidatorTx(tx)) { + const isPrimarySubnet = + tx.subnetValidator.subnetId.toString() === PrimaryNetworkID.toString(); + + return validate( + burnedAmount, + isPrimarySubnet + ? context.addPrimaryNetworkValidatorFee + : context.addSubnetValidatorFee, + ); + } + + if (isAddPermissionlessDelegatorTx(tx)) { + const isPrimarySubnet = + tx.subnetValidator.subnetId.toString() === PrimaryNetworkID.toString(); + return validate( + burnedAmount, + isPrimarySubnet + ? context.addPrimaryNetworkDelegatorFee + : context.addSubnetDelegatorFee, + ); + } + + if ( + isAvmBaseTx(tx) || + isPvmBaseTx(tx) || + isAvmExportTx(tx) || + isAvmImportTx(tx) || + isPvmExportTx(tx) || + isPvmImportTx(tx) || + isRemoveSubnetValidatorTx(tx) || + isTransferSubnetOwnershipTx(tx) + ) { + return validate(burnedAmount, context.baseTxFee); + } + + throw new Error(`tx type is not supported`); +}; + +const validate = (burnedAmount: bigint, expectedAmount: bigint) => ({ + isValid: burnedAmount === expectedAmount, + txFee: expectedAmount, +});