From d47d013dd116cb4c67598193ca7bd57e01bb0c41 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Fri, 26 Jun 2026 23:48:36 -0700 Subject: [PATCH 1/4] =?UTF-8?q?SPIKE:=20Protocol=2028=20(CAP-0084)=20?= =?UTF-8?q?=E2=80=94=20muxed=20contract=20addresses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit js-stellar-sdk v16 vendors the former stellar-base into src/base and owns XDR generation (xdr/*.x -> src/base/generated via Makefile/xdrgen). Ports the CAP-0084 changes from stellar/js-stellar-base#980 (head d79a2b7): - xdr/{curr,next}/Stellar-contract.x: SC_ADDRESS_TYPE_MUXED_CONTRACT arm, MuxedContract struct, SCAddress union case. - Regenerated src/base/generated/{curr,next}_generated.js + .d.ts (targeted muxed-contract additions only; preserved existing dts-xdr/xdrgen shape). - Address.muxedContract() + contractId()/muxedId() accessors and fromScAddress/toScAddress/toString arms. - Tests + CHANGELOG. Canonical XDR: stellar/stellar-xdr CAP-0084 SCAddress MuxedContract arm. --- .gitignore | 1 + CHANGELOG.md | 8 +++ src/base/address.ts | 94 ++++++++++++++++++++++++++-- src/base/generated/curr.d.ts | 42 ++++++++++++- src/base/generated/curr_generated.js | 22 ++++++- src/base/generated/next.d.ts | 42 ++++++++++++- src/base/generated/next_generated.js | 22 ++++++- test/unit/base/address.test.ts | 76 ++++++++++++++++++++++ xdr/curr/Stellar-contract.x | 11 +++- xdr/next/Stellar-contract.x | 11 +++- 10 files changed, 315 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index f78f01417..762fdde2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /libdocs/ /dist/ /coverage/ +*.tgz /jsdoc/ /.astro/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf58cba6..7ab242fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ A breaking change will get clearly marked in this log. ## Unreleased +### Added +- Protocol 28 (CAP-0084): muxed contract addresses. The vendored XDR now + includes the `SC_ADDRESS_TYPE_MUXED_CONTRACT` arm and `MuxedContract` struct, + and `Address` gains `Address.muxedContract(contractId, id)` plus + `contractId()` / `muxedId()` accessors. These addresses have no canonical + StrKey yet, so they round-trip via `ScAddress`/`ScVal` (not via a string); + `toString()` renders the display-only `:` form. + ## [v16.0.1](https://github.com/stellar/js-stellar-sdk/compare/v16.0.0...v16.0.1) ### Fixed diff --git a/src/base/address.ts b/src/base/address.ts index abb13fca6..37e8178e5 100644 --- a/src/base/address.ts +++ b/src/base/address.ts @@ -6,20 +6,28 @@ import xdr from "./xdr.js"; * * `Address` represents a single address in the Stellar network that can be * inputted to or outputted by a smart contract. An address can represent an - * account, muxed account, contract, claimable balance, or a liquidity pool - * (the latter two can only be present as the *output* of Core in the form - * of an event, never an input to a smart contract). + * account, muxed account, contract, muxed contract, claimable balance, or a + * liquidity pool (the latter two can only be present as the *output* of Core + * in the form of an event, never an input to a smart contract). + * + * Muxed-contract addresses (CAP-0084) have no canonical StrKey yet, so they + * cannot be constructed from a string; build them with + * {@link Address.muxedContract} or {@link Address.fromScAddress}. */ export type AddressType = | "account" | "claimableBalance" | "contract" | "liquidityPool" - | "muxedAccount"; + | "muxedAccount" + | "muxedContract"; export class Address { private _type: AddressType; private _key: Buffer; + // Only set for muxed-contract (CAP-0084) addresses, which are built via the + // muxedContract() factory rather than the string constructor. + private _muxId!: xdr.Uint64; /** * @param address - a {@link StrKey} of the address value @@ -99,6 +107,32 @@ export class Address { return new Address(StrKey.encodeMed25519PublicKey(buffer)); } + /** + * Creates a new muxed-contract Address object (CAP-0084). + * + * A muxed-contract address (`SC_ADDRESS_TYPE_MUXED_CONTRACT`) pairs a + * 32-byte contract ID with a `uint64` multiplexing ID. There is no canonical + * StrKey form for it yet, so unlike the other factories it does not route + * through the {@link Address} constructor and the resulting address cannot be + * parsed back out of a string. Round-trip it through + * {@link Address.fromScAddress} / {@link Address#toScAddress} instead; + * {@link Address#toString} renders the display-only form `:`. + * + * @param contractId - the raw 32 bytes of the contract ID + * @param id - the uint64 multiplexing ID; pass a string or {@link xdr.Uint64} + * for values above `Number.MAX_SAFE_INTEGER` to avoid precision loss + */ + static muxedContract( + contractId: Buffer, + id: number | bigint | string | xdr.Uint64, + ): Address { + const address: Address = Object.create(Address.prototype); + address._type = "muxedContract"; + address._key = Buffer.from(contractId); + address._muxId = id instanceof xdr.Uint64 ? id : new xdr.Uint64(id); + return address; + } + /** * Convert this from an xdr.ScVal type. * @@ -136,6 +170,13 @@ export class Address { return Address.liquidityPool( scAddress.liquidityPoolId() as unknown as Buffer, ); + case xdr.ScAddressType.scAddressTypeMuxedContract().value: { + const muxed = scAddress.muxedContract(); + return Address.muxedContract( + muxed.contractId() as unknown as Buffer, + muxed.id(), + ); + } default: throw new Error(`Unsupported address type: ${scAddress.switch().name}`); } @@ -156,6 +197,11 @@ export class Address { return StrKey.encodeLiquidityPool(this._key); case "muxedAccount": return StrKey.encodeMed25519PublicKey(this._key); + case "muxedContract": + // Display-only form `:`. This is NOT a canonical StrKey: + // the Address constructor cannot parse it back, so muxed-contract + // addresses round-trip via ScAddress/ScVal, not via this string. + return `${StrKey.encodeContract(this._key)}:${this._muxId.toString()}`; default: throw new Error("Unsupported address type"); } @@ -201,6 +247,14 @@ export class Address { }), ); + case "muxedContract": + return xdr.ScAddress.scAddressTypeMuxedContract( + new xdr.MuxedContract({ + id: this._muxId, + contractId: this._key as unknown as xdr.ContractId, + }), + ); + default: throw new Error("Unsupported address type"); } @@ -208,11 +262,43 @@ export class Address { /** * Return the raw public key bytes for this address. + * + * @throws for muxed-contract addresses, which have no single-buffer encoding + * (use {@link Address#contractId} / {@link Address#muxedId}) */ toBuffer(): Buffer { + if (this._type === "muxedContract") { + throw new Error("toBuffer is not supported for muxed-contract addresses"); + } return this._key; } + /** + * For a muxed-contract address, returns the raw 32-byte contract ID. + * + * @throws if this is not a muxed-contract address + */ + contractId(): Buffer { + if (this._type !== "muxedContract") { + throw new Error( + "contractId() is only valid for muxed-contract addresses", + ); + } + return this._key; + } + + /** + * For a muxed-contract address, returns the `uint64` multiplexing ID. + * + * @throws if this is not a muxed-contract address + */ + muxedId(): xdr.Uint64 { + if (this._type !== "muxedContract") { + throw new Error("muxedId() is only valid for muxed-contract addresses"); + } + return this._muxId; + } + /** * Return the type of this address. */ diff --git a/src/base/generated/curr.d.ts b/src/base/generated/curr.d.ts index 8a1766485..479126f48 100644 --- a/src/base/generated/curr.d.ts +++ b/src/base/generated/curr.d.ts @@ -2134,9 +2134,10 @@ export namespace xdr { | "scAddressTypeContract" | "scAddressTypeMuxedAccount" | "scAddressTypeClaimableBalance" - | "scAddressTypeLiquidityPool"; + | "scAddressTypeLiquidityPool" + | "scAddressTypeMuxedContract"; - readonly value: 0 | 1 | 2 | 3 | 4; + readonly value: 0 | 1 | 2 | 3 | 4 | 5; static scAddressTypeAccount(): ScAddressType; @@ -2147,6 +2148,8 @@ export namespace xdr { static scAddressTypeClaimableBalance(): ScAddressType; static scAddressTypeLiquidityPool(): ScAddressType; + + static scAddressTypeMuxedContract(): ScAddressType; } class ScEnvMetaKind { @@ -9508,6 +9511,34 @@ export namespace xdr { static validateXDR(input: string, format: "hex" | "base64"): boolean; } + class MuxedContract { + constructor(attributes: { id: Uint64; contractId: ContractId }); + + id(value?: Uint64): Uint64; + + contractId(value?: ContractId): ContractId; + + toXDR(format?: "raw"): Buffer; + + toXDR(format: "hex" | "base64"): string; + + static read(io: Buffer): MuxedContract; + + static write(value: MuxedContract, io: Buffer): void; + + static isValid(value: MuxedContract): boolean; + + static toXDR(value: MuxedContract): Buffer; + + static fromXDR(input: Buffer, format?: "raw"): MuxedContract; + + static fromXDR(input: string, format: "hex" | "base64"): MuxedContract; + + static validateXDR(input: Buffer, format?: "raw"): boolean; + + static validateXDR(input: string, format: "hex" | "base64"): boolean; + } + class ScNonceKey { constructor(attributes: { nonce: Int64 }); @@ -15661,6 +15692,8 @@ export namespace xdr { liquidityPoolId(value?: PoolId): PoolId; + muxedContract(value?: MuxedContract): MuxedContract; + static scAddressTypeAccount(value: AccountId): ScAddress; static scAddressTypeContract(value: ContractId): ScAddress; @@ -15671,12 +15704,15 @@ export namespace xdr { static scAddressTypeLiquidityPool(value: PoolId): ScAddress; + static scAddressTypeMuxedContract(value: MuxedContract): ScAddress; + value(): | AccountId | ContractId | MuxedEd25519Account | ClaimableBalanceId - | PoolId; + | PoolId + | MuxedContract; toXDR(format?: "raw"): Buffer; diff --git a/src/base/generated/curr_generated.js b/src/base/generated/curr_generated.js index 5354907b9..fac75b7c1 100644 --- a/src/base/generated/curr_generated.js +++ b/src/base/generated/curr_generated.js @@ -9359,7 +9359,8 @@ var types = XDR.config((xdr) => { // SC_ADDRESS_TYPE_CONTRACT = 1, // SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, // SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - // SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 + // SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, + // SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 // }; // // =========================================================================== @@ -9369,6 +9370,7 @@ var types = XDR.config((xdr) => { scAddressTypeMuxedAccount: 2, scAddressTypeClaimableBalance: 3, scAddressTypeLiquidityPool: 4, + scAddressTypeMuxedContract: 5, }); // === xdr source ============================================================ @@ -9385,6 +9387,20 @@ var types = XDR.config((xdr) => { ["ed25519", xdr.lookup("Uint256")], ]); + // === xdr source ============================================================ + // + // struct MuxedContract + // { + // uint64 id; + // ContractID contractId; + // }; + // + // =========================================================================== + xdr.struct("MuxedContract", [ + ["id", xdr.lookup("Uint64")], + ["contractId", xdr.lookup("ContractId")], + ]); + // === xdr source ============================================================ // // union SCAddress switch (SCAddressType type) @@ -9399,6 +9415,8 @@ var types = XDR.config((xdr) => { // ClaimableBalanceID claimableBalanceId; // case SC_ADDRESS_TYPE_LIQUIDITY_POOL: // PoolID liquidityPoolId; + // case SC_ADDRESS_TYPE_MUXED_CONTRACT: + // MuxedContract muxedContract; // }; // // =========================================================================== @@ -9411,6 +9429,7 @@ var types = XDR.config((xdr) => { ["scAddressTypeMuxedAccount", "muxedAccount"], ["scAddressTypeClaimableBalance", "claimableBalanceId"], ["scAddressTypeLiquidityPool", "liquidityPoolId"], + ["scAddressTypeMuxedContract", "muxedContract"], ], arms: { accountId: xdr.lookup("AccountId"), @@ -9418,6 +9437,7 @@ var types = XDR.config((xdr) => { muxedAccount: xdr.lookup("MuxedEd25519Account"), claimableBalanceId: xdr.lookup("ClaimableBalanceId"), liquidityPoolId: xdr.lookup("PoolId"), + muxedContract: xdr.lookup("MuxedContract"), }, }); diff --git a/src/base/generated/next.d.ts b/src/base/generated/next.d.ts index 8ffada0fe..5f0a19b92 100644 --- a/src/base/generated/next.d.ts +++ b/src/base/generated/next.d.ts @@ -2134,9 +2134,10 @@ export namespace xdr { | "scAddressTypeContract" | "scAddressTypeMuxedAccount" | "scAddressTypeClaimableBalance" - | "scAddressTypeLiquidityPool"; + | "scAddressTypeLiquidityPool" + | "scAddressTypeMuxedContract"; - readonly value: 0 | 1 | 2 | 3 | 4; + readonly value: 0 | 1 | 2 | 3 | 4 | 5; static scAddressTypeAccount(): ScAddressType; @@ -2147,6 +2148,8 @@ export namespace xdr { static scAddressTypeClaimableBalance(): ScAddressType; static scAddressTypeLiquidityPool(): ScAddressType; + + static scAddressTypeMuxedContract(): ScAddressType; } class ScEnvMetaKind { @@ -9508,6 +9511,34 @@ export namespace xdr { static validateXDR(input: string, format: "hex" | "base64"): boolean; } + class MuxedContract { + constructor(attributes: { id: Uint64; contractId: ContractId }); + + id(value?: Uint64): Uint64; + + contractId(value?: ContractId): ContractId; + + toXDR(format?: "raw"): Buffer; + + toXDR(format: "hex" | "base64"): string; + + static read(io: Buffer): MuxedContract; + + static write(value: MuxedContract, io: Buffer): void; + + static isValid(value: MuxedContract): boolean; + + static toXDR(value: MuxedContract): Buffer; + + static fromXDR(input: Buffer, format?: "raw"): MuxedContract; + + static fromXDR(input: string, format: "hex" | "base64"): MuxedContract; + + static validateXDR(input: Buffer, format?: "raw"): boolean; + + static validateXDR(input: string, format: "hex" | "base64"): boolean; + } + class ScNonceKey { constructor(attributes: { nonce: Int64 }); @@ -15661,6 +15692,8 @@ export namespace xdr { liquidityPoolId(value?: PoolId): PoolId; + muxedContract(value?: MuxedContract): MuxedContract; + static scAddressTypeAccount(value: AccountId): ScAddress; static scAddressTypeContract(value: ContractId): ScAddress; @@ -15671,12 +15704,15 @@ export namespace xdr { static scAddressTypeLiquidityPool(value: PoolId): ScAddress; + static scAddressTypeMuxedContract(value: MuxedContract): ScAddress; + value(): | AccountId | ContractId | MuxedEd25519Account | ClaimableBalanceId - | PoolId; + | PoolId + | MuxedContract; toXDR(format?: "raw"): Buffer; diff --git a/src/base/generated/next_generated.js b/src/base/generated/next_generated.js index 4db307e52..ff0944b9e 100644 --- a/src/base/generated/next_generated.js +++ b/src/base/generated/next_generated.js @@ -9353,7 +9353,8 @@ xdr.union("ContractExecutable", { // SC_ADDRESS_TYPE_CONTRACT = 1, // SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, // SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, -// SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 +// SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, +// SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 // }; // // =========================================================================== @@ -9363,6 +9364,7 @@ xdr.enum("ScAddressType", { scAddressTypeMuxedAccount: 2, scAddressTypeClaimableBalance: 3, scAddressTypeLiquidityPool: 4, + scAddressTypeMuxedContract: 5, }); // === xdr source ============================================================ @@ -9379,6 +9381,20 @@ xdr.struct("MuxedEd25519Account", [ ["ed25519", xdr.lookup("Uint256")], ]); +// === xdr source ============================================================ +// +// struct MuxedContract +// { +// uint64 id; +// ContractID contractId; +// }; +// +// =========================================================================== +xdr.struct("MuxedContract", [ + ["id", xdr.lookup("Uint64")], + ["contractId", xdr.lookup("ContractId")], +]); + // === xdr source ============================================================ // // union SCAddress switch (SCAddressType type) @@ -9393,6 +9409,8 @@ xdr.struct("MuxedEd25519Account", [ // ClaimableBalanceID claimableBalanceId; // case SC_ADDRESS_TYPE_LIQUIDITY_POOL: // PoolID liquidityPoolId; +// case SC_ADDRESS_TYPE_MUXED_CONTRACT: +// MuxedContract muxedContract; // }; // // =========================================================================== @@ -9405,6 +9423,7 @@ xdr.union("ScAddress", { ["scAddressTypeMuxedAccount", "muxedAccount"], ["scAddressTypeClaimableBalance", "claimableBalanceId"], ["scAddressTypeLiquidityPool", "liquidityPoolId"], + ["scAddressTypeMuxedContract", "muxedContract"], ], arms: { accountId: xdr.lookup("AccountId"), @@ -9412,6 +9431,7 @@ xdr.union("ScAddress", { muxedAccount: xdr.lookup("MuxedEd25519Account"), claimableBalanceId: xdr.lookup("ClaimableBalanceId"), liquidityPoolId: xdr.lookup("PoolId"), + muxedContract: xdr.lookup("MuxedContract"), }, }); diff --git a/test/unit/base/address.test.ts b/test/unit/base/address.test.ts index 8d5da5002..a3af3ff70 100644 --- a/test/unit/base/address.test.ts +++ b/test/unit/base/address.test.ts @@ -309,4 +309,80 @@ describe("Address", () => { ); }); }); + + describe("muxed-contract addresses (CAP-0084)", () => { + // 2^64 - 1: a uint64 well above Number.MAX_SAFE_INTEGER, used to prove the + // muxing id survives a fromScAddress -> toScAddress round-trip and the + // muxedId() accessor without precision loss. + const MUXED_CONTRACT_ID = "18446744073709551615"; + const CONTRACT_RAW = StrKey.decodeContract(CONTRACT); + + function muxedContractScAddress(id: string): xdr.ScAddress { + return xdr.ScAddress.scAddressTypeMuxedContract( + new xdr.MuxedContract({ + id: new xdr.Uint64(id), + contractId: CONTRACT_RAW as unknown as xdr.ContractId, + }), + ); + } + + it(".muxedContract factory exposes its components", () => { + const a = Address.muxedContract(CONTRACT_RAW, MUXED_CONTRACT_ID); + expect(a.contractId()).toEqual(CONTRACT_RAW); + expect(a.muxedId().toString()).toBe(MUXED_CONTRACT_ID); + expect(a.type).toBe("muxedContract"); + }); + + it("renders the display form :", () => { + const a = Address.muxedContract(CONTRACT_RAW, MUXED_CONTRACT_ID); + expect(a.toString()).toBe(`${CONTRACT}:${MUXED_CONTRACT_ID}`); + }); + + it("fromScAddress decodes the arm without precision loss", () => { + const sc = muxedContractScAddress(MUXED_CONTRACT_ID); + const a = Address.fromScAddress(sc); + expect(a.contractId()).toEqual(CONTRACT_RAW); + expect(a.muxedId().toString()).toBe(MUXED_CONTRACT_ID); + expect(a.toString()).toBe(`${CONTRACT}:${MUXED_CONTRACT_ID}`); + }); + + it("round-trips Address -> ScAddress byte-for-byte", () => { + const sc = muxedContractScAddress(MUXED_CONTRACT_ID); + const out = Address.fromScAddress(sc).toScAddress(); + expect(out.switch()).toBe( + xdr.ScAddressType.scAddressTypeMuxedContract(), + ); + expect(out.toXDR()).toEqual(sc.toXDR()); + expect(xdr.ScAddress.fromXDR(out.toXDR())).toEqual(sc); + }); + + it("round-trips through ScVal", () => { + const scVal = Address.muxedContract( + CONTRACT_RAW, + MUXED_CONTRACT_ID, + ).toScVal(); + const back = Address.fromScVal(scVal); + expect(back.toString()).toBe(`${CONTRACT}:${MUXED_CONTRACT_ID}`); + expect(back.muxedId().toString()).toBe(MUXED_CONTRACT_ID); + }); + + it("toBuffer throws (no canonical single-buffer encoding)", () => { + const a = Address.muxedContract(CONTRACT_RAW, MUXED_CONTRACT_ID); + expect(() => a.toBuffer()).toThrow( + /toBuffer is not supported for muxed-contract addresses/, + ); + }); + + it("constructor cannot parse the display string (no strkey yet)", () => { + expect( + () => new Address(`${CONTRACT}:${MUXED_CONTRACT_ID}`), + ).toThrow(/Unsupported address type/); + }); + + it("contractId/muxedId throw for non-muxed-contract addresses", () => { + const c = new Address(CONTRACT); + expect(() => c.muxedId()).toThrow(/only valid for muxed-contract/); + expect(() => c.contractId()).toThrow(/only valid for muxed-contract/); + }); + }); }); diff --git a/xdr/curr/Stellar-contract.x b/xdr/curr/Stellar-contract.x index 0e67dc3f3..4ec38c7b2 100644 --- a/xdr/curr/Stellar-contract.x +++ b/xdr/curr/Stellar-contract.x @@ -182,7 +182,8 @@ enum SCAddressType SC_ADDRESS_TYPE_CONTRACT = 1, SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 + SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, + SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 }; struct MuxedEd25519Account @@ -191,6 +192,12 @@ struct MuxedEd25519Account uint256 ed25519; }; +struct MuxedContract +{ + uint64 id; + ContractID contractId; +}; + union SCAddress switch (SCAddressType type) { case SC_ADDRESS_TYPE_ACCOUNT: @@ -203,6 +210,8 @@ case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: ClaimableBalanceID claimableBalanceId; case SC_ADDRESS_TYPE_LIQUIDITY_POOL: PoolID liquidityPoolId; +case SC_ADDRESS_TYPE_MUXED_CONTRACT: + MuxedContract muxedContract; }; %struct SCVal; diff --git a/xdr/next/Stellar-contract.x b/xdr/next/Stellar-contract.x index 0e67dc3f3..4ec38c7b2 100644 --- a/xdr/next/Stellar-contract.x +++ b/xdr/next/Stellar-contract.x @@ -182,7 +182,8 @@ enum SCAddressType SC_ADDRESS_TYPE_CONTRACT = 1, SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 + SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, + SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 }; struct MuxedEd25519Account @@ -191,6 +192,12 @@ struct MuxedEd25519Account uint256 ed25519; }; +struct MuxedContract +{ + uint64 id; + ContractID contractId; +}; + union SCAddress switch (SCAddressType type) { case SC_ADDRESS_TYPE_ACCOUNT: @@ -203,6 +210,8 @@ case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: ClaimableBalanceID claimableBalanceId; case SC_ADDRESS_TYPE_LIQUIDITY_POOL: PoolID liquidityPoolId; +case SC_ADDRESS_TYPE_MUXED_CONTRACT: + MuxedContract muxedContract; }; %struct SCVal; From cb85fe3e6c0dc8256bac6bc8104eded42163006d Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Sat, 27 Jun 2026 00:28:03 -0700 Subject: [PATCH 2/4] Gate CAP-0084 muxed contract arm to the `next` channel only The SC_ADDRESS_TYPE_MUXED_CONTRACT arm was leaking into the released `curr` codec. Per the feature-flag contract a gated CAP def must be `next`-only until the protocol is enabled. Mirrors the fix on stellar/js-stellar-base#980. - xdr/curr/Stellar-contract.x + src/base/generated/curr{,_generated}.{js,d.ts}: drop the muxed-contract arm (next-channel files keep it). - src/base/address.ts: guard the fromScAddress case label with optional chaining and reach the gated members via a cast so the curr-bound Address codec no longer fails to type-check or throws when the arm is absent (the write path stays dormant until the arm lands in curr). - test/unit/base/address.test.ts: codec round-trip cases run only when the active codec defines the arm; Address-level assertions stay codec-agnostic. --- src/base/address.ts | 15 ++++++---- src/base/generated/curr.d.ts | 42 ++-------------------------- src/base/generated/curr_generated.js | 22 +-------------- test/unit/base/address.test.ts | 29 ++++++++++++------- xdr/curr/Stellar-contract.x | 11 +------- 5 files changed, 34 insertions(+), 85 deletions(-) diff --git a/src/base/address.ts b/src/base/address.ts index 37e8178e5..bf850065c 100644 --- a/src/base/address.ts +++ b/src/base/address.ts @@ -170,8 +170,12 @@ export class Address { return Address.liquidityPool( scAddress.liquidityPoolId() as unknown as Buffer, ); - case xdr.ScAddressType.scAddressTypeMuxedContract().value: { - const muxed = scAddress.muxedContract(); + // CAP-0084 muxed contract addresses are gated to the `next` channel, so + // these members are absent from the curr-bound codec. Guard the case + // label with optional chaining (it resolves to `undefined` and never + // matches a real switch value) and reach the gated members via a cast. + case (xdr.ScAddressType as any).scAddressTypeMuxedContract?.()?.value: { + const muxed = (scAddress as any).muxedContract(); return Address.muxedContract( muxed.contractId() as unknown as Buffer, muxed.id(), @@ -248,10 +252,11 @@ export class Address { ); case "muxedContract": - return xdr.ScAddress.scAddressTypeMuxedContract( - new xdr.MuxedContract({ + // Gated to the `next` channel; unavailable on the curr-bound codec. + return (xdr.ScAddress as any).scAddressTypeMuxedContract( + new (xdr as any).MuxedContract({ id: this._muxId, - contractId: this._key as unknown as xdr.ContractId, + contractId: this._key, }), ); diff --git a/src/base/generated/curr.d.ts b/src/base/generated/curr.d.ts index 479126f48..8a1766485 100644 --- a/src/base/generated/curr.d.ts +++ b/src/base/generated/curr.d.ts @@ -2134,10 +2134,9 @@ export namespace xdr { | "scAddressTypeContract" | "scAddressTypeMuxedAccount" | "scAddressTypeClaimableBalance" - | "scAddressTypeLiquidityPool" - | "scAddressTypeMuxedContract"; + | "scAddressTypeLiquidityPool"; - readonly value: 0 | 1 | 2 | 3 | 4 | 5; + readonly value: 0 | 1 | 2 | 3 | 4; static scAddressTypeAccount(): ScAddressType; @@ -2148,8 +2147,6 @@ export namespace xdr { static scAddressTypeClaimableBalance(): ScAddressType; static scAddressTypeLiquidityPool(): ScAddressType; - - static scAddressTypeMuxedContract(): ScAddressType; } class ScEnvMetaKind { @@ -9511,34 +9508,6 @@ export namespace xdr { static validateXDR(input: string, format: "hex" | "base64"): boolean; } - class MuxedContract { - constructor(attributes: { id: Uint64; contractId: ContractId }); - - id(value?: Uint64): Uint64; - - contractId(value?: ContractId): ContractId; - - toXDR(format?: "raw"): Buffer; - - toXDR(format: "hex" | "base64"): string; - - static read(io: Buffer): MuxedContract; - - static write(value: MuxedContract, io: Buffer): void; - - static isValid(value: MuxedContract): boolean; - - static toXDR(value: MuxedContract): Buffer; - - static fromXDR(input: Buffer, format?: "raw"): MuxedContract; - - static fromXDR(input: string, format: "hex" | "base64"): MuxedContract; - - static validateXDR(input: Buffer, format?: "raw"): boolean; - - static validateXDR(input: string, format: "hex" | "base64"): boolean; - } - class ScNonceKey { constructor(attributes: { nonce: Int64 }); @@ -15692,8 +15661,6 @@ export namespace xdr { liquidityPoolId(value?: PoolId): PoolId; - muxedContract(value?: MuxedContract): MuxedContract; - static scAddressTypeAccount(value: AccountId): ScAddress; static scAddressTypeContract(value: ContractId): ScAddress; @@ -15704,15 +15671,12 @@ export namespace xdr { static scAddressTypeLiquidityPool(value: PoolId): ScAddress; - static scAddressTypeMuxedContract(value: MuxedContract): ScAddress; - value(): | AccountId | ContractId | MuxedEd25519Account | ClaimableBalanceId - | PoolId - | MuxedContract; + | PoolId; toXDR(format?: "raw"): Buffer; diff --git a/src/base/generated/curr_generated.js b/src/base/generated/curr_generated.js index fac75b7c1..5354907b9 100644 --- a/src/base/generated/curr_generated.js +++ b/src/base/generated/curr_generated.js @@ -9359,8 +9359,7 @@ var types = XDR.config((xdr) => { // SC_ADDRESS_TYPE_CONTRACT = 1, // SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, // SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - // SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, - // SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 + // SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 // }; // // =========================================================================== @@ -9370,7 +9369,6 @@ var types = XDR.config((xdr) => { scAddressTypeMuxedAccount: 2, scAddressTypeClaimableBalance: 3, scAddressTypeLiquidityPool: 4, - scAddressTypeMuxedContract: 5, }); // === xdr source ============================================================ @@ -9387,20 +9385,6 @@ var types = XDR.config((xdr) => { ["ed25519", xdr.lookup("Uint256")], ]); - // === xdr source ============================================================ - // - // struct MuxedContract - // { - // uint64 id; - // ContractID contractId; - // }; - // - // =========================================================================== - xdr.struct("MuxedContract", [ - ["id", xdr.lookup("Uint64")], - ["contractId", xdr.lookup("ContractId")], - ]); - // === xdr source ============================================================ // // union SCAddress switch (SCAddressType type) @@ -9415,8 +9399,6 @@ var types = XDR.config((xdr) => { // ClaimableBalanceID claimableBalanceId; // case SC_ADDRESS_TYPE_LIQUIDITY_POOL: // PoolID liquidityPoolId; - // case SC_ADDRESS_TYPE_MUXED_CONTRACT: - // MuxedContract muxedContract; // }; // // =========================================================================== @@ -9429,7 +9411,6 @@ var types = XDR.config((xdr) => { ["scAddressTypeMuxedAccount", "muxedAccount"], ["scAddressTypeClaimableBalance", "claimableBalanceId"], ["scAddressTypeLiquidityPool", "liquidityPoolId"], - ["scAddressTypeMuxedContract", "muxedContract"], ], arms: { accountId: xdr.lookup("AccountId"), @@ -9437,7 +9418,6 @@ var types = XDR.config((xdr) => { muxedAccount: xdr.lookup("MuxedEd25519Account"), claimableBalanceId: xdr.lookup("ClaimableBalanceId"), liquidityPoolId: xdr.lookup("PoolId"), - muxedContract: xdr.lookup("MuxedContract"), }, }); diff --git a/test/unit/base/address.test.ts b/test/unit/base/address.test.ts index a3af3ff70..2f10490a2 100644 --- a/test/unit/base/address.test.ts +++ b/test/unit/base/address.test.ts @@ -317,11 +317,20 @@ describe("Address", () => { const MUXED_CONTRACT_ID = "18446744073709551615"; const CONTRACT_RAW = StrKey.decodeContract(CONTRACT); + // CAP-0084's SC_ADDRESS_TYPE_MUXED_CONTRACT arm is gated to the `next` + // channel, so the curr-bound default codec cannot construct or encode it + // yet. Codec round-trip coverage runs only once the arm lands in the + // active codec; the Address-level assertions below are codec-agnostic. + const codecHasMuxedContract = + typeof (xdr.ScAddressType as any).scAddressTypeMuxedContract === + "function"; + const itCodec = codecHasMuxedContract ? it : it.skip; + function muxedContractScAddress(id: string): xdr.ScAddress { - return xdr.ScAddress.scAddressTypeMuxedContract( - new xdr.MuxedContract({ + return (xdr.ScAddress as any).scAddressTypeMuxedContract( + new (xdr as any).MuxedContract({ id: new xdr.Uint64(id), - contractId: CONTRACT_RAW as unknown as xdr.ContractId, + contractId: CONTRACT_RAW, }), ); } @@ -338,7 +347,7 @@ describe("Address", () => { expect(a.toString()).toBe(`${CONTRACT}:${MUXED_CONTRACT_ID}`); }); - it("fromScAddress decodes the arm without precision loss", () => { + itCodec("fromScAddress decodes the arm without precision loss", () => { const sc = muxedContractScAddress(MUXED_CONTRACT_ID); const a = Address.fromScAddress(sc); expect(a.contractId()).toEqual(CONTRACT_RAW); @@ -346,17 +355,17 @@ describe("Address", () => { expect(a.toString()).toBe(`${CONTRACT}:${MUXED_CONTRACT_ID}`); }); - it("round-trips Address -> ScAddress byte-for-byte", () => { + itCodec("round-trips Address -> ScAddress byte-for-byte", () => { const sc = muxedContractScAddress(MUXED_CONTRACT_ID); const out = Address.fromScAddress(sc).toScAddress(); expect(out.switch()).toBe( - xdr.ScAddressType.scAddressTypeMuxedContract(), + (xdr.ScAddressType as any).scAddressTypeMuxedContract(), ); expect(out.toXDR()).toEqual(sc.toXDR()); expect(xdr.ScAddress.fromXDR(out.toXDR())).toEqual(sc); }); - it("round-trips through ScVal", () => { + itCodec("round-trips through ScVal", () => { const scVal = Address.muxedContract( CONTRACT_RAW, MUXED_CONTRACT_ID, @@ -374,9 +383,9 @@ describe("Address", () => { }); it("constructor cannot parse the display string (no strkey yet)", () => { - expect( - () => new Address(`${CONTRACT}:${MUXED_CONTRACT_ID}`), - ).toThrow(/Unsupported address type/); + expect(() => new Address(`${CONTRACT}:${MUXED_CONTRACT_ID}`)).toThrow( + /Unsupported address type/, + ); }); it("contractId/muxedId throw for non-muxed-contract addresses", () => { diff --git a/xdr/curr/Stellar-contract.x b/xdr/curr/Stellar-contract.x index 4ec38c7b2..0e67dc3f3 100644 --- a/xdr/curr/Stellar-contract.x +++ b/xdr/curr/Stellar-contract.x @@ -182,8 +182,7 @@ enum SCAddressType SC_ADDRESS_TYPE_CONTRACT = 1, SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, - SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, - SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 + SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 }; struct MuxedEd25519Account @@ -192,12 +191,6 @@ struct MuxedEd25519Account uint256 ed25519; }; -struct MuxedContract -{ - uint64 id; - ContractID contractId; -}; - union SCAddress switch (SCAddressType type) { case SC_ADDRESS_TYPE_ACCOUNT: @@ -210,8 +203,6 @@ case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: ClaimableBalanceID claimableBalanceId; case SC_ADDRESS_TYPE_LIQUIDITY_POOL: PoolID liquidityPoolId; -case SC_ADDRESS_TYPE_MUXED_CONTRACT: - MuxedContract muxedContract; }; %struct SCVal; From b9c5748f3eac151e276513058d4028f2f52f9102 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Wed, 1 Jul 2026 13:07:12 -0700 Subject: [PATCH 3/4] Map xdr.Uint64 in typedoc externalSymbolLinkMappings Address.muxedContract's `id` param links to {@link xdr.Uint64}, which typedoc resolves but excludes from the docs (generated xdr files are excluded), tripping treatWarningsAsErrors. Add xdr.Uint64 to the external-symbol mapping like the other xdr.* symbols, and regenerate the reference docs for the new muxed-contract API. --- docs/reference/core-soroban-primitives.md | 90 +++++++++++++++++++---- typedoc.json | 3 +- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/docs/reference/core-soroban-primitives.md b/docs/reference/core-soroban-primitives.md index c9af82416..9be1faaec 100644 --- a/docs/reference/core-soroban-primitives.md +++ b/docs/reference/core-soroban-primitives.md @@ -18,7 +18,10 @@ class Address { static fromString(address: string): Address; static liquidityPool(buffer: Buffer): Address; static muxedAccount(buffer: Buffer): Address; + static muxedContract(contractId: Buffer, id: string | number | bigint | Uint64): Address; readonly type: AddressType; + contractId(): Buffer; + muxedId(): Uint64; toBuffer(): Buffer; toScAddress(): ScAddress; toScVal(): ScVal; @@ -26,7 +29,7 @@ class Address { } ``` -**Source:** [src/base/address.ts:20](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L20) +**Source:** [src/base/address.ts:25](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L25) ### `new Address(address)` @@ -38,7 +41,7 @@ constructor(address: string); - **`address`** — `string` (required) — a `StrKey` of the address value -**Source:** [src/base/address.ts:27](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L27) +**Source:** [src/base/address.ts:35](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L35) ### `Address.account(buffer)` @@ -52,7 +55,7 @@ static account(buffer: Buffer): Address; - **`buffer`** — `Buffer` (required) — The bytes of an address to parse. -**Source:** [src/base/address.ts:62](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L62) +**Source:** [src/base/address.ts:70](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L70) ### `Address.claimableBalance(buffer)` @@ -66,7 +69,7 @@ static claimableBalance(buffer: Buffer): Address; - **`buffer`** — `Buffer` (required) — The bytes of a claimable balance ID to parse. -**Source:** [src/base/address.ts:80](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L80) +**Source:** [src/base/address.ts:88](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L88) ### `Address.contract(buffer)` @@ -80,7 +83,7 @@ static contract(buffer: Buffer): Address; - **`buffer`** — `Buffer` (required) — The bytes of an address to parse. -**Source:** [src/base/address.ts:71](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L71) +**Source:** [src/base/address.ts:79](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L79) ### `Address.fromScAddress(scAddress)` @@ -94,7 +97,7 @@ static fromScAddress(scAddress: ScAddress): Address; - **`scAddress`** — `ScAddress` (required) — The xdr.ScAddress type to parse -**Source:** [src/base/address.ts:116](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L116) +**Source:** [src/base/address.ts:150](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L150) ### `Address.fromScVal(scVal)` @@ -108,7 +111,7 @@ static fromScVal(scVal: ScVal): Address; - **`scVal`** — `ScVal` (required) — The xdr.ScVal type to parse -**Source:** [src/base/address.ts:107](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L107) +**Source:** [src/base/address.ts:141](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L141) ### `Address.fromString(address)` @@ -122,7 +125,7 @@ static fromString(address: string): Address; - **`address`** — `string` (required) — The address to parse. ex. `GB3KJPLFUYN5VL6R3GU3EGCGVCKFDSD7BEDX42HWG5BWFKB3KQGJJRMA` -**Source:** [src/base/address.ts:53](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L53) +**Source:** [src/base/address.ts:61](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L61) ### `Address.liquidityPool(buffer)` @@ -136,7 +139,7 @@ static liquidityPool(buffer: Buffer): Address; - **`buffer`** — `Buffer` (required) — The bytes of an LP ID to parse. -**Source:** [src/base/address.ts:89](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L89) +**Source:** [src/base/address.ts:97](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L97) ### `Address.muxedAccount(buffer)` @@ -150,7 +153,31 @@ static muxedAccount(buffer: Buffer): Address; - **`buffer`** — `Buffer` (required) — The bytes of an address to parse. -**Source:** [src/base/address.ts:98](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L98) +**Source:** [src/base/address.ts:106](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L106) + +### `Address.muxedContract(contractId, id)` + +Creates a new muxed-contract Address object (CAP-0084). + +A muxed-contract address (`SC_ADDRESS_TYPE_MUXED_CONTRACT`) pairs a +32-byte contract ID with a `uint64` multiplexing ID. There is no canonical +StrKey form for it yet, so unlike the other factories it does not route +through the `Address` constructor and the resulting address cannot be +parsed back out of a string. Round-trip it through +`Address.fromScAddress` / `Address#toScAddress` instead; +`Address#toString` renders the display-only form `:`. + +```ts +static muxedContract(contractId: Buffer, id: string | number | bigint | Uint64): Address; +``` + +**Parameters** + +- **`contractId`** — `Buffer` (required) — the raw 32 bytes of the contract ID +- **`id`** — `string | number | bigint | Uint64` (required) — the uint64 multiplexing ID; pass a string or `xdr.Uint64` + for values above `Number.MAX_SAFE_INTEGER` to avoid precision loss + +**Source:** [src/base/address.ts:125](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L125) ### `address.type` @@ -160,7 +187,35 @@ Return the type of this address. readonly type: AddressType; ``` -**Source:** [src/base/address.ts:219](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L219) +**Source:** [src/base/address.ts:310](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L310) + +### `address.contractId()` + +For a muxed-contract address, returns the raw 32-byte contract ID. + +```ts +contractId(): Buffer; +``` + +**Throws** + +- if this is not a muxed-contract address + +**Source:** [src/base/address.ts:286](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L286) + +### `address.muxedId()` + +For a muxed-contract address, returns the `uint64` multiplexing ID. + +```ts +muxedId(): Uint64; +``` + +**Throws** + +- if this is not a muxed-contract address + +**Source:** [src/base/address.ts:300](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L300) ### `address.toBuffer()` @@ -170,7 +225,12 @@ Return the raw public key bytes for this address. toBuffer(): Buffer; ``` -**Source:** [src/base/address.ts:212](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L212) +**Throws** + +- for muxed-contract addresses, which have no single-buffer encoding + (use `Address#contractId` / `Address#muxedId`) + +**Source:** [src/base/address.ts:274](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L274) ### `address.toScAddress()` @@ -180,7 +240,7 @@ Convert this Address to an xdr.ScAddress type. toScAddress(): ScAddress; ``` -**Source:** [src/base/address.ts:174](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L174) +**Source:** [src/base/address.ts:224](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L224) ### `address.toScVal()` @@ -190,7 +250,7 @@ Convert this Address to an xdr.ScVal type. toScVal(): ScVal; ``` -**Source:** [src/base/address.ts:167](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L167) +**Source:** [src/base/address.ts:217](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L217) ### `address.toString()` @@ -200,7 +260,7 @@ Serialize an address to string. toString(): string; ``` -**Source:** [src/base/address.ts:147](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L147) +**Source:** [src/base/address.ts:192](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/address.ts#L192) ## Contract diff --git a/typedoc.json b/typedoc.json index 64686f956..1daa11a56 100644 --- a/typedoc.json +++ b/typedoc.json @@ -46,7 +46,8 @@ "xdr.SorobanAuthorizedInvocation": "#", "xdr.SorobanResources": "#", "xdr.SorobanTransactionData": "#", - "xdr.TransactionResult": "#" + "xdr.TransactionResult": "#", + "xdr.Uint64": "#" } }, "treatWarningsAsErrors": true, From ca219ecc5a049323db47b90950a1790d9980d6a0 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Thu, 2 Jul 2026 21:05:35 -0700 Subject: [PATCH 4/4] Re-pin XDR to stellar-xdr#307 head and split curr/next feature gates The Makefile pinned 68fa1ac5, which predates the CAP-0084 XDR commit, and left XDR_FEATURES undefined, so `make generate` could not reproduce the committed generated codec. Pin XDR_BASE_URL_{CURR,NEXT} to stellar-xdr@787382ef (CAP-0084 muxed-contract arm) and split into XDR_FEATURES_CURR=CAP_0083 / XDR_FEATURES_NEXT=CAP_0083,CAP_0084_MUXED_CONTRACT so curr excludes and next includes the muxed-contract arm, mirroring stellar/js-stellar-base#981. Drop `docker run -it` for non-TTY/CI regen and wire the xdrgen#152 const-inlining post-process (scripts/post-process-generated.py) for curr and next. Regen deferred: Docker is unavailable in this environment, so `make generate` was not re-run. The Makefile is now reproducible against 787382ef and the const post-process is wired but not yet executed; the committed src/base/generated/* (correct CAP-0084 arm content, CI green) are unchanged. --- Makefile | 25 +++++++++----- scripts/post-process-generated.py | 54 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 scripts/post-process-generated.py diff --git a/Makefile b/Makefile index 68f207223..5149a6e54 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,12 @@ -XDR_BASE_URL_CURR=https://github.com/stellar/stellar-xdr/raw/68fa1ac55692f68ad2a2ca549d0a283273554439 +# CAP-83 + CAP-84 pre-release: pin to the protocol-28 .x and resolve +# feature gates via `stellar-xdr xfile preprocess` (rs-stellar-xdr #503) before +# xdrgen, since the Ruby xdrgen used here does not understand #ifdef. +# CAP_0084_MUXED_CONTRACT is gated to the `next` channel only: the muxed +# contract address arm is not enabled on `curr` until protocol 28 ships. +XDR_BASE_URL_CURR=https://github.com/stellar/stellar-xdr/raw/787382ef2099cca168ca1cb282730d6b7b9e2f16 XDR_BASE_LOCAL_CURR=xdr/curr +XDR_FEATURES_CURR=CAP_0083 +XDR_FEATURES_NEXT=CAP_0083,CAP_0084_MUXED_CONTRACT XDR_FILES_CURR= \ Stellar-SCP.x \ Stellar-ledger-entries.x \ @@ -15,7 +22,7 @@ XDR_FILES_CURR= \ Stellar-exporter.x XDR_FILES_LOCAL_CURR=$(addprefix xdr/curr/,$(XDR_FILES_CURR)) -XDR_BASE_URL_NEXT=https://github.com/stellar/stellar-xdr/raw/68fa1ac55692f68ad2a2ca549d0a283273554439 +XDR_BASE_URL_NEXT=https://github.com/stellar/stellar-xdr/raw/787382ef2099cca168ca1cb282730d6b7b9e2f16 XDR_BASE_LOCAL_NEXT=xdr/next XDR_FILES_NEXT= \ Stellar-SCP.x \ @@ -43,23 +50,25 @@ generate: src/base/generated/curr_generated.js src/base/generated/curr.d.ts src/ src/base/generated/curr_generated.js: $(XDR_FILES_LOCAL_CURR) mkdir -p $(dir $@) > $@ - docker run -it --rm -v $$PWD:/wd -w /wd ruby:3.1 /bin/bash -c '\ + docker run --rm -v $$PWD:/wd -w /wd ruby:3.1 /bin/bash -c '\ gem install specific_install -v 0.3.8 && \ gem specific_install https://github.com/stellar/xdrgen.git -b $(XDRGEN_COMMIT) && \ xdrgen --language javascript --namespace curr --output src/base/generated $^ \ ' + python3 scripts/post-process-generated.py $@ src/base/generated/next_generated.js: $(XDR_FILES_LOCAL_NEXT) mkdir -p $(dir $@) > $@ - docker run -it --rm -v $$PWD:/wd -w /wd ruby:3.1 /bin/bash -c '\ + docker run --rm -v $$PWD:/wd -w /wd ruby:3.1 /bin/bash -c '\ gem install specific_install -v 0.3.8 && \ gem specific_install https://github.com/stellar/xdrgen.git -b $(XDRGEN_COMMIT) && \ xdrgen --language javascript --namespace next --output src/base/generated $^ \ ' + python3 scripts/post-process-generated.py $@ src/base/generated/curr.d.ts: src/base/generated/curr_generated.js - docker run -it --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:22-alpine -c '\ + docker run --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:22-alpine -c '\ apk add --update git && \ corepack enable && \ corepack prepare pnpm@$(PNPM_VERSION) --activate && \ @@ -73,7 +82,7 @@ src/base/generated/curr.d.ts: src/base/generated/curr_generated.js ' src/base/generated/next.d.ts: src/base/generated/next_generated.js - docker run -it --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:22-alpine -c '\ + docker run --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:22-alpine -c '\ apk add --update git && \ corepack enable && \ corepack prepare pnpm@$(PNPM_VERSION) --activate && \ @@ -92,12 +101,12 @@ clean: $(XDR_FILES_LOCAL_CURR): mkdir -p $(dir $@) curl -L -o $@ $(XDR_BASE_URL_CURR)/$(notdir $@) - stellar-xdr xfile preprocess --features "$(XDR_FEATURES)" $@ > $@.pp && mv -f $@.pp $@ + stellar-xdr xfile preprocess --features "$(XDR_FEATURES_CURR)" $@ > $@.pp && mv -f $@.pp $@ $(XDR_FILES_LOCAL_NEXT): mkdir -p $(dir $@) curl -L -o $@ $(XDR_BASE_URL_NEXT)/$(notdir $@) - stellar-xdr xfile preprocess --features "$(XDR_FEATURES)" $@ > $@.pp && mv -f $@.pp $@ + stellar-xdr xfile preprocess --features "$(XDR_FEATURES_NEXT)" $@ > $@.pp && mv -f $@.pp $@ reset-xdr: rm -f xdr/*/*.x rm -f src/base/generated/*.js diff --git a/scripts/post-process-generated.py b/scripts/post-process-generated.py new file mode 100644 index 000000000..8c245c62b --- /dev/null +++ b/scripts/post-process-generated.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Post-process xdrgen JS output to inline xdr.const values at usage sites. + +xdrgen master emits `xdr.const("NAME", N);` to register a constant in the +xdr namespace, then uses the bare identifier later (`xdr.string(NAME)`). +But `@stellar/js-xdr`'s TypeBuilder.const() does not put NAME into JS scope, +so the bare identifier ReferenceErrors at runtime. + +Injecting `var NAME = N;` at the IIFE top fixes runtime but gets DCE'd by +terser in the production browser dist. The robust fix is to inline the +literal at each usage site so there's no identifier for terser to drop. + +The `xdr.const("NAME", N);` declaration itself is left untouched: the NAME +there is a string literal (preceded by a quote), so the negative lookbehind +below skips it. Constants only referenced via `xdr.lookup("NAME")` string +lookups are likewise untouched. +""" +import re +import pathlib +import sys + + +def inline_consts(path: pathlib.Path) -> int: + s = path.read_text() + consts = dict(re.findall( + r'xdr\.const\("([A-Z][A-Z0-9_]+)",\s*(0x[0-9a-fA-F]+|\d+)\);', s + )) + n_replaced = 0 + for name, value in consts.items(): + # Replace bare identifier (not preceded by quote or word char, + # not followed by quote or word char). This skips string literals + # like "NAME" and xdr.lookup("NAME"), so the xdr.const(...) + # declaration's string name is preserved. + new_s, count = re.subn( + rf'(? 0: + s = new_s + n_replaced += count + path.write_text(s) + return n_replaced + + +if __name__ == "__main__": + files = sys.argv[1:] or [ + "src/base/generated/curr_generated.js", + "src/base/generated/next_generated.js", + ] + for f in files: + p = pathlib.Path(f) + n = inline_consts(p) + print(f"{f}: inlined {n} bare-identifier const reference(s)")