Skip to content

[DRAFT] Class XDR Implementation#1422

Draft
Ryang-21 wants to merge 27 commits into
modernizationfrom
class-xdr
Draft

[DRAFT] Class XDR Implementation#1422
Ryang-21 wants to merge 27 commits into
modernizationfrom
class-xdr

Conversation

@Ryang-21
Copy link
Copy Markdown
Contributor

@Ryang-21 Ryang-21 commented May 26, 2026

Why

The legacy @stellar/js-xdr runtime had several rough edges that
constrained the SDK from inside:

  • Union access required method-call accessors (value.switch(),
    value.value(), value.arm()) with no TypeScript narrowing — every
    union access needed an as cast.
  • Enum access required a function call (AssetType.assetTypeNative())
    even though the members are cached singletons under the hood. The
    function-call ergonomics broke pattern-matching on enum identity and
    made switch statements over enums verbose.
  • Wide ints had a multi-arg parts constructor (new Int128(lo, hi))
    and exposed slice() for accessing 32- or 64-bit chunks. The bigint
    value was only reachable via .toBigInt().
  • Int64 / Uint64 were object-boxed (Hyper / UnsignedHyper
    extending LargeInt) rather than native bigint.
  • No SEP-0051 JSON outputstellar-xdr-json was a separate process.
  • Method names used all-caps acronyms (toXDR, fromXDR) where the
    SDK's broader convention is single-initial-cap (toString, etc.).

