Skip to content

feat(xdr): support CAP-83 empty tx set values and CAP-85 external executables - #1577

Merged
Ryang-21 merged 2 commits into
xdr-regen-cap83-cap85from
xdr-cap83-cap85
Jul 31, 2026
Merged

feat(xdr): support CAP-83 empty tx set values and CAP-85 external executables#1577
Ryang-21 merged 2 commits into
xdr-regen-cap83-cap85from
xdr-cap83-cap85

Conversation

@Ryang-21

@Ryang-21 Ryang-21 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Builds on #1576, which regenerated the XDR classes with CAP-83 and CAP-85 ungated. That PR only did the minimum to keep tsc green; this one teaches the hand-written layers what the new variants mean, and adds round-trip coverage.

Reading a tag. scValToNative now handles scvExecutableTag, decoding the SCString payload via asStringOrBytes() — the same treatment scvString and scvSymbol get, so a tag holding non-UTF-8 bytes comes back as a Uint8Array rather than a string full of U+FFFD replacement characters. Previously the variant fell through to default and leaked the raw XdrString wrapper.

Rendering a creation. buildInvocationTree renders external-executable creations instead of throwing "unknown creation type". CreateInvocation.type gains an "external" case whose details live in a new external field, typed by the new ExternalRefCreateDetails: owner, tag, address, salt, and constructorArgs for CREATE_CONTRACT_V2. The executable/preimage validation is restructured: an external ref derives its contract ID from deployer address plus salt, exactly as Wasm does, so it belongs with Wasm on the address side of that check rather than tripping it.

Resolving a reference. A contract created from an external reference carries no code hash of its own. Per CAP-85 the owner contract holds a persistent contract data entry keyed by the tag whose value is a 32-byte Wasm hash, and the host resolves the reference by reading that entry — it does not invoke the owner. New rpc.Server.getExternalRefWasmHash(ref) performs that lookup, and contract.Client.from and rpc.Server.getContractWasmByContractId use it, so both work on these contracts: resolve the reference to a hash, then load the spec or code as for any Wasm contract. getContractMethods and queryContract follow, since they build on those two.

Generating bindings. src/bindings/wasm_fetcher.ts had its own instance-to-code path that rejected every non-Wasm executable, so BindingGenerator.fromContractId and the CLI behind it failed on these contracts even once the methods above worked. It now resolves an external reference through the same getExternalRefWasmHash before fetching code, restating the server's { code, message } rejection as a WasmFetchError to match the rest of that module. The SAC branch is untouched.

CAP-83 (empty transaction set values). No behavior change is needed — StellarValueExt's new stellarValueEmptyTxSet arm and the StellarValueProposedValue struct are consumed generically. This PR covers them with tests instead.

Build fix. make xdr-json piped curl into tar. sh has no pipefail and tar can exit 0 on an empty stream, so a failed or truncated schema download passed silently. It now downloads to a file, then extracts.

Why

A tag is half of what identifies the code being deployed, so it cannot be decoded leniently: two distinct binary tags would render identically, and the rendered tree is what a signer reads before approving. Hence string | Uint8Array everywhere a tag surfaces.

Rejecting external refs in Client.from would have been a needless dead end. The reference is resolvable with lookups the SDK already has, so a client for one of these contracts costs one extra getLedgerEntries call, not a manual spec. The binding fetcher gets the same treatment for the same reason, and because a contract that Client.from can talk to but fromContractId cannot generate bindings for is a confusing split.

