Skip to content

feat(tx): surface pre-inclusion validity failures as typed TxValidityError#261

Open
ReinhardHatko wants to merge 1 commit into
mainfrom
rh/bce-tx
Open

feat(tx): surface pre-inclusion validity failures as typed TxValidityError#261
ReinhardHatko wants to merge 1 commit into
mainfrom
rh/bce-tx

Conversation

@ReinhardHatko

Copy link
Copy Markdown
Contributor

What

A pre-inclusion validity/submission failure (e.g. InvalidTransaction::Payment — submitter can't pay or isn't authorized) previously surfaced as an opaque error — either a base TxError wrapping raw JSON, or a TxDispatchError whose message was the placeholder "unknown error" (that field only exists for an included-and-failed tx).

Change

  • New TxValidityError extends TxError (inherits the SdkError marker), carrying the raw failure payload on .reason + a human-readable .formatted.
  • polkadot-api's InvalidTxError subscription rejection is now detected and surfaced as err(TxValidityError) with the decoded reason ({ type: "Invalid", value: { type: "Payment" } }"Invalid.Payment").
  • withRetry's isNonRetryable treats TxValidityError as non-retryable (these are deterministic for the same account state).

Implements proposal cluster E2 (error classification: opaque tx failures). Changeset included (tx minor + umbrella minor).

🤖 Generated with Claude Code

…Error

A pre-inclusion validity/submission failure (e.g.
InvalidTransaction::Payment when the submitter can't pay or isn't
authorized) previously surfaced as an opaque error — either a base
TxError wrapping raw JSON (when polkadot-api rejected the subscription)
or a TxDispatchError whose message was the placeholder "unknown error"
(when a failure event arrived without a dispatchError; that field only
exists for an included-and-failed transaction).

- New `TxValidityError extends TxError` (inherits the SdkError marker)
  carrying the raw failure payload on `reason` plus a human-readable
  `formatted` string.
- polkadot-api's `InvalidTxError` subscription rejection — the path a
  real validity failure actually takes — is now detected
  (`isInvalidTxError`) and surfaced as err(TxValidityError) with the
  decoded reason formatted via the new `formatValidityError` helper
  ({ type: "Invalid", value: { type: "Payment" } } → "Invalid.Payment").
- A failure event with no `dispatchError` (defensive) is classified by a
  single `classifyFailure` helper shared by the best-block and finalized
  paths; TxDispatchError is unchanged for the included-and-failed case.
- formatDispatchError describes the missing-dispatchError case instead
  of returning "unknown error".
- `withRetry`'s `isNonRetryable` treats TxValidityError as
  non-retryable, matching how these failures were classified when they
  surfaced as TxDispatchError.

Implements proposal cluster E2 (error classification: opaque tx failures).
@github-actions

Copy link
Copy Markdown

📦 Bundle size impact

Comparing 2026-07-20T20:33:27.524Z2026-07-20T20:32:47.621Z

Package Entry Bundled before Bundled after Δ Ship gzip Δ Shake ratio
🟢 @parity/product-sdk ./cloud-storage 6.26 MB 6.26 MB +633 B (+0.0%) 0 B 3% (was 3%)
🟢 @parity/product-sdk ./contracts 67.5 KB 68.5 KB +1.1 KB (+1.6%) 0 B 26% (was 26%)
🟢 @parity/product-sdk-cloud-storage . 6.26 MB 6.26 MB +633 B (+0.0%) 0 B 3% (was 3%)
🟢 @parity/product-sdk-contracts . 67.5 KB 68.5 KB +1.1 KB (+1.6%) 0 B 26% (was 26%)
🟢 @parity/product-sdk-tx . 121.6 KB 122.6 KB +1.0 KB (+0.8%) +532 B 5% (was 5%)

Thresholds — warn: ≥10% or ≥5.0 KB · fail: ≥20% or ≥15.0 KB (bundled). Percentage only applies once the baseline is ≥ 10 KB.

@@ -0,0 +1,32 @@
---

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the changeset in pending-changesets folder

*/
function classifyFailure(event: { dispatchError?: unknown }, formatted: string): TxError {
return event.dispatchError == null
? new TxValidityError(event, "validity/submission failure (no dispatch error)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] classifyFailure discards its formatted arg in the no-dispatchError branch and hardcodes a near-duplicate of formatDispatchError's string. Not a mismatch (both say the same thing) - just two literals to keep in sync. Consider reusing formatted or a shared constant.

block: event.block,
});
settleErr(new TxDispatchError(event.dispatchError, formatted));
settleErr(classifyFailure(event, formatted));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] classifyFailure is called from the best-block (L162) and finalized (line 213) paths, where the tx was included - but it emits TxValidityError, whose message asserts "failed before inclusion." For that defensive branch a neutral message would avoid a misleading log. (The genuinely pre-inclusion path at L242 is correctly labelled.)

export class TxValidityError extends TxError {
/**
* Raw underlying failure payload: the decoded validity error from
* polkadot-api's `InvalidTxError` (e.g. `{ type: "Invalid", value:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it is breaking the docs CI build.

The { type: "Invalid", value: { type: "Payment" } } example is split across two lines inside the backticks. The docs builder can't parse a code snippet that wraps, so it crashes on api/tx/index.mdx.

Fix: put the example on one line:

* polkadot-api's `InvalidTxError`
* (e.g. `{ type: "Invalid", value: { type: "Payment" } }`), or the failed
* tx event when the node reported failure without any payload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants