As of v17, every public SDK API that used Node’s Buffer uses the web-standard
Uint8Array instead (#1457).
The buffer dependency is gone, and the browser bundle no longer needs (or
ships) a Buffer polyfill, so the SDK now runs in browsers, edge runtimes, Deno,
and Bun with no shims.
Inputs are not breaking. Buffer is a subclass of Uint8Array, so
everywhere the SDK now accepts a Uint8Array you can keep passing a Buffer.
Returns are breaking. Methods that returned Buffer now return a plain
Uint8Array, which lacks Buffer’s convenience methods. If you called
.toString("hex"), .toString("base64"), .equals(), .readBigUInt64BE(),
etc. on an SDK result, that code needs updating.
1. Recipes: replacing Buffer methods on SDK results
The SDK ships the hex and base64 conversions itself: xdr.encodeBytes and
xdr.decodeBytes (the same functions behind every toXdr / fromXdr call).
XDR values need no helper at all, since toXdr takes a format directly. Cases
the SDK does not cover (equals, concat, compare, UTF-8) are handled by
uint8array-extras or
plain DataView, as noted per row.
import { xdr } from "@stellar/stellar-sdk";
xdr.encodeBytes(tx.hash(), "hex"); // "deadbeef…"xdr.encodeBytes(keypair.sign(data), "base64");xdr.decodeBytes("deadbeef", "hex"); // Uint8Array
// XDR values encode directly — no helper needed:scVal.toXdr("hex");entry.toXdr("base64");| Before (Buffer) | After (Uint8Array) |
|---|---|
xdrVal.toXDR("hex") / ("base64") | xdrVal.toXdr("hex") / ("base64") — returns string, no helper needed |
buf.toString("hex") | xdr.encodeBytes(bytes, "hex") |
buf.toString("base64") | xdr.encodeBytes(bytes, "base64") |
Buffer.from(hex, "hex") | xdr.decodeBytes(hex, "hex") |
Buffer.from(b64, "base64") | xdr.decodeBytes(b64, "base64") |
buf.toString("utf8") / .toString() | uint8ArrayToString(bytes) (uint8array-extras) |
Buffer.from(str) (UTF-8) | stringToUint8Array(str) (uint8array-extras) |
Buffer.concat([a, b]) | concatUint8Arrays([a, b]) (uint8array-extras) |
a.equals(b) | areUint8ArraysEqual(a, b) (uint8array-extras) |
a.compare(b) | compareUint8Arrays(a, b) (uint8array-extras) |
Buffer.alloc(n) | new Uint8Array(n) |
Buffer.isBuffer(x) | x instanceof Uint8Array |
buf.readUInt32BE(o) | new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(o) |
buf.readBigUInt64BE(o) | …same DataView….getBigUint64(o) |
buf.slice(a, b) (view) | bytes.subarray(a, b). Note that Uint8Array.prototype.slice copies, while Buffer.prototype.slice returned a view |
As a stopgap in Node you can wrap a result back into a Buffer:
Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength). Treat that as
temporary; it reintroduces the Buffer dependency v17 removed, and it does not
work in browsers without a polyfill.
Three semantic traps to check for:
-
.toString("hex")fails silently.Uint8Array.prototype.toStringignores its argument and returns comma-joined decimals ("185,77,39,…"). Code comparing that to a hex string stops matching, and nothing throws. Grep your codebase for.toString("hex")/.toString("base64")applied to SDK results. -
Decoding is stricter. Where the SDK parses hex/base64 strings you hand it (e.g. hex signer keys in
Operation.setOptions, base64 envelopes), invalid input now throws (Invalid Hex character…) instead of being silently truncated the wayBuffer.from(str, "hex")was. -
Test assertions stop matching. A
Bufferand aUint8Arrayholding identical bytes are not deep-equal under vitest or Jest.toEqual,toStrictEqual, and nested comparisons all fail, because the two report different types. So a test whose expected value is aBufferfixture now fails against an SDK result. Normalize one side, or compare encodings:expect(xdr.encodeBytes(actual, "hex")).toBe(expectedHex); // preferredexpect(Array.from(actual)).toEqual(Array.from(expectedBuffer));
2. Method-by-method: returns that changed Buffer → Uint8Array
base
| API |
|---|
hash(data) |
Keypair.rawPublicKey() / rawSecretKey() |
Keypair.sign(data) / signMessage(message) |
Keypair.signatureHint() |
StrKey.decodeEd25519PublicKey / decodeEd25519SecretSeed / decodeMed25519PublicKey / decodePreAuthTx / decodeSha256Hash / decodeSignedPayload / decodeContract / decodeClaimableBalance / decodeLiquidityPool |
Transaction.hash() / signatureBase() (also on FeeBumpTransaction) |
Memo.value for MemoHash / MemoReturn (and the decoded bytes of a MemoText read back via Memo.fromXdrObject) |
Address.toBuffer() (name kept, now returns Uint8Array) |
Operation.fromXdrObject records: manageData’s value, setOptions/revokeSponsorship signer sha256Hash / preAuthTx |
sign(data, rawSecret) (the top-level signing helper) |
getLiquidityPoolId(type, params) |
rpc / contract / horizon
| API |
|---|
rpc.Server.getContractWasmByContractId() / getContractWasmByHash() |
contract.Spec byte-typed spec entries and specFromWasm results |
contract.Spec.scValToNative / funcResToNative for Bytes / BytesN, including values nested in structs, vecs, and maps. These are generically typed (T), so TypeScript will not flag the change: a client.get_hash().result.toString("hex") keeps compiling and starts returning comma-joined decimals. |
auth (CAP-71 / Soroban)
SigningCallbackmust now resolve to aUint8Array(or{ signature: Uint8Array; publicKey: string }). Returning aBufferstill works; returning a rawArrayBufferno longer does, so wrap it:new Uint8Array(arrayBuffer).- The signing payload handed to a
SigningCallbackas its second argument is aUint8Arraytoo (it used to be aBuffer). A callback that logs or forwards it withpayload.toString("hex")silently gets decimals. AuthEntrySignature.signature(frominspectAuthEntry) is aUint8Array.- Not
DecoratedSignature.signature/.hint, despite the matching name: whattx.signatures[i]holds arexdr.Signature/xdr.SignatureHintwrappers, not bytes. Unwrap with.toBytes(). See the XDR migration guide § 6.1.
3. Inputs that got more flexible
Operation.manageData’svalueacceptsstring | Uint8Array | nulldirectly (Buffers still work).Memo.textacceptsstring | Uint8Array. It no longer takes a plainnumber[], which 16.2.0 did accept, so passnew Uint8Array(arr)instead. Note thatMemo.text([])was a valid zero-byte memo and now throws.- Everything that accepted
Bufferaccepts anyUint8Arraynow, including ones backed bySharedArrayBuffer-free views fromfetch()responses,crypto.getRandomValues, WASM memory, etc.
4. Environment changes
- The
buffernpm package is no longer a dependency; bundlers no longer need a Buffer global or polyfill configuration for the SDK. base32.js(which required a Buffer global in browsers) was replaced with@exodus/bytes; strkey behavior is unchanged, except that malformed strkeys are rejected by its strict decoder with more specific errors (still a thrownError/isValid* === false).- Node
Bufferkeeps working everywhere as an input since it is aUint8Array. The SDK just never hands one back.