Skip to content

Commit

Permalink
Merge pull request near#1275 from denbite/rpc_error_message_formatting
Browse files Browse the repository at this point in the history
Display user-friendly messages for JSON RPC errors
  • Loading branch information
vikinatora authored Feb 8, 2024
2 parents 8a94f69 + 2815f1e commit bdaed65
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 477 deletions.
11 changes: 11 additions & 0 deletions .changeset/red-mice-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@near-js/providers": minor
"@near-js/utils": minor
---

Display user-friendly messages for JSON RPC errors:

- MethodNotFound
- CodeDoesNotExist
- AccessKeyDoesNotExist
- AccountDoesNotExist
3 changes: 2 additions & 1 deletion packages/accounts/test/account_multisig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const BN = require('bn.js');
const fs = require('fs');
const semver = require('semver');

const { Account2FA, MULTISIG_DEPOSIT, MULTISIG_GAS } = require('../lib');
const { Account2FA, MULTISIG_DEPOSIT, MULTISIG_GAS, MultisigStateStatus } = require('../lib');
const testUtils = require('./test-utils');

const { functionCall, transfer } = actionCreators;
Expand Down Expand Up @@ -47,6 +47,7 @@ const getAccount2FA = async (account, keyMapping = ({ public_key: publicKey }) =
account2fa.getRecoveryMethods = () => ({
data: keys.map(keyMapping)
});
account2fa.checkMultisigCodeAndStateStatus = () => ({ codeStatus: 1, stateStatus: MultisigStateStatus.STATE_NOT_INITIALIZED });
await account2fa.deployMultisig([...fs.readFileSync('./test/wasm/multisig.wasm')]);
return account2fa;
};
Expand Down
109 changes: 101 additions & 8 deletions packages/accounts/test/providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ const BN = require('bn.js');
const base58 = require('bs58');

const testUtils = require('./test-utils');
const { KeyPair } = require('@near-js/crypto');
let ERRORS_JSON = require('../../utils/lib/errors/error_messages.json');

jest.setTimeout(20000);
jest.setTimeout(60000);

