diff --git a/packages/relay/src/lib/clients/mirrorNodeClient.ts b/packages/relay/src/lib/clients/mirrorNodeClient.ts index ad794386e1..876b488b13 100644 --- a/packages/relay/src/lib/clients/mirrorNodeClient.ts +++ b/packages/relay/src/lib/clients/mirrorNodeClient.ts @@ -91,7 +91,7 @@ export class MirrorNodeClient { private readonly MIRROR_NODE_RETRY_DELAY = parseInt(process.env.MIRROR_NODE_RETRY_DELAY || '250'); static acceptedErrorStatusesResponsePerRequestPathMap: Map> = new Map([ - [MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, [400, 404]], + [MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, []], [MirrorNodeClient.GET_BALANCE_ENDPOINT, [400, 404]], [MirrorNodeClient.GET_BLOCK_ENDPOINT, [400, 404]], [MirrorNodeClient.GET_BLOCKS_ENDPOINT, [400, 404]], @@ -388,30 +388,66 @@ export class MirrorNodeClient { } } + private handleAccountNotFound(error: any, idOrAliasOrEvmAddress, requestIdPrefix) : null { + if(error instanceof MirrorNodeClientError) { + if (error.statusCode == 404) { + return null; + + } else if (error.statusCode == 400) { + this.logger.debug(`${formatRequestIdMessage(requestIdPrefix)} Got Invalid Parameter when trying to fetch account from mirror node: ${JSON.stringify(error)}`); + throw predefined.INVALID_PARAMETER(idOrAliasOrEvmAddress, `Invalid 'address' field in transaction param. Address must be a valid 20 bytes hex string`); + } + } + // if is not a mirror node client error 400 or 404, rethrow + this.logger.error(`${formatRequestIdMessage(requestIdPrefix)} Unexpected error raised while fetching account from mirror-node: ${JSON.stringify(error)}`); + throw error; + } + public async getAccount(idOrAliasOrEvmAddress: string, requestIdPrefix?: string) { return this.get(`${MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT}${idOrAliasOrEvmAddress}?transactions=false`, MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, requestIdPrefix); } - public async getAccountLatestEthereumTransactionsByTimestamp(idOrAliasOrEvmAddress: string, timestampTo: string, numberOfTransactions: number = 1, requestIdPrefix?: string) { + /** + * Returns account or null if account does not exist + * @param {string} idOrAliasOrEvmAddress commonly an evm address + * @param {string} requestIdPrefix optional request id prefix + */ + public async getAccountOrNull(idOrAliasOrEvmAddress: string, requestIdPrefix?: string) { + try { + return await this.getAccount(idOrAliasOrEvmAddress, requestIdPrefix); + } catch (error: any) { + return this.handleAccountNotFound(error, idOrAliasOrEvmAddress, requestIdPrefix); + } + } + + public async getAccountLatestEthereumTransactionsByTimestampOrNull(idOrAliasOrEvmAddress: string, timestampTo: string, numberOfTransactions: number = 1, requestIdPrefix?: string) { const queryParamObject = {}; this.setQueryParam(queryParamObject, MirrorNodeClient.ACCOUNT_TRANSACTION_TYPE_PROPERTY, MirrorNodeClient.ETHEREUM_TRANSACTION_TYPE); this.setQueryParam(queryParamObject, MirrorNodeClient.ACCOUNT_TIMESTAMP_PROPERTY, `lte:${timestampTo}`); this.setLimitOrderParams(queryParamObject, this.getLimitOrderQueryParam(numberOfTransactions, constants.ORDER.DESC)); // get latest 2 transactions to infer for single case const queryParams = this.getQueryParams(queryParamObject); - return this.get( - `${MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT}${idOrAliasOrEvmAddress}${queryParams}`, - MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, - requestIdPrefix - ); + try { + return await this.get( + `${MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT}${idOrAliasOrEvmAddress}${queryParams}`, + MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, + requestIdPrefix + ); + } catch (error: any) { + return this.handleAccountNotFound(error, idOrAliasOrEvmAddress, requestIdPrefix); + } } - public async getAccountPageLimit(idOrAliasOrEvmAddress: string, requestIdPrefix?: string) { - return this.get(`${MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT}${idOrAliasOrEvmAddress}?limit=${constants.MIRROR_NODE_QUERY_LIMIT}`, - MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, - requestIdPrefix); + public async getAccountPageLimitOrNull(idOrAliasOrEvmAddress: string, requestIdPrefix?: string) { + try { + return await this.get(`${MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT}${idOrAliasOrEvmAddress}?limit=${constants.MIRROR_NODE_QUERY_LIMIT}`, + MirrorNodeClient.GET_ACCOUNTS_BY_ID_ENDPOINT, + requestIdPrefix); + } catch (error: any) { + return this.handleAccountNotFound(error, idOrAliasOrEvmAddress, requestIdPrefix); + } } /******************************************************************************* * To be used to make paginated calls for the account information when the @@ -924,7 +960,7 @@ export class MirrorNodeClient { let data; try { const promises = [ - searchableTypes.find(t => t === constants.TYPE_ACCOUNT) ? buildPromise(this.getAccount(entityIdentifier, requestIdPrefix).catch(() => {return null;})) : Promise.reject(), + searchableTypes.find(t => t === constants.TYPE_ACCOUNT) ? buildPromise(this.getAccountOrNull(entityIdentifier, requestIdPrefix).catch(() => {return null;})) : Promise.reject(), ]; // only add long zero evm addresses for tokens as they do not refer to actual contract addresses but rather encoded entity nums diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index f45c878590..994a97ab94 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -513,7 +513,7 @@ export class EthImpl implements Eth { const accountCacheKey = `${constants.CACHE_KEY.ACCOUNT}_${transaction.to}`; let toAccount: object | null = this.cache.get(accountCacheKey, EthImpl.ethEstimateGas); if (!toAccount) { - toAccount = await this.mirrorNodeClient.getAccount(transaction.to, requestIdPrefix); + toAccount = await this.mirrorNodeClient.getAccountOrNull(transaction.to, requestIdPrefix); } // when account exists return default base gas, otherwise return the minimum amount of gas to create an account entity @@ -807,7 +807,7 @@ export class EthImpl implements Eth { else { let currentBalance = 0; let balanceFromTxs = 0; - mirrorAccount = await this.mirrorNodeClient.getAccountPageLimit(account, requestIdPrefix); + mirrorAccount = await this.mirrorNodeClient.getAccountPageLimitOrNull(account, requestIdPrefix); if (mirrorAccount.balance) { currentBalance = mirrorAccount.balance.balance; } @@ -843,7 +843,7 @@ export class EthImpl implements Eth { if (!balanceFound && !mirrorAccount) { // If no balance and no account, then we need to make a request to the mirror node for the account. - mirrorAccount = await this.mirrorNodeClient.getAccountPageLimit(account, requestIdPrefix); + mirrorAccount = await this.mirrorNodeClient.getAccountPageLimitOrNull(account, requestIdPrefix); // Test if exists here if(mirrorAccount !== null && mirrorAccount !== undefined) { balanceFound = true; @@ -1194,7 +1194,7 @@ export class EthImpl implements Eth { if (tx?.transactions?.length) { const result = tx.transactions[0].result; if (result === constants.TRANSACTION_RESULT_STATUS.WRONG_NONCE) { - const accountInfo = await this.mirrorNodeClient.getAccount(parsedTx.from!, requestIdPrefix); + const accountInfo = await this.mirrorNodeClient.getAccountOrNull(parsedTx.from!, requestIdPrefix); const accountNonce = accountInfo.ethereum_nonce; if (parsedTx.nonce > accountNonce) { throw predefined.NONCE_TOO_HIGH(parsedTx.nonce, accountNonce); @@ -1439,7 +1439,7 @@ export class EthImpl implements Eth { const accountCacheKey = `${constants.CACHE_KEY.ACCOUNT}_${fromAddress}`; let accountResult: any | null = this.cache.get(accountCacheKey, EthImpl.ethGetTransactionByHash); if (!accountResult) { - accountResult = await this.mirrorNodeClient.getAccount(fromAddress, requestIdPrefix); + accountResult = await this.mirrorNodeClient.getAccountOrNull(fromAddress, requestIdPrefix); if (accountResult) { this.cache.set(accountCacheKey, accountResult, EthImpl.ethGetTransactionByHash, undefined, requestIdPrefix); } @@ -1747,7 +1747,7 @@ export class EthImpl implements Eth { } private async getAccountLatestEthereumNonce(address: string, requestId?: string) { - const accountData = await this.mirrorNodeClient.getAccount(address, requestId); + const accountData = await this.mirrorNodeClient.getAccountOrNull(address, requestId); if (accountData) { // with HIP 729 ethereum_nonce should always be 0+ and null. Historical contracts may have a null value as the nonce was not tracked, return default EVM compliant 0x1 in this case return accountData.ethereum_nonce !== null ? numberTo0x(accountData.ethereum_nonce) : EthImpl.oneHex; @@ -1773,7 +1773,7 @@ export class EthImpl implements Eth { } // get the latest 2 ethereum transactions for the account - const ethereumTransactions = await this.mirrorNodeClient.getAccountLatestEthereumTransactionsByTimestamp(address, block.timestamp.to, 2, requestIdPrefix); + const ethereumTransactions = await this.mirrorNodeClient.getAccountLatestEthereumTransactionsByTimestampOrNull(address, block.timestamp.to, 2, requestIdPrefix); if (ethereumTransactions == null || ethereumTransactions.transactions.length === 0) { return EthImpl.zeroHex; } diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index 0220f4a774..da7ed32d7a 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -67,7 +67,7 @@ export class Precheck { async verifyAccount(tx: Transaction, requestId?: string) { const requestIdPrefix = formatRequestIdMessage(requestId); // verify account - const accountInfo = await this.mirrorNodeClient.getAccount(tx.from!, requestId); + const accountInfo = await this.mirrorNodeClient.getAccountOrNull(tx.from!, requestId); if (accountInfo == null) { this.logger.trace(`${requestIdPrefix} Failed to retrieve address '${tx.from}' account details from mirror node on verify account precheck for sendRawTransaction(transaction=${JSON.stringify(tx)})`); throw predefined.RESOURCE_NOT_FOUND(`address '${tx.from}'.`); diff --git a/packages/relay/src/lib/relay.ts b/packages/relay/src/lib/relay.ts index 2a27dad179..064ff741c4 100644 --- a/packages/relay/src/lib/relay.ts +++ b/packages/relay/src/lib/relay.ts @@ -111,10 +111,14 @@ export class RelayImpl implements Relay { // Invoked when the registry collects its metrics' values. // Allows for updated account balance tracking try { - const account = await mirrorNodeClient.getAccount(clientMain.operatorAccountId!.toString()); - const accountBalance = account.balance?.balance; - this.labels({ 'accountId': clientMain.operatorAccountId?.toString() }) - .set(accountBalance); + const account = await mirrorNodeClient.getAccountOrNull(clientMain.operatorAccountId!.toString()); + if(account) { + const accountBalance = account.balance?.balance; + this.labels({'accountId': clientMain.operatorAccountId?.toString()}) + .set(accountBalance); + } else { + logger.warn(`Account for given operator: ${clientMain.operatorAccountId?.toString()} not found on mirror node`); + } } catch (e: any) { logger.error(e, `Error collecting operator balance. Skipping balance set`); } diff --git a/packages/relay/tests/helpers.ts b/packages/relay/tests/helpers.ts index 155ee114c9..284e8ffcb5 100644 --- a/packages/relay/tests/helpers.ts +++ b/packages/relay/tests/helpers.ts @@ -309,6 +309,19 @@ const mockData = { } ] } + }, + + invalidParameter: { + "_status": { + "messages": [ + { + "message": "Invalid parameter: account.id" + }, + { + "message": "Invalid Transaction id. Please use \\shard.realm.num-sss-nnn\\ format where sss are seconds and nnn are nanoseconds" + } + ] + } } }; diff --git a/packages/relay/tests/lib/mirrorNodeClient.spec.ts b/packages/relay/tests/lib/mirrorNodeClient.spec.ts index be855eee90..6bafa83c74 100644 --- a/packages/relay/tests/lib/mirrorNodeClient.spec.ts +++ b/packages/relay/tests/lib/mirrorNodeClient.spec.ts @@ -249,7 +249,7 @@ describe('MirrorNodeClient', async function () { } }); - const result = await mirrorNodeInstance.getAccount(alias); + const result = await mirrorNodeInstance.getAccountOrNull(alias); expect(result).to.exist; expect(result.links).to.exist; expect(result.links.next).to.equal(null); @@ -358,19 +358,46 @@ describe('MirrorNodeClient', async function () { it('`getAccount`', async () => { mock.onGet(`accounts/${mockData.accountEvmAddress}${noTransactions}`).reply(200, mockData.account); - const result = await mirrorNodeInstance.getAccount(mockData.accountEvmAddress); + const result = await mirrorNodeInstance.getAccountOrNull(mockData.accountEvmAddress); expect(result).to.exist; expect(result.account).equal('0.0.1014'); }); - it('`getAccount` not found', async () => { + it('`getAccountOrNull` not found', async () => { const evmAddress = '0x00000000000000000000000000000000000003f6'; mock.onGet(`accounts/${evmAddress}${noTransactions}`).reply(404, mockData.notFound); - const result = await mirrorNodeInstance.getAccount(evmAddress); + const result = await mirrorNodeInstance.getAccountOrNull(evmAddress); expect(result).to.be.null; }); + it('getAccountOrNull 500 Unexpected error', async () => { + const evmAddress = '0x00000000000000000000000000000000000004f7'; + mock.onGet(`accounts/${evmAddress}${noTransactions}`).reply(500, { error: 'unexpected error' }); + let errorRaised = false; + try { + await mirrorNodeInstance.getAccountOrNull(evmAddress); + } + catch (error: any) { + errorRaised = true; + expect(error.message).to.equal(`Request failed with status code 500`); + } + expect(errorRaised).to.be.true; + }); + + it(`getAccountOrNull validation error`, async () => { + const invalidAddress = "0x123"; + mock.onGet(`accounts/${invalidAddress}${noTransactions}`).reply(400); + let errorRaised = false; + try { + await mirrorNodeInstance.getAccountOrNull(invalidAddress); + } catch (error: any) { + errorRaised = true; + expect(error.message).to.equal(`Invalid parameter ${invalidAddress}: Invalid 'address' field in transaction param. Address must be a valid 20 bytes hex string`); + } + expect(errorRaised).to.be.true; + }); + it('`getTokenById`', async () => { mock.onGet(`tokens/${mockData.tokenId}`).reply(200, mockData.token); @@ -1026,7 +1053,7 @@ describe('MirrorNodeClient', async function () { it('if the method returns an immediate result it is called only once', async () => { mock.onGet(uri).reply(200, mockData.account); - const result = await mirrorNodeInstance.repeatedRequest('getAccount', [mockData.accountEvmAddress], 3); + const result = await mirrorNodeInstance.repeatedRequest('getAccountOrNull', [mockData.accountEvmAddress], 3); expect(result).to.exist; expect(result.account).equal('0.0.1014'); @@ -1038,7 +1065,7 @@ describe('MirrorNodeClient', async function () { mock.onGet(uri).replyOnce(404, mockData.notFound) .onGet(uri).reply(200, mockData.account) - const result = await mirrorNodeInstance.repeatedRequest('getAccount', [mockData.accountEvmAddress], 3); + const result = await mirrorNodeInstance.repeatedRequest('getAccountOrNull', [mockData.accountEvmAddress], 3); expect(result).to.exist; expect(result.account).equal('0.0.1014'); @@ -1046,7 +1073,7 @@ describe('MirrorNodeClient', async function () { }); it('method is repeated the specified number of times if no result is found', async () => { - const result = await mirrorNodeInstance.repeatedRequest('getAccount', [mockData.accountEvmAddress], 3); + const result = await mirrorNodeInstance.repeatedRequest('getAccountOrNull', [mockData.accountEvmAddress], 3); expect(result).to.be.null; expect(mock.history.get.length).to.eq(3); // is called three times }); @@ -1058,7 +1085,7 @@ describe('MirrorNodeClient', async function () { .onGet(uri).replyOnce(404, mockData.notFound) .onGet(uri).reply(200, mockData.account) - const result = await mirrorNodeInstance.repeatedRequest('getAccount', [mockData.accountEvmAddress], 3); + const result = await mirrorNodeInstance.repeatedRequest('getAccountOrNull', [mockData.accountEvmAddress], 3); expect(result).to.be.null; expect(mock.history.get.length).to.eq(3); // is called three times }); @@ -1113,29 +1140,57 @@ describe('MirrorNodeClient', async function () { ] }; - it('should fail to fetch transaction by non existing account', async() => { + it('should fail to fetch transaction by non existing account and return null', async() => { mock.onGet(transactionPath(evmAddress, 1)).reply(404, mockData.notFound); - const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestamp(evmAddress, timestamp); + const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(evmAddress, timestamp); expect(transactions).to.be.null; }); + it('should throw Error with unexpected exception if mirror node returns unexpected error', async() => { + const address = '0x00000000000000000000000000000000000007b8'; + mock.onGet(transactionPath(address, 1)).reply(500, { error: 'unexpected error' }); + let errorRaised = false; + try { + await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(address, timestamp); + } + catch (error: any) { + errorRaised = true; + expect(error.message).to.equal(`Request failed with status code 500`); + } + expect(errorRaised).to.be.true; + }); + + it('should throw invalid address error if mirror node returns 400 error status', async() => { + const invalidAddress = '0x123'; + mock.onGet(transactionPath(invalidAddress, 1)).reply(400, mockData.invalidParameter); + let errorRaised = false; + try { + await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(invalidAddress, timestamp); + } catch (error: any) { + errorRaised = true; + expect(error.message).to.equal(`Invalid parameter ${invalidAddress}: Invalid 'address' field in transaction param. Address must be a valid 20 bytes hex string`); + } + expect(errorRaised).to.be.true; + }); + + it('should be able to fetch empty ethereum transactions for an account', async() => { mock.onGet(transactionPath(evmAddress, 1)).reply(200, { transactions: [] }); - const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestamp(evmAddress, timestamp); + const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(evmAddress, timestamp); expect(transactions).to.exist; expect(transactions.transactions.length).to.equal(0); }); it('should be able to fetch single ethereum transactions for an account', async() => { mock.onGet(transactionPath(evmAddress, 1)).reply(200, { transactions: [defaultTransaction.transactions[0]] }); - const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestamp(evmAddress, timestamp); + const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(evmAddress, timestamp); expect(transactions).to.exist; expect(transactions.transactions.length).to.equal(1); }); it('should be able to fetch ethereum transactions for an account', async() => { mock.onGet(transactionPath(evmAddress, 2)).reply(200, defaultTransaction); - const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestamp(evmAddress, timestamp, 2); + const transactions = await mirrorNodeInstance.getAccountLatestEthereumTransactionsByTimestampOrNull(evmAddress, timestamp, 2); expect(transactions).to.exist; expect(transactions.transactions.length).to.equal(2); });