Additionally, this PR introduces several capabilities the legacy runtime
didn't have at all (not fixes, additions):

  • toJson / fromJson on every XDR value, SEP-0051-compliant.
  • toXdrObject / fromXdrObject bridging methods (legacy types held
    wire shape directly, so this distinction wasn't meaningful).
  • The XdrString wrapper for byte-faithful string<N> handling.

What

A full in-tree replacement of the XDR layer (this will be moved to js-xdr but for testing purposes it lives here for now):

  • src/xdr/core/Reader, Writer, BaseType<T>. Buffer I/O
    primitives.
  • src/xdr/types/ — schema primitives (struct, union, enum,
    opaque, varOpaque, string, int32/int64/etc., array,
    fixedArray, option, lazy). Charset-agnostic; no dependency on
    consumer value classes. Designed to be liftable into a standalone XDR
    runtime.
  • src/xdr/values/ — consumer-facing bases. XdrValue (universal
    base with toXdr/fromXdr/toJson/fromJson), EnumValue,
    BytesValue, BigIntValue, XdrString. The SEP-0051 JSON walker
    lives here (to-json.ts).
  • src/xdr/generated/ — 447 generated classes produced by
    tools/xdrgen/generate.mjs reading xdr/xdr.json. TSDoc on each class
    carries the original .x source so hovers show the upstream XDR
    declaration.
  • src/xdr/dx/ — hand-written DX overlays (Int128/Uint128/
    Int256/Uint256 exposing a single bigint) and primitive shims
    (Int64/Uint64/Int32/Uint32 returning native primitives via a
    Proxy construct trap).

Plus:

  • Method-name normalization: toXDRtoXdr, fromXDRfromXdr
    on every type. toXDRObject / fromXDRObject / fromJSON listed
    elsewhere are new methods with no legacy analogue.
  • Drop of the LargeInt classes in src/base/numbers/Int128,
    Uint128, Int256, Uint256 now hold a single bigint.
  • XdrString wrapper for string<N> fields — bytes-faithful, with
    explicit .toString() / .toStringStrict() / .asStringOrBytes() /
    .bytes / .toJson() access patterns.
  • SEP-0051 JSON walker — value.toJson() and Type.fromJson(json) on
    every XDR class.
  • Layered test suite validating wire-compatibility with the legacy SDK:
    hand-written smoke tests (~15), real-traffic mainnet corpus (~15),
    schema-driven exhaustive coverage (~2000), JSON walker round-trips
    (~120).
  • docs/XDR_MIGRATION.md — caller-facing migration guide.
  • docs/ARCHITECTURE.md — contributor-facing internals.

Example changes

Union access.type literal narrowing replaces method-call accessors:

// Before 
if (op.body().switch() === xdr.OperationType.invokeHostFunction()) {
  const args = op.body().value().invokeHostFunctionOp();
  args.hostFunction().value();  // any
}

// After
if (op.body.type === "invokeHostFunction") {
  const args = op.body.invokeHostFunctionOp;  // typed
  args.hostFunction.invokeContract;           // typed
}

Enum access — drop the parens; the singleton was already cached:

// Before — function call returning a cached singleton
xdr.AssetType.assetTypeNative();
xdr.ScValType.scvU32();

// After — property access, same identity guarantee
xdr.AssetType.assetTypeNative;
xdr.ScValType.scvU32;

Wide ints — single bigint instead of parts arithmetic:

// Before
const v = new xdr.Int128(lo, hi);       // multi-arg parts constructor
v.toBigInt();                            // need a method to get the bigint
v.slice(64);                             // get 64-bit chunks

// After
const v = new Int128(42n);              // bigint-direct
v.value;                                 // bigint property
v.toParts();                             // { hi, lo } for wire-shape access

XDR strings — XdrString wrapper with explicit decoding:

// Before — string<N> read returned a raw Buffer; opt-in .toString("utf8")
const memo = parsedMemo;
memo.value();                            // Buffer
memo.value().toString("utf8");           // string (lossy if non-UTF-8)

// After — explicit access patterns
const memo = Memo.memoText("Hi");        // accepts string | Uint8Array
memo.text.toString();                    // "Hi" — lenient UTF-8
memo.text.toStringStrict();              // "Hi" — throws on invalid
memo.text.bytes;                         // Uint8Array — raw wire bytes
memo.text.toJson();                      // "Hi" — SEP-0051 escape form

SEP-0051 JSON output (new — no legacy equivalent):

ScVal.scvI128(new Int128Parts({ hi: 0n, lo: 7n })).toJson();
// → { i128: "7" }

ScAddress.scAddressTypeAccount(PublicKey.publicKeyTypeEd25519(bytes)).toJson();
// → "GAAQEAYEAUDA…"  (StrKey per SEP-0051)

Asset.assetTypeNative().toJson();
// → "native"  (snake_case case-name, prefix stripped)

new AlphaNum4({ assetCode: bytes, issuer: pk }).toJson();
// → { asset_code: "USD", issuer: "GAAQ…" }   (snake_case keys)

Method renames:

// Before                         // After
obj.toXDR("base64")               obj.toXdr("base64")
Foo.fromXDR(bytes)                Foo.fromXdr(bytes)
asset.toChangeTrustXDRObject()    asset.toChangeTrustXdrObject()
asset.toTrustLineXDRObject()      asset.toTrustLineXdrObject()

Bytes — explicit wrappers required:

// Before
xdr.ContractExecutable.contractExecutableWasm(Buffer.alloc(32));
xdr.ScVal.scvBytes(Buffer.from([1, 2, 3]));

// After
xdr.ContractExecutable.contractExecutableWasm(new xdr.Hash(Buffer.alloc(32)));
xdr.ScVal.scvBytes(new xdr.ScBytes(Buffer.from([1, 2, 3])));

Typedef-opaque aliases became distinct classes:

// Before — PoolId === Hash at runtime
xdr.ScAddress.scAddressTypeContract(new xdr.Hash(bytes));        // worked
xdr.ScAddress.scAddressTypeLiquidityPool(new xdr.Hash(bytes));   // worked

// After — distinct types with strkey-aware JSON output
xdr.ScAddress.scAddressTypeContract(new xdr.ContractId(bytes));
xdr.ScAddress.scAddressTypeLiquidityPool(new xdr.PoolId(bytes));

@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX May 26, 2026
@Ryang-21 Ryang-21 mentioned this pull request May 27, 2026
@Ryang-21 Ryang-21 changed the title Class xdr [DRAFT] Class XDR Implementation May 27, 2026
@Ryang-21 Ryang-21 requested a review from Copilot May 27, 2026 16:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@Ryang-21 Ryang-21 requested review from Shaptic and quietbits May 27, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

2 participants