describe('providers', () => {
let provider;
let near;
let provider;
let near;

beforeAll(async () => {
near = await testUtils.setUpTestConnection();
provider = near.connection.provider;
});
beforeAll(async () => {
near = await testUtils.setUpTestConnection();
provider = near.connection.provider;
});

describe('providers', () => {
test('txStatus with string hash and buffer hash', async () => {
const sender = await testUtils.createAccount(near);
const receiver = await testUtils.createAccount(near);
Expand Down Expand Up @@ -188,3 +190,94 @@ describe('providers', () => {
await expect(provider.lightClientProof(lightClientRequest)).rejects.toThrow(/.+ block .+ is ahead of head block .+/);
});
});

describe('providers errors', () => {
test('JSON RPC Error - MethodNotFound', async () => {
const account = await testUtils.createAccount(near);
const contract = await testUtils.deployContract(
account,
testUtils.generateUniqueString('test')
);

await contract.setValue({ args: { value: 'hello' } });

try {
const response = await provider.query({
request_type: 'call_function',
finality: 'optimistic',
account_id: contract.contractId,
method_name: 'methodNameThatDoesNotExist',
args_base64: '',
});
expect(response).toBeUndefined();
} catch (e) {
const errorType = 'MethodNotFound';
expect(e.type).toEqual(errorType);
expect(e.message).toEqual(ERRORS_JSON[errorType]);
}
});

test('JSON RPC Error - CodeDoesNotExist', async () => {
const { accountId } = await testUtils.createAccount(near);

try {
const response = await provider.query({
request_type: 'call_function',
finality: 'optimistic',
account_id: accountId,
method_name: 'methodNameThatDoesNotExistOnContractNotDeployed',
args_base64: '',
});
expect(response).toBeUndefined();
} catch (e) {
const errorType = 'CodeDoesNotExist';
expect(e.type).toEqual(errorType);
expect(e.message.split(' ').slice(0, 5)).toEqual(
ERRORS_JSON[errorType].split(' ').slice(0, 5)
);
}
});

test('JSON RPC Error - AccountDoesNotExist', async () => {
const accountName = 'abc.near';
try {
const response = await provider.query({
request_type: 'call_function',
finality: 'optimistic',
account_id: accountName,
method_name: 'methodNameThatDoesNotExistOnContractNotDeployed',
args_base64: '',
});
expect(response).toBeUndefined();
} catch (e) {
const errorType = 'AccountDoesNotExist';
expect(e.type).toEqual(errorType);
expect(e.message.split(' ').slice(0, 5)).toEqual(
ERRORS_JSON[errorType].split(' ').slice(0, 5)
);
}
});

test('JSON RPC Error - AccessKeyDoesNotExist', async () => {
const { accountId } = await testUtils.createAccount(near);

try {
const response = await provider.query({
request_type: 'view_access_key',
finality: 'optimistic',
account_id: accountId,
public_key: KeyPair.fromRandom('ed25519')
.getPublicKey()
.toString(),
});
expect(response).toBeUndefined();
} catch (e) {
const errorType = 'AccessKeyDoesNotExist';
expect(e.type).toEqual(errorType);
expect(e.message.split(' ').slice(0, 5)).toEqual(
ERRORS_JSON[errorType].split(' ').slice(0, 5)
);
}
});
});

14 changes: 13 additions & 1 deletion packages/providers/src/json-rpc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
*/
import {
baseEncode,
formatError,
getErrorTypeFromErrorMessage,
Logger,
parseRpcError,
ServerError,
} from '@near-js/utils';
import {
AccessKeyWithPublicKey,
Expand Down Expand Up @@ -366,7 +368,17 @@ export class JsonRpcProvider extends Provider {
throw new TypedError(errorMessage, 'TimeoutError');
}

throw new TypedError(errorMessage, getErrorTypeFromErrorMessage(response.error.data, response.error.name));
const errorType = getErrorTypeFromErrorMessage(response.error.data, '');
if (errorType) {
throw new TypedError(formatError(errorType, params), errorType);
}
throw new TypedError(errorMessage, response.error.name);
}
} else if (typeof response.result?.error === 'string') {
const errorType = getErrorTypeFromErrorMessage(response.result.error, '');

if (errorType) {
throw new ServerError(formatError(errorType, params), errorType);
}
}
// Success when response.error is not exist
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/fetch_error_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const https = require('https');
const fs = require('fs');

const ERROR_SCHEMA_URL =
'https://raw.githubusercontent.com/nearprotocol/nearcore/4c1149974ccf899dbcb2253a3e27cbab86dc47be/chain/jsonrpc/res/rpc_errors_schema.json';
const TARGET_SCHEMA_FILE_PATH = `${process.argv[2] || process.cwd()}/src/errors/rpc_error_schema.json'`;
'https://raw.githubusercontent.com/near/nearcore/master/chain/jsonrpc/res/rpc_errors_schema.json';
const TARGET_SCHEMA_FILE_PATH = `${process.argv[2] || process.cwd()}/src/errors/rpc_error_schema.json`;

https
.get(ERROR_SCHEMA_URL, resp => {
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/errors/error_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"AccountAlreadyExists": "Can't create a new account {{account_id}}, because it already exists",
"InvalidChain": "Transaction parent block hash doesn't belong to the current chain",
"AccountDoesNotExist": "Can't complete the action because account {{account_id}} doesn't exist",
"AccessKeyDoesNotExist": "Can't complete the action because access key {{public_key}} doesn't exist",
"MethodNameMismatch": "Transaction method name {{method_name}} isn't allowed by the access key",
"DeleteAccountHasRent": "Account {{account_id}} can't be deleted. It has {{#formatNear}}{{balance}}{{/formatNear}}, which is enough to cover the rent",
"DeleteAccountHasEnoughBalance": "Account {{account_id}} can't be deleted. It has {{#formatNear}}{{balance}}{{/formatNear}}, which is enough to cover it's storage",
Expand Down
Loading

0 comments on commit bdaed65

Please sign in to comment.