Handling OKRW Precompile Errors

integration intermediate

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 IOkrwInvalidAddress(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/catch and Ethers.js.

The three revert shapes

A mint call fails in exactly one of three ways:

TriggerRevert shapeOwner
msg.sender ≠ configured minterUnauthorizedMinter(address caller, address authorizedMinter)IOkrw (module)
recipient == 0x0 or unencodableInvalidAddress(string bad)IPrecompile (shared)
amount == 0InvalidAmount(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 (addressstring, uint256string), 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.
ESC
Type to search