Skip to content

Commit

Permalink
Merge pull request #171 from Concordium/improve-error-code-decoding-d…
Browse files Browse the repository at this point in the history
…isclaimer

Prettified error reporting
  • Loading branch information
DOBEN authored Jun 24, 2024
2 parents 43adc73 + 1be52a3 commit ed3d0bc
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 63 deletions.
75 changes: 60 additions & 15 deletions front-end-tools/src/components/ReadComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@concordium/web-sdk';

import Box from './Box';
import { read, getEmbeddedSchema, getContractInfo } from '../reading_from_blockchain';
import { read, parseReturnValue, getEmbeddedSchema, getContractInfo, parseError } from '../reading_from_blockchain';
import { getObjectExample, getArrayExample } from '../utils';
import { INPUT_PARAMETER_TYPES_OPTIONS } from '../constants';

Expand Down Expand Up @@ -65,8 +65,9 @@ export default function ReadComponenet(props: ConnectionProps) {
| undefined
>(undefined);
const [returnValue, setReturnValue] = useState<string | undefined>(undefined);
const [errorContractInvoke, setErrorContractInvoke] = useState<string | undefined>(undefined);
const [errorContractInvoke, setErrorContractInvoke] = useState<string[] | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [addDisclaimer, setAddDisclaimer] = useState<boolean | undefined>(false);

const [entryPointTemplate, setEntryPointTemplate] = useState<string | undefined>(undefined);

Expand Down Expand Up @@ -135,16 +136,23 @@ export default function ReadComponenet(props: ConnectionProps) {
function onSubmit(data: FormType) {
setErrorContractInvoke(undefined);
setReturnValue(undefined);
setAddDisclaimer(false);

if (data.entryPointName === undefined) {
throw new Error(`Set entry point name`);
}

const entryPoint = EntrypointName.fromString(data.entryPointName);
const schema = data.deriveFromSmartContractIndex ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64;
const contractName = ContractName.fromString(data.smartContractName);
const contractIndex = BigInt(data.smartContractIndex);

// Invoke smart contract (read)

const promise = read(
client,
ContractName.fromString(data.smartContractName),
BigInt(data.smartContractIndex),
data.entryPointName ? EntrypointName.fromString(data.entryPointName) : undefined,
contractName,
contractIndex,
entryPoint,
data.hasInputParameter,
data.inputParameter,
data.inputParameterType,
Expand All @@ -153,10 +161,19 @@ export default function ReadComponenet(props: ConnectionProps) {
);

promise
.then((value) => {
setReturnValue(value);
.then((res) => {
if (res.tag === 'failure') {
const parsedError = parseError(res, contractName, contractIndex, entryPoint, schema);
setAddDisclaimer(parsedError.addDisclaimer);
setErrorContractInvoke(parsedError.errors);
} else {
const parsedValue = parseReturnValue(res, contractName, contractIndex, entryPoint, schema);
setReturnValue(parsedValue);
}
})
.catch((err: Error) => setErrorContractInvoke((err as Error).message));
.catch((err: Error) => {
setErrorContractInvoke([(err as Error).message]);
});
}

return (
Expand Down Expand Up @@ -526,21 +543,49 @@ export default function ReadComponenet(props: ConnectionProps) {

<br />
<br />
{errorContractInvoke && (

{errorContractInvoke && errorContractInvoke?.length !== 0 && (
<Alert variant="danger">
{' '}
Error: {errorContractInvoke}.
<strong>Error:</strong>

{errorContractInvoke.map((err, index) => (
/* eslint-disable-next-line react/no-array-index-key */
<div key={index}>{err}</div>
))}

<br />
<a href="https://developer.concordium.software/en/mainnet/smart-contracts/tutorials/piggy-bank/deploying.html#concordium-std-crate-errors">
Developer documentation: Explanation of error codes
<a
className="link"
target="_blank"
rel="noreferrer"
href="https://developer.concordium.software/en/mainnet/smart-contracts/tutorials/piggy-bank/deploying.html#concordium-std-crate-errors"
>
Developer documentation: Explanation of errors
</a>
<br />
<a href="https://docs.rs/concordium-std/latest/concordium_std/#signalling-errors">
<a
className="link"
target="_blank"
rel="noreferrer"
href="https://docs.rs/concordium-std/latest/concordium_std/#signalling-errors"
>
`Concordium-std` crate signalling errors
</a>
</Alert>
)}

{error && <Alert variant="danger"> Error: {error}.</Alert>}
{addDisclaimer && (
<Alert variant="warning">
Disclaimer: A smart contract can have logic to overwrite/change the meaning of the error codes
as defined in the concordium-std crate. While it is not advised to overwrite these error codes
and is rather unusual to do so, it&apos;s important to note that this tool decodes the error
codes based on the definitions in the concordium-std crate (assuming they have not been
overwritten with other meanings in the smart contract logic). No guarantee are given as such
that the meaning of the displayed prettified reject reason haven&apos;t been altered by the
smart contract logic.
</Alert>
)}
{returnValue && (
<div className="actionResultBox">
Read value:
Expand Down
121 changes: 82 additions & 39 deletions front-end-tools/src/reading_from_blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ReceiveName,
Parameter,
ReturnValue,
InvokeContractResult,
} from '@concordium/web-sdk';
import JSONbig from 'json-bigint';
import { CONTRACT_SUB_INDEX } from './constants';
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function getContractInfo(rpcClient: ConcordiumGRPCClient | undefine
* @throws If the `rpcClient` is undefined.
* @throws If the `moduleRef` is undefined.
*/
export async function getModuleSource(
export function getModuleSource(
rpcClient: ConcordiumGRPCClient | undefined,
moduleRef: ModuleReference.Type | undefined
) {
Expand All @@ -80,7 +81,7 @@ export async function getModuleSource(
* @throws If the `rpcClient` is undefined.
* @throws If the `moduleRef` is undefined.
*/
export async function getEmbeddedSchema(
export function getEmbeddedSchema(
rpcClient: ConcordiumGRPCClient | undefined,
moduleRef: ModuleReference.Type | undefined
) {
Expand Down Expand Up @@ -131,34 +132,27 @@ export function getAccountInfo(
}

/**
* Invokes a smart contract entry point and returns its return value.
* This function expects that the entry point is a `typical` smart contract view/read/getter function that returns a return value.
* This function throws an error if the entry point does not return a return value.
* If the moduleSchema parameter is undefined, the return value is in raw bytes.
* If a valid moduleSchema is provided, the return value is deserialized.
* Invokes a smart contract entry point and returns its response as a promise.
*
* @param rpcClient the rpcClient to query.
* @param contractName the contract name to be invoked.
* @param contractIndex the contract index to be invoked.
* @param entryPoint an optional entry point to be invoked. This function will throw if the entryPoint is undefined.
* @param entryPoint the entry point to be invoked.
* @param hasInputParameter a boolean signaling if the invoke should be executed with an input parameter.
* @param inputParameter an optional input parameter.
* @param inputParameterType an optional input parameter type (`string`/`number`/`array`/`object`).
* @param moduleSchema an optional module schema to serialize the input parameter and deserialize the return value.
* @param moduleSchema an optional module schema to serialize the input parameter.
* @param deriveContractInfoFromIndex a boolean signaling if values were derived from the contract index or manually inputted by the user.
*
* @returns the return value from the smart contract invoke in raw bytes (if no valid moduleSchema is provided) or deserialized (if a valid moduleSchema is provided).
* @returns the response as a promise from the smart contract invoke.
* @throws If the `rpcClient` is undefined.
* @throws If the `entryPoint` is undefined.
* @throws If the `hasInputParameter` is true but the input parameter cannot be serialized.
* @throws If the request to the node fails.
* @throws In case of a valid moduleSchema: if the deserialization of the return value fails.
*/
export async function read(
export function read(
rpcClient: ConcordiumGRPCClient | undefined,
contractName: ContractName.Type,
contractIndex: bigint,
entryPoint: EntrypointName.Type | undefined,
entryPoint: EntrypointName.Type,
hasInputParameter: boolean,
inputParameter: string | undefined,
inputParameterType: string | undefined,
Expand All @@ -169,10 +163,6 @@ export async function read(
throw new Error(`rpcClient undefined`);
}

if (entryPoint === undefined) {
throw new Error(`Set entry point name`);
}

let param = Parameter.empty();

if (hasInputParameter) {
Expand Down Expand Up @@ -219,34 +209,42 @@ export async function read(
}
}

const res = await rpcClient.invokeContract({
return rpcClient.invokeContract({
method: ReceiveName.create(contractName, entryPoint),
contract: ContractAddress.create(contractIndex, CONTRACT_SUB_INDEX),
parameter: param,
});
}

/**
* Parse the return value from a smart contract invoke.
* This function expects that the entry point that was invoked is a `typical` smart contract view/read/getter
* function that returns a return value. As such, this function throws an error if there is no return value.
* If the moduleSchema parameter is undefined, the return value is in raw bytes.
* If a valid moduleSchema is provided, the return value is deserialized.
*
* @param res the response of a smart contract invoke.
* @param contractName the contract name that was invoked.
* @param contractIndex the contract index that was invoked.
* @param entryPoint the entry point that was invoked.
* @param moduleSchema an optional module schema to deserialize the return value.
*
* @returns the return value from the smart contract invoke in raw bytes (if no valid moduleSchema is provided) or deserialized (if a valid moduleSchema is provided).
* @throws If there is no `returnValue` in the response.
* @throws In case of a valid moduleSchema: if the deserialization of the return value fails.
*/
export function parseReturnValue(
res: InvokeContractResult,
contractName: ContractName.Type,
contractIndex: bigint,
entryPoint: EntrypointName.Type,
moduleSchema: string | undefined
) {
const fullEntryPointName = `${contractName.value}.${entryPoint.value}`;

if (!res || res.tag === 'failure') {
const [rejectReasonCode, humanReadableError] = decodeRejectReason(res, contractName, entryPoint, moduleSchema);

throw new Error(
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' failed
${rejectReasonCode !== undefined ? `. Reject reason code: ${rejectReasonCode}` : ''} ${
humanReadableError !== undefined
? `. Prettified reject reason: ${humanReadableError} (Warning: A smart contract can have logic to
overwrite/change the meaning of the error codes as defined in the concordium-std crate.
While it is not advised to overwrite these error codes and is rather unusual to do so, it's important to note that
this tool decodes the error codes based on the definitions in the concordium-std crate (assuming they have not been overwritten
with other meanings in the smart contract logic). No guarantee are given as such that the meaning of the displayed prettified reject reason haven't been altered by the smart contract logic.)`
: ''
}`
);
}

if (!res.returnValue) {
if (!res.returnValue || !res.returnValue?.buffer.length) {
throw new Error(
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' returned no return_value`
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' returned no return_value.`
);
}

Expand Down Expand Up @@ -282,3 +280,48 @@ export async function read(
return JSONbig.stringify(returnValue);
}
}

/**
* This function parses the error from a response of a smart contract invoke.
* If the response tag is a 'failure', this function extracts the error code and decodes the error as much as possible
* into human-readable error text snippets. In addition, this function signals with the returned `addDisclaimer` value
* if the error text snippets contain decoded human-readable error information which should be displayed with
* a disclaimer since the decoding step is done based on the assumption that error codes have not been
* manually overwritten by the smart contract developer.
*
* @param res the response of a smart contract invoke.
* @param contractName the contract name that was invoked.
* @param contractIndex the contract index that was invoked.
* @param entryPoint the entry point that was invoked.
* @param moduleSchema an optional module schema to decode the error code.
*
* @returns error text snippets and the `addDisclaimer` boolean which signals if the error text snippets contain
* decoded human-readable error information which should be displayed with a disclaimer.
*/
export function parseError(
res: InvokeContractResult,
contractName: ContractName.Type,
contractIndex: bigint,
entryPoint: EntrypointName.Type,
moduleSchema: string | undefined
) {
const fullEntryPointName = `${contractName.value}.${entryPoint.value}`;
const returnValue = { addDisclaimer: false, errors: [] as string[] };

if (res.tag === 'failure') {
const [rejectReasonCode, humanReadableError] = decodeRejectReason(res, contractName, entryPoint, moduleSchema);

if (humanReadableError) {
returnValue.errors.push(`Prettified reject reason: ${humanReadableError}.`);
returnValue.addDisclaimer = true;
}
if (rejectReasonCode) {
returnValue.errors.push(`Reject reason code: ${rejectReasonCode}.`);
}
returnValue.errors.push(
`RPC call 'invokeContract' on method '${fullEntryPointName}' of contract '${contractIndex}' failed.`
);
}

return returnValue;
}
17 changes: 8 additions & 9 deletions front-end-tools/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,20 +188,19 @@ export function decodeRejectReason(
// to deserialize the error with the schema before falling back to decode the `concordium-std` crate errors manually.
// Only the `NotPayableError` is treated special since it has no matched error in the errorSchema.
if (moduleSchema !== undefined && failedResult.returnValue !== undefined) {
const decodedError = deserializeReceiveError(
ReturnValue.toBuffer(failedResult.returnValue),
toBuffer(moduleSchema, 'base64'),
contractName,
entryPoint
);

if (decodedError !== undefined) {
try {
const decodedError = deserializeReceiveError(
ReturnValue.toBuffer(failedResult.returnValue),
toBuffer(moduleSchema, 'base64'),
contractName,
entryPoint
);
// 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.
humanReadableError = JSON.stringify(key);
} else {
} catch (_error) {
// Falling back to decode the `concordium-std` crate errors manually
// Decode the `rejectReason` based on the error codes defined in `concordium-std` crate, and decode it into a human-readable string if possible.
humanReadableError = decodeConcordiumStdError(rejectReasonCode);
Expand Down

0 comments on commit ed3d0bc

Please sign in to comment.