Skip to content

Rewrite SEP-0011 TxRep to use generated XDR serialization#134

Merged
christian-rogobete merged 8 commits intomasterfrom
sep-11
Mar 12, 2026
Merged

Rewrite SEP-0011 TxRep to use generated XDR serialization#134
christian-rogobete merged 8 commits intomasterfrom
sep-11

Conversation

@christian-rogobete
Copy link
Member

Summary

  • Replace ~4000-line hand-written TxRep implementation with ~370-line version that delegates to code-generated toTxRep()/fromTxRep() methods on XDR types
  • Add TxRepHelper utility class and compact format support for asset types, accounts, and signer keys
  • Fix memo text round-trip bug (JSON escape handling) and add backward compatibility for legacy tx.timeBounds._present format
  • Clean up dead code: formatAmount, parseAmount, dateComment, octal parsing, formatAssetCode, parseAssetCode, txrep_legacy.dart

Test plan

  • All 1196 SEP unit tests pass
  • All 1316 XDR unit tests pass
  • All integration tests pass
  • 0 analyzer issues
  • Memo text with quotes and backslashes round-trips correctly
  • Fee bump transactions encode/decode correctly
  • Legacy tx.timeBounds._present format parses correctly

🤖 Generated with Claude Code

christian-rogobete and others added 3 commits March 11, 2026 21:05
… methods

Replace the ~4000-line hand-written TxRep implementation with a ~370-line
version that delegates serialization to code-generated toTxRep()/fromTxRep()
methods on XDR types. The top-level envelope structure, memo text encoding,
and signatures are still handled manually for correctness.

Key changes:
- Add toTxRep()/fromTxRep() methods to all generated XDR types
- Add TxRepHelper utility class for shared parsing/formatting
- Add compact format support for XdrAsset, XdrAccountID, XdrMuxedAccount,
  XdrSignerKey, XdrAllowTrustOpAsset via TXREP_COMPACT_TYPES
- Add hybrid compact/expanded format for XdrChangeTrustAsset and
  XdrTrustlineAsset (compact for non-pool-share, expanded for pool share)
- Fix memo text round-trip: use json.decode() to reverse JsonEncoder escapes
- Add backward compatibility for legacy tx.timeBounds._present format
- Update test expectations for stroops-based amount representation
- Remove dead code: formatAmount, parseAmount, dateComment, octal parsing,
  formatAssetCode, parseAssetCode, unused toTxRep/fromTxRep on XdrAllowTrustOpAsset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run dart format on all generated XDR files to match CI expectations.
Fix generator to only emit 'import txrep_helper.dart' when the type
actually references TxRepHelper methods, eliminating 111 unused import
warnings. Update snapshots.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove redundant '!' operator on optional list variable that was just
assigned an empty list, fixing an unnecessary_non_null_assertion warning
that caused CI analyze step to fail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 93.19106% with 201 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.98%. Comparing base (4decc8a) to head (f0c2b6e).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
lib/src/xdr/xdr_sc_error.dart 50.00% 16 Missing ⚠️
lib/src/xdr/txrep_helper.dart 96.70% 9 Missing ⚠️
lib/src/xdr/xdr_sc_contract_instance.dart 55.00% 9 Missing ⚠️
lib/src/xdr/xdr_contract_data_durability.dart 63.15% 7 Missing ⚠️
lib/src/xdr/xdr_contract_executable_type.dart 63.15% 7 Missing ⚠️
lib/src/xdr/xdr_contract_id_preimage_type.dart 63.15% 7 Missing ⚠️
lib/src/xdr/xdr_host_function_type.dart 69.56% 7 Missing ⚠️
lib/src/xdr/xdr_liquidity_pool_type.dart 58.82% 7 Missing ⚠️
lib/src/xdr/xdr_precondition_type.dart 66.66% 7 Missing ⚠️
.../src/xdr/xdr_soroban_authorized_function_type.dart 66.66% 7 Missing ⚠️
... and 31 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #134      +/-   ##
==========================================
+ Coverage   86.21%   88.98%   +2.77%     
==========================================
  Files         622      623       +1     
  Lines       27746    28711     +965     
