A small TypeScript library that normalizes heterogeneous Cardano stack errors into a stable CardanoAppError shape.
Current package version in this repository: 0.3.0.
npm install @gulla0/cardano-error-normalizerUse 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;
}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"
}
}meshProviderPreset(...)/cip30WalletPreset(...)withErrorSafety(...)for non-preset objectscreateNormalizer({ defaults })for manual boundaries@gulla0/cardano-error-normalizer/reactfor React operations
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.
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.
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.
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" });
}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";| 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. |
resolutionfrom canonical error code mapping is authoritative for that code family.resolutionattached after heuristic message matching (smartMatcher) is best-effort guidance.- For
UNKNOWN, treatresolutionas optional troubleshooting help, not truth.
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.
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 });
}createNormalizer({ config }) runs adapters in this order:
fromMeshError(unwrap nested errors first)fromWalletErrorfromBlockfrostErrorfromNodeStringError- fallback to
UNKNOWN
| 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.
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 |
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_FAILEDwhen no inner specific tag is found
Validate this repository locally:
npm install
npm test
npm run typecheck
npm run buildCapture runtime fixtures before rethrowing:
console.error("RUNTIME_ERROR_SAMPLE", {
err,
ctx: { source: "provider_submit", stage: "submit", provider: "blockfrost", network: "preprod" }
});This example reflects a working integration using:
- Next.js (App Router)
@meshsdk/core@meshsdk/react- Blockfrost
- Eternl (CIP-30)
meshProviderPresetcip30WalletPreset- Shared
createNormalizer
// src/errors/normalizer.ts
import { createNormalizer } from "@gulla0/cardano-error-normalizer";
export const normalizer = createNormalizer({
defaults: {
provider: "blockfrost",
network: "preprod"
}
});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
});"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.
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");
}
}See examples/mesh-blockfrost-eternl.ts in the repository for an end-to-end Mesh + Blockfrost + Eternl flow.