diff --git a/CHANGELOG.md b/CHANGELOG.md index 72068e3bc..8c03fed56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## [`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. + ### Breaking Changes * `TransactionBase.networkPassphrase` setter now throws an error to enforce immutability ([#891](https://github.com/stellar/js-stellar-base/pull/891)). diff --git a/docs/migration-guide/v15.md b/docs/migration-guide/v15.md new file mode 100644 index 000000000..315e37bca --- /dev/null +++ b/docs/migration-guide/v15.md @@ -0,0 +1,362 @@ +# Migrating to @stellar/stellar-base v15 (Protocol 26) + +This guide walks you through upgrading from `@stellar/stellar-base` v14.x to v15.x. It covers every change that requires action, with before/after code examples and severity ratings. + +For the full list of changes, see the [CHANGELOG](../../CHANGELOG.md). + +--- + +## Table of Contents + +- [Who Needs to Migrate](#who-needs-to-migrate) +- [Prerequisites](#prerequisites) +- [Step 1: Update Dependencies](#step-1-update-dependencies) +- [Step 2: Fix Breaking Changes](#step-2-fix-breaking-changes) + - [2a. Immutable `networkPassphrase` (Critical)](#2a-immutable-networkpassphrase) + - [2b. XDR Integer Overflow Now Throws (Critical)](#2b-xdr-integer-overflow-now-throws) + - [2c. Hermes Typed-Array Polyfill Removed (Critical — React Native only)](#2c-hermes-typed-array-polyfill-removed) +- [Step 3: Handle Behavioral Fixes That May Affect You](#step-3-handle-behavioral-fixes-that-may-affect-you) + - [3a. `Memo.id` Rejects Invalid Values (Critical)](#3a-memoid-rejects-invalid-values) + - [3b. `Soroban.parseTokenAmount` Rejects Excess Decimals (Critical)](#3b-sorobanparsetokenamount-rejects-excess-decimals) + - [3c. `Keypair.verify` Returns `false` Instead of Throwing (Behavioral)](#3c-keypairverify-returns-false-instead-of-throwing) + - [3d. `TransactionBuilder.cloneFrom` Fixes (Behavioral)](#3d-transactionbuilderclonefrom-fixes) + - [3e. Other Behavioral Fixes (Low Impact)](#3e-other-behavioral-fixes) +- [Step 4: Update TypeScript Exhaustive Switches](#step-4-update-typescript-exhaustive-switches) +- [Step 5: Verify Your Upgrade](#step-5-verify-your-upgrade) +- [FAQ / Troubleshooting](#faq--troubleshooting) + +--- + +## Who Needs to Migrate + +You need this guide if you: +- Depend on `@stellar/stellar-base` directly (not just through `@stellar/stellar-sdk`) +- Are bumping from any v14.x to v15.x +- Build or sign transactions, interact with Soroban contracts, or work with XDR directly + +Even if none of the above apply directly, **we recommend all consumers upgrade** to ensure everything works as expected with the latest fixes and Protocol 26 support. + +If you only use `@stellar/stellar-sdk`, see the [SDK migration guide](https://github.com/stellar/js-stellar-sdk/blob/master/docs/upgrade/v15.md) instead — it covers everything here plus SDK-specific changes. + +--- + +## Prerequisites + +| Requirement | Minimum Version | +|---|---| +| Node.js | >= 20 (unchanged from v14) | +| npm / yarn / pnpm | Any recent version | + +--- + +## Step 1: Update Dependencies + +```bash +npm install @stellar/stellar-base@^15.0.0 +``` + +This will pull in `@stellar/js-xdr@^4.0.0` transitively. Verify: + +```bash +npm ls @stellar/js-xdr +# Should show 4.x.x +``` + +--- + +## Step 2: Fix Breaking Changes + +### 2a. Immutable `networkPassphrase` + +**Severity: Critical** — code will throw at runtime + +The `networkPassphrase` property on `Transaction` and `FeeBumpTransaction` is now read-only. The setter throws `Error('Transaction is immutable')`. + +**Why:** Mutating the passphrase after construction silently invalidated signatures and changed the transaction hash, leading to hard-to-debug failures. + +**Before (v14):** +```js +const tx = new Transaction(envelope, Networks.TESTNET); +tx.networkPassphrase = Networks.PUBLIC; // silently worked, but dangerous +``` + +**After (v15):** +```js +// Pass the correct passphrase at construction time +const tx = new Transaction(envelope, Networks.PUBLIC); + +// Or when building: +const tx = new TransactionBuilder(account, { + networkPassphrase: Networks.PUBLIC, + fee: '100', +}).build(); +``` + +**Find affected code:** +```bash +grep -rnE '\.networkPassphrase[[:space:]]*=' src/ +``` + +--- + +### 2b. XDR Integer Overflow Now Throws + +**Severity: Critical** — code will throw at runtime + +Sized XDR integer types now throw on overflow/underflow instead of silently clamping. This comes from `@stellar/js-xdr` v4.0.0. + +**Why:** Silent clamping masked bugs where incorrect values were encoded on-chain. + +**Before (v14):** +```js +// These silently clamped to max/min values +new xdr.Uint32(5000000000); // > 2^32-1, silently became 4294967295 +new xdr.Uint32(-1); // negative, silently became 0 +new xdr.Int64('99999999999999999999'); // overflow, silently truncated +``` + +**After (v15):** +```js +// All of these now throw RangeError +new xdr.Uint32(5000000000); // throws! +new xdr.Uint32(-1); // throws! +new xdr.Int64('99999999999999999999'); // throws! + +// Validate before constructing: +const value = 5000000000; +if (value < 0 || value > 4294967295) { + throw new Error(`Value ${value} out of Uint32 range`); +} +new xdr.Uint32(value); +``` + +**Valid ranges:** +| Type | Min | Max | +|---|---|---| +| `Uint32` | `0` | `4,294,967,295` (2^32 - 1) | +| `Int32` | `-2,147,483,648` | `2,147,483,647` | +| `Uint64` / `UnsignedHyper` | `0` | `2^64 - 1` | +| `Int64` / `Hyper` | `-2^63` | `2^63 - 1` | + +**Find affected code:** +```bash +grep -rnE 'new xdr\.(Uint32|Int32|UnsignedHyper|Hyper|Uint64|Int64)' src/ +``` + +--- + +### 2c. Hermes Typed-Array Polyfill Removed + +**Severity: Critical** — React Native + Hermes only + +`@stellar/js-xdr` v4.0.0 no longer ships the internal polyfill for broken `subarray` on `TypedArray` in the Hermes engine. + +**Who is affected:** Only React Native apps using the Hermes JavaScript engine. Node.js and browser environments are **not** affected. + +**Important:** Without the polyfill, you will **not** get an error. Hermes's broken `TypedArray.subarray` silently returns incorrect data, so operations like signing, XDR encoding, and hashing will produce wrong results without any indication that something is wrong. This makes the issue difficult to diagnose — your code runs without errors but behaves incorrectly. + +**Before (v14):** +```js +// Worked out of the box on Hermes — polyfill was bundled internally +import { Keypair } from '@stellar/stellar-base'; +``` + +**After (v15):** +```bash +npm install @exodus/patch-broken-hermes-typed-arrays +``` +```js +// Add this BEFORE any Stellar imports, in your app entry point +import '@exodus/patch-broken-hermes-typed-arrays'; + +import { Keypair } from '@stellar/stellar-base'; +``` + +--- + +## Step 3: Handle Behavioral Fixes That May Affect You + +These are officially bug fixes, not breaking changes. But if your code relied on the old (incorrect) behavior, it will behave differently now. + +### 3a. `Memo.id` Rejects Invalid Values + +**Severity: Critical** — values that previously "worked" now throw + +`Memo.id()` now validates that the input is a non-negative integer within `[0, 2^64 - 1]`. + +**Before (v14):** +```js +Memo.id('-1'); // accepted (silently corrupted) +Memo.id('1.5'); // accepted (silently corrupted) +Memo.id('18446744073709551616'); // accepted (> 2^64-1, corrupted) +``` + +**After (v15):** +```js +Memo.id('-1'); // throws: "Expects a uint64 as a string. Got -1" +Memo.id('1.5'); // throws: "Expects a uint64 as a string. Got 1.5" +Memo.id('18446744073709551616'); // throws: "Expects a uint64 as a string. Got 18446744073709551616" + +// Valid usage: +Memo.id('0'); // ok +Memo.id('18446744073709551615'); // ok (2^64 - 1) +``` + +**Find affected code:** +```bash +grep -rn 'Memo\.id(' src/ +``` + +--- + +### 3b. `Soroban.parseTokenAmount` Rejects Excess Decimals + +**Severity: Critical** — values that previously "worked" now throw + +**Before (v14):** +```js +Soroban.parseTokenAmount('1.999999', 2); // silently truncated extra decimals +``` + +**After (v15):** +```js +Soroban.parseTokenAmount('1.999999', 2); +// throws: 'Too many decimal places in "1.999999": expected at most 2, got 6' + +// Truncate/round yourself first: +Soroban.parseTokenAmount('1.99', 2); // ok +``` + +--- + +### 3c. `Keypair.verify` Returns `false` Instead of Throwing + +**Severity: Behavioral** — no crashes, but different control flow + +**Before (v14):** +```js +try { + const valid = keypair.verify(data, malformedSig); +} catch (e) { + // Malformed signatures threw here + console.log('Malformed signature detected via exception'); +} +``` + +**After (v15):** +```js +const valid = keypair.verify(data, malformedSig); // returns false, no throw + +// Simplified pattern: +if (!keypair.verify(data, sig)) { + console.log('Invalid or malformed signature'); +} +``` + +If you differentiated "malformed" from "wrong" signatures by catching exceptions, that distinction is gone. Both now return `false`. + +--- + +### 3d. `TransactionBuilder.cloneFrom` Fixes + +**Severity: Behavioral** — fixes bugs, may change output + +Two fixes to `TransactionBuilder.cloneFrom()`: + +1. **`extraSigners`** are now properly re-encoded as StrKey strings (previously passed as raw XDR objects, which could silently fail) +2. **`unscaledFee`** is now floored via `Math.floor()` (previously could be fractional when fee didn't divide evenly by operation count) + +**Migration:** If you were working around either bug, remove your workaround. + +--- + +### 3e. Other Behavioral Fixes (Low Impact) + +These are correctness fixes. No migration is needed unless you were depending on the old buggy behavior: + +| Fix | What changed | +|---|---| +| `TransactionBuilder` timebounds | `Date` objects are now floored to integer UNIX timestamps | +| `Auth.bytesToInt64` | Upper-32-bit bytes are now processed correctly (nonces were previously biased toward zero) | +| `ScInt` constructor | String inputs are now converted to `BigInt` before processing | +| `SignerKey.decodeSignerKey` | Reads exact payload length from the 4-byte prefix (no over-read) | +| `Operation._toXDRPrice` | `{ n: 0, d: 1 }` is now handled correctly (zero numerator was previously falsy) | +| XDR array decoding | Fails fast when declared length exceeds remaining bytes | + +--- + +## Step 4: Update TypeScript Exhaustive Switches + +If your TypeScript code has exhaustive `switch` statements on any of these XDR enums, add cases for the new variants: + +**`TransactionResultCode`:** +```ts +case xdr.TransactionResultCode.txFrozenKeyAccessed(): + // New in Protocol 26: a frozen ledger key was accessed + break; +``` + +**`ClaimClaimableBalanceResultCode`:** +```ts +case xdr.ClaimClaimableBalanceResultCode.claimClaimableBalanceTrustlineFrozen(): + break; +``` + +**`LiquidityPoolDepositResultCode`:** +```ts +case xdr.LiquidityPoolDepositResultCode.liquidityPoolDepositTrustlineFrozen(): + break; +``` + +**`LiquidityPoolWithdrawResultCode`:** +```ts +case xdr.LiquidityPoolWithdrawResultCode.liquidityPoolWithdrawTrustlineFrozen(): + break; +``` + +**`ContractCostType`:** 16 new BN254 curve variants (IDs 70-85). If you have an exhaustive switch on `ContractCostType`, add cases for all `bn254*` variants. + +**Find affected code:** +```bash +grep -rnE 'TransactionResultCode|ClaimClaimableBalanceResultCode|LiquidityPoolDepositResultCode|LiquidityPoolWithdrawResultCode|ContractCostType' src/ +``` + +--- + +## Step 5: Verify Your Upgrade + +1. **Run your test suite** — watch for new `RangeError` from XDR integers and `Error('Transaction is immutable')` from passphrase mutation +2. **TypeScript users:** run `tsc --noEmit` and fix any new exhaustive-switch errors +3. **Check dependency resolution:** + ```bash + npm ls @stellar/stellar-base + # Should show 15.x.x + npm ls @stellar/js-xdr + # Should show 4.x.x + ``` +4. **Submit a test transaction on Testnet** to confirm signing and submission still work end-to-end + +--- + +## FAQ / Troubleshooting + +**Q: I get `Error: Transaction is immutable`** +A: You're assigning to `tx.networkPassphrase` after construction. Pass the correct passphrase when creating the transaction. See [Step 2a](#2a-immutable-networkpassphrase). + +**Q: I get `RangeError` from XDR integer construction that worked before** +A: Values that exceeded the type's range were previously silently clamped. Validate your inputs. See [Step 2b](#2b-xdr-integer-overflow-now-throws). + +**Q: I get `Error: Expects a uint64 as a string. Got ` from `Memo.id`** +A: `Memo.id` now rejects negative, decimal, and overflow values. Ensure you're passing a valid non-negative integer string within `[0, 2^64-1]`. See [Step 3a](#3a-memoid-rejects-invalid-values). + +**Q: I get `Too many decimal places` from `Soroban.parseTokenAmount`** +A: The function now validates decimal precision. Truncate or round your input to the token's decimal count before calling. See [Step 3b](#3b-sorobanparsetokenamount-rejects-excess-decimals). + +**Q: My React Native app on Hermes returns incorrect or corrupted data (e.g., wrong signatures, garbled XDR)** +A: Without the polyfill, Hermes's broken `TypedArray.subarray` silently returns wrong results rather than throwing an error. Install `@exodus/patch-broken-hermes-typed-arrays` and import it before any Stellar imports. See [Step 2c](#2c-hermes-typed-array-polyfill-removed). + +**Q: TypeScript compilation fails with "not all code paths return a value" or missing switch cases** +A: Protocol 26 added new XDR enum variants. Add the new cases to your exhaustive switches. See [Step 4](#step-4-update-typescript-exhaustive-switches). + +**Q: Do I need to update if I'm not on a Protocol 26 network yet?** +A: Yes — the breaking changes (immutable passphrase, integer overflow, Hermes polyfill) apply regardless of protocol version. The new XDR types only appear on Protocol 26 networks, but the behavioral changes affect all usage. Protocol 26 is scheduled for Testnet on **April 16, 2026** and the Mainnet upgrade vote on **May 6, 2026**. See the [Protocol 26 Upgrade Guide](https://stellar.org/blog/foundation-news/stellar-yardstick-protocol-26-upgrade-guide) for the full timeline.