==========================================
+ Hits        23920    25549    +1629     
+ Misses       3826     3162     -664     
Files with missing lines Coverage Δ
lib/src/sep/0011/txrep.dart 85.02% <ø> (+27.90%) ⬆️
lib/src/xdr/xdr_account_id.dart 100.00% <100.00%> (ø)
lib/src/xdr/xdr_account_id_base.dart 94.44% <100.00%> (+1.58%) ⬆️
lib/src/xdr/xdr_allow_trust_op.dart 91.42% <100.00%> (+4.47%) ⬆️
lib/src/xdr/xdr_allow_trust_op_asset.dart 94.11% <100.00%> (+3.79%) ⬆️
lib/src/xdr/xdr_asset.dart 96.15% <100.00%> (+2.40%) ⬆️
lib/src/xdr/xdr_asset_alpha_num12.dart 92.85% <100.00%> (+3.38%) ⬆️
lib/src/xdr/xdr_asset_alpha_num4.dart 92.85% <100.00%> (+3.38%) ⬆️
...c/xdr/xdr_begin_sponsoring_future_reserves_op.dart 95.65% <100.00%> (+1.90%) ⬆️
lib/src/xdr/xdr_bump_sequence_op.dart 95.23% <100.00%> (+1.90%) ⬆️
... and 155 more

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

christian-rogobete and others added 3 commits March 12, 2026 00:40
…p roundtrip tests

Only generate toTxRep()/fromTxRep() for 143 types reachable from
TransactionEnvelope, restoring test coverage from 69% back to 87%.

- Add TXREP_TYPES frozen set (txrep_types.rb) gating TxRep generation
- Fix simple typedef fromTxRep for named XDR inner types (was incorrectly
  calling getValue for types that emit sub-keys like unions/structs)
- Add 349 TxRep roundtrip tests to the test generator (859 total)
- Add parseTxRepLines helper with malformed-line detection
- Remove dead XdrSCSpecTypeDef.fromTxRep (never called)
- Update snapshots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix TxRep string interpolation for primitive types: `$_field!` produced
`true!`/`false!` instead of `true`/`false` — changed to `${_field!}`.

Expand generated TxRep tests to cover all enum members and union arms
(was testing only one each). Add fallback values for deep struct types
(XdrFeeBumpTransaction, XdrTransaction, etc.) to enable tests that
previously exceeded MAX_DEPTH. Add dedicated TxRepHelper unit tests
(93 tests) covering parse, escape/unescape, hex, asset formatting,
signer keys, and all edge cases.

Generated tests: 510 → 1101. Total unit tests: 5706 → 6041.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate TxRep type field: reject null or unrecognized envelope types
- Validate feeBump.tx.innerTx.type during decode
- Replace JsonEncoder() allocation with jsonEncode() top-level function
- Use jsonDecode() instead of json.decode() for consistency
- Throw explicit error for invalid 64-char hex in pool share parsing
- Remove always-true hash.length == 32 check
- Add doc comment on _decodeSignatures empty-list fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@christian-rogobete
Copy link
Member Author

@claude review this PR. It rewrites SEP-0011 TxRep to delegate to generated toTxRep()/fromTxRep() methods on XDR types. Only the 151 XDR types reachable from TransactionEnvelope were extended — other XDR types are unchanged.

Focus on these hand-written files:

  • lib/src/sep/0011/txrep.dart — slimmed facade (was 4200+ lines)
  • lib/src/xdr/txrep_helper.dart — new shared utility class
  • lib/src/xdr/xdr_change_trust_asset.dart — compact vs expanded format override
  • lib/src/xdr/xdr_trustline_asset.dart — compact vs expanded format override

