From 2b54a0f81e8c38c6eb4b9d560251fe87b7b77f55 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Tue, 24 Dec 2024 14:32:30 +0200 Subject: [PATCH 1/8] DASH-1267 Always get highest value of the gas price suggestion. --- controller/main.js | 8 ++-- controller/utils/prices.js | 65 +++++--------------------------- controller/utils/transactions.js | 1 - 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/controller/main.js b/controller/main.js index 77b9d97..56c5b45 100644 --- a/controller/main.js +++ b/controller/main.js @@ -28,8 +28,8 @@ import { import { convertWeiToEth, convertWeiToGwei, - getMiddleEthGasPriceSuggestion, - getMiddlePolygonGasPriceSuggestion, + getHighestEthGasPriceSuggestion, + getHighestPolygonGasPriceSuggestion, } from './utils/prices.js'; import config from '../config/config.js'; @@ -80,7 +80,7 @@ class MainCtrl { // Check is INFURA_ETH and INFURA_POLYGON variables are valid const isUsingGasApi = !!parseInt(USE_GAS_API); if (isUsingGasApi) { - const ethGasPriceSuggestion = await getMiddleEthGasPriceSuggestion(); + const ethGasPriceSuggestion = await getHighestEthGasPriceSuggestion(); console.log( convertWeiToGwei(ethGasPriceSuggestion), @@ -91,7 +91,7 @@ class MainCtrl { 'Please, check "INFURA_ETH" variable: ' + JSON.stringify(ethGasPriceSuggestion), ); - const polyGasPriceSuggestion = await getMiddlePolygonGasPriceSuggestion(); + const polyGasPriceSuggestion = await getHighestPolygonGasPriceSuggestion(); console.log( convertWeiToGwei(polyGasPriceSuggestion), 'GWEI - safe gas price for Polygon', diff --git a/controller/utils/prices.js b/controller/utils/prices.js index 8d0ebdf..8585e00 100644 --- a/controller/utils/prices.js +++ b/controller/utils/prices.js @@ -55,90 +55,45 @@ export const getEthGasPriceSuggestion = async () => { ]); }; -// base gas price value + 10% +// base gas price value + 20% const calculateAverageGasPrice = (baseGasPrice) => { - return Math.ceil(baseGasPrice * 1.1); + return Math.ceil(baseGasPrice * 1.2); }; -// base gas price value + 20% +// base gas price value + 40% const calculateHighGasPrice = (baseGasPrice) => { - return Math.ceil(baseGasPrice * 1.2); + return Math.ceil(baseGasPrice * 1.4); }; -const getMiddleGasPriceValue = (gasPriceSuggestions) => { - let gasPriceSuggestion = null; - const numSuggestions = gasPriceSuggestions.length; - - switch (numSuggestions) { - case 1: - gasPriceSuggestion = gasPriceSuggestions[0]; - break; - case 2: - gasPriceSuggestion = Math.max(...gasPriceSuggestions); - break; - case 3: { - gasPriceSuggestions.sort((a, b) => a - b); - gasPriceSuggestion = gasPriceSuggestions[1]; - break; - } - default: { - gasPriceSuggestion = gasPriceSuggestions[0]; - } - } - - return gasPriceSuggestion; -}; +const getHighestGasPriceValue = (gasPriceSuggestions) => Math.max(...gasPriceSuggestions); -export const getMiddleEthGasPriceSuggestion = async () => { +export const getHighestEthGasPriceSuggestion = async () => { const ethGasPriceSuggestions = await getEthGasPriceSuggestion(); - return getMiddleGasPriceValue(ethGasPriceSuggestions); + return getHighestGasPriceValue(ethGasPriceSuggestions); }; -export const getMiddlePolygonGasPriceSuggestion = async () => { +export const getHighestPolygonGasPriceSuggestion = async () => { const polygonGasPriceSuggestions = await getPolygonGasPriceSuggestion(); - return getMiddleGasPriceValue(polygonGasPriceSuggestions); + return getHighestGasPriceValue(polygonGasPriceSuggestions); }; export const getGasPrice = async ({ defaultGasPrice, getGasPriceSuggestionFn, logPrefix, - retryCount, }) => { const isUsingGasApi = !!parseInt(USE_GAS_API); let gasPrice = 0; - let gasPriceSuggestion = 0; if (isUsingGasApi && getGasPriceSuggestionFn) { console.log(`${logPrefix} using gasPrice value from the api:`); const gasPriceSuggestions = await getGasPriceSuggestionFn(); - const numSuggestions = gasPriceSuggestions.length; - - switch (numSuggestions) { - case 1: - gasPriceSuggestion = gasPriceSuggestions[0]; - break; - case 2: - gasPriceSuggestion = Math.max(...gasPriceSuggestions); - break; - case 3: { - if (retryCount === 0) { - gasPriceSuggestions.sort((a, b) => a - b); - gasPriceSuggestion = gasPriceSuggestions[1]; - } else if (retryCount > 0) { - gasPriceSuggestion = Math.max(...gasPriceSuggestions); - } - break; - } - default: { - gasPriceSuggestion = gasPriceSuggestions[0]; - } - } + const gasPriceSuggestion = getHighestGasPriceValue(gasPriceSuggestions); switch (GAS_PRICE_LEVEL) { case 'low': diff --git a/controller/utils/transactions.js b/controller/utils/transactions.js index bd51b36..d9dc0b1 100644 --- a/controller/utils/transactions.js +++ b/controller/utils/transactions.js @@ -54,7 +54,6 @@ export const polygonTransaction = async ({ defaultGasPrice, getGasPriceSuggestionFn, logPrefix, - retryCount, }); } From b56ad14e9629c23782fa8b5a15a05d4c933bdab5 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Wed, 8 Jan 2025 16:36:03 +0200 Subject: [PATCH 2/8] DASH-1269 Make unique obtid. Add there tokenid to parse it on wrap status page. --- controller/api/fio.js | 4 ++-- controller/utils/general.js | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/controller/api/fio.js b/controller/api/fio.js index f065248..c8cea7e 100644 --- a/controller/api/fio.js +++ b/controller/api/fio.js @@ -33,7 +33,7 @@ import { getFioDeltasV2, runUnwrapFioTransaction, } from '../utils/fio-chain.js'; -import { sleep, convertTimestampIntoMs, formatDateYYYYMMDD } from '../utils/general.js'; +import { sleep, convertTimestampIntoMs } from '../utils/general.js'; import { addLogMessage, updateBlockNumberFIOForBurnNFT, @@ -772,7 +772,7 @@ class FIOCtrl { ); if (existingDomainInBurnList) { - const trxId = `AutomaticDomainBurn${formatDateYYYYMMDD(new Date())}${name}`; + const trxId = `${token_id}AutomaticDomainBurn${name}`; nftsListToBurn.push({ tokenId: token_id, diff --git a/controller/utils/general.js b/controller/utils/general.js index 74c6c46..af270a4 100644 --- a/controller/utils/general.js +++ b/controller/utils/general.js @@ -123,11 +123,3 @@ export const convertTimestampIntoMs = (timestamp) => { // If it's neither a valid timestamp nor a valid Date string throw new Error('Invalid input: Unable to convert timestamp into milliseconds.'); }; - -export const formatDateYYYYMMDD = (date) => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); // +1 because months are 0-indexed - const day = String(date.getDate()).padStart(2, '0'); - - return `${year}${month}${day}`; -}; From 264c4a884aad784abb9ee61ba03bb963f70733a4 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Wed, 8 Jan 2025 16:36:44 +0200 Subject: [PATCH 3/8] DASH-1269 Save transaction error on Error.log file. --- controller/utils/transactions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/utils/transactions.js b/controller/utils/transactions.js index d9dc0b1..8166a35 100644 --- a/controller/utils/transactions.js +++ b/controller/utils/transactions.js @@ -14,7 +14,7 @@ import { REVERTED_BY_THE_EVM, } from '../constants/transactions.js'; -import { addLogMessage } from '../utils/log-files.js'; +import { addLogMessage, handleChainError } from '../utils/log-files.js'; import { getGasPrice, getWeb3Balance, convertWeiToGwei } from '../utils/prices.js'; export const polygonTransaction = async ({ @@ -126,6 +126,7 @@ export const polygonTransaction = async ({ }) .on('error', (error, receipt) => { console.log(`${logPrefix} Transaction has been failed in the chain.`); + handleChainError(error); if (receipt && receipt.blockHash && !receipt.status) console.log( From 338e34591629413c1803c77dd0206f24e291e9f8 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Wed, 8 Jan 2025 16:48:48 +0200 Subject: [PATCH 4/8] DASH-1269 Add timeout between burn calls to Polygon chain. --- controller/api/polygon.js | 5 +++++ controller/constants/transactions.js | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/controller/api/polygon.js b/controller/api/polygon.js index eda04bf..5c5bc84 100644 --- a/controller/api/polygon.js +++ b/controller/api/polygon.js @@ -17,7 +17,9 @@ import { ORACLE_CACHE_KEYS } from '../constants/cron-jobs.js'; import { NON_VALID_ORACLE_ADDRESS } from '../constants/errors.js'; import { LOG_FILES_PATH_NAMES } from '../constants/log-files.js'; import { DEFAULT_POLYGON_GAS_PRICE, POLYGON_GAS_LIMIT } from '../constants/prices.js'; +import { TRANSACTION_DELAY } from '../constants/transactions.js'; import { handlePolygonChainCommon, isOraclePolygonAddressValid } from '../utils/chain.js'; +import { sleep } from '../utils/general.js'; import { addLogMessage, updatePolygonNonce, @@ -208,6 +210,9 @@ class PolyCtrl { const burnABI = burnNFTFunction.encodeABI(); + // Need to set timeout to handle a big amount of burn calls to blockchain + await sleep(TRANSACTION_DELAY); + const chainNonce = await this.web3.eth.getTransactionCount( oraclePublicKey, 'pending', diff --git a/controller/constants/transactions.js b/controller/constants/transactions.js index 3942f35..95b331c 100644 --- a/controller/constants/transactions.js +++ b/controller/constants/transactions.js @@ -1,6 +1,10 @@ +import { SECOND_IN_MILLISECONDS } from './general'; + export const NONCE_TOO_LOW_ERROR = 'nonce too low'; export const ALREADY_KNOWN_TRANSACTION = 'already known'; export const LOW_GAS_PRICE = 'was not mined'; export const REVERTED_BY_THE_EVM = 'reverted by the EVM'; export const MAX_RETRY_TRANSACTION_ATTEMPTS = 3; + +export const TRANSACTION_DELAY = SECOND_IN_MILLISECONDS * 3; // 3 seconds From 06f4bd7c7c05cfe0be09b60f2e174f6d27910f20 Mon Sep 17 00:00:00 2001 From: andrewEze Date: Tue, 7 Jan 2025 14:26:27 +0200 Subject: [PATCH 5/8] DASH-1276. Throw an error when MathOp receives invalid data. --- controller/utils/math.js | 53 +++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/controller/utils/math.js b/controller/utils/math.js index ea58d2a..f4060a2 100644 --- a/controller/utils/math.js +++ b/controller/utils/math.js @@ -4,7 +4,25 @@ class MathOp { value; constructor(x) { - this.value = !isNaN(+x) ? x : 0; + try { + if (!isNaN(+x) && Big(x)) { + this.value = x; + } else { + throw new Error(`${x} is not a number`); + } + } catch (err) { + console.error(`${err.message}. Received input - ${x}`); + throw err; + } + } + + abs() { + try { + return Big(this.value).abs(); + } catch (err) { + console.error(err); + throw err; + } } add(x) { @@ -12,6 +30,7 @@ class MathOp { this.value = Big(this.value).plus(x); } catch (err) { console.error(err); + throw err; } return this; } @@ -21,6 +40,7 @@ class MathOp { this.value = Big(this.value).minus(x); } catch (err) { console.error(err); + throw err; } return this; } @@ -30,6 +50,7 @@ class MathOp { this.value = Big(this.value).times(x); } catch (err) { console.error(err); + throw err; } return this; } @@ -39,6 +60,7 @@ class MathOp { this.value = Big(this.value).div(x); } catch (err) { console.error(err); + throw err; } return this; } @@ -48,6 +70,7 @@ class MathOp { this.value = args.reduce((sum, current) => Big(sum).plus(current), 0); } catch (err) { console.error(err); + throw err; } return this; } @@ -57,6 +80,7 @@ class MathOp { this.value = Big(this.value).mod(modDigit); } catch (err) { console.error(err); + throw err; } return this; } @@ -66,52 +90,53 @@ class MathOp { this.value = Big(this.value).round(decimalPlaces, roundingMode); } catch (err) { console.error(err); + throw err; } return this; } eq(x) { try { - return Big(this.value).eq(x || 0); + return Big(this.value).eq(x); } catch (err) { console.error(err); - return this.value === x; + throw err; } } gt(x) { try { - return Big(this.value).gt(x || 0); + return Big(this.value).gt(x); } catch (err) { console.error(err); - return this.value > x; + throw err; } } gte(x) { try { - return Big(this.value).gte(x || 0); + return Big(this.value).gte(x); } catch (err) { console.error(err); - return this.value >= x; + throw err; } } lt(x) { try { - return Big(this.value).lt(x || 0); + return Big(this.value).lt(x); } catch (err) { console.error(err); - return this.value < x; + throw err; } } lte(x) { try { - return Big(this.value).lte(x || 0); + return Big(this.value).lte(x); } catch (err) { console.error(err); - return this.value <= x; + throw err; } } @@ -120,7 +145,7 @@ class MathOp { return Big(this.value).toNumber(); } catch (err) { console.error(err); - return +this.value; + throw err; } } @@ -129,7 +154,7 @@ class MathOp { return Big(this.value).toString(); } catch (err) { console.error(err); - return '-'; + throw err; } } @@ -138,7 +163,7 @@ class MathOp { return Big(this.value).toFixed(toFixedDigit); } catch (err) { console.error(err); - return this.value.toString(); + throw err; } } } From 10d3ec7e1e49a5002fd7034d2801be77f8619791 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Thu, 16 Jan 2025 17:50:38 +0200 Subject: [PATCH 6/8] DASH-1269 Handle pending transactions. Refactor blockChainTransaction method. Update and remove unused libraries. --- controller/api/eth.js | 84 ++------ controller/api/fio.js | 13 +- controller/api/polygon.js | 107 +++------- controller/constants/log-files.js | 2 + controller/constants/prices.js | 7 + controller/constants/transactions.js | 6 +- controller/jobs/transactions.js | 152 +++++++++++++++ controller/main.js | 45 +++-- controller/utils/chain.js | 52 ++++- controller/utils/general.js | 15 ++ controller/utils/log-files.js | 48 ++++- controller/utils/prices.js | 41 +++- controller/utils/transactions.js | 281 ++++++++++++++++++++------- controller/utils/web3-services.js | 49 +++++ package.json | 17 +- scripts/oracle.js | 2 +- scripts/oracleutils.js | 155 +++------------ 17 files changed, 665 insertions(+), 411 deletions(-) create mode 100644 controller/jobs/transactions.js create mode 100644 controller/utils/web3-services.js diff --git a/controller/api/eth.js b/controller/api/eth.js index de7defe..c862fc4 100644 --- a/controller/api/eth.js +++ b/controller/api/eth.js @@ -1,10 +1,8 @@ import 'dotenv/config'; import fs from 'fs'; -import Web3 from 'web3'; +import { isAddress } from 'web3-validator'; -import fioABI from '../../config/ABI/FIO.json' assert { type: 'json' }; -import fioNftABI from '../../config/ABI/FIONFT.json' assert { type: 'json' }; import config from '../../config/config.js'; import { @@ -16,40 +14,19 @@ import { import { ORACLE_CACHE_KEYS } from '../constants/cron-jobs.js'; import { NON_VALID_ORACLE_ADDRESS } from '../constants/errors.js'; import { LOG_FILES_PATH_NAMES } from '../constants/log-files.js'; -import { DEFAULT_ETH_GAS_PRICE, ETH_GAS_LIMIT } from '../constants/prices.js'; -import { - handleEthChainCommon, - isOracleEthAddressValid, - convertNativeFioIntoFio, -} from '../utils/chain.js'; +import { isOracleEthAddressValid, convertNativeFioIntoFio } from '../utils/chain.js'; import { addLogMessage, - updateEthNonce, handleChainError, handleLogFailedWrapItem, - handleEthNonceValue, - handleUpdatePendingPolygonItemsQueue, + handleUpdatePendingItemsQueue, handleServerError, } from '../utils/log-files.js'; -import { getEthGasPriceSuggestion } from '../utils/prices.js'; -import { polygonTransaction } from '../utils/transactions.js'; +import { blockChainTransaction } from '../utils/transactions.js'; -const { - eth: { ETH_ORACLE_PUBLIC, ETH_ORACLE_PRIVATE, ETH_CONTRACT, ETH_NFT_CONTRACT }, - infura: { eth }, - oracleCache, -} = config; +const { oracleCache } = config; class EthCtrl { - constructor() { - this.web3 = new Web3(eth); - this.fioContract = new this.web3.eth.Contract(fioABI, ETH_CONTRACT); - this.fioNftContract = new this.web3.eth.Contract(fioNftABI, ETH_NFT_CONTRACT); - } - - // It handles both wrap actions (domain and tokens) on ETH chain, this is designed to prevent nonce collisions, - // when asynchronous jobs make transactions with same nonce value from one address (oracle public address), - // so it causes "replacing already existing transaction in the chain". async handleWrap() { if (!oracleCache.get(ORACLE_CACHE_KEYS.isWrapOnEthJobExecuting)) oracleCache.set(ORACLE_CACHE_KEYS.isWrapOnEthJobExecuting, true, 0); // ttl = 0 means that value shouldn't ever been expired @@ -80,61 +57,30 @@ class EthCtrl { let isTransactionProceededSuccessfully = false; try { - const oraclePublicKey = ETH_ORACLE_PUBLIC; - const oraclePrivateKey = ETH_ORACLE_PRIVATE; - - if ( - this.web3.utils.isAddress(pubaddress) === true && - chaincode === ETH_TOKEN_CODE - ) { + if (isAddress(pubaddress) === true && chaincode === ETH_TOKEN_CODE) { //check validation if the address is ERC20 address console.log(`${logPrefix} preparing wrap action.`); - const wrapFunction = this.fioContract.methods.wrap( - pubaddress, - amount, - wrapOracleId, - ); - - const wrapABI = wrapFunction.encodeABI(); - - const chainNonce = await this.web3.eth.getTransactionCount( - oraclePublicKey, - 'pending', - ); - const txNonce = handleEthNonceValue({ chainNonce }); - - const common = handleEthChainCommon(); const onSussessTransaction = (receipt) => { addLogMessage({ filePath: LOG_FILES_PATH_NAMES.ETH, - message: `${ETH_CHAIN_NAME_CONSTANT} ${CONTRACT_NAMES.ERC_20} ${ACTION_NAMES.WRAP_TOKENS} receipt ${JSON.stringify(receipt)}`, + message: `${ETH_CHAIN_NAME_CONSTANT} ${CONTRACT_NAMES.ERC_20} ${ACTION_NAMES.WRAP_TOKENS} receipt ${receipt}`, }); isTransactionProceededSuccessfully = true; }; - await polygonTransaction({ - amount: amount, + await blockChainTransaction({ action: ACTION_NAMES.WRAP_TOKENS, chainName: ETH_CHAIN_NAME_CONSTANT, - common, - contract: ETH_CONTRACT, - contractName: CONTRACT_NAMES.ERC_20, - data: wrapABI, - defaultGasPrice: DEFAULT_ETH_GAS_PRICE, - getGasPriceSuggestionFn: getEthGasPriceSuggestion, - gasLimit: ETH_GAS_LIMIT, - handleSuccessedResult: onSussessTransaction, - logFilePath: LOG_FILES_PATH_NAMES.ETH, + contractActionParams: { + amount, + obtId: wrapOracleId, + pubaddress, + }, logPrefix, - oraclePrivateKey, - oraclePublicKey, shouldThrowError: true, - tokenCode: ETH_TOKEN_CODE, - txNonce, - updateNonce: updateEthNonce, - web3Instance: this.web3, + handleSuccessedResult: onSussessTransaction, }); } else { console.log(`${logPrefix} Invalid Address`); @@ -155,7 +101,7 @@ class EthCtrl { }); } - handleUpdatePendingPolygonItemsQueue({ + handleUpdatePendingItemsQueue({ action: this.handleWrap.bind(this), logPrefix, logFilePath: LOG_FILES_PATH_NAMES.wrapEthTransactionQueue, diff --git a/controller/api/fio.js b/controller/api/fio.js index c8cea7e..5534ee9 100644 --- a/controller/api/fio.js +++ b/controller/api/fio.js @@ -2,7 +2,7 @@ import 'dotenv/config'; import fs from 'fs'; -import Web3 from 'web3'; +import { Web3 } from 'web3'; import ethCtrl from './eth.js'; import moralis from './moralis.js'; @@ -47,7 +47,7 @@ import { getLastProcessedFioOracleItemId, updateFioOracleId, handleLogFailedWrapItem, - handleUpdatePendingPolygonItemsQueue, + handleUpdatePendingItemsQueue, handleServerError, handleChainError, } from '../utils/log-files.js'; @@ -150,7 +150,7 @@ const handleUnwrapFromEthToFioChainJob = async () => { }); } - handleUpdatePendingPolygonItemsQueue({ + handleUpdatePendingItemsQueue({ action: handleUnwrapFromEthToFioChainJob, logPrefix, logFilePath: LOG_FILES_PATH_NAMES.unwrapEthTransactionQueue, @@ -232,7 +232,7 @@ const handleUnwrapFromPolygonToFioChainJob = async () => { }); } - handleUpdatePendingPolygonItemsQueue({ + handleUpdatePendingItemsQueue({ action: handleUnwrapFromPolygonToFioChainJob, logPrefix, logFilePath: LOG_FILES_PATH_NAMES.unwrapPolygonTransactionQueue, @@ -414,7 +414,8 @@ class FIOCtrl { const getUnprocessedActionsLogs = async (isTokens = false) => { const chainBlockNumber = await web3.eth.getBlockNumber(); - const lastInChainBlockNumber = new MathOp(chainBlockNumber) + + const lastInChainBlockNumber = new MathOp(parseInt(chainBlockNumber)) .sub(blocksOffset) .toNumber(); const lastProcessedBlockNumber = isTokens @@ -575,7 +576,7 @@ class FIOCtrl { }; const getUnprocessedActionsLogs = async () => { - const lastInChainBlockNumber = await polyWeb3.eth.getBlockNumber(); + const lastInChainBlockNumber = parseInt(await polyWeb3.eth.getBlockNumber()); const lastProcessedBlockNumber = getLastProceededBlockNumberOnPolygonChainForDomainUnwrapping(); diff --git a/controller/api/polygon.js b/controller/api/polygon.js index 5c5bc84..d0bb686 100644 --- a/controller/api/polygon.js +++ b/controller/api/polygon.js @@ -1,9 +1,8 @@ import 'dotenv/config'; import fs from 'fs'; -import Web3 from 'web3'; +import { isAddress } from 'web3-validator'; -import fioNftABI from '../../config/ABI/FIOMATICNFT.json' assert { type: 'json' }; import config from '../../config/config.js'; import { @@ -16,34 +15,24 @@ import { import { ORACLE_CACHE_KEYS } from '../constants/cron-jobs.js'; import { NON_VALID_ORACLE_ADDRESS } from '../constants/errors.js'; import { LOG_FILES_PATH_NAMES } from '../constants/log-files.js'; -import { DEFAULT_POLYGON_GAS_PRICE, POLYGON_GAS_LIMIT } from '../constants/prices.js'; import { TRANSACTION_DELAY } from '../constants/transactions.js'; -import { handlePolygonChainCommon, isOraclePolygonAddressValid } from '../utils/chain.js'; +import { isOraclePolygonAddressValid } from '../utils/chain.js'; import { sleep } from '../utils/general.js'; import { addLogMessage, - updatePolygonNonce, handleLogFailedWrapItem, handleLogFailedBurnNFTItem, - handlePolygonNonceValue, - handleUpdatePendingPolygonItemsQueue, + handleUpdatePendingItemsQueue, handleServerError, handleChainError, } from '../utils/log-files.js'; -import { getPolygonGasPriceSuggestion } from '../utils/prices.js'; -import { polygonTransaction } from '../utils/transactions.js'; +import { blockChainTransaction } from '../utils/transactions.js'; -const { - infura: { polygon }, - oracleCache, - polygon: { POLYGON_ORACLE_PUBLIC, POLYGON_ORACLE_PRIVATE, POLYGON_CONTRACT }, -} = config || {}; +const { oracleCache } = config || {}; class PolyCtrl { constructor() { - this.web3 = new Web3(polygon); - this.fioNftContract = new this.web3.eth.Contract(fioNftABI, POLYGON_CONTRACT); this.contractName = CONTRACT_NAMES.ERC_721; } @@ -81,11 +70,8 @@ class PolyCtrl { let isTransactionProceededSuccessfully = false; try { - const pubKey = POLYGON_ORACLE_PUBLIC; - const signKey = POLYGON_ORACLE_PRIVATE; - if ( - this.web3.utils.isAddress(pubaddress) === true && + isAddress(pubaddress) === true && (chaincode === MATIC_TOKEN_CODE || chaincode === POLYGON_TOKEN_CODE) ) { //check validation if the address is ERC20 address @@ -94,50 +80,27 @@ class PolyCtrl { `${logPrefix} requesting wrap domain action for ${nftname} FIO domain to ${pubaddress}`, ); - const wrapDomainFunction = this.fioNftContract.methods.wrapnft( - pubaddress, - nftname, - wrapOracleId, - ); - - const wrapABI = wrapDomainFunction.encodeABI(); - - const chainNonce = await this.web3.eth.getTransactionCount(pubKey, 'pending'); - - const txNonce = handlePolygonNonceValue({ chainNonce }); - const onSussessTransaction = (receipt) => { addLogMessage({ filePath: LOG_FILES_PATH_NAMES.POLYGON, - message: `${POLYGON_CHAIN_NAME} ${this.contractName} ${actionName} ${JSON.stringify(receipt)}`, + message: `${POLYGON_CHAIN_NAME} ${this.contractName} ${actionName} ${receipt}`, + addTimestamp: false, }); isTransactionProceededSuccessfully = true; }; - const common = handlePolygonChainCommon(); - - await polygonTransaction({ + await blockChainTransaction({ action: actionName, chainName: POLYGON_CHAIN_NAME, - common, - contract: POLYGON_CONTRACT, - contractName: this.contractName, - data: wrapABI, - defaultGasPrice: DEFAULT_POLYGON_GAS_PRICE, - domain: nftname, - getGasPriceSuggestionFn: getPolygonGasPriceSuggestion, - gasLimit: POLYGON_GAS_LIMIT, + contractActionParams: { + domain: nftname, + obtId: wrapOracleId, + pubaddress, + }, handleSuccessedResult: onSussessTransaction, - logFilePath: LOG_FILES_PATH_NAMES.POLYGON, logPrefix, - oraclePrivateKey: signKey, - oraclePublicKey: pubKey, shouldThrowError: true, - tokenCode: POLYGON_TOKEN_CODE, - txNonce, - updateNonce: updatePolygonNonce, - web3Instance: this.web3, }); } else { console.log(`${logPrefix} Invalid Address`); @@ -158,7 +121,7 @@ class PolyCtrl { }); } - handleUpdatePendingPolygonItemsQueue({ + handleUpdatePendingItemsQueue({ action: this.wrapFioDomain.bind(this), logPrefix, logFilePath: LOG_FILES_PATH_NAMES.wrapPolygonTransactionQueue, @@ -203,54 +166,28 @@ class PolyCtrl { let isTransactionProceededSuccessfully = false; try { - const oraclePublicKey = POLYGON_ORACLE_PUBLIC; - const oraclePrivateKey = POLYGON_ORACLE_PRIVATE; - - const burnNFTFunction = this.fioNftContract.methods.burnnft(tokenId, obtId); - - const burnABI = burnNFTFunction.encodeABI(); - // Need to set timeout to handle a big amount of burn calls to blockchain await sleep(TRANSACTION_DELAY); - const chainNonce = await this.web3.eth.getTransactionCount( - oraclePublicKey, - 'pending', - ); - - const txNonce = handlePolygonNonceValue({ chainNonce }); - const onSussessTransaction = (receipt) => { addLogMessage({ filePath: LOG_FILES_PATH_NAMES.POLYGON, - message: `${POLYGON_CHAIN_NAME} ${this.contractName} ${actionName} ${JSON.stringify(receipt)}`, + message: `${POLYGON_CHAIN_NAME} ${this.contractName} ${actionName} ${receipt}`, }); isTransactionProceededSuccessfully = true; }; - const common = handlePolygonChainCommon(); - - await polygonTransaction({ + await blockChainTransaction({ action: actionName, chainName: POLYGON_CHAIN_NAME, - common, - contract: POLYGON_CONTRACT, - data: burnABI, - defaultGasPrice: DEFAULT_POLYGON_GAS_PRICE, - domain: domainName, - getGasPriceSuggestionFn: getPolygonGasPriceSuggestion, - gasLimit: POLYGON_GAS_LIMIT, + contractActionParams: { + tokenId, + obtId, + }, handleSuccessedResult: onSussessTransaction, - logFilePath: LOG_FILES_PATH_NAMES.POLYGON, logPrefix, - oraclePrivateKey, - oraclePublicKey, shouldThrowError: true, - tokenCode: POLYGON_TOKEN_CODE, - txNonce, - updateNonce: updatePolygonNonce, - web3Instance: this.web3, }); } catch (error) { handleChainError({ @@ -267,7 +204,7 @@ class PolyCtrl { }); } - handleUpdatePendingPolygonItemsQueue({ + handleUpdatePendingItemsQueue({ action: this.burnNFTOnPolygon.bind(this), logPrefix, logFilePath: LOG_FILES_PATH_NAMES.burnNFTTransactionsQueue, diff --git a/controller/constants/log-files.js b/controller/constants/log-files.js index 7efe91b..b9f1a13 100644 --- a/controller/constants/log-files.js +++ b/controller/constants/log-files.js @@ -9,7 +9,9 @@ export const LOG_FILES_PATH_NAMES = { ETH: LOG_DIRECTORY_PATH_NAME + 'ETH.log', //log events and errors on ETH side POLYGON: LOG_DIRECTORY_PATH_NAME + 'POLYGON.log', ethNonce: LOG_DIRECTORY_PATH_NAME + 'ethNonce.log', // store last used ETH nonce to aviod too low nonce issue on concurrency calls + ethPendingTransactions: LOG_DIRECTORY_PATH_NAME + 'ethPendingTransactions.log', // store ETH pending transactions polygonNonce: LOG_DIRECTORY_PATH_NAME + 'polygonNonce.log', // store last used Polygon nonce to aviod too low nonce issue on concurrency calls + polygonPendingTransactions: LOG_DIRECTORY_PATH_NAME + 'polygonPendingTransactions.log', // store Polygon pending transactions fioOracleItemId: LOG_DIRECTORY_PATH_NAME + 'fioOracleItemId.log', // store last processed fio.oracle wrapped item blockNumberFIOForBurnNFT: LOG_DIRECTORY_PATH_NAME + 'blockNumberFIOForBurnNFT.log', // store FIO block number for burn domain action blockNumberUnwrapTokensETH: LOG_DIRECTORY_PATH_NAME + 'blockNumberETH.log', //store ETH blockNumber for unwrap tokens action diff --git a/controller/constants/prices.js b/controller/constants/prices.js index e379363..89a3f6c 100644 --- a/controller/constants/prices.js +++ b/controller/constants/prices.js @@ -9,3 +9,10 @@ export const ETH_GAS_LIMIT = parseFloat(T_GAS_LIMIT); export const DEFAULT_POLYGON_GAS_PRICE = parseFloat(P_GAS_PRICE); export const DEFAULT_ETH_GAS_PRICE = parseFloat(T_GAS_PRICE); + +export const GAS_PRICE_MULTIPLIERS = { + REPLACEMENT: 1.5, // 50% increase for replace + RETRY: 1.2, // 20% increase for retry + AVERAGE: 1.2, // 20% increase for average priority + HIGH: 1.4, // 40% increase for high priority +}; diff --git a/controller/constants/transactions.js b/controller/constants/transactions.js index 95b331c..8030f4d 100644 --- a/controller/constants/transactions.js +++ b/controller/constants/transactions.js @@ -1,10 +1,14 @@ -import { SECOND_IN_MILLISECONDS } from './general'; +import { SECOND_IN_MILLISECONDS, MINUTE_IN_MILLISECONDS } from './general.js'; export const NONCE_TOO_LOW_ERROR = 'nonce too low'; export const ALREADY_KNOWN_TRANSACTION = 'already known'; export const LOW_GAS_PRICE = 'was not mined'; export const REVERTED_BY_THE_EVM = 'reverted by the EVM'; +export const ALREADY_APPROVED_HASH = 'has already approved'; +export const TRANSACTION_NOT_FOUND = 'transaction not found'; export const MAX_RETRY_TRANSACTION_ATTEMPTS = 3; export const TRANSACTION_DELAY = SECOND_IN_MILLISECONDS * 3; // 3 seconds + +export const MAX_TRANSACTION_AGE = MINUTE_IN_MILLISECONDS * 3; // 3 minutes diff --git a/controller/jobs/transactions.js b/controller/jobs/transactions.js new file mode 100644 index 0000000..c31e23f --- /dev/null +++ b/controller/jobs/transactions.js @@ -0,0 +1,152 @@ +import { ETH_CHAIN_NAME_CONSTANT, POLYGON_CHAIN_NAME } from '../constants/chain.js'; +import { TRANSACTION_NOT_FOUND, MAX_TRANSACTION_AGE } from '../constants/transactions.js'; +import { readLogFile, removePendingTransaction } from '../utils/log-files.js'; +import { + blockChainTransaction, + getDefaultTransactionParams, +} from '../utils/transactions.js'; + +export const checkAndReplacePendingTransactions = async () => { + const handlePendingTransaction = async ({ chainName }) => { + const defaultTransactionParams = await getDefaultTransactionParams(chainName); + const { oraclePublicKey, pendingTransactionFilePath, web3Instance } = + defaultTransactionParams; + + const logPrefix = 'Pending transactions handle: --> '; + try { + const currentTime = Date.now(); + + const latestNonce = Number( + await web3Instance.eth.getTransactionCount(oraclePublicKey, 'latest'), + ); + + let pendingTransactions = []; + try { + const fileContent = readLogFile(pendingTransactionFilePath); + + if (!fileContent || !fileContent.trim()) { + console.log(`${logPrefix} No pending transactions.`); + return; // No pending transactions + } + + pendingTransactions = fileContent + .split('\n') + .filter((line) => line.trim()) // Remove empty lines + .map((line) => { + try { + const [hash, dataStr] = line.split(' '); + return { + hash, + ...JSON.parse(dataStr), + }; + } catch (e) { + console.error(`${logPrefix} Error parsing transaction data:`, e); + return null; + } + }) + .filter(Boolean); + } catch (error) { + console.error(`${logPrefix} Error reading pending transactions file:`, error); + return; + } + + // Sort by nonce to handle them in order + pendingTransactions.sort((a, b) => a.txNonce - b.txNonce); + + // Remove transactions with nonce less than latest confirmed nonce or trasaction has been already replaced + pendingTransactions = pendingTransactions.filter((tx) => { + const isReplaced = pendingTransactions.some( + (pendingTx) => pendingTx.originalTxHash === tx.hash, + ); + if (tx.txNonce < latestNonce || isReplaced) { + console.log( + `${logPrefix} Removing old transaction with nonce ${tx.txNonce} (latest nonce: ${latestNonce}): ${tx.hash}`, + ); + removePendingTransaction({ + hash: tx.hash, + logFilePath: pendingTransactionFilePath, + logPrefix, + }); + return false; + } + return true; + }); + + const getTransactionFromChain = async ({ txHash, isReplaceTx }) => { + try { + const tx = await web3Instance.eth.getTransaction(txHash); + console.log(`${logPrefix} Found transaction ${txHash}`); + return tx; + } catch (error) { + if (!error.message.toLowerCase().includes(TRANSACTION_NOT_FOUND)) { + throw error; + } + console.log( + `${logPrefix} (replaced: ${isReplaceTx}) ${error.message}: ${txHash}`, + ); + return null; + } + }; + + for (const { + action, + chainName, + contractActionParams, + hash, + isReplaceTx, + timestamp, + txNonce: pendingTxNonce, + originalTxHash, + } of pendingTransactions) { + let tx; + + // Try to get original transaction first if it exists + if (originalTxHash) { + tx = await getTransactionFromChain({ txHash: originalTxHash, isReplaceTx }); + } + + // If original not found or doesn't exist, try the replacement + if (!tx) { + tx = await getTransactionFromChain({ txHash: hash, isReplaceTx }); + } + + // Transaction is mined + if (tx && tx.blockNumber) { + removePendingTransaction({ + hash, + logFilePath: pendingTransactionFilePath, + logPrefix, + }); + continue; + } + + if (!tx || currentTime - timestamp > MAX_TRANSACTION_AGE) { + // If no tx - transaction not in mempool + if (tx) { + console.log(`${logPrefix} Found stuck transaction: ${hash}`); + } else { + console.log( + `${logPrefix} Transaction ${hash} not in mempool and timed out, attempting replacement`, + ); + } + // Only replace the earliest stuck transaction + await blockChainTransaction({ + action, + chainName, + contractActionParams, + isReplaceTx: true, + logPrefix, + originalTxHash: hash, + pendingTxNonce: pendingTxNonce, + }); + } + } + } catch (error) { + console.error(`${logPrefix} Error checking pending transactions:`, error); + } + }; + + await handlePendingTransaction({ chainName: ETH_CHAIN_NAME_CONSTANT }); + + await handlePendingTransaction({ chainName: POLYGON_CHAIN_NAME }); +}; diff --git a/controller/main.js b/controller/main.js index 56c5b45..1023604 100644 --- a/controller/main.js +++ b/controller/main.js @@ -1,7 +1,6 @@ import 'dotenv/config'; import cors from 'cors'; import express from 'express'; -import Web3 from 'web3'; import fioCtrl from './api/fio.js'; @@ -11,8 +10,10 @@ import { POLYGON_CHAIN_NAME, POLYGON_TOKEN_CODE, } from './constants/chain.js'; +import { MINUTE_IN_MILLISECONDS } from './constants/general.js'; import { LOG_FILES_PATH_NAMES, LOG_DIRECTORY_PATH_NAME } from './constants/log-files.js'; +import { checkAndReplacePendingTransactions } from './jobs/transactions.js'; import fioRoute from './routes/fio.js'; import { getLastIrreversibleBlockOnFioChain, @@ -31,13 +32,13 @@ import { getHighestEthGasPriceSuggestion, getHighestPolygonGasPriceSuggestion, } from './utils/prices.js'; +import { Web3Service } from './utils/web3-services.js'; import config from '../config/config.js'; const { gas: { USE_GAS_API }, eth: { ETH_ORACLE_PUBLIC, BLOCKS_OFFSET_ETH }, - infura: { eth, polygon }, mode, polygon: { POLYGON_ORACLE_PUBLIC }, jobTimeouts: { DEfAULT_JOB_TIMEOUT, BURN_DOMAINS_JOB_TIMEOUT }, @@ -50,20 +51,21 @@ class MainCtrl { const logPrefix = `Startup -->`; try { - this.web3 = new Web3(eth); - this.polyWeb3 = new Web3(polygon); - // Check oracle addresses balances on ETH and Polygon chains - await this.web3.eth.getBalance(ETH_ORACLE_PUBLIC, 'latest', (error, result) => { - if (error) { - console.log(`${logPrefix} ${error.stack}`); - } else { - console.log( - `${logPrefix} Oracle ${ETH_CHAIN_NAME_CONSTANT} Address Balance: ${convertWeiToEth(result)} ${ETH_TOKEN_CODE}`, - ); - } - }); - await this.polyWeb3.eth.getBalance( + await Web3Service.getEthWeb3().eth.getBalance( + ETH_ORACLE_PUBLIC, + 'latest', + (error, result) => { + if (error) { + console.log(`${logPrefix} ${error.stack}`); + } else { + console.log( + `${logPrefix} Oracle ${ETH_CHAIN_NAME_CONSTANT} Address Balance: ${convertWeiToEth(result)} ${ETH_TOKEN_CODE}`, + ); + } + }, + ); + await Web3Service.getPolygonWeb3().eth.getBalance( POLYGON_ORACLE_PUBLIC, 'latest', (error, result) => { @@ -139,6 +141,8 @@ class MainCtrl { await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.burnNFTErroredTransactions, }); + await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.ethPendingTransactions }); + await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.polygonPendingTransactions }); console.log(`${logPrefix} logs folders are ready`); @@ -152,17 +156,17 @@ class MainCtrl { }); await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.blockNumberUnwrapTokensETH, - fetchAction: this.web3.eth.getBlockNumber, + fetchAction: () => Web3Service.getEthWeb3().eth.getBlockNumber(), offset: BLOCKS_OFFSET_ETH, }); await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.blockNumberUnwrapDomainETH, - fetchAction: this.web3.eth.getBlockNumber, + fetchAction: () => Web3Service.getEthWeb3().eth.getBlockNumber(), offset: BLOCKS_OFFSET_ETH, }); await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.blockNumberUnwrapDomainPolygon, - fetchAction: this.polyWeb3.eth.getBlockNumber, + fetchAction: () => Web3Service.getPolygonWeb3().eth.getBlockNumber(), }); await prepareLogFile({ filePath: LOG_FILES_PATH_NAMES.ethNonce, @@ -180,6 +184,7 @@ class MainCtrl { fioCtrl.handleUnprocessedUnwrapActionsOnPolygon(); fioCtrl.handleUnprocessedBurnNFTActions(); + checkAndReplacePendingTransactions(); // Start Jobs interval setInterval( fioCtrl.handleUnprocessedWrapActionsOnFioChain, @@ -197,6 +202,10 @@ class MainCtrl { fioCtrl.handleUnprocessedBurnNFTActions, parseInt(BURN_DOMAINS_JOB_TIMEOUT), ); + setInterval( + checkAndReplacePendingTransactions, + parseInt(MINUTE_IN_MILLISECONDS * 3), // check for pending transactions every 3 mins + ); this.initRoutes(app); diff --git a/controller/utils/chain.js b/controller/utils/chain.js index 3e77c72..9629b52 100644 --- a/controller/utils/chain.js +++ b/controller/utils/chain.js @@ -1,10 +1,12 @@ -import { Common, CustomChain } from '@ethereumjs/common'; -import Web3 from 'web3'; +import { Common, CustomChain, Hardfork } from '@ethereumjs/common'; +import { Web3 } from 'web3'; +import { Web3Service } from './web3-services.js'; import fioABI from '../../config/ABI/FIO.json' assert { type: 'json' }; import fioMaticNftABI from '../../config/ABI/FIOMATICNFT.json' assert { type: 'json' }; import fioNftABI from '../../config/ABI/FIONFT.json' assert { type: 'json' }; import config from '../../config/config.js'; +import { ACTION_NAMES } from '../constants/chain.js'; const { eth: { ETH_ORACLE_PUBLIC, ETH_CONTRACT, ETH_NFT_CONTRACT, ETH_CHAIN_NAME }, @@ -17,7 +19,9 @@ import { POLYGON_TESTNET_CHAIN_ID } from '../constants/chain.js'; export const handlePolygonChainCommon = () => { if (isTestnet) { - const customChainInstance = Common.custom(CustomChain.PolygonMumbai); + const customChainInstance = Common.custom(CustomChain.PolygonMumbai, { + hardfork: Hardfork.London, + }); // Polygon Mumbai has been deprecated from 13th of April 2024. // Using Polygon Amoy instead but it's missing on CustomChain. So chainId and networkId should be updated customChainInstance._chainParams.chainId = POLYGON_TESTNET_CHAIN_ID; @@ -26,10 +30,11 @@ export const handlePolygonChainCommon = () => { return customChainInstance; } - return Common.custom(CustomChain.PolygonMainnet); + return Common.custom(CustomChain.PolygonMainnet, { hardfork: Hardfork.London }); }; -export const handleEthChainCommon = () => new Common({ chain: ETH_CHAIN_NAME }); +export const handleEthChainCommon = () => + new Common({ chain: ETH_CHAIN_NAME, hardfork: Hardfork.London }); export const isOracleEthAddressValid = async (isTokens = true) => { const web3 = new Web3(eth); @@ -56,6 +61,43 @@ export const isOraclePolygonAddressValid = async () => { .includes(POLYGON_ORACLE_PUBLIC.toLowerCase()); }; +export const executeContractAction = ({ + actionName, + amount, + domain, + obtId, + pubaddress, + tokenId, +}) => { + let contractFunction = null; + + switch (actionName) { + case ACTION_NAMES.WRAP_TOKENS: { + const contract = Web3Service.getEthContract(); + contractFunction = contract.methods.wrap(pubaddress, amount, obtId); + break; + } + case ACTION_NAMES.WRAP_DOMAIN: { + const contract = Web3Service.getPolygonContract(); + contractFunction = contract.methods.wrapnft(pubaddress, domain, obtId); + break; + } + case ACTION_NAMES.BURN_NFT: { + const contract = Web3Service.getPolygonContract(); + contractFunction = contract.methods.burnnft(tokenId, obtId); + break; + } + default: + null; + } + + if (!contractFunction) { + throw Error('ExecuteContractAction has no contract function'); + } + + return contractFunction.encodeABI(); +}; + export const convertNativeFioIntoFio = (nativeFioValue) => { const fioDecimals = 1000000000; return parseInt(nativeFioValue + '') / fioDecimals; diff --git a/controller/utils/general.js b/controller/utils/general.js index af270a4..546ec05 100644 --- a/controller/utils/general.js +++ b/controller/utils/general.js @@ -123,3 +123,18 @@ export const convertTimestampIntoMs = (timestamp) => { // If it's neither a valid timestamp nor a valid Date string throw new Error('Invalid input: Unable to convert timestamp into milliseconds.'); }; + +export const stringifyWithBigInt = (obj) => { + return JSON.stringify(obj, (key, value) => { + // Handle arrays to maintain their structure + if (Array.isArray(value)) { + return value; + } + // Convert BigInt to string + if (typeof value === 'bigint') { + return value.toString(); + } + // Return all other values as is + return value; + }); +}; diff --git a/controller/utils/log-files.js b/controller/utils/log-files.js index 3346646..c351f13 100644 --- a/controller/utils/log-files.js +++ b/controller/utils/log-files.js @@ -48,7 +48,12 @@ export const prepareLogFile = async ( let lastBlockNumberInChain; if (fetchAction) { const blocksOffset = parseInt(offset) || 0; - lastBlockNumberInChain = (await fetchAction()) - blocksOffset; + const chainBlockNumber = await fetchAction(); + + lastBlockNumberInChain = + typeof chainBlockNumber === 'bigint' + ? parseFloat(chainBlockNumber) + : chainBlockNumber - blocksOffset; } createLogFile({ filePath, @@ -62,7 +67,12 @@ export const prepareLogFile = async ( let lastBlockNumberInChain; if (fetchAction) { const blocksOffset = parseInt(offset) || 0; - lastBlockNumberInChain = (await fetchAction()) - blocksOffset; + const chainBlockNumber = await fetchAction(); + + lastBlockNumberInChain = + typeof chainBlockNumber === 'bigint' + ? parseFloat(chainBlockNumber) + : chainBlockNumber - blocksOffset; } createLogFile({ filePath, @@ -94,6 +104,8 @@ export const addLogMessage = ({ } }; +export const readLogFile = (filePath) => fs.readFileSync(filePath, 'utf8'); + export const updateFioOracleId = (oracleId) => { fs.writeFileSync(LOG_FILES_PATH_NAMES.fioOracleItemId, oracleId); }; @@ -180,10 +192,10 @@ export const handleLogFailedBurnNFTItem = ({ logPrefix, burnData, errorLogFilePa }; export const handlePolygonNonceValue = ({ chainNonce }) => { - let txNonce = chainNonce; + let txNonce = typeof chainNonce === 'bigint' ? parseInt(chainNonce) : chainNonce; const savedNonce = getLastProceededPolygonNonce(); - if (savedNonce && Number(savedNonce) === Number(chainNonce)) { + if (savedNonce && Number(savedNonce) === Number(txNonce)) { txNonce = txNonce++; } @@ -193,10 +205,10 @@ export const handlePolygonNonceValue = ({ chainNonce }) => { }; export const handleEthNonceValue = ({ chainNonce }) => { - let txNonce = chainNonce; + let txNonce = typeof chainNonce === 'bigint' ? parseInt(chainNonce) : chainNonce; const savedNonce = getLastProceededEthNonce(); - if (savedNonce && Number(savedNonce) === Number(chainNonce)) { + if (savedNonce && Number(savedNonce) === Number(txNonce)) { txNonce = txNonce++; } @@ -205,7 +217,29 @@ export const handleEthNonceValue = ({ chainNonce }) => { return txNonce; }; -export const handleUpdatePendingPolygonItemsQueue = ({ +export const removePendingTransaction = ({ hash, logFilePath, logPrefix = '' }) => { + try { + // Read the file contents + const fileContents = fs.readFileSync(logFilePath, 'utf-8'); + + // Split contents into lines + const lines = fileContents.split('\n'); + + // Filter out the line containing the hash + const updatedLines = lines.filter((line) => !line.startsWith(`${hash} `)); + + // Join the lines back and write to the file + fs.writeFileSync(logFilePath, updatedLines.join('\n'), 'utf-8'); + + console.log( + `${logPrefix} Pending transaction with hash "${hash}" has been removed successfully.`, + ); + } catch (error) { + console.error(`${logPrefix} Remove transaction hash error: ${error.message}`); + } +}; + +export const handleUpdatePendingItemsQueue = ({ action, logFilePath, logPrefix, diff --git a/controller/utils/prices.js b/controller/utils/prices.js index 8585e00..bad5162 100644 --- a/controller/utils/prices.js +++ b/controller/utils/prices.js @@ -1,12 +1,13 @@ import fs from 'fs'; -import Web3 from 'web3'; +import { Web3 } from 'web3'; import config from '../../config/config.js'; import { getInfuraPolygonGasPrice, getInfuraEthGasPrice } from '../api/infura.js'; import { getMoralisEthGasPrice, getMoralisPolygonGasPrice } from '../api/moralis.js'; import { getThirdwebEthGasPrice, getThirdwebPolygonGasPrice } from '../api/thirdweb.js'; import { LOG_FILES_PATH_NAMES } from '../constants/log-files.js'; +import { GAS_PRICE_MULTIPLIERS } from '../constants/prices.js'; const { gas: { USE_GAS_API, GAS_PRICE_LEVEL }, @@ -57,12 +58,12 @@ export const getEthGasPriceSuggestion = async () => { // base gas price value + 20% const calculateAverageGasPrice = (baseGasPrice) => { - return Math.ceil(baseGasPrice * 1.2); + return Math.ceil(baseGasPrice * GAS_PRICE_MULTIPLIERS.AVERAGE); }; // base gas price value + 40% const calculateHighGasPrice = (baseGasPrice) => { - return Math.ceil(baseGasPrice * 1.4); + return Math.ceil(baseGasPrice * GAS_PRICE_MULTIPLIERS.HIGH); }; const getHighestGasPriceValue = (gasPriceSuggestions) => Math.max(...gasPriceSuggestions); @@ -83,34 +84,58 @@ export const getGasPrice = async ({ defaultGasPrice, getGasPriceSuggestionFn, logPrefix, + isRetry = false, + isReplace = false, }) => { const isUsingGasApi = !!parseInt(USE_GAS_API); let gasPrice = 0; + let finalMultiplier = 1; if (isUsingGasApi && getGasPriceSuggestionFn) { console.log(`${logPrefix} using gasPrice value from the api:`); const gasPriceSuggestions = await getGasPriceSuggestionFn(); - const gasPriceSuggestion = getHighestGasPriceValue(gasPriceSuggestions); + const baseGasPrice = getHighestGasPriceValue(gasPriceSuggestions); switch (GAS_PRICE_LEVEL) { case 'low': - gasPrice = gasPriceSuggestion; + gasPrice = baseGasPrice; break; case 'average': - gasPrice = calculateAverageGasPrice(gasPriceSuggestion); + gasPrice = calculateAverageGasPrice(baseGasPrice); break; case 'high': - gasPrice = calculateHighGasPrice(gasPriceSuggestion); + gasPrice = calculateHighGasPrice(baseGasPrice); break; default: - gasPrice = gasPriceSuggestion; + gasPrice = baseGasPrice; + } + + // Apply additional multipliers if needed + if (isReplace) { + finalMultiplier = GAS_PRICE_MULTIPLIERS.REPLACEMENT; + gasPrice = Math.ceil(gasPrice * finalMultiplier); + console.log(`${logPrefix} Applied replace multiplier (${finalMultiplier}x)`); + } else if (isRetry) { + finalMultiplier = GAS_PRICE_MULTIPLIERS.RETRY; + gasPrice = Math.ceil(gasPrice * finalMultiplier); + console.log(`${logPrefix} Applied retry multiplier (${finalMultiplier}x)`); } } else if (!isUsingGasApi || !getGasPriceSuggestionFn) { console.log(`${logPrefix} Using gasPrice value from the .env:`); gasPrice = convertGweiToWei(defaultGasPrice.toString()); + + if (isReplace || isRetry) { + const multiplier = isReplace + ? GAS_PRICE_MULTIPLIERS.REPLACEMENT + : GAS_PRICE_MULTIPLIERS.RETRY; + gasPrice = Math.ceil(gasPrice * multiplier); + console.log( + `${logPrefix} Applied ${isReplace ? 'replace' : 'retry'} multiplier (${multiplier}x)`, + ); + } } if (!gasPrice || parseInt(defaultGasPrice) <= 0) diff --git a/controller/utils/transactions.js b/controller/utils/transactions.js index 8166a35..a187678 100644 --- a/controller/utils/transactions.js +++ b/controller/utils/transactions.js @@ -1,10 +1,19 @@ -import { Transaction } from '@ethereumjs/tx'; - +import { Web3Service } from './web3-services.js'; +import config from '../../config/config.js'; import { + CONTRACT_NAMES, ETH_TOKEN_CODE, - MATIC_TOKEN_CODE, POLYGON_TOKEN_CODE, + ETH_CHAIN_NAME_CONSTANT, + POLYGON_CHAIN_NAME, } from '../constants/chain.js'; +import { LOG_FILES_PATH_NAMES } from '../constants/log-files.js'; +import { + DEFAULT_ETH_GAS_PRICE, + DEFAULT_POLYGON_GAS_PRICE, + ETH_GAS_LIMIT, + POLYGON_GAS_LIMIT, +} from '../constants/prices.js'; import { ALREADY_KNOWN_TRANSACTION, @@ -12,35 +21,142 @@ import { NONCE_TOO_LOW_ERROR, LOW_GAS_PRICE, REVERTED_BY_THE_EVM, + ALREADY_APPROVED_HASH, + MAX_TRANSACTION_AGE, } from '../constants/transactions.js'; +import { + handlePolygonChainCommon, + handleEthChainCommon, + executeContractAction, +} from '../utils/chain.js'; + +import { stringifyWithBigInt } from '../utils/general.js'; +import { + addLogMessage, + handleChainError, + handleEthNonceValue, + handlePolygonNonceValue, + updateEthNonce, + updatePolygonNonce, +} from '../utils/log-files.js'; +import { + getGasPrice, + getWeb3Balance, + convertWeiToGwei, + getPolygonGasPriceSuggestion, + getEthGasPriceSuggestion, +} from '../utils/prices.js'; + +const { + eth: { ETH_ORACLE_PRIVATE, ETH_ORACLE_PUBLIC, ETH_CONTRACT }, + polygon: { POLYGON_ORACLE_PRIVATE, POLYGON_ORACLE_PUBLIC, POLYGON_CONTRACT }, +} = config || {}; + +const CHAIN_CONFIG = { + [ETH_CHAIN_NAME_CONSTANT]: { + handleChainCommon: handleEthChainCommon, + contract: ETH_CONTRACT, + contractName: CONTRACT_NAMES.ERC_20, + defaultGasPrice: DEFAULT_ETH_GAS_PRICE, + getGasPriceSuggestionFn: getEthGasPriceSuggestion, + gasLimit: ETH_GAS_LIMIT, + logFilePath: LOG_FILES_PATH_NAMES.ETH, + oraclePrivateKey: ETH_ORACLE_PRIVATE, + oraclePublicKey: ETH_ORACLE_PUBLIC, + pendingTransactionFilePath: LOG_FILES_PATH_NAMES.ethPendingTransactions, + tokenCode: ETH_TOKEN_CODE, + handleNonceValue: handleEthNonceValue, + updateNonce: updateEthNonce, + getWeb3Instance: () => Web3Service.getEthWeb3(), + }, + [POLYGON_CHAIN_NAME]: { + handleChainCommon: handlePolygonChainCommon, + contract: POLYGON_CONTRACT, + contractName: CONTRACT_NAMES.ERC_721, + defaultGasPrice: DEFAULT_POLYGON_GAS_PRICE, + getGasPriceSuggestionFn: getPolygonGasPriceSuggestion, + gasLimit: POLYGON_GAS_LIMIT, + logFilePath: LOG_FILES_PATH_NAMES.POLYGON, + oraclePrivateKey: POLYGON_ORACLE_PRIVATE, + oraclePublicKey: POLYGON_ORACLE_PUBLIC, + pendingTransactionFilePath: LOG_FILES_PATH_NAMES.polygonPendingTransactions, + tokenCode: POLYGON_TOKEN_CODE, + handleNonceValue: handlePolygonNonceValue, + updateNonce: updatePolygonNonce, + getWeb3Instance: () => Web3Service.getPolygonWeb3(), + }, +}; + +export const getDefaultTransactionParams = async (chain) => { + const config = CHAIN_CONFIG[chain]; + + if (!config) { + throw new Error(`Unsupported chain: ${chain}`); + } + + const { + handleChainCommon, + getWeb3Instance, + handleNonceValue, + oraclePublicKey, + ...restConfig + } = config; + + const web3Instance = getWeb3Instance(); + const common = handleChainCommon(); + + const chainNonce = await web3Instance.eth.getTransactionCount( + oraclePublicKey, + 'pending', + ); + + const txNonce = handleNonceValue({ chainNonce }); + + return { + common, + oraclePublicKey, + txNonce, + web3Instance, + ...restConfig, + }; +}; + +export const blockChainTransaction = async (transactionParams) => { + const { + action, + chainName, + contractActionParams, + handleSuccessedResult, + isReplaceTx = false, + logPrefix = '', + manualSetGasPrice, + originalTxHash = null, + pendingTxNonce, + shouldThrowError, + } = transactionParams; + + const data = executeContractAction({ + actionName: action, + ...contractActionParams, + }); + + const { + common, + contract, + contractName, + defaultGasPrice, + getGasPriceSuggestionFn, + gasLimit, + logFilePath, + oraclePrivateKey, + oraclePublicKey, + pendingTransactionFilePath, + tokenCode, + txNonce, + updateNonce, + web3Instance, + } = await getDefaultTransactionParams(chainName); -import { addLogMessage, handleChainError } from '../utils/log-files.js'; -import { getGasPrice, getWeb3Balance, convertWeiToGwei } from '../utils/prices.js'; - -export const polygonTransaction = async ({ - amount, - action, - chainName, - common, - contract, - contractName, - data, - defaultGasPrice, - domain, - getGasPriceSuggestionFn, - gasLimit, - handleSuccessedResult, - logFilePath, - logPrefix = '', - manualSetGasPrice, - oraclePrivateKey, - oraclePublicKey, - shouldThrowError, - tokenCode, - txNonce, - updateNonce, - web3Instance, -}) => { const signAndSendTransaction = async ({ txNonce, retryCount = 0 }) => { let gasPrice = 0; @@ -54,6 +170,8 @@ export const polygonTransaction = async ({ defaultGasPrice, getGasPriceSuggestionFn, logPrefix, + isRetry: retryCount > 0, + isReplace: isReplaceTx, }); } @@ -63,19 +181,13 @@ export const polygonTransaction = async ({ to: contract, from: oraclePublicKey, txNonce, + contractActionParams, + ...(isReplaceTx && { replacingTx: originalTxHash }), }; - if (tokenCode === ETH_TOKEN_CODE) { - submitLogData.amount = amount; - } - - if (tokenCode === MATIC_TOKEN_CODE || tokenCode === POLYGON_TOKEN_CODE) { - submitLogData.domain = domain; - } - addLogMessage({ filePath: logFilePath, - message: `${chainName} ${contractName} ${action} submit ${JSON.stringify(submitLogData)}}`, + message: `${chainName} ${contractName} ${action} ${isReplaceTx ? 'Replace' : ''} submit ${JSON.stringify(submitLogData)}}`, }); // we shouldn't await it to do not block the rest of the actions @@ -89,36 +201,57 @@ export const polygonTransaction = async ({ web3Instance, }); - const preparedTransaction = Transaction.fromTxData( - { - gasPrice: web3Instance.utils.toHex(gasPrice), - gasLimit: web3Instance.utils.toHex(gasLimit), - to: contract, - data, - from: oraclePublicKey, - nonce: web3Instance.utils.toHex(txNonce), - }, - { common }, - ); + const txObject = { + from: oraclePublicKey, + to: contract, + data, + gasPrice: web3Instance.utils.toHex(gasPrice), + gasLimit: web3Instance.utils.toHex(gasLimit), + nonce: web3Instance.utils.toHex(txNonce), + chainId: parseInt(common.chainId()), + hardfork: common.hardfork(), + }; const privateKey = Buffer.from(oraclePrivateKey, 'hex'); - const serializedTx = preparedTransaction.sign(privateKey).serialize().toString('hex'); + + const signedTx = await web3Instance.eth.accounts.signTransaction( + txObject, + privateKey, + ); try { await web3Instance.eth - .sendSignedTransaction('0x' + serializedTx) + .sendSignedTransaction(signedTx.rawTransaction, { + transactionPollingTimeout: MAX_TRANSACTION_AGE, + }) .on('transactionHash', (hash) => { console.log( - `Transaction has been signed and send into the chain. TxHash: ${hash}, nonce: ${txNonce}`, + `${isReplaceTx ? 'Replacement of' : ''} Transaction has been signed and send into the chain. TxHash: ${hash}, nonce: ${txNonce} ${isReplaceTx ? `, replacing: ${originalTxHash}` : ''}`, ); + // Store transaction + addLogMessage({ + filePath: pendingTransactionFilePath, + message: `${hash} ${JSON.stringify({ + action, + chainName, + contractActionParams, + timestamp: Date.now(), + isReplaceTx: isReplaceTx, + originalTxHash: originalTxHash, + txNonce, + })}`, + addTimestamp: false, + }); }) .on('receipt', (receipt) => { console.log( - `${logPrefix} Transaction has been successfully completed in the chain.`, + `${logPrefix} ${isReplaceTx ? 'Replacement of' : ''} Transaction has been successfully completed in the chain.`, ); - if (handleSuccessedResult) { + + if (receipt && handleSuccessedResult) { try { - handleSuccessedResult && handleSuccessedResult(receipt); + const parsedReceipt = stringifyWithBigInt(receipt); + handleSuccessedResult && handleSuccessedResult(parsedReceipt); } catch (error) { console.log('RECEIPT ERROR', error); } @@ -126,12 +259,21 @@ export const polygonTransaction = async ({ }) .on('error', (error, receipt) => { console.log(`${logPrefix} Transaction has been failed in the chain.`); - handleChainError(error); - if (receipt && receipt.blockHash && !receipt.status) - console.log( - `${logPrefix} It looks like the transaction ended out of gas. Or Oracle has already approved this ObtId. Also, check nonce value`, - ); + handleChainError({ + consoleMessage: `${error.message}: ${error.reason}`, + logMessage: `${error.message}: ${error.reason}`, + }); + + if (receipt) { + // status is BigInt after web3 updates to 4x version + if (receipt.blockHash && receipt.status === BigInt(0)) + console.log( + `${logPrefix} It looks like the transaction ended out of gas. Or Oracle has already approved this ObtId. Also, check nonce value`, + ); + } else { + console.log(`${logPrefix} No receipt available for failed transaction`); + } }); } catch (error) { console.log(`${logPrefix} ${error.stack}`); @@ -140,25 +282,32 @@ export const polygonTransaction = async ({ const transactionAlreadyKnown = error.message.includes(ALREADY_KNOWN_TRANSACTION); const lowGasPriceError = error.message.includes(LOW_GAS_PRICE); const revertedByTheEvm = error.message.includes(REVERTED_BY_THE_EVM); + const alreadyApprovedHash = + error.reason && error.reason.includes(ALREADY_APPROVED_HASH); if ( retryCount < MAX_RETRY_TRANSACTION_ATTEMPTS && (nonceTooLowError || transactionAlreadyKnown || lowGasPriceError || - revertedByTheEvm) + revertedByTheEvm) && + !alreadyApprovedHash ) { // Retry with an incremented nonce console.log( `Retrying (attempt ${retryCount + 1}/${MAX_RETRY_TRANSACTION_ATTEMPTS}).`, ); - const incrementedNonce = txNonce + 1; + let newNonce = txNonce; + + if (nonceTooLowError) { + newNonce = txNonce + 1; - updateNonce && updateNonce(incrementedNonce); + updateNonce && updateNonce(newNonce); + } return signAndSendTransaction({ - txNonce: incrementedNonce, + txNonce: newNonce, retryCount: retryCount + 1, }); } else { @@ -167,5 +316,5 @@ export const polygonTransaction = async ({ } }; - await signAndSendTransaction({ txNonce }); + await signAndSendTransaction({ txNonce: pendingTxNonce || txNonce }); }; diff --git a/controller/utils/web3-services.js b/controller/utils/web3-services.js new file mode 100644 index 0000000..4e977c0 --- /dev/null +++ b/controller/utils/web3-services.js @@ -0,0 +1,49 @@ +import { Web3 } from 'web3'; + +import fioABI from '../../config/ABI/FIO.json' assert { type: 'json' }; +import fioNftABI from '../../config/ABI/FIONFT.json' assert { type: 'json' }; + +import config from '../../config/config.js'; + +const { + eth: { ETH_CONTRACT }, + infura: { eth, polygon }, + polygon: { POLYGON_CONTRACT }, +} = config; + +export class Web3Service { + static getEthWeb3() { + if (!this.ethInstance) { + this.ethInstance = new Web3(eth); + } + return this.ethInstance; + } + + static getPolygonWeb3() { + if (!this.polygonInstance) { + this.polygonInstance = new Web3(polygon); + } + return this.polygonInstance; + } + + static getEthContract() { + if (!this.ethContractInstance) { + const ethWeb3Instance = this.getEthWeb3(); + + this.ethContractInstance = new ethWeb3Instance.eth.Contract(fioABI, ETH_CONTRACT); + } + return this.ethContractInstance; + } + + static getPolygonContract() { + if (!this.polygonContractInstance) { + const polygonWeb3Instance = this.getPolygonWeb3(); + + this.polygonContractInstance = new polygonWeb3Instance.eth.Contract( + fioNftABI, + POLYGON_CONTRACT, + ); + } + return this.polygonContractInstance; + } +} diff --git a/package.json b/package.json index 141e7be..25b0b98 100644 --- a/package.json +++ b/package.json @@ -31,27 +31,20 @@ }, "dependencies": { "@babel/runtime": "7.20.1", - "@ethereumjs/common": "3.0.1", - "@ethereumjs/tx": "4.0.1", - "@fioprotocol/fiojs": "1.0.1", + "@ethereumjs/common": "4.4.0", + "@fioprotocol/fiojs": "1.0.2", "big.js": "6.2.1", "body-parser": "1.20.1", "cors": "2.8.5", "dotenv": "16.4.5", "dotenv-safe": "9.1.0", - "esm": "3.2.25", - "ethers": "5.7.2", "express": "4.18.2", - "fs": "0.0.1-security", - "moralis": "2.26.2", + "moralis": "2.27.2", "node-cache": "5.1.2", "node-fetch": "3.3.2", - "node-file-cache": "1.0.2", - "node-libcurl": "4.0.0", "text-encoding": "0.7.0", - "thirdweb": "5.28.0", - "web3": "1.10.2", - "web3-eth-contract": "1.10.2" + "thirdweb": "5.83.0", + "web3": "4.16.0" }, "type": "module" } diff --git a/scripts/oracle.js b/scripts/oracle.js index ac7adb1..e23cdef 100644 --- a/scripts/oracle.js +++ b/scripts/oracle.js @@ -18,7 +18,7 @@ const args = process.argv; const oracle = { usage: - "Usage: npm run oracle ['wrap'|'unwrap'|'burn'] ['tokens'|'domain'] [amount|domain|tokenId] [fio handle or eth address] trxid ['clean'?] [gasPrice?]\n \ + "Usage: npm run oracle ['wrap'|'unwrap'|'burn'] ['tokens'|'domain'] [amount|domain|tokenId] [fio handle or eth address] obtid ['clean'?] [gasPrice?]\n \ Examples: \n \ npm run oracle wrap tokens 12000000000 0xe28FF0D44d533d15cD1f811f4DE8e6b1549945c9 ec52a13e3fd60c1a06ad3d9c0d66b97144aa020426d91cc43565483c743dd320 clean 1650000016\n \ npm run oracle wrap domain fiohacker 0xe28FF0D44d533d15cD1f811f4DE8e6b1549945c9 ec52a13e3fd60c1a06ad3d9c0d66b97144aa020426d91cc43565483c743dd320 clean 1650000016 \n \ diff --git a/scripts/oracleutils.js b/scripts/oracleutils.js index ebd0f85..7b996a3 100644 --- a/scripts/oracleutils.js +++ b/scripts/oracleutils.js @@ -1,143 +1,57 @@ import 'dotenv/config'; -import Web3 from 'web3'; - -import fioABI from '../config/ABI/FIO.json' assert { type: 'json' }; -import fioNftABIonPolygon from '../config/ABI/FIOMATICNFT.json' assert { type: 'json' }; -import config from '../config/config.js'; import { ACTION_NAMES, - CONTRACT_NAMES, ETH_CHAIN_NAME_CONSTANT, - ETH_TOKEN_CODE, POLYGON_CHAIN_NAME, - POLYGON_TOKEN_CODE, } from '../controller/constants/chain.js'; -import { LOG_FILES_PATH_NAMES } from '../controller/constants/log-files.js'; -import { - DEFAULT_ETH_GAS_PRICE, - DEFAULT_POLYGON_GAS_PRICE, - ETH_GAS_LIMIT, - POLYGON_GAS_LIMIT, -} from '../controller/constants/prices.js'; -import { - handleEthChainCommon, - handlePolygonChainCommon, -} from '../controller/utils/chain.js'; + import { runUnwrapFioTransaction } from '../controller/utils/fio-chain.js'; -import { - updateEthNonce, - updatePolygonNonce, - handlePolygonNonceValue, - handleEthNonceValue, -} from '../controller/utils/log-files.js'; -import { - getEthGasPriceSuggestion, - getPolygonGasPriceSuggestion, -} from '../controller/utils/prices.js'; -import { polygonTransaction } from '../controller/utils/transactions.js'; - -const { - eth: { ETH_ORACLE_PUBLIC, ETH_ORACLE_PRIVATE, ETH_CONTRACT }, - infura: { eth, polygon }, - polygon: { POLYGON_ORACLE_PUBLIC, POLYGON_ORACLE_PRIVATE, POLYGON_CONTRACT }, -} = config; - -const web3 = new Web3(eth); -const polygonWeb3 = new Web3(polygon); -const ethContract = new web3.eth.Contract(fioABI, ETH_CONTRACT); -const polygonContract = new web3.eth.Contract(fioNftABIonPolygon, POLYGON_CONTRACT); + +import { blockChainTransaction } from '../controller/utils/transactions.js'; const handleWrapEthAction = async ({ address, amount, domain, - obtId, //txIdOnFioChain + obtId, // oracleId from FIO wrapped table manualSetGasPrice, }) => { console.log( `ETH WRAP --> address: ${address}, obtId: ${obtId}, ${amount ? `amount: ${amount}` : `domain: ${domain}`}`, ); - const oraclePublicKey = ETH_ORACLE_PUBLIC; - const oraclePrivateKey = ETH_ORACLE_PRIVATE; - - const wrapFunction = ethContract.methods.wrap(address, amount, obtId); - - const wrapABI = wrapFunction.encodeABI(); - - const chainNonce = await web3.eth.getTransactionCount(oraclePublicKey, 'pending'); - - const txNonce = handleEthNonceValue({ chainNonce }); - - const common = handleEthChainCommon(); - - await polygonTransaction({ - amount, + await blockChainTransaction({ action: ACTION_NAMES.WRAP_TOKENS, chainName: ETH_CHAIN_NAME_CONSTANT, - common, - contract: ETH_CONTRACT, - contractName: CONTRACT_NAMES.ERC_20, - data: wrapABI, - defaultGasPrice: DEFAULT_ETH_GAS_PRICE, - getGasPriceSuggestionFn: getEthGasPriceSuggestion, - gasLimit: ETH_GAS_LIMIT, - logFilePath: LOG_FILES_PATH_NAMES.ETH, + contractActionParams: { + amount, + obtId, + pubaddress: address, + }, logPrefix: 'ETH WRAP NPM MANUAL ', manualSetGasPrice, - oraclePrivateKey, - oraclePublicKey, - tokenCode: ETH_TOKEN_CODE, - txNonce, - updateNonce: updateEthNonce, - web3Instance: web3, }); }; const handleWrapPolygonAction = async ({ address, domain, - obtId, //txIdOnFioChain + obtId, // oracleId from FIO wrapped table manualSetGasPrice, }) => { console.log(`POLYGON WRAP --> address: ${address}, obtId: ${obtId}, domain: ${domain}`); - const oraclePublicKey = POLYGON_ORACLE_PUBLIC; - const oraclePrivateKey = POLYGON_ORACLE_PRIVATE; - - const wrapDomainFunction = polygonContract.methods.wrapnft(address, domain, obtId); - const wrapABI = wrapDomainFunction.encodeABI(); - - const common = handlePolygonChainCommon(); - - const chainNonce = await polygonWeb3.eth.getTransactionCount( - oraclePublicKey, - 'pending', - ); - - const txNonce = handlePolygonNonceValue({ chainNonce }); - - await polygonTransaction({ + await blockChainTransaction({ action: ACTION_NAMES.WRAP_DOMAIN, chainName: POLYGON_CHAIN_NAME, - common, - contract: POLYGON_CONTRACT, - contractName: CONTRACT_NAMES.ERC_721, - data: wrapABI, - defaultGasPrice: DEFAULT_POLYGON_GAS_PRICE, - domain, - getGasPriceSuggestionFn: getPolygonGasPriceSuggestion, - gasLimit: POLYGON_GAS_LIMIT, - logFilePath: LOG_FILES_PATH_NAMES.POLYGON, + contractActionParams: { + domain, + obtId, + pubaddress: address, + }, logPrefix: 'POLYGON WRAP NPM MANUAL ', manualSetGasPrice, - oraclePrivateKey, - oraclePublicKey, - tokenCode: POLYGON_TOKEN_CODE, - txNonce, - updateNonce: updatePolygonNonce, - web3Instance: polygonWeb3, }); }; @@ -198,40 +112,15 @@ const handleUnwrapFromPolygonToFioChain = async ({ address, domain, obtId }) => const handleBurnNFTInPolygon = async ({ obtId, tokenId, manualSetGasPrice }) => { console.log(`POLYGON BURNNFT --> obtId: ${obtId}, tokenID: ${tokenId}`); - const oraclePublicKey = POLYGON_ORACLE_PUBLIC; - const oraclePrivateKey = POLYGON_ORACLE_PRIVATE; - - const wrapDomainFunction = polygonContract.methods.burnnft(tokenId, obtId); - const wrapABI = wrapDomainFunction.encodeABI(); - - const common = handlePolygonChainCommon(); - - const chainNonce = await polygonWeb3.eth.getTransactionCount( - oraclePublicKey, - 'pending', - ); - - const txNonce = handlePolygonNonceValue({ chainNonce }); - - await polygonTransaction({ + await blockChainTransaction({ action: ACTION_NAMES.BURN_NFT, chainName: POLYGON_CHAIN_NAME, - common, - contract: POLYGON_CONTRACT, - contractName: CONTRACT_NAMES.ERC_721, - data: wrapABI, - defaultGasPrice: DEFAULT_POLYGON_GAS_PRICE, - getGasPriceSuggestionFn: getPolygonGasPriceSuggestion, - gasLimit: POLYGON_GAS_LIMIT, - logFilePath: LOG_FILES_PATH_NAMES.POLYGON, + contractActionParams: { + tokenId, + obtId, + }, logPrefix: 'POLYGON BURNNFT NPM MANUAL ', manualSetGasPrice, - oraclePrivateKey, - oraclePublicKey, - tokenCode: POLYGON_TOKEN_CODE, - txNonce, - updateNonce: updatePolygonNonce, - web3Instance: polygonWeb3, }); }; From 7c8d5dec6ef677da504153a375d583283d24ccc2 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Thu, 16 Jan 2025 18:46:36 +0200 Subject: [PATCH 7/8] DASH-1269 handle bigint response from web3 results. Use web3 from Web3Service class. --- controller/api/fio.js | 38 +++++++++++++++------------------ controller/jobs/transactions.js | 2 +- controller/utils/chain.js | 20 +++++------------ 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/controller/api/fio.js b/controller/api/fio.js index 5534ee9..4d42904 100644 --- a/controller/api/fio.js +++ b/controller/api/fio.js @@ -33,7 +33,7 @@ import { getFioDeltasV2, runUnwrapFioTransaction, } from '../utils/fio-chain.js'; -import { sleep, convertTimestampIntoMs } from '../utils/general.js'; +import { sleep, convertTimestampIntoMs, stringifyWithBigInt } from '../utils/general.js'; import { addLogMessage, updateBlockNumberFIOForBurnNFT, @@ -52,22 +52,16 @@ import { handleChainError, } from '../utils/log-files.js'; import MathOp from '../utils/math.js'; +import { Web3Service } from '../utils/web3-services.js'; const { - eth: { BLOCKS_RANGE_LIMIT_ETH, BLOCKS_OFFSET_ETH, ETH_CONTRACT, ETH_NFT_CONTRACT }, + eth: { BLOCKS_RANGE_LIMIT_ETH, BLOCKS_OFFSET_ETH }, fio: { FIO_TRANSACTION_MAX_RETRIES, FIO_HISTORY_HYPERION_OFFSET, LOWEST_ORACLE_ID }, - infura: { eth, polygon }, nfts: { NFT_CHAIN_NAME }, oracleCache, polygon: { BLOCKS_RANGE_LIMIT_POLY, POLYGON_CONTRACT }, } = config; -const web3 = new Web3(eth); -const polyWeb3 = new Web3(polygon); -const fioTokenContractOnEthChain = new web3.eth.Contract(fioABI, ETH_CONTRACT); -const fioNftContract = new web3.eth.Contract(fioNftABI, ETH_NFT_CONTRACT); -const fioPolygonNftContract = new polyWeb3.eth.Contract(fioPolygonABI, POLYGON_CONTRACT); - // execute unwrap action job const handleUnwrapFromEthToFioChainJob = async () => { if (!oracleCache.get(ORACLE_CACHE_KEYS.isUnwrapOnEthJobExecuting)) @@ -384,9 +378,7 @@ class FIOCtrl { const blocksOffset = parseInt(BLOCKS_OFFSET_ETH) || 0; const getEthActionsLogs = async (from, to, isTokens = false) => { - return await ( - isTokens ? fioTokenContractOnEthChain : fioNftContract - ).getPastEvents( + return await Web3Service.getEthContract().getPastEvents( 'unwrapped', { fromBlock: from, @@ -413,7 +405,7 @@ class FIOCtrl { }; const getUnprocessedActionsLogs = async (isTokens = false) => { - const chainBlockNumber = await web3.eth.getBlockNumber(); + const chainBlockNumber = await Web3Service.getEthWeb3().eth.getBlockNumber(); const lastInChainBlockNumber = new MathOp(parseInt(chainBlockNumber)) .sub(blocksOffset) @@ -473,11 +465,11 @@ class FIOCtrl { if (unwrapTokensData.length > 0) { unwrapTokensData.forEach((item) => { - const logText = `${item.transactionHash} ${JSON.stringify(item.returnValues)}`; + const logText = `${item.transactionHash} ${stringifyWithBigInt(item.returnValues)}`; addLogMessage({ filePath: LOG_FILES_PATH_NAMES.ETH, - message: `${ETH_TOKEN_CODE} ${CONTRACT_NAMES.ERC_20} unwraptokens ${JSON.stringify(item)}`, + message: `${ETH_TOKEN_CODE} ${CONTRACT_NAMES.ERC_20} unwraptokens ${stringifyWithBigInt(item)}`, }); // save tx data into unwrap tokens and domains queue log file @@ -490,11 +482,12 @@ class FIOCtrl { } if (unwrapDomainsData.length > 0) { unwrapDomainsData.forEach((item) => { - const logText = item.transactionHash + ' ' + JSON.stringify(item.returnValues); + const logText = + item.transactionHash + ' ' + stringifyWithBigInt(item.returnValues); addLogMessage({ filePath: LOG_FILES_PATH_NAMES.ETH, - message: `${ETH_CHAIN_NAME_CONSTANT} ${CONTRACT_NAMES.ERC_721} unwrapdomains ${JSON.stringify(item)}`, + message: `${ETH_CHAIN_NAME_CONSTANT} ${CONTRACT_NAMES.ERC_721} unwrapdomains ${stringifyWithBigInt(item)}`, }); // save tx data into unwrap tokens and domains queue log file @@ -551,7 +544,7 @@ class FIOCtrl { const blocksRangeLimit = parseInt(BLOCKS_RANGE_LIMIT_POLY); const getPolygonActionsLogs = async (from, to) => { - return await fioPolygonNftContract.getPastEvents( + return await Web3Service.getPolygonContract().getPastEvents( 'unwrapped', { fromBlock: from, @@ -576,7 +569,9 @@ class FIOCtrl { }; const getUnprocessedActionsLogs = async () => { - const lastInChainBlockNumber = parseInt(await polyWeb3.eth.getBlockNumber()); + const lastInChainBlockNumber = parseInt( + await Web3Service.getPolygonWeb3().eth.getBlockNumber(), + ); const lastProcessedBlockNumber = getLastProceededBlockNumberOnPolygonChainForDomainUnwrapping(); @@ -624,11 +619,12 @@ class FIOCtrl { if (data.length > 0) { data.forEach((item) => { - const logText = item.transactionHash + ' ' + JSON.stringify(item.returnValues); + const logText = + item.transactionHash + ' ' + stringifyWithBigInt(item.returnValues); addLogMessage({ filePath: LOG_FILES_PATH_NAMES.POLYGON, - message: `${POLYGON_CHAIN_NAME} ${CONTRACT_NAMES.ERC_721} unwrapdomains ${JSON.stringify(item)}`, + message: `${POLYGON_CHAIN_NAME} ${CONTRACT_NAMES.ERC_721} unwrapdomains ${stringifyWithBigInt(item)}`, }); // save tx data into unwrap tokens and domains queue log file diff --git a/controller/jobs/transactions.js b/controller/jobs/transactions.js index c31e23f..60ed802 100644 --- a/controller/jobs/transactions.js +++ b/controller/jobs/transactions.js @@ -12,7 +12,7 @@ export const checkAndReplacePendingTransactions = async () => { const { oraclePublicKey, pendingTransactionFilePath, web3Instance } = defaultTransactionParams; - const logPrefix = 'Pending transactions handle: --> '; + const logPrefix = `${chainName} Pending transactions handle: --> `; try { const currentTime = Date.now(); diff --git a/controller/utils/chain.js b/controller/utils/chain.js index 9629b52..764a238 100644 --- a/controller/utils/chain.js +++ b/controller/utils/chain.js @@ -1,18 +1,13 @@ import { Common, CustomChain, Hardfork } from '@ethereumjs/common'; -import { Web3 } from 'web3'; import { Web3Service } from './web3-services.js'; -import fioABI from '../../config/ABI/FIO.json' assert { type: 'json' }; -import fioMaticNftABI from '../../config/ABI/FIOMATICNFT.json' assert { type: 'json' }; -import fioNftABI from '../../config/ABI/FIONFT.json' assert { type: 'json' }; import config from '../../config/config.js'; import { ACTION_NAMES } from '../constants/chain.js'; const { - eth: { ETH_ORACLE_PUBLIC, ETH_CONTRACT, ETH_NFT_CONTRACT, ETH_CHAIN_NAME }, - infura: { eth, polygon }, + eth: { ETH_ORACLE_PUBLIC, ETH_CHAIN_NAME }, isTestnet, - polygon: { POLYGON_ORACLE_PUBLIC, POLYGON_CONTRACT }, + polygon: { POLYGON_ORACLE_PUBLIC }, } = config; import { POLYGON_TESTNET_CHAIN_ID } from '../constants/chain.js'; @@ -36,12 +31,8 @@ export const handlePolygonChainCommon = () => { export const handleEthChainCommon = () => new Common({ chain: ETH_CHAIN_NAME, hardfork: Hardfork.London }); -export const isOracleEthAddressValid = async (isTokens = true) => { - const web3 = new Web3(eth); - const contract = new web3.eth.Contract( - isTokens ? fioABI : fioNftABI, - isTokens ? ETH_CONTRACT : ETH_NFT_CONTRACT, - ); +export const isOracleEthAddressValid = async () => { + const contract = Web3Service.getEthContract(); const registeredOraclesPublicKeys = await contract.methods.getOracles().call(); @@ -51,8 +42,7 @@ export const isOracleEthAddressValid = async (isTokens = true) => { }; export const isOraclePolygonAddressValid = async () => { - const web3 = new Web3(polygon); - const contract = new web3.eth.Contract(fioMaticNftABI, POLYGON_CONTRACT); + const contract = Web3Service.getPolygonContract(); const registeredOraclesPublicKeys = await contract.methods.getOracles().call(); From c6e8b253d5214ea1e81689b336da90ebe8580514 Mon Sep 17 00:00:00 2001 From: Oleksii Trukhanov Date: Thu, 16 Jan 2025 18:47:57 +0200 Subject: [PATCH 8/8] DASH-1269 Update version to 1.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25b0b98..179f040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oracle", - "version": "1.4.0", + "version": "1.4.2", "description": "Oracle for moving tokens and code to and from the FIO chain", "main": "index.js", "scripts": {