Coverage

  • test/unit/xdr/protocol_cap83_cap85.test.ts round-trips each new type and union arm through XDR bytes and SEP-0051 JSON, and asserts the pre-existing arms of the widened unions still decode as before. These need hand-written coverage because schema_exhaustive.test.ts validates against a pinned legacy SDK build as its on-wire oracle, and that build predates both CAPs — it silently skips types the legacy SDK does not expose.
  • test/unit/base/scval.test.ts covers the scvExecutableTag conversion in both its string and raw-bytes forms.
  • test/unit/base/invocation.test.ts covers external-ref creation trees for V1 and V2, a binary tag, and the mismatch error. Owners in these fixtures are contracts, since only a contract can hold the tag entry.
  • test/unit/server/soroban/get_contract_wasm.test.ts covers the three-hop resolution (instance → owner's tag entry → contract code), that a binary tag keys the lookup undecoded, a tag entry not holding a 32-byte hash, and a non-contract owner.
  • test/unit/contract/client_from.test.ts covers Client.from on an external-reference contract end to end, and that a non-contract owner rejects before the second lookup.
  • test/unit/bindings/wasm_fetcher.test.ts is new — the module had no tests. It covers the Wasm and SAC paths plus the three external-reference cases: resolution, a binary tag keying the lookup undecoded, and a non-contract owner.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds CAP-83/CAP-85 handling across XDR conversion, invocation parsing, RPC Wasm resolution, and contract clients.

Changes:

  • Resolves CAP-85 external executable references and decodes executable tags.
  • Adds CAP-83/CAP-85 round-trip and behavior tests.
  • Updates generated references, changelog, and XDR download reliability.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/base/scval.ts Converts executable tags to native values.
src/base/invocation.ts Represents external-reference contract creation.
src/rpc/server.ts Resolves references and retrieves their Wasm.
src/contract/client.ts Builds clients from referenced Wasm.
test/unit/base/scval.test.ts Tests executable-tag conversion.
test/unit/base/invocation.test.ts Tests external creation parsing.
test/unit/server/soroban/get_contract_wasm.test.ts Tests external-reference resolution.
test/unit/xdr/protocol_cap83_cap85.test.ts Tests new XDR variants.
Makefile Makes XDR archive downloads safer.
docs/reference/network-rpc.md Updates generated RPC reference.
docs/reference/core-soroban-primitives.md Updates generated invocation reference.
docs/reference/contracts-client.md Updates generated client reference.
CHANGELOG.md Records CAP-83/CAP-85 support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/base/invocation.ts
Comment thread test/unit/xdr/protocol_cap83_cap85.test.ts
Comment thread src/contract/client.ts
Comment thread src/contract/client.ts
Comment thread src/rpc/server.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (7)

src/base/invocation.ts:210

  • This branch accepts an account-valued executableOwner, but CAP-85 resolves the tag from the owner contract's persistent storage; getExternalRefWasmHash below rejects account owners for the same reason. As written, the invocation tree renders protocol-invalid creations instead of raising the existing invalid-creation error. Require scAddressTypeContract before constructing the external details.
      } else if (
        exec.type === "contractExecutableExternalRef" &&
        preimage.type === "contractIdPreimageFromAddress"

src/base/invocation.ts:23

  • CAP-85 requires the executable owner to be a contract, since the referenced tag is a contract-data entry; account addresses cannot own that entry. The public API documentation currently claims account owners are valid and conflicts with getExternalRefWasmHash, which rejects them.
 * - `owner` is the strkey of the account or contract that owns the external
 *   executable being referenced

test/unit/base/invocation.test.ts:391

  • This binary-tag fixture also uses an account as the executable owner, so the invocation is invalid independently of the tag bytes. Use a contract address so the test isolates and verifies binary-tag rendering on a valid CAP-85 creation.
    const owner = randomKey();

src/contract/client.ts:275

  • There is no external-reference case in test/unit/contract/client_from.test.ts; it still covers only direct Wasm and SAC instances, despite the PR's coverage statement. Add the described end-to-end success case and non-contract-owner rejection so this new multi-lookup path is exercised.
    const wasmHash =
      executable.type === "contractExecutableExternalRef"
        ? await server.getExternalRefWasmHash(executable.externalRef)
        : executable.wasmHash.value;

test/unit/xdr/protocol_cap83_cap85.test.ts:69

  • This new union arm is only round-tripped through XDR bytes, although the file header and PR coverage claim SEP-0051 JSON coverage for each new arm. Add a ScVal.fromJson(scv.toJson()) assertion here; otherwise the executable-tag JSON path remains untested.
    const decoded = ScVal.fromXdr(scv.toXdr());
    expect(decoded.type).toBe("scvExecutableTag");
    expect(decoded.toXdr()).toEqual(scv.toXdr());

src/rpc/server.ts:652

  • The PR description says binding generation was wired through this resolver, but src/bindings/wasm_fetcher.ts:114-115 still rejects every non-Wasm executable and test/unit/bindings/wasm_fetcher.test.ts is absent. Consequently BindingGenerator.fromContractId and its CLI still fail for external-reference contracts. Please update the fetcher to resolve this arm through the new helper and add the stated tests.
  public async getExternalRefWasmHash(
    ref: ContractExecutableExternalRef,
  ): Promise<Uint8Array> {

test/unit/base/invocation.test.ts:320

  • This fixture uses a G-address as the executable owner, but CAP-85 requires a contract owner because the tag is read from contract storage. It therefore locks in rendering an invocation that the host cannot execute; use a C-address fixture instead.

This issue also appears on line 391 of the same file.

    const owner = randomKey();

@Ryang-21
Ryang-21 requested a review from quietbits July 30, 2026 21:45
Comment thread src/base/invocation.ts
Comment on lines +54 to +57
type: "sac" | "wasm" | "external";
asset?: string;
wasm?: WasmCreateDetails;
external?: ExternalRefCreateDetails;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if type is not "external" and external prop is set?

@Ryang-21 Ryang-21 Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This should not be possible given that this is mean't to be used by buildInvocationTree which takes an xdr SorobanAuthorizedInvocation. Ideally this type would be a discriminated union so that its not possible

@Ryang-21
Ryang-21 merged commit cf0659b into v17-feature-branch Jul 31, 2026
13 checks passed
@Ryang-21
Ryang-21 deleted the xdr-cap83-cap85 branch July 31, 2026 18:01
@github-project-automation github-project-automation Bot moved this from Backlog (Not Ready) to Done in DevX Jul 31, 2026
Ryang-21 added a commit that referenced this pull request Jul 31, 2026
Resolves CAP-83/CAP-85 XDR work against main's guide-snippet pipeline, event
bindings, and released changelog sections.

- .gitignore, package.json: keep both sides (guides scripts + split
  test:browser:fetch/axios entries).
- tests.yml: keep main's Guide Snippets Check step in build_and_test, drop its
  inline Browser Tests step in favor of the new per-transport browser_test job,
  and bump that job's actions to the versions dependabot already applied.
- CHANGELOG.md: keep the released v16.1.0/v16.2.0 sections and move the branch's
  #1577 CAP-83/CAP-85 entries into a new Unreleased section.
- docs/reference: regenerated with pnpm docs:reference.
Ryang-21 added a commit that referenced this pull request Aug 10, 2026
* Class XDR Implementation (#1422)

* feat(xdr): codegen tool + schema source

* feat(xdr): add class-based XDR runtime and sep51 JSON walker

* refactor(numbers): drop LargeInt classes, delegate to new XDR layer

* refactor(base): migrate src/base to new XDR layer; drop legacy xdr.ts + generated

* refactor: migrate downstream consumers (bindings/contract/horizon/rpc/webauth)

* feat(base/scval): add bool to ScValType

* allow opaque xdr types to be initalized via string

* refactor xdr strings to be represented soley via bytes with a dx friendly XdrString wrapper class

* generate a value getter function for void union cases

* add a is() function to the generated XDR union classes for instanceOf checks

* feat(xdr): regenerate schemas against @stellar/js-xdr and add CAP-71 credentials

* feat(xdr): wire the toJSON hook so JSON.stringify emits SEP-0051

* fix(bindings): emit Uint8Array for bytes/bytesN to match scValToNative

* refactor(contract): rename fromJSON to fromJson with deprecated aliases

* feat(xdr): accept ASCII asset codes with zero padding in constructors

* Migrate public API from Buffer to Uint8Array (#1564)

* feat(base): migrate crypto and strkey APIs to Uint8Array

* feat(base)!: migrate value types to Uint8Array

* feat(base)!: migrate transactions, operations, and auth to Uint8Array

* feat!: migrate contract, rpc, and webauth layers to Uint8Array

* build!: drop buffer polyfill and dependency

* fix(horizon): type manage_data value as string to match runtime API

* fix(xdr): emit SEP-51 key `type` instead of Rust-escaped `type_` (#1571)

* build(xdr): regenerate xdr.json via docker from pinned stellar-xdr commit (#1575)

* build(xdr): regenerate schema from stellar-xdr with CAP-83 and CAP-85 ungated (#1576)

* build(xdr): regenerate schema from stellar-xdr with CAP-83 and CAP-85 ungated

* fix(xdr): keep consumers compiling against the regenerated union arms

* fix(vitest): isolate browser dep cache per transport

* feat(xdr): support CAP-83 empty tx set values and CAP-85 external executables (#1577)

* build(xdr): fail the schema download instead of masking it in a pipe

* feat(xdr): support CAP-83 and CAP-85 protocol values

* fix(xdr): bound decimal string length before BigInt parse in JSON decode (#1581)

* fix(xdr): bound decimal string length before BigInt parse in json decode

* refactor(xdr): name the digit-budget constants in bigint-parts

* fix(xdr): restrict fromJson to SEP-0051 keys and reject unknown fields (#1582)

* fix(xdr): restrict fromJson to SEP-51 keys and reject unknown fields

* fix(test): correct horizon corpus fixture path so corpus tests run

* fix(strkey): bound decodeCheck input length before base32 decode (#1583)

* fix(xdr): reject AssetCode12 JSON codes shorter than 5 bytes (#1585)

* fix(horizon)!: make TransactionFailedExtras result_codes.operations optional (#1586)

* Fix: signed payload strkey framing (#1588)

* fix(strkey): validate signed payload framing in decodeCheck

* Fix: xdr json decode validation (#1592)

* fix(xdr): throw on unknown union discriminant in fromXdrObject

* fix(strkey): validate the claimable balance discriminant byte

* fix(xdr): reject non-decimal integer strings in JSON decoding

* V17.0.0 rc.1 (#1593)

* chore(release): cut v17.0.0-rc.1

* fix(spec): restore instanceof Map check lost in the v17 merge

* feat(xdr): add validateXdr static to every generated type (#1597)

* feat(xdr): add validateXdr static to every generated type

* fix(contract): declare error classes, make types self-contained (#1627)

* perf(strkey): reject by length and prefix before decodeCheck throws (#1629)

* fix: restore wide-int bounds statics, document Memo.text break (#1628)

* fix(xdr): restore wide-int MIN_VALUE/MAX_VALUE statics

---------

Co-authored-by: Iveta <quietbits@users.noreply.github.com>
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.

Support externally managed contract executables (CAP-85)

3 participants