Skip to content

Errors

Every error the SDK raises inherits WalletError and carries a stable, machine-readable code. Recovery is therefore a matter of switching on that code — never of matching message text, which is not part of the API.

isWalletError(err, code?) is the guard to reach for. It narrows to the concrete class, so the variant's context fields are typed without an instanceof chain — and it is duck-typed, so it keeps working when two copies of the SDK end up in one bundle.

ts
import { 
isWalletError
} from "@lelantos-org/sdk";
try { await
wallet
.
transfer
({
to
,
amount
});
} catch (
e
) {
if (
isWalletError
(
e
, "INSUFFICIENT_COVER")) {
await
wallet
.
transfer
({
to
,
amount
,
autoConsolidate
: true });
} else if (
isWalletError
(
e
)) {
switch (
e
.
code
) {
case "RELAYER_TIMEOUT":
console
.
error
(
e
.
url
,
e
.
status
,
e
.
body
);
break; case "WALLET_CONFIG":
console
.
error
(
e
.
missing
);
break; default: throw
e
;
} } else throw
e
;
}

WALLET_ERROR_CODES is the runtime list of every code; AnyWalletError is the union of every class, discriminated on code.

Shielded-fee rejections

A relayer that refuses a submission over its shielded fee answers 402, which arrives as a NetworkError. isShieldedFeeRejection is the guard.

The status alone is decisive — the relayer returns 402 for nothing else — but a named predicate says which 402 a call site means, and keeps the check honest if that ever stops being true.

ts
import { 
isShieldedFeeRejection
} from "@lelantos-org/sdk/relayer";
try { await
relayer
.
submitTransact
(
payload
);
} catch (
e
) {
if (!
isShieldedFeeRejection
(
e
)) throw
e
;
// `e.body` carries the relayer's reason in prose: which asset, what was // paid, what was required, and the grace band. The quote went stale — // re-estimate and rebuild. Resubmitting the same payload is refused again. }

Every error class

ClassCodeNotes
InsufficientCoverErrorINSUFFICIENT_COVERNo 1/2-note cover. Pass autoConsolidate or read consolidate: StoredNote[].
WalletConfigErrorWALLET_CONFIGmissing: string[] lists all problems.
NetworkErrorRELAYER_* / FMD_* / QUOTER_*Wraps fetch failures and timeouts. Fields: url, status?, body?, cause?.
ProverErrorPROVER_FAILEDProof generation failed.
ProverArtifactsMissingErrorPROVER_ARTIFACTS_MISSINGField tried: string[]. Pass proverArtifacts, install the companion package, or set LELANTOS_PROVER_ARTIFACTS_DIR.
PermitRejectedErrorPERMIT_REJECTEDUser rejected the EIP-2612 signature.
DepositAdapterErrorDEPOSIT_ADAPTERStrategy mismatch (native / allowance / witness).
SelectionErrorSELECTIONCoin-selector failure. Field asset?. Also raised when a cross-asset fee has no input slot left.
InvalidArgumentErrorINVALID_ARGUMENTField argument? names the offending parameter. The rejected value is never in the message — it would reach logs verbatim.
WireFormatErrorWIRE_FORMATA response did not match the documented wire contract. Field path is the JSON path, e.g. $.min_out.
TxMiningErrorTX_MININGChain transaction submitted but not mined, or reverted.
NetworkNotDeployedErrorNETWORK_NOT_DEPLOYEDField network: string. Pick a deployed preset or pass a NetworkPreset literal.

Timeouts and retries

Every HTTP pluggable takes HttpClientOptions{ timeoutMs, retries, backoffMs } — at construction. To change them, build the client yourself and inject it.

ts
import { 
connect
,
FmdNoteSource
,
HttpRelayerSubmitter
} from "@lelantos-org/sdk";
import {
FmdClient
} from "@lelantos-org/sdk/fmd-server";
// A poll on a latency budget: fail fast rather than retry for a minute. const
fmd
= new
FmdClient
(
fmdUrl
,
chainId
, {
timeoutMs
: 3_000,
retries
: 1,
backoffMs
: 100 });
const
wallet
= await
connect
({
privateKey
: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
network
: "anvil",
rpcUrl
: "http://localhost:8545",
noteSource
: new
FmdNoteSource
(
fmd
),
submitter
: new
HttpRelayerSubmitter
(
relayerUrl
, {
timeoutMs
: 60_000,
retries
: 2 }),
});

fetchSwapQuote accepts the same options per call and defaults timeoutMs to 5000 — a stale quote is worth less than a fast failure.

Defaults: 3 retries after the first attempt, 250 ms backoff doubling with ±25% jitter, and a per-attempt timeout of 15 000 ms for idempotent requests (GET/HEAD/OPTIONS) or 30 000 ms for submits.

The timeout is per attempt, not per call

A fully retried request can outlive timeoutMs several times over — at the defaults, a failing GET takes upwards of a minute before it throws. Set timeoutMs and retries when a call sits on a latency budget, as a poll does.

HTTP clients retry 5xx, 408, 429, and network errors. 402 is never retried.

To route all SDK egress through a proxy or a recording shim, pass fetchImpl instead — it is used by every default HTTP pluggable, and ignored for ones you build yourself.

Next

  • Fees — shielded-fee rejections in context
  • Logging — seeing what the SDK is doing before it throws