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
xdrnamespace, built on js-xdr v5. Affects anyone who reads or buildsxdr.*values, plus anyone callingtx.toXDR()orTransactionBuilder.fromXDR(), which were renamed. - Uint8Array migration guide covers
Buffer→Uint8Arrayon 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:
Buffermethods on aUint8Arrayfail 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
nullinstead ofundefined, so=== undefinedchecks stop matching. Prefer== null. scValToNativecan now return aUint8Arrayfor ascvStringorscvSymbolwhose contents aren’t valid UTF-8, where it always returned a string before.
The method renames (toXDR → toXdr, fromXDRObject → fromXdrObject, 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.simulateTransactionasks RPC to recordADDRESS_V2entries. ItsuseUpgradedAuthflag defaults totrue, and is now always sent on the wire (it used to be omitted when unset). The same default reaches everything built on it, includingcontract.AssembledTransaction.simulate()and theuseUpgradedAuthmethod option.authorizeInvocationbuildsADDRESS_V2credentials. ItsauthV2flag defaults totrue.
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:
nvm install && nvm useInstallation: @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.
npm uninstall @stellar/stellar-baseImports: 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.hostnameURL.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
Transactiontype annotations, such asTransaction<Memo<MemoType.Text>>. - Convert
tx.extraSignersto StrKey strings before treating it asstring[]; it is now typed asxdr.SignerKey[]. - Delete calls to
Operation.isValidAmount(),Operation.constructAmountRequirementsError(), andOperation.setSourceAccount(). They are no longer runtime methods onOperation. - 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 discardedtx.tx.operations(newOps) // silently discardedtx.tx.cond(newCond) // silently discardedBecause 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 v16const 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:
FastSigningwas removed. Signing now goes through@noble/ed25519.TransactionIwas removed. UseTransactionBaseinstead.
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.tokenwas renamed toCreateInvocation.asset.ScIntTypeadds"timepoint"and"duration"; update exhaustive switches.XdrLargeInt.getType()returnsScIntType | undefinedinstead of a raw lowercased string. Non-integer types returnundefined.SorobanDataBuilder.fromXDRreturnsxdr.SorobanTransactionData.SetOptions.clearFlagsandSetOptions.setFlagsaccept arbitrary numeric bitmasks throughAuthFlags, so combined flags no longer need a cast.- The ignored
supportMuxingparameter was removed from thedecodeAddressToMuxedAccountandencodeMuxedAccountToAddressdeclarations.
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:
toXDRPricerejects zero, negative,NaN, andInfinitynumeric prices before reachingbest_r(). Zero denominators are rejected too.- Constructors and operation builders validate more strictly:
MuxedAccountvalidates uint64 IDs,Claimantrejects falsy destinations,AccountrejectsNaNsequences,Memois immutable and throws on invalid types,Memo.id()rejects non-plain-digit strings,allow_trustrequiresauthorize,setTrustLineFlagsrequires boolean flag values, andAsset.getAssetType()throws for unknown types instead of returning"unknown". TransactionBuilder.build()throws on total-fee overflow pastuint32max,cloneFrom()throws on zero-operation inputs, and the constructor rejects negative or invertedtimeboundsandledgerbounds.Operation.setOptions()rejects malformed numeric strings such as"123abc"for flag, weight, and threshold fields instead of parsing only the leading digits.nativeToScValbounds-checksu32andi32values and rejects malformed numeric strings.XdrLargeInt.toI128()andtoI256()reject unsigned values outside the signed range instead of silently flipping the sign bit.XdrLargeIntandScIntbuilt 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 includescloseTime,headerXdr, andmetadataXdr, with the XDR fields parsed into objects rather than base64.- A
parseSuccessfulprecedence bug that silently dropped simulation results and state changes is fixed, so they now appear in the parsed response. pollTransactionnow runs the configured number of attempts (it previously ran one fewer).maxRedirectsandmaxContentLengthare now enforced on the default client (they were no-ops before), tightening the SSRF and DoS guards used byStellarToml.Resolver.resolveandFederationServer.
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) // unchangedauthorizeEntry(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.revocableconst flag = balance.authorizedToMaintainLiabilitiesDeprecated: 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(),})