Skip to content

Class XDR Implementation#1422

Merged
Ryang-21 merged 89 commits into
v17-feature-branchfrom
class-xdr
Jul 23, 2026
Merged

Class XDR Implementation#1422
Ryang-21 merged 89 commits into
v17-feature-branchfrom
class-xdr

Conversation

@Ryang-21

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

Copy link
Copy Markdown
Contributor

Why

The legacy @stellar/js-xdr v4 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. The
    JavaScript-standard toJSON() hook delegates to toJson(), so
    JSON.stringify emits SEP-0051 too — including for XDR values nested
    inside plain objects (loggers, res.json, snapshots), where it would
    otherwise throw on bigint fields.
  • 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.
  • decodeStream(Type, bytes) for decoding several concatenated values
    of one type (contract-spec entry streams, etc.).
  • Typed union-narrowing helpers expectUnionVariant / isUnionVariant.

What

The XDR layer is rebuilt on @stellar/js-xdr v5, which now provides
the generic RFC 4506 runtime (Reader/Writer byte I/O, the
BaseType<T> schema interface, and the schema primitives — struct,
union, enumType, opaque, varOpaque, string, ints, array,
option, lazy, …). Everything Stellar-specific lives in this repo
under 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), with per-schema-name overrides for StrKey forms,
    wide-int decimal strings, and asset-code trimming.
  • generated/ — 449 generated classes produced by
    tools/xdrgen/generate.mjs reading xdr/xdr.json (regenerated
    against protocol 23, including the CAP-71 SOROBAN_CREDENTIALS_ADDRESS_V2
    credentials). TSDoc on each class carries the original .x source so
    hovers show the upstream XDR declaration.
  • dx/ — hand-written DX overlays (Int128/Uint128/Int256/
    Uint256 exposing a single bigint) plus Int64/Uint64/Int32/
    Uint32 shims returning native primitives from the call form
    (xdr.Int64(v)); calling them with new throws a descriptive
    TypeError at the call site.
  • index.ts — the single public entry, exported as the xdr
    namespace. The js-xdr Reader/Writer and schema-builder factories
    are deliberately not re-exported; streaming callers use
    xdr.decodeStream instead.

Plus:

  • Method-name normalization: toXDRtoXdr, fromXDRfromXdr
    on every type. toXdrObject / fromXdrObject / fromJson 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.
    XdrLargeInt keeps the legacy multi-slice constructor contract
    (little-endian, parts[0] = least significant) and now range-checks
    each slice at construction.
  • 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.
  • CAP-71 auth support on top of the regenerated schemas:
    authorizeEntry/authorizeInvocation gain opt-in ADDRESS_V2
    credentials (authV2: true) and low-level delegate-signing primitives
    (buildAuthorizationEntryPreimage, buildWithDelegatesEntry).
  • Layered test suite under test/unit/xdr/ validating wire
    compatibility with the legacy SDK: hand-written smoke tests, a
    real-traffic mainnet corpus, schema-driven exhaustive coverage
    (~2000 generated cases), and JSON walker round-trips.
  • docs/XDR_MIGRATION.md — caller-facing migration guide.
  • src/xdr/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
}

// Or narrow with the helpers when a full switch is overkill:
const v1 = xdr.expectUnionVariant(tx.toEnvelope(), "envelopeTypeTx").v1;
if (xdr.isUnionVariant(scv, "scvU32")) scv.u32; // number

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

// Legacy parts→bigint reconstruction still works, little-endian as before:
new XdrLargeInt("i128", [i128.lo, i128.hi]).toBigInt();

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)

// JSON.stringify fires the toJSON hook, so implicit serialization works —
// previously this threw "Do not know how to serialize a BigInt":
JSON.stringify({ note: "swap", val: ScVal.scvI64(123n) });
// → '{"note":"swap","val":{"i64":"123"}}'

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));

Ryang-21 and others added 30 commits April 22, 2026 13:41
* Replace __USE_AXIOS__ dynamic require with static fetch default

* Add babel and webpack aliasing to emit axios variant from shared source

* Flip package.json exports: fetch default, /axios opt-in, drop /no-axios

* Update vitest configs and test harness for TRANSPORT=axios variant

* Update CI, README, and stale references for new fetch-default build matrix
* update eventsource to v4.1.0 and remove conditional import of eventsource

* remove no-eventsource lib bundle as the eventsource dependency runs in the workerd runtime

* remove the dom-monkeypatch as its now included in the tsconfig lib field

* add additional tests for eventsource behavior
* add type: module in pkg.json and update js config files to esm style

* build and export cjs and esm build varients, remove export of umd bundles

* remove unused dependency (causing issue with hook files being cjs format)
* replace webpack + babel dependencies for rollup

* migrate build tools to rollup
* move stellar-base src under src/base

* add unit tests from stellar-base

* port xdr and makefile for generating xdr

* port manual type declarations for js only dependencies

* inline js-xdr for node esm compatibility

* disable bundle_size workflow
* migrate from classic yarn to pnpm

* add pnpm setup action to git workflows
* remove randomBytes for universal crypto.getRandomValues

* replace sha.js with noble/hashes and update to version 2.2.0

* update BigNumber to v11.0.0

* replace noble/curves with noble/ed25519 for reduced bundle size

* move tsconfig project root

* replace toml with smol-toml
* refactor: replace URI usage for native URL + URLSearchParam objects

* remove usage of non-null assertion

* allow expandUriTemplate to handle relative templated links
* test src code directly instead of the built lib
…p nyc (#1408)

* Update husky config + remove nyc

* Root .nvmrc + bump Node to v22

* pnpm minimumReleaseAge config

* Cleanup
@Ryang-21
Ryang-21 marked this pull request as ready for review July 13, 2026 23:07
@Ryang-21 Ryang-21 changed the title [DRAFT] Class XDR Implementation Class XDR Implementation Jul 13, 2026
@Ryang-21
Ryang-21 changed the base branch from main to v17-feature-branch July 13, 2026 23:08
Ryang-21 added 21 commits July 14, 2026 10:25
@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​stellar/​stellar-xdr-json@​26.0.0761008094100
Added@​stellar/​js-xdr@​5.0.0-rc.110010010091100

View full report

@Ryang-21
Ryang-21 merged commit 688c18b into v17-feature-branch Jul 23, 2026
8 checks passed
@Ryang-21
Ryang-21 deleted the class-xdr branch July 23, 2026 18:19
@github-project-automation github-project-automation Bot moved this from Backlog (Not Ready) to Done in DevX Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants