Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add decoding of error codes #156

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions front-end-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.1.1

- Add decoding of a failed invoke transaction into a human-readable error.

## 3.1.0

- Add option to `deriveFromChain` given a module reference in Step 2.
Expand Down
2 changes: 1 addition & 1 deletion front-end-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "front-end-tools",
"packageManager": "yarn@4.0.2",
"version": "3.1.0",
"version": "3.1.1",
"license": "Apache-2.0",
"engines": {
"node": ">=16.x"
Expand Down
2 changes: 1 addition & 1 deletion front-end-tools/src/components/InitComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ export default function InitComponent(props: ConnectionProps) {
)}
{smartContractIndex !== undefined && (
<div className="actionResultBox">
Smart Contract Inedex:
Smart Contract Index:
<div>{smartContractIndex}</div>
</div>
)}
Expand Down
8 changes: 3 additions & 5 deletions front-end-tools/src/reading_from_blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
deserializeReceiveReturnValue,
serializeUpdateContractParameters,
ModuleReference,
InvokeContractFailedResult,
RejectedReceive,
AccountAddress,
AccountInfo,
ContractAddress,
Expand All @@ -17,6 +15,7 @@ import {
} from '@concordium/web-sdk';
import JSONbig from 'json-bigint';
import { CONTRACT_SUB_INDEX } from './constants';
import { decodeRejectReason } from './utils';

