Skip to content

Commit

Permalink
Refactor methods that used mirror node endpoint `GET_ACCOUNTS_BY_ID_E…
Browse files Browse the repository at this point in the history
…NDPOINT`: getAccount, getAccountLatestEthereumTransactionsByTimestamp, getAccountPageLimit to stop accepting error codes 400 and 404, but instead to throw the error and moved the handling of the error to be specific of those methods and not as a general handler for all errors on the mirror node requests

Signed-off-by: Alfredo Gutierrez <[email protected]>
  • Loading branch information
AlfredoG87 committed Aug 18, 2023
1 parent 6724e2a commit 53b8cd0
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 37 deletions.
60 changes: 48 additions & 12 deletions packages/relay/src/lib/clients/mirrorNodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class MirrorNodeClient {
private readonly MIRROR_NODE_RETRY_DELAY = parseInt(process.env.MIRROR_NODE_RETRY_DELAY || '250');

static acceptedErrorStatusesResponsePerRequestPathMap: Map<string, Array<number>> = 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]],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions packages/relay/src/lib/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/relay/src/lib/precheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.`);
Expand Down
12 changes: 8 additions & 4 deletions packages/relay/src/lib/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/relay/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
}
};

Expand Down
81 changes: 68 additions & 13 deletions packages/relay/tests/lib/mirrorNodeClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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');

Expand All @@ -1038,15 +1065,15 @@ 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');

expect(mock.history.get.length).to.eq(2); // is called twice
});

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
});
Expand All @@ -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
});
Expand Down Expand Up @@ -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);
});
Expand Down

0 comments on commit 53b8cd0

Please sign in to comment.