Skip to content

Migration Guide

If you are coming from an earlier version of @stellar/stellar-sdk, update the APIs listed below.

Versioning. Always run the latest @stellar/stellar-sdk; an older SDK may fail to decode newer network data as the protocol upgrades. These docs cover the latest version only. See Versioning and compatibility.

17.x.x Breaking changes

The 17.x.x release changes two things that touch nearly every codebase: the xdr namespace is rebuilt as a class-based API, and every public API that returned Node’s Buffer now returns a Uint8Array. The wire format is unchanged: bytes and base64 written by older SDKs still decode.

Each has a dedicated deep-dive guide, because the surface is too large to summarize here:

  • XDR migration guide covers the class-based xdr namespace, built on js-xdr v5. Affects anyone who reads or builds xdr.* values, plus anyone calling tx.toXDR() or TransactionBuilder.fromXDR(), which were renamed.
  • Uint8Array migration guide covers BufferUint8Array on every byte-returning public API. Affects anyone calling .toString("hex"), .equals(), or another Buffer method on an SDK result.

Read both. The two interact, and each has failure modes that produce no error at all:

  • Buffer methods on a Uint8Array fail silently. .toString("hex") returns comma-joined decimals ("185,77,39,…"), and .toString("utf8") returns "104,105". Nothing throws, so the wrong string is used downstream.
  • Absent optional XDR fields decode to null instead of undefined, so === undefined checks stop matching. Prefer == null.
  • scValToNative can now return a Uint8Array for a scvString or scvSymbol whose contents aren’t valid UTF-8, where it always returned a string before.

The method renames (toXDRtoXdr, fromXDRObjectfromXdrObject, and eleven more) ship without back-compat aliases, so those at least fail loudly.

Installation: the Node floor is now 22.12.0