Verify: (1) correct handling of all envelope types, memo encoding, and signatures, (2) compact vs expanded format logic for change trust and trustline assets, (3) backward compat with legacy tx.timeBounds._present format, (4) no dead code or stale imports, (5) TxRep string interpolation uses ${accessor} not $accessor for null-asserted expressions.

Skip all other lib/src/xdr/ files — they are auto-generated. Skip test/ and tools/.

@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Claude finished @christian-rogobete's task in 7m 37s —— View job


PR Review: SEP-0011 TxRep Rewrite

Tasks:

  • Read CLAUDE.md for repo guidelines
  • Read lib/src/sep/0011/txrep.dart
  • Read lib/src/xdr/txrep_helper.dart
  • Read lib/src/xdr/xdr_change_trust_asset.dart
  • Read lib/src/xdr/xdr_trustline_asset.dart
  • Verify envelope type handling, memo encoding, signatures
  • Verify compact vs expanded format logic
  • Verify backward compat with legacy tx.timeBounds._present
  • Check for dead code / stale imports
  • Check string interpolation for null-asserted expressions
  • Post review findings

Overall the refactoring is clean and well-structured. The generated-code delegation strategy works correctly, and the facade layer is easy to reason about. A few issues worth addressing are noted below.


1. Misleading comment on memo override

lib/src/sep/0011/txrep.dart line 108

// Memo — handle manually to avoid double-quoting bug in generated code.

The current generated XdrMemo.toTxRep (line 114 of xdr_memo.dart) correctly uses TxRepHelper.escapeString(_text!) — there is no double-quoting bug in the generated code. The real reason to keep manual handling is that the façade uses jsonEncode/jsonDecode (matching the legacy hand-written format), while the generated code uses TxRepHelper.escapeString/unescapeString (custom \xNN hex escaping). These two schemes are not cross-compatible for non-ASCII content: a value like \x0A from escapeString would cause jsonDecode to throw (since \x is not a valid JSON escape sequence).

The comment should be updated to explain this format-compatibility reason. Fix this →


2. Implicit null promotion on feeBumpEnv

lib/src/sep/0011/txrep.dart lines 90–96

if (isFeeBump) {
  String feeSource =
      TxRepHelper.formatMuxedAccount(feeBumpEnv!.tx.feeSource); // ← uses !
  lines.add('feeBump.tx.feeSource: $feeSource');
  lines.add('feeBump.tx.fee: ${feeBumpEnv.tx.fee.int64}');      // ← no !
  lines.add('feeBump.tx.innerTx.type: ENVELOPE_TYPE_TX');
}

This relies on Dart's null-flow promotion: after feeBumpEnv! on line 92, the analyser promotes feeBumpEnv to non-null for the rest of the block, so line 94 compiles without an explicit !. This is technically valid, but the inconsistency is confusing for readers who might not realise the ! on line 92 doubles as a type-promotion hint. The same inconsistency appears in the second if (isFeeBump) block at line 132 where feeBumpEnv! is needed again because promotion from the earlier block does not carry over.

Consider assigning to a local non-null variable at the start of each if (isFeeBump) block to make the intent explicit:

if (isFeeBump) {
  final fb = feeBumpEnv!;   // single assertion, explicit promotion
  lines.add('feeBump.tx.feeSource: ${TxRepHelper.formatMuxedAccount(fb.tx.feeSource)}');
  lines.add('feeBump.tx.fee: ${fb.tx.fee.int64}');
  ...
}

3. Inconsistent exception type in XdrTrustlineAsset.fromXdrAsset

lib/src/xdr/xdr_trustline_asset.dart lines 57–59

case XdrAssetType.ASSET_TYPE_POOL_SHARE:
  throw Exception("Unsupported asset type");

XdrChangeTrustAsset.fromXdrAsset (lines 57–61) throws an ArgumentError with a descriptive message explaining why pool share assets cannot come from XdrAsset. XdrTrustlineAsset.fromXdrAsset throws a generic, undescriptive Exception. Should use ArgumentError with a message matching the sister class for consistency. Fix this →


4. Compact vs expanded format logic — correct ✓

The override in both XdrChangeTrustAsset and XdrTrustlineAsset is correct:

  • Encoding: Non-pool-share → single compact line $prefix: CODE:ISSUER; pool share → calls super.toTxRep which writes $prefix.type: ASSET_TYPE_POOL_SHARE and expanded fields.
  • Decoding: First checks for compact value at the exact key prefix; if absent, falls back to expanded format via the base class. Pool share assets written by super.toTxRep store fields under $prefix.type (not $prefix), so compactValue will be null and the fallback path is correctly taken.

The format detection is unambiguous because ASSET_TYPE_POOL_SHARE is the only case that does not produce a value at the bare prefix key.


5. Backward compatibility with legacy tx.timeBounds._present — correct ✓

lib/src/sep/0011/txrep.dart lines 299–312

The two-step check (new format first, then legacy fallback) is correct. The legacy path correctly handles both tbPresent == 'true' (creates PRECOND_TIME with parsed timebounds) and all other values including absent/'false' (creates PRECOND_NONE).


6. Envelope type handling — correct ✓

  • V0 → encode: Correctly converted to V1 in-memory before serialisation; TxRep output carries type: ENVELOPE_TYPE_TX. V0 is intentionally not re-emitted (lossless only at the XDR level, not TxRep level).
  • V1 → encode/decode: Clean, no issues.
  • Fee bump: Inner tx correctly assumed to be V1 (v1!); validation of feeBump.tx.innerTx.type is optional and only acts when present, which is the correct lenient behaviour.
  • Unsupported type: default throws a useful exception with the discriminant value.

7. Signatures — correct ✓

_encodeSignatures/_decodeSignatures are symmetric and handle the empty-signatures case (unsigned transactions) by returning an empty list when signatures.len is absent. Hint and signature are encoded as lowercase hex via Util.bytesToHex, and decoded with Util.hexToBytes.


8. No dead code or stale imports ✓

All imports in both txrep.dart and txrep_helper.dart are used. txrep_legacy.dart is gone. The removed helpers (formatAmount, parseAmount, dateComment, formatAssetCode, parseAssetCode) are absent from the codebase. TxRepHelper.escapeString/unescapeString are used by generated XDR files (xdr_string32.dart, xdr_string64.dart, xdr_memo.dart, etc.).


9. String interpolation — correct ✓

No $accessor! patterns inside string literals exist in the hand-written files. All null-asserted expressions that appear inside string templates use the ${...} form (e.g., '${tx.seqNum.sequenceNumber}', '${feeBumpEnv.tx.fee.int64}'). The generated xdr_memo.dart line 114 uses ${TxRepHelper.escapeString(_text!)} — the ! is correctly inside ${} braces.


Summary

Area Result
Envelope type handling (V0/V1/fee bump) ✓ Correct
Memo encoding/decoding ✓ Correct (but see item 1)
Compact vs expanded asset format ✓ Correct
Legacy timeBounds._present compat ✓ Correct
Dead code / stale imports ✓ Clean
String interpolation null assertions ✓ Correct
feeBumpEnv null consistency ⚠ Minor (item 2)
Exception type in TrustlineAsset ⚠ Minor (item 3)
Comment accuracy on memo manual override ⚠ Misleading (item 1)

…otion

- Update memo comment to explain jsonEncode/jsonDecode vs escapeString
  format incompatibility (not a double-quoting bug)
- Use local variable for feeBumpEnv null promotion instead of relying
  on implicit flow promotion after bang operator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@christian-rogobete
Copy link
Member Author

Thanks for the thorough review!

Item 1 — Fixed in bebc2d6. Updated the comment to explain the actual reason: the facade uses jsonEncode/jsonDecode for legacy format compatibility, while the generated code uses TxRepHelper.escapeString which emits \xNN hex escapes that jsonDecode cannot parse.

Item 2 — Fixed in bebc2d6. Both isFeeBump blocks now use final fb = feeBumpEnv! for explicit null promotion.

