Skip to content

gulla0/cardano-error-normalizer

Repository files navigation

Cardano Error Normalizer

A small TypeScript library that normalizes heterogeneous Cardano stack errors into a stable CardanoAppError shape.

Current package version in this repository: 0.3.0.

Install

npm install @gulla0/cardano-error-normalizer

Quickstart (Recommended: Preset + Wrapper)

Use wrapper presets first so context is mostly automatic.

import {
  cip30WalletPreset,
  isCardanoAppError,
  meshProviderPreset
} from "@gulla0/cardano-error-normalizer";

const safeMeshProvider = meshProviderPreset(rawMeshProvider, {
  provider: "blockfrost"
});

const safeWalletApi = cip30WalletPreset(rawWalletApi, {
  walletHint: "eternl"
});

try {
  await safeMeshProvider.submitTx(txCborHex);
} catch (err) {
  if (isCardanoAppError(err)) {
    console.error(err.code, err.resolution?.steps);
    throw err;
  }

  throw err;
}

What You Get (CardanoAppError)

walletHint from context maps to error.wallet?.name unless an adapter provides a richer wallet object.

{
  "name": "CardanoAppError",
  "source": "provider_submit",
  "stage": "submit",
  "code": "QUOTA_EXCEEDED",
  "severity": "warn",
  "message": "Daily request limit has been exceeded",
  "timestamp": "2026-02-19T12:00:00.000Z",
  "network": "preprod",
  "provider": "blockfrost",
  "wallet": { "name": "eternl" },
  "resolution": {
    "title": "Upgrade or wait for quota reset",
    "steps": ["Verify project quota", "Retry after reset window"]
  },
  "meta": {
    "blockfrostReason": "daily_limit",
    "safeProviderWrapped": true,
    "safeProviderMethod": "submitTx"
  }
}

Canonical Integration Path

  1. meshProviderPreset(...) / cip30WalletPreset(...)
  2. withErrorSafety(...) for non-preset objects
  3. createNormalizer({ defaults }) for manual boundaries
  4. @gulla0/cardano-error-normalizer/react for React operations

Wrapper-First Usage

Preset Helpers (Preferred)

import {
  cip30WalletPreset,
  meshProviderPreset
} from "@gulla0/cardano-error-normalizer";

const safeMeshProvider = meshProviderPreset(rawMeshProvider, {
  provider: "blockfrost"
});

const safeWalletApi = cip30WalletPreset(rawWalletApi, {
  walletHint: "eternl"
});

meshProviderPreset maps provider methods (for example submitTx, fetchAddressUTxOs) to provider context. cip30WalletPreset maps CIP-30 wallet methods (for example getUtxos, signTx, submitTx) to wallet context.

Generic Wrapper

import { withErrorSafety } from "@gulla0/cardano-error-normalizer";

const safeProvider = withErrorSafety(rawProvider, {
  ctx: {
    source: "provider_submit",
    stage: "submit",
    provider: "blockfrost",
    network: "preprod"
  },
  onError(normalized, details) {
    console.error("normalized provider error", {
      method: details.method,
      code: normalized.code,
      meta: normalized.meta
    });
  }
});

await safeProvider.submitTx(txCborHex);

withErrorSafety rethrows CardanoAppError and annotates meta.safeProviderWrapped=true and meta.safeProviderMethod.

Advanced: Dynamic ctx(method)

import { withErrorSafety } from "@gulla0/cardano-error-normalizer";

const safeProvider = withErrorSafety(rawProvider, {
  ctx: (method) => ({
    source: method === "submitTx" ? "provider_submit" : "provider_query",
    stage: method === "submitTx" ? "submit" : "build",
    provider: "blockfrost",
    network: "preprod"
  })
});

Use this only when presets are not suitable.

Manual Boundary Usage

import { createNormalizer } from "@gulla0/cardano-error-normalizer";

const normalizer = createNormalizer({
  config: { includeFingerprint: true },
  defaults: {
    source: "provider_submit",
    stage: "submit",
    provider: "blockfrost",
    network: "preprod"
  }
});

try {
  await provider.submitTx(txCborHex);
} catch (err) {
  throw normalizer.normalize(err, { walletHint: "eternl" });
}

