Skip to content

Commit 9df0fd2

Browse files
authored
fix(xdr): emit uint256 as a Uint256Bytes class, not inline bytes (#1651)
* fix(xdr): emit uint256 as a Uint256Bytes class, not inline bytes
1 parent 623bed0 commit 9df0fd2

40 files changed

Lines changed: 441 additions & 258 deletions

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ A breaking change will get clearly marked in this log.
3030
* **Start here: [`docs/XDR_MIGRATION.md`](./docs/XDR_MIGRATION.md) covers every change below with before/after examples and a quick-reference table.**
3131
* Unions are discriminated classes. `.switch()` becomes a `.type` string literal, arm getters like `.contractData()` become properties, and `new xdr.LedgerEntryData(disc, val)` becomes a factory call such as `xdr.LedgerEntryData.contractData(val)`.
3232
* Enums are singletons, not factory calls: `xdr.ContractDataDurability.persistent()` becomes `xdr.ContractDataDurability.persistent`.
33-
* Primitives are plain JS values. Integers are `number` or `bigint` instead of class wrappers, `LargeInt` subclasses are gone, byte fields are `Uint8Array`, and fields are `readonly`.
33+
* Primitives are plain JS values. Integers are `number` or `bigint` instead of class wrappers, anonymous `opaque` fields are `Uint8Array`, `LargeInt` subclasses are gone, and fields are `readonly`.
34+
* Named byte aliases (`Hash`, `Signature`, `AssetCode4`, `PoolId`, `ContractId`, …) are classes wrapping the bytes, not bare `Uint8Array`. They take raw bytes or a string on the way in and validate length at construction; read the bytes back with `.toBytes()`. The string form is hex, except for `AssetCode4` / `AssetCode12`, which take the asset code as ASCII text and zero-pad it (`new xdr.AssetCode4("USD")`). This includes `uint256`, whose class is named `Uint256Bytes` because `xdr.Uint256` is the bigint wrapper over `Uint256Parts` — it covers the ed25519 keys, salts, and nonces on `PublicKey` (and its alias `AccountId`), `SignerKey`, `MuxedAccount`, `MuxedAccountMed25519`, `MuxedEd25519Account`, `TransactionV0`, `SignerKeyEd25519SignedPayload`, `ContractIdPreimageFromAddress`, `ClaimOfferAtomV0`, and the `Hello`, `DontHave`, and `StellarMessage` overlay messages.
3435
* Absent optional fields decode to `null` instead of `undefined`, so `=== undefined` checks silently stop matching. Prefer `== null`.
3536
* Acronyms in method names collapse to single-initial-cap form, with no back-compat aliases (e.g. `validateXDR()` is now `validateXdr()`). This reaches beyond the `xdr` namespace to the wrapper classes: `Transaction.toXDR()`, `TransactionBuilder.fromXDR()`, `Operation.fromXDRObject()`, `Asset.toXDRObject()`, `contract.AssembledTransaction.toXDR()` and others all gained the `Xdr` spelling.
3637
* Struct field names are unchanged, but a few type names moved: `UInt128Parts` / `UInt256Parts` are now `Uint128Parts` / `Uint256Parts`, `ThresholdIndices` is now `ThresholdIndexes`, and the typedef aliases `Duration`, `TimePoint`, `SequenceNumber`, `ScVec`, `ScMap`, `LedgerEntryChanges`, `ContractCostParams`, `SorobanAuthorizationEntries`, `ScString`, `ScSymbol`, `String32`, `String64`, and `SponsorshipDescriptor` are gone in favor of what they stood for.

docs/XDR_MIGRATION.md

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,15 @@ underneath it, so inherited members changed for both classes:
320320

321321
---
322322

323-
## 6. Bytes: `Uint8Array` everywhere
323+
## 6. Bytes: `Uint8Array`, not `Buffer`
324324

325-
Every fixed-length and variable-length **byte** field (`opaque[N]`, `opaque<N>`,
326-
`Hash`, `Signature`, `ScBytes`, …) is a `Uint8Array`. The SDK used to surface
327-
`Buffer` in many places; now it's `Uint8Array`. `Buffer` **is** a `Uint8Array`
328-
subclass so most code that just reads bytes (indexing, `.length`) keeps working.
329-
The differences appear when:
325+
Byte fields no longer surface `Buffer`. An anonymous `opaque[N]` / `opaque<N>`
326+
field is a plain `Uint8Array`; a **named** byte alias (`Hash`, `Signature`,
327+
`ScBytes`, `AssetCode4`, `PoolId`, `Uint256Bytes`, …) is a small class wrapping
328+
one, and `.toBytes()` gives you the `Uint8Array` — see § 6.1. Either way the
329+
underlying bytes are a `Uint8Array` where they used to be a `Buffer`.
330+
`Buffer` **is** a `Uint8Array` subclass so most code that just reads bytes
331+
(indexing, `.length`) keeps working. The differences appear when:
330332

331333
(**Note:** XDR _string_ fields are a separate story; see § 11.)
332334

@@ -355,35 +357,69 @@ xdr.ScVal.scvBytes(new xdr.ScBytes(new Uint8Array([1, 2, 3])));
355357
new xdr.LedgerKeyContractCode({ hash: someBytes });
356358
```
357359

358-
The one place a specific class **is** required is the typedef-opaque aliases;
359-
see § 6.1.
360+
The one place a specific class **is** required is the named byte aliases; see
361+
§ 6.1.
360362

361363
Byte-class constructors also accept hex strings as a convenience, so
362364
`new xdr.Hash("aabbcc…")` works the same as passing 32 bytes.
363365

364-
### 6.1 Typedef-opaque aliases became distinct classes
366+
### 6.1 Named byte aliases are classes
365367

366-
`PoolId`, `ContractId`, and similar typedef-aliases-of-`Hash` used to be plain
367-
re-exports (`export const PoolId = Hash`). They now emit as their own
368-
`BytesValue<"PoolId">` / `BytesValue<"ContractId">` subclasses with distinct
369-
named schemas. Byte semantics are identical, but class identity isn't.
368+
Every `typedef opaque` in the schema emits its own `BytesValue` subclass with a
369+
distinct named schema. Writing is forgiving — a constructor or factory takes raw
370+
bytes, a string, or the wrapper itself — but **reading gives you the wrapper**,
371+
so unwrap with `.toBytes()`:
370372

371373
```ts
372-
// Before — PoolId === Hash at runtime
373-
new xdr.Hash(bytes) instanceof xdr.Hash; // true
374-
xdr.ScAddress.scAddressTypeContract(new xdr.Hash(bytes)); // worked
374+
// Writing — all three work
375+
xdr.PublicKey.publicKeyTypeEd25519(rawBytes);
376+
xdr.PublicKey.publicKeyTypeEd25519("3f0c34bf…");
377+
xdr.PublicKey.publicKeyTypeEd25519(new xdr.Uint256Bytes(rawBytes));
375378

376-
// After
377-
new xdr.PoolId(bytes) instanceof xdr.Hash; // false — distinct class
378-
xdr.ScAddress.scAddressTypeContract(new xdr.ContractId(bytes)); // required
379-
xdr.ScAddress.scAddressTypeLiquidityPool(new xdr.PoolId(bytes));
379+
// Reading — the wrapper needs unwrapping
380+
StrKey.encodeEd25519PublicKey(key.ed25519.toBytes());
380381
```
381382

382-
This is what lets JSON output (§ 12) render a `PoolId` as an `L`-strkey and a
383-
`ContractId` as a `C`-strkey while a plain `Hash` stays hex.
384-
385-
Note that this break is type-level: at runtime a `Hash` still encodes to the
386-
same 32 bytes, so plain JavaScript callers see no error.
383+
The string form is hex, except for `AssetCode4` / `AssetCode12`, which take the
384+
code as ASCII and zero-pad it (`new xdr.AssetCode4("USD")`). Length is checked
385+
at construction rather than at encode time, so a wrong-sized array throws where
386+
you built it.
387+
388+
Here is the full set, and the types whose fields hand you one. `.toBytes()` is
389+
what you need at every read site below:
390+
391+
| Class | Width | Read it from |
392+
| --- | --- | --- |
393+
| `Hash` | 32 | pervasive — ledger headers, SCP statements, `ContractExecutable`, `ContractCodeEntry`, `TtlEntry`, `LedgerKeyContractCode`, `TransactionResultPair`, `HashIdPreimage`, and ~25 more |
394+
| `Uint256Bytes` | 32 | `MuxedAccount`, `MuxedAccountMed25519`, `MuxedEd25519Account`, `PublicKey` (and its alias `AccountId`), `SignerKey`, `SignerKeyEd25519SignedPayload`, `TransactionV0`, `ContractIdPreimageFromAddress`, `ClaimOfferAtomV0`, `Hello`, `DontHave`, `StellarMessage` |
395+
| `ContractId` | 32 | `ScAddress`, `ContractEvent`, `ConfigUpgradeSetKey` |
396+
| `PoolId` | 32 | `ScAddress`, `TrustLineAsset`, `LiquidityPoolEntry`, `LedgerKeyLiquidityPool`, `LiquidityPoolDepositOp`, `LiquidityPoolWithdrawOp`, `HashIdPreimageRevokeId`, `ClaimLiquidityAtom` |
397+
| `Signature` | ≤64 | `DecoratedSignature`, `ScpEnvelope`, `AuthCert`, `LedgerCloseValueSignature`, the signed survey messages |
398+
| `SignatureHint` | 4 | `DecoratedSignature` |
399+
| `ScBytes` | unbounded | `ScVal` |
400+
| `AssetCode4` / `AssetCode12` | 4 / 12 | `AssetCode`, `AlphaNum4`, `AlphaNum12` |
401+
| `Thresholds` | 4 | `AccountEntry` |
402+
| `DataValue` | ≤64 | `DataEntry`, `ManageDataOp` |
403+
| `Value` | unbounded | `ScpBallot`, `ScpNomination` |
404+
| `UpgradeType` | ≤128 | `StellarValue` |
405+
| `EncodedLedgerKey` | unbounded | `FrozenLedgerKeys`, `FrozenLedgerKeysDelta` |
406+
| `EncryptedBody` | ≤64000 | `SurveyResponseMessage` |
407+
408+
Two of these need extra care:
409+
410+
- **`PoolId` and `ContractId`** used to be plain re-exports of `Hash`
411+
(`export const PoolId = Hash`). They are now distinct classes, so
412+
`new xdr.PoolId(bytes) instanceof xdr.Hash` is `false` and
413+
`xdr.ScAddress.scAddressTypeContract(new xdr.Hash(bytes))` no longer
414+
typechecks — pass `new xdr.ContractId(bytes)`. This is what lets JSON output
415+
(§ 12) render a `PoolId` as an `L`-strkey and a `ContractId` as a `C`-strkey
416+
while a plain `Hash` stays hex. The break is type-level: at runtime all three
417+
still encode to the same 32 bytes, so plain JavaScript callers see no error.
418+
- **`Uint256Bytes`** wraps `typedef opaque uint256[32]`. It carries the `Bytes`
419+
suffix because `xdr.Uint256` is the bigint wrapper over `Uint256Parts` — a
420+
different type with a confusingly similar name. Watch the `.value` getter on
421+
the single-arm `PublicKey` / `AccountId` union, which returns the wrapper too:
422+
`accountId.value` becomes `accountId.value.toBytes()`.
387423

388424
---
389425

@@ -544,6 +580,9 @@ scvBytes(buf) → (unchanged; raw bytes still accepted)
544580
new xdr.Hash(buf) → (still works; also accepts hex strings)
545581
new xdr.Hash(bytes) for PoolId/ContractIduse new xdr.PoolId(bytes) /
546582
new xdr.ContractId(bytes)
583+
someHashsomeHash.toBytes() // reading a named alias
584+
key.ed25519, preimage.saltkey.ed25519.toBytes(), preimage.salt.toBytes()
585+
// uint256 fields are xdr.Uint256Bytes now
547586

548587
// ============== STRINGS ==============
549588
memo.textmemo.text.toString() or memo.text.bytes

0 commit comments

Comments
 (0)