Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Fee error while sending a transaction from sidechain to mainchain #1928

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
# Fetch project source with GitHub Actions Checkout.
- uses: actions/checkout@v3
# Run the "semgrep ci" command on the command line of the docker image.
- run: semgrep ci --sarif --output=semgrep.sarif --metrics off
- run: semgrep --sarif --output=semgrep.sarif --metrics off
env:
# Connect to Semgrep Cloud Platform through your SEMGREP_APP_TOKEN.
# Generate a token from Semgrep Cloud Platform > Settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const {
EVENT_NAME_INVALID_CERTIFICATE_SIGNATURE,
EVENT_NAME_INVALID_REGISTRATION_SIGNATURE,
EVENT_NAME_CHAIN_ACCOUNT_UPDATED,
EVENT_NAME_CCM_SENT_SUCCESS,
EVENT_NAME_CCM_SEND_SUCCESS,
EVENT_NAME_CCM_SENT_FAILED,
EVENT_NAME_CCM_PROCESSED,
EVENT_NAME_TERMINATED_STATE_CREATED,
Expand Down Expand Up @@ -124,7 +124,7 @@ const EVENT_TOPIC_MAPPINGS_BY_MODULE = {
[EVENT_NAME_INVALID_CERTIFICATE_SIGNATURE]: ['transactionID', 'chainID'],
[EVENT_NAME_INVALID_REGISTRATION_SIGNATURE]: ['transactionID', 'chainID'],
[EVENT_NAME_CHAIN_ACCOUNT_UPDATED]: ['transactionID', 'sendingChainID'],
[EVENT_NAME_CCM_SENT_SUCCESS]: [
[EVENT_NAME_CCM_SEND_SUCCESS]: [
'transactionID',
'sendingChainID',
'receivingChainID',
Expand Down
4 changes: 2 additions & 2 deletions services/blockchain-connector/shared/sdk/constants/names.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const MODULE_NAME_INTEROPERABILITY = 'interoperability';
const EVENT_NAME_INVALID_CERTIFICATE_SIGNATURE = 'invalidCertificateSignature';
const EVENT_NAME_INVALID_REGISTRATION_SIGNATURE = 'invalidRegistrationSignature';
const EVENT_NAME_CHAIN_ACCOUNT_UPDATED = 'chainAccountUpdated';
const EVENT_NAME_CCM_SENT_SUCCESS = 'ccmSendSuccess';
const EVENT_NAME_CCM_SEND_SUCCESS = 'ccmSendSuccess';
const EVENT_NAME_CCM_SENT_FAILED = 'ccmSentFailed';
const EVENT_NAME_CCM_PROCESSED = 'ccmProcessed';
const EVENT_NAME_TERMINATED_STATE_CREATED = 'terminatedStateCreated';
Expand Down Expand Up @@ -127,7 +127,7 @@ module.exports = {
EVENT_NAME_INVALID_CERTIFICATE_SIGNATURE,
EVENT_NAME_INVALID_REGISTRATION_SIGNATURE,
EVENT_NAME_CHAIN_ACCOUNT_UPDATED,
EVENT_NAME_CCM_SENT_SUCCESS,
EVENT_NAME_CCM_SEND_SUCCESS,
EVENT_NAME_CCM_SENT_FAILED,
EVENT_NAME_CCM_PROCESSED,
EVENT_NAME_TERMINATED_STATE_CREATED,
Expand Down
10 changes: 10 additions & 0 deletions services/blockchain-indexer/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ const EVENT = Object.freeze({
COMMAND_EXECUTION_RESULT: 'commandExecutionResult',
REWARD_MINTED: 'rewardMinted',
CCM_SEND_SUCCESS: 'ccmSendSuccess',
CCM_SENT_FAILED: 'ccmSentFailed',
});

const CCM_SENT_FAILED_ERROR_MESSAGE = Object.freeze({
1: 'Receiving chain is not active.',
11: 'Failed to pay message fee.',
12: 'Invalid params provided.',
13: 'Invalid CCM format.',
14: 'Sending chain cannot be the receiving chain.',
});

const EVENT_TOPIC_PREFIX = Object.freeze({
Expand Down Expand Up @@ -239,4 +248,5 @@ module.exports = {
LENGTH_BYTE_ID,
LENGTH_ID,
DEFAULT_NUM_OF_SIGNATURES,
CCM_SENT_FAILED_ERROR_MESSAGE,
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const {
getCurrentChainID,
} = require('./interoperability');
const { dryRunTransactions } = require('./transactionsDryRun');
const { tokenHasUserAccount, getTokenConstants } = require('./token');
const { tokenHasUserAccount, getTokenConstants, getTokenBalances } = require('./token');
const { getSchemas } = require('./schemas');

const {
Expand All @@ -42,6 +42,7 @@ const {
LENGTH_BYTE_SIGNATURE,
LENGTH_BYTE_ID,
DEFAULT_NUM_OF_SIGNATURES,
CCM_SENT_FAILED_ERROR_MESSAGE,
} = require('../../constants');

const { getLisk32AddressFromPublicKey } = require('../../utils/account');
Expand Down Expand Up @@ -157,12 +158,28 @@ const getCcmBuffer = async transaction => {
if (transaction.module !== MODULE.TOKEN || transaction.command !== COMMAND.TRANSFER_CROSS_CHAIN)
return null;

// TODO: Add error handling
const {
data: { events },
} = await dryRunTransactions({ transaction, skipVerify: true });
const ccmSendSuccess = events.find(event => event.name === EVENT.CCM_SEND_SUCCESS);

if (!ccmSendSuccess) {
const { data: dryRunResult } = await dryRunTransactions({ transaction, skipVerify: false });
if (dryRunResult.errorMessage) {
throw new ValidationException(dryRunResult.errorMessage);
}

const ccmSentFailed = dryRunResult.events.find(event => event.name === EVENT.CCM_SENT_FAILED);
if (ccmSentFailed) {
throw new ValidationException(CCM_SENT_FAILED_ERROR_MESSAGE[ccmSentFailed.code]);
}

// If none of the known reasons are matched, do not assign messageFee
// No messageFee will result in not bouncing the failed CCM
logger.warn(JSON.stringify({ transaction, dryRunResult }, null, '\t'));
return Buffer.from('', 'hex');
}

// Encode CCM (required to calculate CCM length)
const { ccm } = ccmSendSuccess.data;
const ccmEncoded = await requestConnector('encodeCCM', { ccm });
Expand Down Expand Up @@ -297,6 +314,25 @@ const validateTransactionParams = async transaction => {
}
}

if (transaction.params.tokenID) {
const senderAddress = getLisk32AddressFromPublicKey(transaction.senderPublicKey);
const {
data: { extraCommandFees },
} = await getTokenConstants();
const {
data: [balanceInfo],
} = await getTokenBalances({ address: senderAddress, tokenID: transaction.params.tokenID });

if (
BigInt(balanceInfo.availableBalance) <
BigInt(transaction.params.amount) + BigInt(extraCommandFees.userAccountInitializationFee)
) {
throw new ValidationException(
`${senderAddress} has insufficient balance for ${transaction.params.tokenID} to send the transaction.`,
);
}
}

const allSchemas = await getSchemas();
const txCommand = allSchemas.data.commands.find(
e => e.moduleCommand === `${transaction.module}:${transaction.command}`,
Expand All @@ -318,6 +354,16 @@ const validateTransactionParams = async transaction => {
}
};

const validateUserHasTokenAccount = async (tokenID, address) => {
const response = await tokenHasUserAccount({ tokenID, address });

if (!response.data.isExists) {
throw new ValidationException(
`${address} has no balance for tokenID: ${tokenID}, necessary to make this transaction. Please top-up the account with some balance and retry.`,
);
}
};

const estimateTransactionFees = async params => {
const estimateTransactionFeesRes = {
data: {
Expand All @@ -326,9 +372,13 @@ const estimateTransactionFees = async params => {
meta: {},
};

const senderAddress = getLisk32AddressFromPublicKey(params.transaction.senderPublicKey);
const feeEstimatePerByte = getFeeEstimates();

// Validate if the sender has balance for transaction fee
await validateUserHasTokenAccount(feeEstimatePerByte.feeTokenID, senderAddress);
await validateTransactionParams(params.transaction);

const senderAddress = getLisk32AddressFromPublicKey(params.transaction.senderPublicKey);
const numberOfSignatures = await getNumberOfSignatures(senderAddress);

const trxWithMockProps = await mockTransaction(params.transaction, numberOfSignatures);
Expand All @@ -337,20 +387,21 @@ const estimateTransactionFees = async params => {
transaction: trxWithMockProps,
additionalFee: additionalFees.total.toString(),
});
const feeEstimatePerByte = getFeeEstimates();

// Calculate message fee for cross-chain transfers
if (
params.transaction.module === MODULE.TOKEN &&
params.transaction.command === COMMAND.TRANSFER_CROSS_CHAIN
) {
const channelInfo = await resolveChannelInfo(params.transaction.params.receivingChainID);
await validateUserHasTokenAccount(channelInfo.messageFeeTokenID, senderAddress);

// Calculate message fee
const ccmBuffer = await getCcmBuffer({
...formattedTransaction,
fee: formattedTransaction.minFee,
});
const ccmLength = ccmBuffer.length;
const channelInfo = await resolveChannelInfo(params.transaction.params.receivingChainID);

const ccmByteFee = BigInt(ccmLength) * BigInt(channelInfo.minReturnFeePerByte);
const totalMessageFee = additionalFees.params.messageFee
Expand Down Expand Up @@ -398,7 +449,6 @@ const estimateTransactionFees = async params => {

const { minFee, size } = formattedTransaction;

// TODO: Remove BUFFER_BYTES_LENGTH support after RC is tagged
const estimatedMinFee =
BigInt(minFee) + BigInt(BUFFER_BYTES_LENGTH * feeEstimatePerByte.minFeePerByte);

Expand Down Expand Up @@ -446,10 +496,12 @@ module.exports = {
estimateTransactionFees,

// Export for the unit tests
getCcmBuffer,
calcDynamicFeeEstimates,
mockTransaction,
calcAdditionalFees,
filterOptionalProps,
getNumberOfSignatures,
validateTransactionParams,
validateUserHasTokenAccount,
};
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,7 @@ const mockTxResult = {
fee: {
minimum: {
byteFee: '160000',
additionalFees: {
userAccountInitializationFee: '1',
},
additionalFees: {},
},
},
},
Expand Down Expand Up @@ -242,7 +240,7 @@ const mockRegisterValidatorTxResult = {
},
};

const mockTxsenderAddress = 'lskguo9kqnea2zsfo3a6qppozsxsg92nuuma3p7ad';
const mockTxSenderAddress = 'lskguo9kqnea2zsfo3a6qppozsxsg92nuuma3p7ad';

const mockTxAuthAccountInfo = {
data: {
Expand All @@ -253,7 +251,7 @@ const mockTxAuthAccountInfo = {
},
};

const mockTxrequestConnector = {
const mockTxRequestConnector = {
module: 'token',
command: 'transfer',
fee: '100000000',
Expand All @@ -273,7 +271,7 @@ const mockTxrequestConnector = {
minFee: '166000',
};

const mockTransferCrossChainTxrequestConnector = {
const mockTransferCrossChainTxRequestConnector = {
module: 'token',
command: 'transferCrossChain',
fee: '100000000',
Expand All @@ -296,7 +294,7 @@ const mockTransferCrossChainTxrequestConnector = {
minFee: '166000',
};

const mockRegisterValidatorTxrequestConnector = {
const mockRegisterValidatorTxRequestConnector = {
transaction: {
module: 'pos',
command: 'registerValidator',
Expand Down Expand Up @@ -400,17 +398,17 @@ module.exports = {
mockTxRequest,
mockTransferCrossChainTxRequest,
mockTxResult,
mockTxsenderAddress,
mockTxSenderAddress,
mockTxAuthAccountInfo,
mockTxrequestConnector,
mockTxRequestConnector,
mockTxFeeEstimate,
posConstants,
mockEscrowAccountExistsRequestConnector,
mockTransferCrossChainTxrequestConnector,
mockTransferCrossChainTxRequestConnector,
mockTransferCrossChainTxResult,
mockAuthAccountInfo,
mockAuthInfoForMultisigAccount,
mockRegisterValidatorTxrequestConnector,
mockRegisterValidatorTxRequestConnector,
mockRegisterValidatorTxResult,

mockInteroperabilitySubmitMainchainCrossChainUpdateTxRequest,
Expand Down
Loading