React Usage

Use the React subpath with explicit hook bindings from React:

import { useCallback, useState } from "react";
import { useCardanoError } from "@gulla0/cardano-error-normalizer/react";

const tx = useCardanoError({
  operation: submitTx,
  defaults: { source: "provider_submit", stage: "submit" },
  config: {
    hooks: { useState, useCallback }
  }
});

useCardanoError returns loading, data, error, run, normalize, and reset. run(...) rethrows normalized CardanoAppError.

Legacy runtimes that expose globalThis.React can use compatibility mode:

import { useCardanoError } from "@gulla0/cardano-error-normalizer/react/compat";

Context Contract

Field Required Notes
source yes (defaulted) Defaults to provider_query if omitted.
stage yes (defaulted) Defaults to build if omitted.
provider no Copied to CardanoAppError.provider unless adapter overrides it.
network no Defaults to unknown in final output.
walletHint no Hint only; lands in CardanoAppError.wallet.name when no richer wallet data exists.
txHash no Copied to CardanoAppError.txHash.
timestamp no Uses current ISO timestamp when omitted.

Resolution Hints (Authority Level)

  • resolution from canonical error code mapping is authoritative for that code family.
  • resolution attached after heuristic message matching (smartMatcher) is best-effort guidance.
  • For UNKNOWN, treat resolution as optional troubleshooting help, not truth.

Debug and Trace Modes

import { withErrorSafety } from "@gulla0/cardano-error-normalizer";

const safeProvider = withErrorSafety(rawProvider, {
  ctx: { source: "provider_submit", stage: "submit", provider: "blockfrost" },
  normalizerConfig: {
    debug: true,
    parseTraces: true
  }
});

Warning: debug may log portions of raw error payloads (input, context, output). Do not enable in production unless that is acceptable for your data-handling policy.

Rendering Actionable Hints

import {
  isCardanoAppError,
  withErrorSafety
} from "@gulla0/cardano-error-normalizer";

try {
  await withErrorSafety(provider, {
    ctx: { source: "provider_submit", stage: "submit" }
  }).submitTx(txCborHex);
} catch (err) {
  if (!isCardanoAppError(err)) {
    throw err;
  }

  const title = err.resolution?.title ?? "Troubleshoot transaction failure";
  const steps = err.resolution?.steps ?? ["Inspect logs and retry"];
  renderHintCard({ title, steps });
}

Default Adapter Order

createNormalizer({ config }) runs adapters in this order:

  1. fromMeshError (unwrap nested errors first)
  2. fromWalletError
  3. fromBlockfrostError
  4. fromNodeStringError
  5. fallback to UNKNOWN

Wallet Mapping Table (CIP-30/CIP-95)

Wallet error family Numeric code Meaning CardanoErrorCode
APIError -1 InvalidRequest WALLET_INVALID_REQUEST
APIError -2 InternalError WALLET_INTERNAL
APIError -3 Refused WALLET_REFUSED
APIError -4 AccountChange WALLET_ACCOUNT_CHANGED
TxSignError 1 ProofGeneration WALLET_SIGN_PROOF_GENERATION
TxSignError 2 UserDeclined WALLET_SIGN_USER_DECLINED
TxSignError (CIP-95 ext) 3 DeprecatedCertificate TX_LEDGER_VALIDATION_FAILED
DataSignError (wallet-specific observed behavior) 1 ProofGeneration WALLET_DATA_SIGN_PROOF_GENERATION
DataSignError (wallet-specific observed behavior) 2 AddressNotPK WALLET_DATA_SIGN_ADDRESS_NOT_PK
DataSignError (wallet-specific observed behavior) 3 UserDeclined WALLET_DATA_SIGN_USER_DECLINED
PaginateError (wallet-specific observed behavior) n/a (maxSize) requested page exceeds range WALLET_PAGINATION_OUT_OF_RANGE
TxSendError 1 Refused WALLET_SUBMIT_REFUSED
TxSendError 2 Failure WALLET_SUBMIT_FAILURE

Submit-path disambiguation note: APIError code=-2 normally maps to WALLET_INTERNAL, but maps to WALLET_SUBMIT_FAILURE when submit intent is explicit (source=wallet_submit or stage=submit) or when info indicates submitTx.