Item 3 — Intentionally keeping Exception here. Changing to ArgumentError would be a breaking change for callers catching Exception, and the benefit doesn't justify the cost.

@christian-rogobete
Copy link
Member Author

@claude review this PR. It rewrites SEP-0011 TxRep to delegate to generated toTxRep()/fromTxRep() methods on XDR types. Only the 151 XDR types reachable from TransactionEnvelope were extended — other XDR types are unchanged.

Focus on these hand-written files:

  • lib/src/sep/0011/txrep.dart — slimmed facade (was 4200+ lines)
  • lib/src/xdr/txrep_helper.dart — new shared utility class
  • lib/src/xdr/xdr_change_trust_asset.dart — compact vs expanded format override
  • lib/src/xdr/xdr_trustline_asset.dart — compact vs expanded format override

Verify: (1) correct handling of all envelope types, memo encoding, and signatures, (2) compact vs expanded format logic for change trust and trustline assets, (3) backward compat with legacy tx.timeBounds._present format, (4) no dead code or stale imports, (5) TxRep string interpolation uses ${accessor} not $accessor for null-asserted expressions.

Skip all other lib/src/xdr/ files — they are auto-generated. Skip test/ and tools/.

@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Claude finished @christian-rogobete's task in 4m 13s —— View job


PR Review: SEP-0011 TxRep Rewrite (updated state)

Tasks:

  • Read relevant files
  • Verify envelope type handling, memo encoding, signatures
  • Verify compact vs expanded format logic
  • Verify backward compat with legacy tx.timeBounds._present
  • Check for dead code / stale imports
  • Check string interpolation for null-asserted expressions
  • Post review findings

The two issues from the prior review that were fixed in bebc2d6 are confirmed resolved. A few additional observations are noted below; none are blocking.


Previously reported items — status

Item 1 (memo comment) — Fixed. txrep.dart:107–109 now correctly explains that manual memo handling is required because the facade uses jsonEncode/jsonDecode for legacy format compatibility, while generated code uses TxRepHelper.escapeString which emits \xNN hex escapes that jsonDecode cannot parse.

Item 2 (feeBumpEnv null promotion) — Fixed. Both isFeeBump blocks now open with final fb = feeBumpEnv!, giving an explicit local non-null binding. No mixed !/no-! inconsistency remains.

Item 3 (exception type in fromXdrAsset) — Acknowledged, intentionally kept as Exception per author's decision. Not re-raised.


New observations

1. feeBump.tx.ext.v round-trip is one-sided

txrep.dart line 133 (encode) / txrep.dart line 199 (decode)

Encoding writes:

lines.add('feeBump.tx.ext.v: 0');

Decoding always constructs:

XdrFeeBumpTransactionExt fbExt = XdrFeeBumpTransactionExt(0);

without reading feeBump.tx.ext.v from the map. For the current Stellar protocol (ext always 0 for fee bump) this is not a bug, but the parsed value is silently ignored while the inner tx.ext.v is used (lines 329–337). If a future protocol version adds a non-zero feeBump.tx.ext.v, the decoder will silently lose it. Consider adding a commented note to match the intent, similar to how the inner ext.v == 1 path is handled explicitly. Informational only.

2. Compact pool-share decode path in XdrTrustlineAsset.fromTxRep is unreachable from this encoder

xdr_trustline_asset.dart lines 31–44

toTxRep always calls super.toTxRep() for ASSET_TYPE_POOL_SHARE, which emits the expanded format ($prefix.type: ASSET_TYPE_POOL_SHARE / $prefix.liquidityPoolID: …). The fromTxRep compact path (64-char hex at bare prefix key) is therefore unreachable from any TxRep produced by this library, but is reachable from externally-authored TxRep. parseTrustlineAsset correctly handles it via the 64-char hex detection heuristic (value.length == 64 && !value.contains(':') at txrep_helper.dart:450). This is a minor documentation gap rather than a bug — the comment on line 37 already says "Fall back to expanded format (pool share)" but could clarify that the compact path is for inbound interoperability only.

