Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
.vscode/
.copilot/
tsconfig.tsbuildinfo
*.tgz
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Added
* XDR definitions updated for Protocol 28 / CAP-0084 (muxed contract addresses): new `SC_ADDRESS_TYPE_MUXED_CONTRACT` arm on `ScAddress` and `MuxedContract` struct, gated behind the `CAP_0084_MUXED_CONTRACT` feature in the regen. `Address` now decodes/encodes the muxed-contract arm (`Address.muxedContract(contractId, id)`, `Address.fromScAddress`/`toScAddress`) with `contractId()` / `muxedId()` accessors, and renders it as `<C-strkey>:<id>` (display only — there is no canonical strkey yet, so the string is not parsable back via the `Address` constructor, and `toBuffer()` is unsupported for this arm).

## [`v15.0.0`](https://github.com/stellar/js-stellar-base/compare/v14.1.0...v15.0.0): Protocol 26

**[Migration Guide](docs/migration-guide/v15.md)** — step-by-step upgrade instructions with code examples and severity ratings.
Expand Down
27 changes: 19 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
XDR_BASE_URL_CURR=https://github.com/stellar/stellar-xdr/raw/cff714a5ebaaaf2dac343b3546c2df73f0b7a36e
# 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/7b5618146590e15d2e250538dccbc7c89ac55c58
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 \
Expand All @@ -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/cff714a5ebaaaf2dac343b3546c2df73f0b7a36e
XDR_BASE_URL_NEXT=https://github.com/stellar/stellar-xdr/raw/7b5618146590e15d2e250538dccbc7c89ac55c58
XDR_BASE_LOCAL_NEXT=xdr/next
XDR_FILES_NEXT= \
Stellar-SCP.x \
Expand All @@ -42,24 +49,26 @@ generate: src/generated/curr_generated.js types/curr.d.ts src/generated/next_gen
src/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/generated $^ \
'
python3 scripts/post-process-generated.py $@

src/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/generated $^ \
'
python3 scripts/post-process-generated.py $@

types/curr.d.ts: src/generated/curr_generated.js
docker run -it --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:alpine -c '\
apk add --update git && \
docker run --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:lts-alpine -c '\
apk add --update git && apk add --update yarn && \
git clone --depth 1 https://github.com/stellar/dts-xdr -b $(DTSXDR_COMMIT) --single-branch && \
cd /dts-xdr && \
yarn install --network-concurrency 1 && \
Expand All @@ -69,8 +78,8 @@ types/curr.d.ts: src/generated/curr_generated.js
'

types/next.d.ts: src/generated/next_generated.js
docker run -it --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:alpine -c '\
apk add --update git && \
docker run --rm -v $$PWD:/wd -w / --entrypoint /bin/sh node:lts-alpine -c '\
apk add --update git && apk add --update yarn && \
git clone --depth 1 https://github.com/stellar/dts-xdr -b $(DTSXDR_COMMIT) --single-branch && \
cd /dts-xdr && \
yarn install --network-concurrency 1 && \
Expand All @@ -85,10 +94,12 @@ clean:
$(XDR_FILES_LOCAL_CURR):
mkdir -p $(dir $@)
curl -L -o $@ $(XDR_BASE_URL_CURR)/$(notdir $@)
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_NEXT)" $@ > $@.pp && mv -f $@.pp $@

reset-xdr:
rm -f xdr/*/*.x
Expand Down
54 changes: 54 additions & 0 deletions scripts/post-process-generated.py
Original file line number Diff line number Diff line change
@@ -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'(?<![\w"\'$]){re.escape(name)}(?![\w"\'$])',
value,
s,
)
if count > 0:
s = new_s
n_replaced += count
path.write_text(s)
return n_replaced


if __name__ == "__main__":
files = sys.argv[1:] or [
"src/generated/curr_generated.js",
"src/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)")
89 changes: 86 additions & 3 deletions src/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import xdr from './xdr';
*
* `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}.
*
* @constructor
*
Expand Down Expand Up @@ -96,6 +100,31 @@ 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 `<C-strkey>:<id>`.
*
* @param {Buffer} contractId - the raw 32 bytes of the contract ID
* @param {number|bigint|string|xdr.Uint64} id - the uint64 multiplexing ID;
* pass a string or {@link xdr.Uint64} for values above
* `Number.MAX_SAFE_INTEGER` to avoid precision loss
* @returns {Address}
*/
static muxedContract(contractId, id) {
const 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.
*
Expand Down Expand Up @@ -133,6 +162,14 @@ export class Address {
}
case xdr.ScAddressType.scAddressTypeLiquidityPool().value:
return Address.liquidityPool(scAddress.liquidityPoolId());
// CAP-0084 muxed contract addresses are gated to the `next` channel:
// `scAddressTypeMuxedContract` is absent from the `curr` codec, so guard
// the case label with optional chaining (it resolves to `undefined` and
// never matches a real switch value when the arm is not defined).
case xdr.ScAddressType.scAddressTypeMuxedContract?.()?.value: {
const muxed = scAddress.muxedContract();
return Address.muxedContract(muxed.contractId(), muxed.id());
}
default:
throw new Error(`Unsupported address type: ${scAddress.switch().name}`);
}
Expand All @@ -155,6 +192,11 @@ export class Address {
return StrKey.encodeLiquidityPool(this._key);
case 'muxedAccount':
return StrKey.encodeMed25519PublicKey(this._key);
case 'muxedContract':
// Display-only form `<C-strkey>:<id>`. 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');
}
Expand Down Expand Up @@ -201,6 +243,14 @@ export class Address {
})
);

case 'muxedContract':
return xdr.ScAddress.scAddressTypeMuxedContract(
new xdr.MuxedContract({
id: this._muxId,
contractId: this._key
})
);

default:
throw new Error(`Unsupported address type: ${this._type}`);
}
Expand All @@ -210,8 +260,41 @@ export class Address {
* Return the raw public key bytes for this address.
*
* @returns {Buffer}
* @throws {Error} for muxed-contract addresses, which have no single-buffer
* encoding (use {@link Address#contractId} / {@link Address#muxedId})
*/
toBuffer() {
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.
*
* @returns {Buffer}
* @throws {Error} if this is not a muxed-contract address
*/
contractId() {
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.
*
* @returns {xdr.Uint64}
* @throws {Error} if this is not a muxed-contract address
*/
muxedId() {
if (this._type !== 'muxedContract') {
throw new Error('muxedId() is only valid for muxed-contract addresses');
}
return this._muxId;
}
}
Loading
Loading