Blockfrost Mapping Table

fromBlockfrostError uses key-based parsing (status_code, error, message) across nested payloads, so mapping does not depend on JSON property order.

HTTP status Meaning CardanoErrorCode Notes (meta)
400 invalid request BAD_REQUEST
402 daily request limit exceeded QUOTA_EXCEEDED blockfrostReason="daily_limit"
403 not authenticated UNAUTHORIZED
404 resource does not exist NOT_FOUND
418 auto-banned after flooding FORBIDDEN blockfrostReason="auto_banned"
425 mempool full MEMPOOL_FULL blockfrostReason="mempool_full"
429 rate limited RATE_LIMITED
5xx server side error PROVIDER_INTERNAL
other 4xx other client error BAD_REQUEST preserve raw

Node String Heuristics

The following are heuristic regex matches (best-effort, not protocol-authoritative):

  • /DeserialiseFailure|DecoderFailure|expected word/i -> TX_DESERIALISE_FAILURE
  • /BadInputsUTxO/i -> TX_INPUTS_MISSING_OR_SPENT
  • /OutputTooSmallUTxO|BabbageOutputTooSmallUTxO/i -> TX_OUTPUT_TOO_SMALL
  • /ValueNotConservedUTxO/i -> TX_VALUE_NOT_CONSERVED
  • /ScriptFailure|PlutusFailure|EvaluationFailure|ValidationTagMismatch|redeemer.*execution units/i -> TX_SCRIPT_EVALUATION_FAILED
  • /ShelleyTxValidationError|ApplyTxError/i -> TX_LEDGER_VALIDATION_FAILED when no inner specific tag is found

Contributing / Local Development

Validate this repository locally:

npm install
npm test
npm run typecheck
npm run build

Capture runtime fixtures before rethrowing:

console.error("RUNTIME_ERROR_SAMPLE", {
  err,
  ctx: { source: "provider_submit", stage: "submit", provider: "blockfrost", network: "preprod" }
});

Next.js + Mesh + Blockfrost + useWallet Integration

This example reflects a working integration using:

  • Next.js (App Router)
  • @meshsdk/core
  • @meshsdk/react
  • Blockfrost
  • Eternl (CIP-30)
  • meshProviderPreset
  • cip30WalletPreset
  • Shared createNormalizer

1) Create a Shared Normalizer

// src/errors/normalizer.ts
import { createNormalizer } from "@gulla0/cardano-error-normalizer";

export const normalizer = createNormalizer({
  defaults: {
    provider: "blockfrost",
    network: "preprod"
  }
});

2) Wrap Mesh Blockfrost Provider (Server)

import { BlockfrostProvider } from "@meshsdk/core";
import { meshProviderPreset } from "@gulla0/cardano-error-normalizer";
import { normalizer } from "@/errors/normalizer";

const rawProvider = new BlockfrostProvider(process.env.BLOCKFROST!);

export const blockchain_provider = meshProviderPreset(rawProvider, {
  provider: "blockfrost",
  normalizer
});

3) Wrap useWallet() (Client)

"use client";

import { useWallet } from "@meshsdk/react";
import { useMemo } from "react";
import { cip30WalletPreset } from "@gulla0/cardano-error-normalizer";
import { normalizer } from "@/errors/normalizer";

const { wallet: rawWallet, connected } = useWallet();

const wallet = useMemo(() => {
  if (!rawWallet) return null;

  return cip30WalletPreset(rawWallet, {
    walletHint: "eternl",
    normalizer
  });
}, [rawWallet]);

Now:

await wallet.signTx(...);
await wallet.submitTx(...);
await wallet.getUtxos();

throw normalized CardanoAppError.

4) Handle Errors in UI

import { isCardanoAppError } from "@gulla0/cardano-error-normalizer";

try {
  await wallet.signTx(tx);
} catch (error) {
  if (isCardanoAppError(error)) {
    setError(
      error.resolution?.title ??
      error.message ??
      error.code
    );
  } else {
    setError("Unexpected application error");
  }
}

Example

See examples/mesh-blockfrost-eternl.ts in the repository for an end-to-end Mesh + Blockfrost + Eternl flow.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors