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

Commit

Permalink
Merge pull request #1928 from LiskHQ/1925-fee-error-while-sending-a-t…
Browse files Browse the repository at this point in the history
…ransaction-from-sidechain-to-mainchain

Fee error while sending a transaction from sidechain to mainchain
  • Loading branch information
sameersubudhi authored Nov 17, 2023
2 parents e5ac04f + 08c7cb6 commit 4df7bb4
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 56 deletions.
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

0 comments on commit 4df7bb4

Please sign in to comment.