engines.node is >=22.12.0 (16.x said >=22.0.0). The CommonJS build loads ESM-only dependencies through require(), which Node only supports unflagged from 22.12.0 — on 22.0–22.11 require("@stellar/stellar-sdk") fails with ERR_REQUIRE_ESM. Installing on an earlier 22.x now produces an EBADENGINE warning instead of a package that cannot be required (#1664). ESM consumers were never affected. Upgrade Node, or use the ESM entry point.

Auth: CAP-71 v2 address credentials are now the default

Protocol 27 (CAP-71) added the address-bound SOROBAN_CREDENTIALS_ADDRESS_V2 credential, which 16.x kept behind opt-in flags. Both flags now default to v2:

  • rpc.Server.simulateTransaction asks RPC to record ADDRESS_V2 entries. Its useUpgradedAuth flag defaults to true, and is now always sent on the wire (it used to be omitted when unset). The same default reaches everything built on it, including contract.AssembledTransaction.simulate() and the useUpgradedAuth method option.
  • authorizeInvocation builds ADDRESS_V2 credentials. Its authV2 flag defaults to true.

Pass false to either one for the legacy ADDRESS format:

await server.simulateTransaction(tx, undefined, undefined, false);
await server.prepareTransaction(tx, false);
await assembled.simulate({ useUpgradedAuth: false });
await authorizeInvocation({ ...params, authV2: false });

Both flags are transitional and become no-ops once v2 is mandatory in protocol 28, so don’t rely on false to keep the v1 format long-term. useUpgradedAuth only affects the recording auth modes.

Reading entries is where this surfaces. Code that picks the credential arm by hand must handle addressV2, not just address:

const creds = entry.credentials;
if (creds.type === "sorobanCredentialsAddress") {
const { nonce, signatureExpirationLedger } = creds.address;
} else if (creds.type === "sorobanCredentialsAddressV2") {
// now the default arm, from simulation and from authorizeInvocation
const { nonce, signatureExpirationLedger } = creds.addressV2;
}

Or let inspectAuthEntry normalize the arms for you.

Signing needs no change if you use the SDK for it. contract.Client, authorizeEntry, signAuthEntries, and buildAuthorizationEntryPreimage all sign whichever credential the entry carries, and pick the address-bound payload for ADDRESS_V2. A hand-rolled signer that hardcodes the legacy ENVELOPE_TYPE_SOROBAN_AUTHORIZATION preimage now produces signatures the network rejects. See Protocol 27 Soroban auth for the address-bound payload.

16.x.x Breaking changes

The 16.x.x release is a major modernization. @stellar/stellar-base is folded into the SDK, native fetch replaces axios as the default transport, the package is ESM-first, and Node 22 is required. It also lands Protocol 27 Soroban authorization (AddressV2).

Most apps only need the Installation, Imports, and Transport changes. Review the transaction, asset, and behavior sections if your code depends on lower-level base types or previously lenient validation. The Auth changes only affect code that signs Soroban contract authorization entries.

Installation: Node 22 is now required

The minimum Node version is now 22 (up from 20). Installing on older Node produces an EBADENGINE warning. Two runtime globals are now assumed and are built into Node 22: fetch (the default transport) and crypto.getRandomValues (used by SEP-10 buildChallengeTx, which dropped the randombytes dependency). Constrained runtimes such as React Native need polyfills for both.

A .nvmrc pinned to v22 is included:

Terminal window
nvm install && nvm use

Installation: @stellar/stellar-base is folded into @stellar/stellar-sdk

@stellar/stellar-base is now bundled into and re-exported from @stellar/stellar-sdk, and is no longer a separate dependency. Symbol names are unchanged. If you imported the base package directly, change the import source:

import { Keypair, TransactionBuilder, Asset } from "@stellar/stellar-base"
import { Keypair, TransactionBuilder, Asset } from "@stellar/stellar-sdk"

Then uninstall it. Keeping both installed causes instanceof checks to fail on values that look correct.

Terminal window
npm uninstall @stellar/stellar-base

Imports: the default export was removed

export default module.exports was removed from the entry points. Replace any default import with a namespace or named import.

import StellarSdk from "@stellar/stellar-sdk"
import * as StellarSdk from "@stellar/stellar-sdk"
import { Keypair, rpc, contract } from "@stellar/stellar-sdk"

Imports: build paths moved under lib/esm

The package is now ESM-first and dual ("type": "module"). ESM resolves lib/esm/, CommonJS resolves lib/cjs/, axios variants resolve under lib/axios/esm/ and lib/axios/cjs/, and type declarations live alongside the ESM output. require() still works. If you deep imported a raw build path, it moved. Use the subpath exports instead.

import { Server } from "@stellar/stellar-sdk/lib/rpc/index.js"
import { Server } from "@stellar/stellar-sdk/rpc"

Available subpaths: ., ./rpc, ./contract, ./axios, ./axios/rpc, ./axios/contract, ./http-client/axios.

Imports: browser auto-resolution was removed

The package.json browser field and browser export conditions were removed. Bundlers now resolve the package entry to ESM or CJS source instead of auto-substituting the prebuilt UMD bundle. If you need the standalone browser bundle, load it from the explicit dist/ path. The UMD filenames are unchanged.

Transport: native fetch is now the default

The default HTTP client switched from axios to native fetch (backed by feaxios). Normal Horizon and RPC use needs no change. Code that relied on axios-specific behavior (the axios config object, axios error shapes, interceptors, httpClient.CancelToken) now gets the fetch client instead.

To opt back into axios, import the axios-backed build:

import * as StellarSdk from "@stellar/stellar-sdk"
import * as StellarSdk from "@stellar/stellar-sdk/axios"

Per module, use @stellar/stellar-sdk/axios/rpc and @stellar/stellar-sdk/axios/contract. For a self-built browser bundle, build with USE_AXIOS=true (pnpm run build:lib:axios).

Removed entrypoints

The no-axios, minimal, and no-eventsource subpaths (and each one’s /contract and /rpc variants) were removed. The plain @stellar/stellar-sdk import is now fetch-based, so it already provides what no-axios and minimal used to.

import { Horizon } from "@stellar/stellar-sdk/no-axios"
import { Horizon } from "@stellar/stellar-sdk/minimal"
import { Horizon } from "@stellar/stellar-sdk/no-eventsource"
import { Horizon } from "@stellar/stellar-sdk"

Streaming: eventsource upgraded to v4

The eventsource dependency jumped from v2 to v4 and is now always bundled (the USE_EVENTSOURCE=false build flag and the “Streaming requires eventsource” guard are gone). Re-test Horizon .stream() flows. v4 uses spec-compliant EventTarget behavior, so exceptions thrown inside onmessage or onerror handlers now surface as uncaught exceptions instead of being swallowed. Handle errors inside stream callbacks.

Utilities: server.serverURL is now a native URL

The serverURL on Horizon.Server and rpc.Server changed from a urijs URI to the global URL. Method calls become property reads.

const host = server.serverURL.hostname()
const host = server.serverURL.hostname

URL.protocol returns the WHATWG form with a trailing colon, so update equality checks.

if (server.serverURL.protocol === "https") { /* ... */ }
if (server.serverURL.protocol === "https:") { /* ... */ }

The Server constructors now require an absolute URL with a scheme. A bare host or scheme-less string throws TypeError: Invalid URL at construction time. If you subclass CallBuilder, its constructor parameter and protected url field are now URL, and there is a new protected setPath(...segments) helper.

Transactions: min account sequence age is now bigint

Transaction’s minAccountSequenceAge is now a native bigint. It is no longer a number or an xdr.UnsignedHyper object.

Search for minAccountSequenceAge and setMinAccountSequenceAge in your code. Change numeric seconds to bigint values when passing them to TransactionBuilder.setMinAccountSequenceAge or TransactionBuilderOptions.minAccountSequenceAge. If you compare a transaction’s minAccountSequenceAge, compare against a bigint.

If you set minAccountSequenceAge: 0n, the builder now preserves that value instead of coercing it to null. Code that checks hasV2Preconditions() may see true when a zero-age precondition is present.

Before:

const builder = new TransactionBuilder(account, {
fee,
networkPassphrase,
minAccountSequenceAge: 60,
})
builder.setMinAccountSequenceAge(120)

After:

const builder = new TransactionBuilder(account, {
fee,
networkPassphrase,
minAccountSequenceAge: 60n,
})
builder.setMinAccountSequenceAge(120n)

Transactions: type and operation changes

Update code that directly types transactions, reads transaction preconditions, mutates transaction internals, or switches on operation types:

  • Remove generic parameters from Transaction type annotations, such as Transaction<Memo<MemoType.Text>>.
  • Convert tx.extraSigners to StrKey strings before treating it as string[]; it is now typed as xdr.SignerKey[].
  • Delete calls to Operation.isValidAmount(), Operation.constructAmountRequirementsError(), and Operation.setSourceAccount(). They are no longer runtime methods on Operation.
  • Replace operation.type === "revokeSponsorship" checks with the specific revoke-sponsorship type strings: "revokeAccountSponsorship", "revokeTrustlineSponsorship", "revokeOfferSponsorship", "revokeDataSponsorship", "revokeClaimableBalanceSponsorship", "revokeLiquidityPoolSponsorship", and "revokeSignerSponsorship".

Before:

import { Memo, MemoType, Transaction } from "@stellar/stellar-sdk"
type TextTransaction = Transaction<Memo<MemoType.Text>>
const signerKeys: string[] = tx.extraSigners ?? []
switch (operation.type) {
case "revokeSponsorship":
handleRevokeSponsorship(operation)
break
}

After:

import { SignerKey, Transaction } from "@stellar/stellar-sdk"
type TextTransaction = Transaction
const signerKeys = tx.extraSigners?.map(SignerKey.encodeSignerKey) ?? []
switch (operation.type) {
case "revokeAccountSponsorship":
case "revokeTrustlineSponsorship":
case "revokeOfferSponsorship":
case "revokeDataSponsorship":
case "revokeClaimableBalanceSponsorship":
case "revokeLiquidityPoolSponsorship":
case "revokeSignerSponsorship":
handleRevokeSponsorship(operation)
break
}

Transactions: mutating .tx is now a silent no-op

This is a silent behavior change. It does not throw, and no types change. TransactionBase.tx now returns a fresh defensive copy on every access. The old pattern of setting fields through it mutates that throwaway copy and has no effect on the transaction that gets signed or serialized:

const tx = new TransactionBuilder(account, opts).addOperation(op).build()
tx.tx.fee("200") // silently discarded
tx.tx.operations(newOps) // silently discarded
tx.tx.cond(newCond) // silently discarded

Because nothing throws, code that relied on this keeps compiling and running while signing and submitting the unmodified transaction. A payment can go out with the wrong fee, operations, or preconditions, and the only signal is the on-chain result. If you were patching a built transaction this way, rebuild it so the change is part of what you sign:

const built = new TransactionBuilder(account, opts).addOperation(op).build()
built.tx.fee(200) // no-op in v16
const tx = TransactionBuilder.cloneFrom(built, { fee: "200" }).build()

Reading .tx to inspect a transaction is unaffected.

Assets, keys, and signing helpers

Asset.code and Asset.issuer are now readonly. Construct a new Asset instead of mutating either field. Asset.issuer is typed as string | undefined, because native assets do not have an issuer.

Keypair.rawSecretKey() now throws Error("no secret seed available") on public-key-only keypairs instead of returning undefined.

Two exports were removed:

  • FastSigning was removed. Signing now goes through @noble/ed25519.
  • TransactionI was removed. Use TransactionBase instead.

Utilities: BigNumber.DEBUG removed

bignumber.js was bumped from v9 to v11, and the SDK now exports a strict .clone()d instance. The introspection flag moved.

if (BigNumber.DEBUG) { /* ... */ }
if (BigNumber.config().STRICT) { /* ... */ }

Internal amount and price conversion logic inherits upstream v10/v11 behavior. The v11 behavior that changed most for SDK callers is that a high-precision JavaScript number passed as an amount or price no longer throws for having more than 15 significant digits; it is rounded to floating-point precision. Pass such values as strings or bigint to avoid quiet precision loss. A BigNumber from your own bignumber.js install is a different class, so instanceof can fail across the boundary.

TypeScript: corrected declarations may affect compile-time checks

The TypeScript declarations now match runtime behavior more closely:

  • CreateInvocation.token was renamed to CreateInvocation.asset.
  • ScIntType adds "timepoint" and "duration"; update exhaustive switches.
  • XdrLargeInt.getType() returns ScIntType | undefined instead of a raw lowercased string. Non-integer types return undefined.
  • SorobanDataBuilder.fromXDR returns xdr.SorobanTransactionData.
  • SetOptions.clearFlags and SetOptions.setFlags accept arbitrary numeric bitmasks through AuthFlags, so combined flags no longer need a cast.
  • The ignored supportMuxing parameter was removed from the decodeAddressToMuxedAccount and encodeMuxedAccountToAddress declarations.

Behavior: stricter ed25519 verification

The ed25519 backend swapped to @noble/ed25519 v3. Keypair’s random, sign, verify, and generate keep the same API and Buffer return types, but v3 verification is stricter about non-canonical and malleable signatures. Edge-case signatures that previously verified may now be rejected.

Behavior: stellar.toml parser swapped to smol-toml

SEP-1 resolution now uses a stricter TOML 1.0 parser. The parsed shape is generally the same, but it accepts and rejects different inputs and throws different error messages, which propagate into the rejected promise as a plain Error.

Behavior: stricter validation and parsing

Several APIs now reject invalid input that older versions accepted or partially parsed:

  • toXDRPrice rejects zero, negative, NaN, and Infinity numeric prices before reaching best_r(). Zero denominators are rejected too.
  • Constructors and operation builders validate more strictly: MuxedAccount validates uint64 IDs, Claimant rejects falsy destinations, Account rejects NaN sequences, Memo is immutable and throws on invalid types, Memo.id() rejects non-plain-digit strings, allow_trust requires authorize, setTrustLineFlags requires boolean flag values, and Asset.getAssetType() throws for unknown types instead of returning "unknown".
  • TransactionBuilder.build() throws on total-fee overflow past uint32 max, cloneFrom() throws on zero-operation inputs, and the constructor rejects negative or inverted timebounds and ledgerbounds.
  • Operation.setOptions() rejects malformed numeric strings such as "123abc" for flag, weight, and threshold fields instead of parsing only the leading digits.
  • nativeToScVal bounds-checks u32 and i32 values and rejects malformed numeric strings. XdrLargeInt.toI128() and toI256() reject unsigned values outside the signed range instead of silently flipping the sign bit.
  • XdrLargeInt and ScInt built from an array of limbs now decode correctly. Code that relied on the old nested-array bug will see different values.

Behavior: recent RPC and simulation response fixes

The following fixes landed shortly before v16. They are listed here because they are observable when upgrading from older 15.x releases to v16.

Several fixes change observable behavior:

  • rpc.Server.getLatestLedger() now includes closeTime, headerXdr, and metadataXdr, with the XDR fields parsed into objects rather than base64.
  • A parseSuccessful precedence bug that silently dropped simulation results and state changes is fixed, so they now appear in the parsed response.
  • pollTransaction now runs the configured number of attempts (it previously ran one fewer).
  • maxRedirects and maxContentLength are now enforced on the default client (they were no-ops before), tightening the SSRF and DoS guards used by StellarToml.Resolver.resolve and FederationServer.

Dependencies: removed transitive packages

If your code reached through the SDK for any of these, declare them yourself now: @stellar/stellar-base, urijs, @noble/curves, sha.js, randombytes, toml.

Auth: Protocol 27 readiness

Protocol 27 (CAP-71) adds two address-bound Soroban credential types, AddressV2 and AddressWithDelegates. This only affects code that signs Soroban authorization entries or inspects their credential arms.

The full deep dive lives in Protocol 27 Soroban auth: the address-bound signature payload, the AddressV2 and AddressWithDelegates credential types, and how to migrate a hand-rolled signer.

In 16.x the SDK used the legacy ADDRESS credential everywhere by default: simulation returned ADDRESS entries and authorizeInvocation built them. ADDRESS_V2 had not activated yet, so it was opt-in, via useUpgradedAuth on simulation and authV2 on authorizeInvocation. Both now default to v2 in 17.x, see Auth: CAP-71 v2 address credentials are now the default. The rest of this section describes the 16.x defaults.

SDK-driven signing (contract.Client, basicNodeSigner, authorizeEntry, signAuthEntries) is forward-compatible with no code change: it signs whichever credential the entry carries, ADDRESS or ADDRESS_V2. The entries below break only code that reads the credential arm or builds the signature payload by hand.

For the full walkthrough of signing Soroban authorization entries, see Authorize a Contract Call.

Auth: authorizeInvocation takes a params object

authorizeInvocation now takes a single params object instead of positional arguments. In 16.x it still built a legacy SOROBAN_CREDENTIALS_ADDRESS entry by default, so keep reading the result with .address(). (In 17.x that default is ADDRESS_V2; see above.)

const entry = await authorizeInvocation(
signer, validUntilLedgerSeq, invocation, publicKey, networkPassphrase,
)
const entry = await authorizeInvocation({
signer, validUntilLedgerSeq, invocation, networkPassphrase, publicKey,
})
const addr = entry.credentials().address()

To build a CAP-71 ADDRESS_V2 entry instead, pass authV2: true and read the result with .addressV2().

const entry = await authorizeInvocation({
signer,
validUntilLedgerSeq,
invocation,
networkPassphrase,
authV2: true,
})
const addr = entry.credentials().addressV2()

Auth: build the signing payload with buildAuthorizationEntryPreimage

Code that reconstructs the authorization payload by hand hardcodes the legacy ENVELOPE_TYPE_SOROBAN_AUTHORIZATION. That is wrong for AddressV2, which signs the address-bound ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS. Use the new buildAuthorizationEntryPreimage, which picks the right payload from the entry’s own credential type, so the same code is correct on ADDRESS today and AddressV2 when simulation returns the newer credential.

const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
new xdr.HashIdPreimageSorobanAuthorization({
networkId: hash(Buffer.from(networkPassphrase)),
nonce: credentials.nonce(),
invocation: entry.rootInvocation(),
signatureExpirationLedger: validUntil,
}),
)
const preimage = buildAuthorizationEntryPreimage(entry, validUntil, networkPassphrase)
const signature = keypair.sign(hash(preimage.toXDR()))

