Skip to content

feat: add SDK transaction build validation pipeline (#249) - #417

Merged
El-swaggerito merged 1 commit into
Axionvera:mainfrom
XxHugheadxX:feat/transaction-build-validation-pipeline
Jul 28, 2026
Merged

feat: add SDK transaction build validation pipeline (#249)#417
El-swaggerito merged 1 commit into
Axionvera:mainfrom
XxHugheadxX:feat/transaction-build-validation-pipeline

Conversation

@XxHugheadxX

Copy link
Copy Markdown
Contributor

Related Issue

Implementation Scope

  • Problem addressed: the duplication was literal, not hypothetical. src/payments/index.ts hand-chained validateSecretKey, validatePublicKey, validateAmount and validateMemoInput in sendXLM, and the same four calls reappeared in sendAsset with validateAssetSpec appended. Neither path used the non-throwing family that already existed, and the seventh validator the issue lists — signer capability — had no call site at all: mapAuthRequirements and assertAuthFullyMapped (src/transactions/auth.ts:162 and :286) appear only in two barrel re-exports and one doc comment.

    The reason the existing helpers could not simply be called in sequence is that they return four different shapes:

    Helper Returns
    safeValidateMemo (src/utils/memo.ts:217) { valid } | { valid, error }
    safeParseAmount (src/utils/amount.ts:261) { valid, amount } | { valid, error }
    safeValidateDestination (src/payments/destination-validation.ts:474) Promise<PocketPayResult<…>>
    validateSendXLMParams (src/payments/validation.ts) { ok } | { ok, errors: ValidationError[] }

    Normalising those into one result is the work here.

  • Modules changed: src/transactions/build-validation.ts (new), src/payments/index.ts, src/transactions/index.ts, src/index.ts, tests/build-validation.test.ts (new), docs/transaction-build-validation.md (new)

  • Public API impact: new exports only, no breaking change. validateTransactionBuild, assertTransactionBuildValid, VALIDATION_ORDER, and the types ValidationStage, TransactionValidationIssue, TransactionValidationResult, TransactionBuildInput, TransactionValidationOptions. No published error code changes — see below.

Design

Seven stages in a documented, exported order (VALIDATION_ORDER): local and
zero-cost first, anything reading configuration or an account last, so a
malformed key is reported without resolving config at all.

Stages are not short-circuited — three malformed fields produce three
issues, so a form can show all of them at once.
assertTransactionBuildValid is the throwing variant for call sites that want
only the first failure.

Error codes are transported, never merged. An issue carries the code its
originating validator already produced. ValidationErrorCode
(src/payments/validation.ts:29) and the published registry are separate
taxonomies on purpose, and both appear in the same issues array unchanged.

That constraint also decided one implementation detail worth flagging: the
amount stage uses validateAmount, not safeParseAmount. The safe parser
belongs to the safe-amount model and raises that model's codes, while the
payment surface publishes INVALID_AMOUNT and INVALID_AMOUNT_PRECISION.
Composing the safe parser would have been a silent breaking change — four tests
caught it during development.

Tests Added / Changed

  • New tests added: tests/build-validation.test.ts — 22 tests
  • Failure paths covered: one invalid input per stage, self-payment, malformed secret with a valid destination, accumulation of multiple issues, ordering, and an assertion that the source secret never reaches an issue
  • Docs-only / config-only PR — not applicable

The block that matters most is the last one: adoption must not change a single
published error code.
It asserts that sendXLM still reports
INVALID_SECRET_KEY and INVALID_AMOUNT, that each flow keeps its own
self-payment wording ("Cannot send XLM to yourself" vs "Cannot send asset to
yourself"), and that sendAsset still validates the asset spec.

One ordering test exists because of a real bug found during development: with a
malformed secret and a valid destination, the self-payment check would still
call Keypair.fromSecret, which throws a raw Error rather than a
PocketPayError. The source key is now derived only once the secret is known to
be well formed.

Verification

  • Ran the verify chain step by step

The steps were run individually rather than through npm run verify, because
that script shells out to npm run for each sub-step and this repository's
dependency tree is managed with a different package manager locally.
test:coverage was not run; every other step was:

tsc --noEmit                          2 errors — both pre-existing, see below
tsx scripts/check-circular-deps.ts    1 cycle — pre-existing, see below
vitest run                            45 failed | 1016 passed | 1 skipped (1062)
tsx scripts/verify-acceptance.ts      checklist issue-249, 0 items remaining

Baseline measured on a clean git archive of upstream/main (cf8e166):

main this branch
tsc errors 2 2
check:circular 1 cycle 1 cycle
Tests failed 45 45
Tests passed 994 1016

Zero regressions, +22 tests.

The 2 tsc errors and the dependency cycle are both in src/network/fee.ts,
introduced by #415, and are present on main:

src/network/fee.ts(39,20): TS2362 …
src/network/fee.ts(40,24): TS2362 …
network/index.ts -> network/fee.ts -> network/index.ts

They are left alone deliberately — they are unrelated to this issue and belong
to the module's author. pnpm build fails because of them, on main as well as
here. This PR does not claim otherwise.

CI Status

  • Red only for a documented, pre-existing reason
Next steps: ✗ Fix failing automated checks, then re-run npm run verify:pr

verify:pr ends red because its gate runs the whole suite and the type check,
and both are already failing on main for the reasons above.

Acceptance Criteria Coverage

  • Reusable transaction validators are added — seven stages in src/transactions/build-validation.ts, usable from any flow
  • Validation order is documentedVALIDATION_ORDER is exported so the order is testable, and docs/transaction-build-validation.md explains why local checks run before configuration
  • Typed errors are returnedTransactionValidationIssue[] from the non-throwing entry point; the originating PocketPayError from the throwing one
  • Tests cover invalid inputs — one per stage plus accumulation, ordering and adoption safety
  • Existing flows use validators where practicalsendXLM and sendAsset both route through the pipeline; the hand-chained calls are gone from both

Reviewer Notes

  • Correction to my own application comment: I cited the chains at src/payments/index.ts:33-36 and :319-323. They are now at :34-37 and :329-333 — the lines moved when the transaction lifecycle orchestrator (Implement SDK transaction lifecycle orchestrator #305) landed. The chains themselves were exactly as described.
  • The seventh validator has a real limit, stated rather than hidden. mapAuthRequirements takes a built transaction, so full threshold analysis cannot run before a transaction exists. The signerCapability stage validates what is knowable pre-build — whether the supplied account declares signing capability — and the docs say plainly that authorisation mapping remains a post-build step. The stage is skipped when no account is supplied.
  • sendXLM and sendAsset skip the network stage via the stages option, because they resolve configuration themselves inside their own try. Including it would have moved when a configuration error surfaces relative to their error handlers — a behaviour change this PR has no reason to make.
  • The self-payment check is shared; only the sentence differs per flow, passed in through selfPaymentMessage. Both wordings are asserted by tests.

Scope — what was deliberately left out

  • The 2 tsc errors and the dependency cycle in src/network/fee.ts. Unrelated to this issue; fixing them would be scope creep into another contributor's module.
  • safeValidateDestination and safeCheckDestinationTrustline are not wired in. Both are async and perform network lookups; the pipeline's stages are synchronous and local by design, so a caller can validate a form without touching Horizon. Network-level destination checks stay where they are, in the preflight that sendAsset already runs.
  • validateSendXLMParams is left as-is. It remains the payment-shaped, form-facing entry point; the pipeline does not replace it, and unifying the two would have merged the taxonomies this PR deliberately keeps apart.
  • No changes to the vault or orchestrator paths (Implement SDK vault capability and action intent system #274, Implement SDK transaction lifecycle orchestrator #305). The pipeline is available to them but wiring them in was not part of this issue.

@El-swaggerito
El-swaggerito merged commit aa17377 into Axionvera:main Jul 28, 2026
1 check passed
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.

Add SDK transaction build validation pipeline

2 participants