3. All other checks — correct ✓

Area Result
Envelope type handling (V0→V1 conversion, V1, fee bump) ✓ Correct
V0 source-account promotion to XdrMuxedAccount ✓ Correct
Fee-bump inner tx assumed V1 (v1!) ✓ Correct
Default envelope type throws with discriminant value ✓ Correct
Memo NONE/TEXT/ID/HASH/RETURN — encode + decode symmetric ✓ Correct
jsonEncode/jsonDecode quoted-string + comment stripping interaction ✓ Correct
removeComment escape handling (\\, \") for JSON-encoded memo ✓ Correct
Compact (single-line) vs expanded (multi-key) format for non-pool assets ✓ Correct
Pool-share XdrChangeTrustAsset expanded encode/decode round-trip ✓ Correct
Pool-share XdrTrustlineAsset expanded encode/decode round-trip ✓ Correct
Legacy tx.timeBounds._present backward compat ✓ Correct
_decodeSignatures returns empty list for unsigned transactions ✓ Correct
Signatures encoded/decoded as lowercase hex ✓ Correct
No dead code / stale imports in hand-written files ✓ Clean
${...} form used for all null-asserted string interpolation ✓ Correct
parseInt/parseBigInt used appropriately for 32-bit vs 64-bit fields ✓ Correct

Overall the rewrite is clean and the delegation strategy is sound. The two new observations above are informational and do not require changes before merging.

…t comments

- Validate feeBump.tx.ext.v during decode, throw on unsupported values
- Add test for unsupported feeBump.tx.ext.v
- Clarify compact format comments in XdrTrustlineAsset and
  XdrChangeTrustAsset to note the path is for external interoperability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@christian-rogobete
Copy link
Member Author

@claude You reviewed this PR twice already and both times all findings were addressed. Review the latest commit (f0c2b6e) which addresses your last two findings, then approve the PR if everything looks good.

@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Claude finished @christian-rogobete's task in 2m 1s —— View job


PR Review: Latest commit f0c2b6e

Tasks:

  • Read CLAUDE.md
  • Read lib/src/sep/0011/txrep.dart — verify feeBump ext validation
  • Read lib/src/xdr/xdr_trustline_asset.dart — verify compact format comment
  • Verify both prior findings are resolved
  • Post final verdict

Both findings from the second review are confirmed resolved in f0c2b6e.

Finding 1 (feeBump.tx.ext.v one-sided round-trip) — Resolved and improved beyond what was suggested. The fix at txrep.dart:199–203 now actively validates the field rather than just adding a comment:

String? fbExtV = TxRepHelper.getValue(map, 'feeBump.tx.ext.v');
if (fbExtV != null && fbExtV != '0') {
  throw Exception('unsupported feeBump.tx.ext.v: $fbExtV');
}

This correctly rejects any future non-zero ext version rather than silently ignoring it, which is better than the documentation-only fix I suggested.

Finding 2 (compact pool-share decode unreachable from this encoder) — Resolved. xdr_trustline_asset.dart:32–35 now has an explicit comment:

// Compact format: native, CODE:ISSUER, or 64-char hex pool ID.
// This path is not produced by this library's encoder (which always
// uses expanded format for pool shares) but supports externally
// authored TxRep.

This clearly documents the intent and removes the ambiguity noted in the prior review.


Verdict

All findings across both review passes have been addressed. The codebase is clean:

Area Result
All prior review findings ✓ Resolved
feeBump.tx.ext.v validation ✓ Now actively validated (improved)
Compact pool-share decode intent ✓ Clearly documented
Overall code quality ✓ Clean

The PR is ready to merge. Note: I'm unable to formally approve PRs through GitHub's review mechanism — you'll need to approve it manually.

@christian-rogobete christian-rogobete merged commit 11bb2a4 into master Mar 12, 2026
7 checks 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.

1 participant