/**
* Retrieves information about a given smart contract instance.
Expand Down Expand Up @@ -229,15 +228,14 @@ export async function read(
const fullEntryPointName = `${contractName.value}.${entryPoint.value}`;

if (!res || res.tag === 'failure') {
const rejectReason = JSON.stringify(
((res as InvokeContractFailedResult)?.reason as RejectedReceive)?.rejectReason
);
const rejectReason = decodeRejectReason(res, contractName, entryPoint, moduleSchema);

throw new Error(
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' failed.
${rejectReason !== undefined ? `Reject reason: ${rejectReason}` : ''}`
DOBEN marked this conversation as resolved.
Show resolved Hide resolved
);
}

if (!res.returnValue) {
throw new Error(
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' returned no return_value`
Expand Down
156 changes: 156 additions & 0 deletions front-end-tools/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import {
toBuffer,
InvokeContractFailedResult,
RejectedReceive,
deserializeReceiveError,
EntrypointName,
ContractName,
} from '@concordium/web-sdk';
import { EXAMPLE_ARRAYS, EXAMPLE_JSON_OBJECT } from './constants';

export function arraysEqual(a: Uint8Array, b: Uint8Array) {
Expand All @@ -20,3 +28,151 @@ export function getObjectExample(template: string | undefined) {
export function getArrayExample(template: string | undefined) {
return template !== undefined ? JSON.stringify(JSON.parse(template), undefined, 2) : EXAMPLE_ARRAYS;
}

/**
* Decodes the reason for the transaction failure into a human-readable format.
*
* If the error is NOT caused by a smart contract logical revert, this function returns the reason in a human-readable format as follows:
* - `a human-readable rejectReason tag`. This can happen for example if the transaction runs out of energy. Such errors have tags that are human-readable (e.g. "OutOfEnergy").
*
* If the error is caused by a smart contract logical revert coming from the `concordium-std` crate, this function returns the reason in a human-readable format as follows:
* - `a human-readable error string` decoded from the `concordium-std` crate error codes.
*
* If the error is caused by a smart contract logical revert coming from the smart contract itself, this function returns the reason as follows:
* - `a rejectReason code` if NO error schema is provided (e.g. -1, -2, -3, ...).
* - `a human-readable error string` if an error schema is provided in the moduleSchema. This error schema is used to decode the above `rejectReason code` into a human-readable string.
*
* @param failedResult the failed invoke contract result.
* @param contractName the name of the contract.
* @param entryPoint the entry point name.
* @param moduleSchema an optional module schema including an error schema. If provided, the rejectReason code as logged by the smart contract can be decoded into a human-readable error string.
*
* @returns a decoded human-readable reject reason string (or falls back to return the error codes if decoding is impossible).
*/
export function decodeRejectReason(
failedResult: InvokeContractFailedResult,
contractName: ContractName.Type,
entryPoint: EntrypointName.Type,
moduleSchema: string | undefined
) {
let rejectReason;

const errorReason = failedResult.reason;

if (errorReason.tag === 'RejectedReceive') {
// If the error is due to a logical smart contract revert (`RejectedReceive` type), get the `rejectReason` code.
// e.g. -1, -2, ... (if the error comes from the smart contract).
// e,g, -2147483647, -2147483646, ... (if the error comes from the `concordium-std` crate).
rejectReason = ((failedResult as InvokeContractFailedResult)?.reason as RejectedReceive)?.rejectReason;

switch (rejectReason) {
// Check if the `rejectReason` comes from the `concordium-std` crate, and decode it into human-readable strings.
abizjak marked this conversation as resolved.
Show resolved Hide resolved
case -2147483647:
rejectReason = 'Error ()';
break;
case -2147483646:
rejectReason = 'ParseError';
break;
case -2147483645:
rejectReason = 'LogError::Full';
break;
case -2147483644:
rejectReason = 'LogError::Malformed';
break;
case -2147483643:
rejectReason = 'NewContractNameError::MissingInitPrefix';
break;
case -2147483642:
rejectReason = 'NewContractNameError::TooLong';
break;
case -2147483641:
rejectReason = 'NewReceiveNameError::MissingDotSeparator';
break;
case -2147483640:
rejectReason = 'NewReceiveNameError::TooLong';
break;
case -2147483639:
rejectReason = 'NewContractNameError::ContainsDot';
break;
case -2147483638:
rejectReason = 'NewContractNameError::InvalidCharacters';
break;
case -2147483637:
rejectReason = 'NewReceiveNameError::InvalidCharacters';
break;
case -2147483636:
rejectReason = 'NotPayableError';
break;
case -2147483635:
rejectReason = 'TransferError::AmountTooLarge';
break;
case -2147483634:
rejectReason = 'TransferError::MissingAccount';
break;
case -2147483633:
rejectReason = 'CallContractError::AmountTooLarge';
break;
case -2147483632:
rejectReason = 'CallContractError::MissingAccount';
break;
case -2147483631:
rejectReason = 'CallContractError::MissingContract';
break;
case -2147483630:
rejectReason = 'CallContractError::MissingEntrypoint';
break;
case -2147483629:
rejectReason = 'CallContractError::MessageFailed';
break;
case -2147483628:
rejectReason = 'CallContractError::LogicReject';
break;
case -2147483627:
rejectReason = 'CallContractError::Trap';
break;
case -2147483626:
rejectReason = 'UpgradeError::MissingModule';
break;
case -2147483625:
rejectReason = 'UpgradeError::MissingContract';
break;
case -2147483624:
rejectReason = 'UpgradeError::UnsupportedModuleVersion';
break;
case -2147483623:
rejectReason = 'QueryAccountBalanceError';
break;
case -2147483622:
rejectReason = 'QueryContractBalanceError';
break;
default:
// If the `rejectReason` comes from the smart contract itself (e.g. -1, -2, -3, ...) and an error schema is provided in the module schema, deserialize the reject reason into a human-readable string.
if (moduleSchema !== undefined) {
// Note: The rejectReason codes are converted to the byte tags of the enum type in Rust. Errors are represented as an enum type in Concordium smart contracts.
// -1 => 0x00
// -2 => 0x01
// -3 => 0x02
// -4 => 0x03
// ...
// This conversion works as long as there are no more then 256 (one byte) of different errors in the smart contract which should be sufficient for practical smart contracts.
const decodedError = deserializeReceiveError(
Uint8Array.from([Math.abs(rejectReason) - 1]).buffer,
toBuffer(moduleSchema, 'base64'),
contractName,
entryPoint
);
DOBEN marked this conversation as resolved.
Show resolved Hide resolved

// The object only includes one key. Its key is the human-readable error string.
const key = Object.keys(decodedError);

// Convert the human-readable error to a JSON string.
return JSON.stringify(key);
}
}
} else {
// If the error is not due to a logical smart contract revert, return the `errorReasonTag` instead (e.g. if the transactions runs out of energy)
rejectReason = errorReason.tag;
}

return rejectReason;
}
Loading