Simpler still, hand the whole entry to authorizeEntry, which builds, signs, verifies, and writes the signature back:

const signed = await authorizeEntry(entry, keypair, validUntil, networkPassphrase)

Auth: authorizeEntry is stricter and gains a forAddress parameter

authorizeEntry now handles ADDRESS, ADDRESS_V2, and ADDRESS_WITH_DELEGATES, short-circuits only for an explicit source-account credential, and throws unsupported credential type for other non-address credentials. It no longer defaults networkPassphrase to Networks.FUTURENET, so pass the network passphrase explicitly at every call site.

It also gained an optional 5th parameter, forAddress, that routes the signature to a specific node (the top-level account or a nested delegate) for delegated auth. Existing four-argument calls are unaffected if they already pass networkPassphrase.

authorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase) // unchanged
authorizeEntry(entry, signer, validUntilLedgerSeq, networkPassphrase, forAddress)

For delegated authorization (ADDRESS_WITH_DELEGATES, CAP-71-01), build the wrapper with buildWithDelegatesEntry (it sorts each delegate array by address and rejects duplicates, as the protocol requires), then route each signer’s signature with forAddress. Every signer signs the same payload, bound to the top-level address.

Earlier 15.x deprecation: BalanceResponse.revocable

On BalanceResponse, use authorizedToMaintainLiabilities, which reflects the correct trustline flag semantics.

const flag = balance.revocable
const flag = balance.authorizedToMaintainLiabilities

Deprecated: bare Buffer return from SigningCallback

A custom SigningCallback passed to authorizeEntry should return { signature, publicKey }. The bare Buffer return is deprecated.

const signAuthEntry = async (preimage) => keypair.sign(hash(preimage.toXDR()))
const signAuthEntry = async (preimage) => ({
signature: keypair.sign(hash(preimage.toXDR())),
publicKey: keypair.publicKey(),
})