Handling OKRW Precompile Errors
Decode every revert path from the OKRW precompile — the module-owned UnauthorizedMinter, the shared InvalidAddress / InvalidAmount errors inherited from IPrecompile (now with string payloads), and the plain-string reverts for nil / negative amounts.
Since the shared-error refactor,
IOkrw is IPrecompile. Two errors that used to live on IOkrw — InvalidAddress(address) and InvalidAmount(uint256) — were removed and are now inherited from IPrecompile as InvalidAddress(string bad) and InvalidAmount(string amount). The 4-byte selectors changed (new argument types → new selectors), so any ABI fragment carrying the old shapes will fail to match on the client side even though the on-chain revert is still fully typed. This guide gives you a working decoder for every path.Prerequisites
- Basic knowledge of Solidity
try/catch. - Familiarity with JavaScript
try/catchand Ethers.js.
The three revert shapes
A
Every inherited
mint call fails in exactly one of three ways:| Trigger | Revert shape | Owner |
|---|---|---|
msg.sender ≠ configured minter | UnauthorizedMinter(address caller, address authorizedMinter) | IOkrw (module) |
recipient == 0x0 or unencodable | InvalidAddress(string bad) | IPrecompile (shared) |
amount == 0 | InvalidAmount(string amount) | IPrecompile (shared) |
amount is nil or negative (ABI-level abuse only) | plain string revert ("amount must not be nil" / "amount must not be negative") | not typed |
Every inherited
IPrecompile error (UnknownMethod, InvalidNumberOfArgs, SDKUnauthorized, EventEmitFailed, …) may also surface for internal / boundary faults — decode them the same way.// The full IOkrw error surface — module-owned + inherited.
import {IPrecompile} from "@maroo-chain/contracts/precompiles/common/interfaces/IPrecompile.sol";
interface IOkrw is IPrecompile {
error UnauthorizedMinter(address caller, address authorizedMinter);
// InvalidAddress(string), InvalidAmount(string), and every other shared
// error come from IPrecompile — do not redeclare them on IOkrw.
} Client-side ABI fragment you need
Copy this ABI into your dApp. Note the
string inputs on InvalidAddress and InvalidAmount — if you keep the old address / uint256 typing your decodeErrorResult (or interface.parseError in ethers) call silently returns undefined on a real revert.// Post-refactor ABI. Keep this next to your OKRW.mint ABI.
export const okrwErrorsAbi = [
{ type: "error", name: "UnauthorizedMinter",
inputs: [
{ name: "caller", type: "address" },
{ name: "authorizedMinter", type: "address" },
] },
{ type: "error", name: "InvalidAddress", inputs: [{ name: "bad", type: "string" }] },
{ type: "error", name: "InvalidAmount", inputs: [{ name: "amount", type: "string" }] },
// Inherited errors that may surface on internal faults.
{ type: "error", name: "UnknownMethod", inputs: [{ name: "methodName", type: "string" }] },
{ type: "error", name: "InvalidNumberOfArgs", inputs: [
{ name: "expected", type: "uint256" },
{ name: "got", type: "uint256" },
] },
{ type: "error", name: "SDKUnauthorized", inputs: [] },
{ type: "error", name: "EventEmitFailed", inputs: [
{ name: "eventKind", type: "string" },
{ name: "reason", type: "string" },
] },
] as const; Decoding with viem
Wrap
writeContract in try/catch and route the revert data through decodeErrorResult with the ABI above. Note the branch on errorName: UnauthorizedMinter gives you both offending caller and current authorized minter, so you can render a precise user-facing message.import { createWalletClient, http, parseEther, decodeErrorResult } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { okrwErrorsAbi } from "./okrwErrors";
const OKRW = "0x1000000000000000000000000000000000000001" as const;
const mintAbi = [{
type: "function", name: "mint", stateMutability: "nonpayable",
inputs: [{ name: "recipient", type: "address" }, { name: "amount", type: "uint256" }],
outputs: [{ type: "bool" }],
}] as const;
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.MINTER_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
try {
await wallet.writeContract({
address: OKRW,
abi: mintAbi,
functionName: "mint",
// TODO: replace with the real treasury recipient before production.
args: ["0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62", parseEther("10000000")],
});
} catch (err: any) {
if (!err?.data) {
// Plain-string revert (e.g. nil / negative amount) — inspect err.shortMessage.
throw err;
}
const decoded = decodeErrorResult({ abi: [...okrwErrorsAbi, ...mintAbi], data: err.data });
switch (decoded.errorName) {
case "UnauthorizedMinter": {
const [caller, authorizedMinter] = decoded.args as [string, string];
console.error(`Not the minter. Caller=${caller}, expected=${authorizedMinter}`);
break;
}
case "InvalidAddress": {
const [bad] = decoded.args as [string];
console.error(`OKRW.mint rejected recipient ${bad}`);
break;
}
case "InvalidAmount": {
const [amount] = decoded.args as [string];
console.error(`OKRW.mint rejected amount ${amount}`);
break;
}
default:
console.error(`OKRW.mint reverted: ${decoded.errorName}`, decoded.args);
}
} Decoding inside Solidity
For on-chain error routing, catch the low-level bytes and dispatch on the 4-byte selector. Because
InvalidAddress / InvalidAmount now take string arguments, their selectors changed — always resolve the selectors through IPrecompile.InvalidAddress.selector (which reaches the inherited definition), never hard-code a hex value copied from an older version.// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;
import "@maroo-chain/contracts/precompiles/okrw/IOkrw.sol";
import {IPrecompile} from "@maroo-chain/contracts/precompiles/common/interfaces/IPrecompile.sol";
contract SafeMinter {
IOkrw constant okrw = IOkrw(0x1000000000000000000000000000000000000001);
event MintRejected(bytes4 selector);
function safeMint(address to, uint256 amount) external {
try okrw.mint(to, amount) returns (bool ok) {
require(ok, "mint returned false");
} catch (bytes memory raw) {
bytes4 sel;
assembly { sel := mload(add(raw, 32)) }
if (sel == IOkrw.UnauthorizedMinter.selector) {
emit MintRejected(sel);
} else if (sel == IPrecompile.InvalidAddress.selector) {
// InvalidAddress(string) — selector is new after the refactor.
emit MintRejected(sel);
} else if (sel == IPrecompile.InvalidAmount.selector) {
// InvalidAmount(string) — selector is new after the refactor.
emit MintRejected(sel);
} else {
// Unknown revert — bubble up (plain-string reverts land here).
assembly { revert(add(raw, 32), mload(raw)) }
}
}
}
} Warning: Do not hardcode the historical selectors for InvalidAddress / InvalidAmount from before this refactor. Their signatures changed (
address → string, uint256 → string), so the 4-byte selectors changed too. Always reference IPrecompile.InvalidAddress.selector / IPrecompile.InvalidAmount.selector via the current interface import. Sanity-check the change with cast
Confirm the current typed-error selectors on the deployed precompile with
cast. Any decoding hiccup usually traces back to an ABI fragment stuck on the pre-refactor shape.# Selectors — regenerate if you keep any hex constants in your code.
cast sig-error 'UnauthorizedMinter(address,address)'
cast sig-error 'InvalidAddress(string)'
cast sig-error 'InvalidAmount(string)'
# Simulate a zero-amount mint against testnet and inspect the revert data.
MAROO_RPC=https://rpc-testnet.maroo.io
cast call 0x1000000000000000000000000000000000000001 \
'mint(address,uint256)' \
0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62 0 \
--rpc-url $MAROO_RPC Conclusion
The shared-error refactor centralises invalid-input and boundary errors on
IPrecompile, which is why IOkrw no longer redeclares InvalidAddress / InvalidAmount. The trade-off for callers is a one-time ABI fragment update: both errors now carry a string payload, and their 4-byte selectors changed accordingly. Keep the ABI fragment in this guide next to your OKRW import, and the same decoding pattern will work for any other Maroo precompile whose interface is is IPrecompile.