Repository navigation
chore(deps): upgrade @stellar/stellar-sdk to 17.4.0 for Protocol 30 - #1034
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Cover the Protocol 30 SDK surface the wallet depends on: the SEP-23 muxed contract (W) strkey vector, ScAddress arm 5 round-trips, the CAP-87 ML-DSA cost types (86-94) and the CAP-88 millisecond StellarValue arms. All four fail against the 17.0.1 SDK. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Pin which transaction memos fold into a muxed (M...) address on stellar-sdk 17.4.0. Decimal uint64 ids fold; hex, padded, empty and out-of-range values do not. The tests run against the real SDK, with only services/backend mocked. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
fast-text-encoding, the app's global TextDecoder, throws when it is asked
for { fatal: true }. SDK 17.4.0 asks for it in its bounded-fetch adapter, so
federation addresses did not resolve and stellar.toml asset icons did not
load on device. The SDK also asks for it in XdrString.toStringStrict(), so
scValToNative returned raw bytes instead of strings for symbols and strings.
Wrap the global TextDecoder so a UTF-8 decoder built with { fatal: true }
validates its input with a WHATWG-conformant check and throws a TypeError on
malformed bytes. Every other decoder is built and used as before, and the
wrapper does nothing where TextDecoder already supports fatal, as on Node
and in Jest.
The polyfill requires fast-text-encoding itself and installs Buffer first,
as shim.js does later, so the decoder keeps decoding through Buffer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A TextDecoder built with { fatal: true } and the default ignoreBOM now drops
one leading U+FEFF from its output, as the native decoder does.
fast-text-encoding never strips it, so a stellar.toml or federation response
that starts with a BOM failed to parse on device: smol-toml and JSON.parse
both reject the BOM.
The SDK's bounded fetch builds its decoder with the default ignoreBOM, so it
gets the stripping. XdrString.toStringStrict() and the WASM name parser pass
ignoreBOM: true and keep the BOM. A strict decoder now reports the ignoreBOM
option it honors. Non-fatal decoders are unchanged and still keep a BOM.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A TextDecoder built with { fatal: true } now reads only the window of a
DataView or non-Uint8Array typed array that byteOffset and byteLength
describe, as the native decoder does. It used to validate and decode the
whole backing buffer, as fast-text-encoding does. Every caller in the app
passes a Uint8Array, which is unchanged, so this brings the strict path in
line with the native decoder rather than changing what the app decodes.
Non-fatal decoders still hand their input straight to the base.
The UTF8_LABEL comment now says which labels the strict path treats as
UTF-8, and that the Buffer-backed base accepts only utf-8 and utf8. The
tests gain a view-window check against the native decoder, a parity check for
decode() with no input on a non-fatal decoder, and a JSDoc on the hex helper.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The decodeUtf8At comment said React Native's TextDecoder throws on fatal, which stopped being true with the strict TextDecoder polyfill. It now says why the function exists: the escape renderer needs one code point and its width at an index, which a decoder of a whole buffer cannot give. The strkey scrubbing tests add a letter before, a letter after and one more base32 character after a W key, so the leading anchor, the trailing anchor and the exact length are each pinned on their own. The memo folding tests split the non-folding memos by reason: not a canonical decimal id, and not a uint64. The SDK compatibility suite's header and top-level title now cover SDK 17 and Protocols 27 and 30. No production code changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
stellarSdkV15.test.ts has covered more than SDK 15 since it gained the Protocol 27 (CAP-71) block and the Protocol 30 block, so it becomes stellarSdkCompat.test.ts, with a version-free top-level describe. It also gains a block for the Horizon submission errors of the SDK 16+ HTTP client. SDK 16 replaced axios with fetch, and 16.1.0 / 17.0.0 made submitTransaction reject with TransactionFailedError or BadResponseError. The app reads those errors through isHorizonError: response.status for the 504 retry and the log-severity split, and response.data.extras.result_codes in the transaction builder and useManageTokens. Every app test built that error by hand, so nothing pinned it to the real SDK. The new tests drive the real Horizon.Server against a stubbed fetch for a 400 with result codes, a 429 without them, and a 504. If the SDK stops exposing response.data, the first two fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-625b0f13bf0caf551c53 |
|
iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-5be3c2d0bf7d2522e10f |
17.4.0 is now older than the 7-day npmMinimalAgeGate, so the temporary npmPreapprovedPackages entry is no longer needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A moderate active issue remains because the strict UTF-8 path passes the valid alias "unicode-1-1-utf-8" to a constructor that rejects it, causing decoder construction to throw on Hermes.
1 open issue
What changed in this PR
This pull request upgrades @stellar/stellar-sdk to 17.4.0 for Protocol 30, adds shared muxed contract (W…) support, and introduces a Hermes-compatible strict UTF-8 TextDecoder polyfill.
Changes:
- Updates the Stellar SDK and lockfile dependencies.
- Adds muxed contract helpers, fixtures, StrKey scrubbing, and Protocol 30 compatibility coverage.
- Adds and bootstraps the strict UTF-8 decoder polyfill with integration tests.
- Adds coverage for memo folding, Horizon submission errors, and SDK behavior changes.
| File | Reviewed changes |
|---|---|
yarn.lock |
Records the Stellar SDK upgrade and transitive dependency resolutions. |
src/polyfills/textDecoder.ts |
Implements strict UTF-8 decoding, BOM handling, and Hermes compatibility. Moderate issue (2 votes): normalize the valid unicode-1-1-utf-8 alias to utf-8 on the strict path while preserving invalid and empty labels for the base constructor. |
src/polyfills/README.md |
Documents the decoder polyfill and bootstrap ordering. |
src/helpers/stellarStrKey.ts |
Extends sensitive StrKey scrubbing to muxed contract addresses. |
src/helpers/soroban.ts |
Updates UTF-8 decoder documentation. |
src/helpers/muxedContract.ts |
Adds validation, extraction, and construction helpers for W… addresses. |
src/bootstrap.js |
Loads the TextDecoder polyfill during application startup. |
package.json |
Bumps @stellar/stellar-sdk to 17.4.0. |
__tests__/support/muxedContractFixtures.ts |
Provides shared muxed contract and account test vectors. |
__tests__/polyfills/textDecoder.test.ts |
Tests strict decoding, BOM behavior, Hermes simulation, and SDK integrations. |
__tests__/helpers/stellarStrKey.test.ts |
Tests W… scrubbing and word-boundary behavior. |
__tests__/helpers/stellarSdkCompat.test.ts |
Adds Protocol 30 and Horizon submission compatibility coverage. |
__tests__/helpers/muxedContract.test.ts |
Tests muxed contract validation, extraction, construction, and invalid inputs. |
__tests__/helpers/memoFolding.test.ts |
Pins SDK behavior for decimal mux IDs and memo folding. |
This pull request requires changes.
Review effort: Lite
…eros The scrubber tests checked a W with one extra character but not one with a character missing: a 68-character run must stay untouched, since only a 69-character M or W is a key. buildMuxedContract's comment says it drops leading zeros from the id; nothing asserted it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md and the dependencies guide still said Node 20, but package.json's engines field asks for >= 22.12.0 and every workflow runs Node 22. Prettier, run by the pre-commit hook, also re-pads the Best Practices table in AGENTS.md, which wasn't formatted before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.0.0 pins @stellar/stellar-sdk 17.4.0, the version the app already depends on, so the lockfile no longer resolves a second stellar-sdk (17.0.1) for the key manager. Its one breaking change is in KeyManager.fetchAuthToken, which the app doesn't call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

TL;DR
Bumps
@stellar/stellar-sdkfrom 17.0.1 to 17.4.0 for Protocol 30, and adds the shared groundwork for muxed contract (W…) addresses: parsing helpers, log and Sentry scrubbing, and Protocol 30 SDK tests. The send, signing-screen and history changes are separate PRs based on this one.The new SDK decodes federation and stellar.toml responses with a strict UTF-8
TextDecoder. The decoder the app gets on Hermes can't decode strictly, so federation address lookup and stellar.toml asset icons broke on device, while Jest, which runs on Node, stayed green. This PR adds a strict-UTF-8TextDecoderpolyfill that fixes both.Changed lines
Source code is the app code under
src/. Tests are the unit tests and their fixtures. Other is the lockfile,package.json, the polyfill README,AGENTS.mdand the dependencies guide.Screenshots and Videos
Federation lookup (
freighter.c7*lobstr.co) and a stellar.toml asset icon (testnet CARBON), before and after theTextDecoderpolyfill.Implementation details (for agents)
Every link below is a permalink at
9e5e0d2a4229be8bb97d5c50d05bb4c2a72903fb.What changed
9c4cae6c).@stellar/stellar-sdk17.0.1 to 17.4.0 inpackage.json#L89.yarn.lock#L4746-L4768gains the 17.4.0 entry and its new transitive dependencies; 17.0.1 stays in the lockfile only because@stellar/typescript-wallet-sdk-km@5.0.0pins it.W…) helpers (dbabee9e,50946fc4).src/helpers/muxedContract.tsexportsisMuxedContract,isContractDestination,getMuxedContractBaseId,getMuxedContractMuxIdandbuildMuxedContract. It is a leaf module that imports only the SDK, sohelpers/stellarandhelpers/sorobancan both use it without an import cycle. The getters and the builder throw on invalid input (theM…helpers returnnull), and mux ids are decimal strings, neverNumber, because a uint64 does not survive aNumberround trip. Nothing insrcimports it yet; the follow-up PRs wire it in. The shared SEP-23 fixtures are in__tests__/support/muxedContractFixtures.ts, and__tests__/helpers/muxedContract.test.tscovers the helpers, including a real checksum failure.84d377c8). The strkey pattern insrc/helpers/stellarStrKey.ts#L12is now[MW][A-Z2-7]{68}for the 69-character muxed forms, so aW…is masked asW***the way anM…is masked asM***.__tests__/helpers/stellarStrKey.test.tspins each word-boundary anchor and the length.654e7848).__tests__/helpers/stellarSdkCompat.test.ts#L208-L262uses the real SDK, with no mocks, to round-trip the SEP-23Wvector throughStrKeyandnew Address(W)throughScAddressarm 5, its XDR bytes andScVal, and to check the CAP-87 ML-DSA cost types (86 to 94) and the CAP-88 millisecondStellarValuearms.9e5e0d2a).stellarSdkV15.test.tsis nowstellarSdkCompat.test.ts, with a version-free top-level describe. It had already grown past SDK 15, with CAP-71 (Protocol 27) and now Protocol 30 blocks.9e5e0d2a).stellarSdkCompat.test.ts#L362-L511drives the realHorizon.Server.submitTransactionagainst a stubbedfetch.TransactionFailedErrororBadResponseError. The app reads those errors throughisHorizonError:response.statusfor the 504 retry and the log-severity split, andresponse.data.extras.result_codesin the transaction builder anduseManageTokens. Until now every app test built those errors by hand.response.datafrom the SDK's errors fails the first two.fe7c70ce).__tests__/helpers/memoFolding.test.tspins, against the real SDK, which memos fold into anM…address (see "Behaviour changes").TextDecoderpolyfill (a0b66717,46930ea3).src/polyfills/textDecoder.ts, required fromsrc/bootstrap.js#L22-L28, documented insrc/polyfills/README.md#L20-L53, with 103 tests in__tests__/polyfills/textDecoder.test.ts. The README's "Usage" section now says polyfills are required fromsrc/bootstrap.js, whichindex.jsloads before the app, not fromsrc/index.ts. See the next section.4ffbfdd1,8b84ab04). A strict decode now reads only the bytes that a non-Uint8Arrayview covers (toUint8Array). A stale comment insrc/helpers/soroban.ts#L214-L226no longer claims React Native's decoder throws onfatal(comment only). Several test titles were clarified.900fd03a,b80ca267). Two tests the code review found missing: the log scrubber leaves a 68-characterWalone, andbuildMuxedContractdrops leading zeros from the id.AGENTS.mdand the dependencies guide now say Node 22, whichpackage.jsonand every workflow already require.The
TextDecoderpolyfillWhy. SDK 17.4.0's bounded fetch, which
StellarToml.ResolverandFederation.Serveruse, decodes every response body withnew TextDecoder("utf-8", { fatal: true }). Hermes has noTextDecoder, so the app's global one isfast-text-encoding1.0.6, whose constructor throws "the 'fatal' option is unsupported". Node's decoder supportsfatal, so Jest could not see it. An on-device smoke test found it.How.
installFatalTextDecoderprobesnew TextDecoder("utf-8", { fatal: true })and, only when that throws, replaces the global with a wrapper. A UTF-8 decoder built withfatal: truevalidates its input withisValidUtf8, a WHATWG-conformant check (#L120-L184), and throws aTypeErroron malformed bytes. It also follows the WHATWG byte order mark rule (one leading U+FEFF is removed from the output unlessignoreBOMis set), which is what the SDK's bounded fetch gets from Node and browsers. Every other decoder delegates tofast-text-encodinguntouched. Wherefatalalready works (Node and Jest, or a future Hermes) the module does nothing.freighter-mobile/src/polyfills/textDecoder.ts
Lines 291 to 314 in 9e5e0d2
Load order.
index.jsrunsbootstrap.jsbeforeshim.js, which is where the app first installs a globalBuffer, andfast-text-encodingpicks its decode path at load time depending on whetherBufferexists. So the polyfill first installs the samebuffermodule'sBuffer(the assignmentshim.jsmakes, which then skips its own) and requiresfast-text-encodingitself. Without that, the app's decoder would silently use a different path for every caller. On device this ran under Metro's defaultinlineRequires.Side effect.
XdrString.toStringStrict()also asks for a strict decoder, and it sits behindscValToNativefor Soroban symbols, strings and executable tags. With the app's old decoder it threw and fell back to raw bytes (already the case onmainwith 17.0.1); it now returns strings, as under Node. That is covered by a Jest harness that simulates the Hermes setup (fast-text-encodingas the global decoder); it was not asserted on a device.Third site. The SDK's WASM custom-section name parsing (
contract/utils) also asks for a strict decoder and gets the same one. No test covers that path.Behaviour changes
M…address. Since SDK 17.3,MuxedAccountaccepts decimal-only ids. A memo such as"0x10"or" 16 "used to fold into anM…address and no longer does; decimaluint64memos ("0"through"18446744073709551615") fold as before. Pinned bymemoFolding.test.ts#L34-L39and#L58-L69.Audit of SDK behaviour changes, 17.0.1 to 17.4.0
Each SDK change was checked against the app's call sites. Paths are under
src/, and line numbers are at9e5e0d2a….getTransactionDetailsreturns early on an empty hash and runs in a try (services/stellar.ts:321-345).loadAccountis async, so a bad id rejects rather than throws:services/stellar.ts:274,:311;services/transactionService.ts:381,:531,:622,:720;helpers/getIconUrlFromIssuer.ts:27(G-checked at:19, in a try).checkMemoRequired(hooks/useValidateTransactionMemo.ts:63) only loads the destination of payment-type operations, and skipsM…destinations. The app opens no streams.allowedRedirectsvalidationhelpers/getIconUrlFromIssuer.ts:51(in a try, returns"").ducks/sendRecipient.ts:211(in a try, showsfederationNotFound). React Native still follows redirects through XHR, and the app never passesallowedRedirects.Asset.fromOperationkeeps the alphanum12 arm,Asset.equalscompares the typehelpers/soroban.ts:1036only calls.toString(). Nothing callsAsset.equals(the.equalsathelpers/walletKitValidation.ts:166is on anxdr.Hash).nativeToScValandscvSortedMap; 17.3nativeToScValrejects function valuesservices/transactionService.ts:307).Waddresses;contract_creditedmuxed fields; regenerated XDR armsnew Address(W)andAddress.fromScAddressnow succeed, soaddressToStringreturns theW(helpers/soroban.ts:139-144). Auth-entry address validation compares against the wallet's G, so aWnever matches (helpers/walletKitValidation.ts:206-212, unchanged).isValidStellarAddressstill rejectsWhere (helpers/stellar.ts:179). History envelopes that contain aWnow decode (helpers/soroban.ts:786). The app's switches have default branches, andtscpasses.setTimeoutthrows when the timeout falls beforeminTimeservices/stellar.ts:284sets no timebounds.fatal: true)helpers/getIconUrlFromIssuer.ts:51,ducks/sendRecipient.ts:211.scValToNativethrows on colliding map keyshelpers/soroban.ts:626(scvErroronly),:646and:1168(numeric scalars only),:694-705(insidegetArgsForTokenInvocation, whose only caller,:748, runs in a try).MuxedAccountaccepts decimal-only idscreateMuxedAccount(helpers/stellar.ts:124, constructor call at:140), reached throughdetermineMuxedDestination(helpers/muxedAddress.ts:110).MuxedAccount.fromAddress(M, "0")athelpers/stellar.ts:105and:164only parses an existingM….new Asset("xlm", issuer)keeps its caseservices/stellar.ts:263,services/transactionService.ts:248and:647,helpers/soroban.ts:812,helpers/balances.ts:528,useSwapForXlmReserve.ts:94.xlmatXLM:issuerTransactioncopies its envelope, so an unencodable envelope fails in the constructoruseSignTransactionDetails.ts:88,helpers/walletKitUtil.ts:529,hooks/useValidateTransactionMemo.ts:141. Every other site parses XDR that the app built, Horizon returned, or the app's backend prepared (services/backend.ts:1405).build()throws for a Soroban transaction with a memo or anM…sourceservices/transactionService.ts:401). The collectible builder (:749) adds none. Sources always come fromloadAccount(G).@stellar/typescript-wallet-sdk-km@5.0.0pins 17.0.1. It handles key storage only, and unlock and signing passed on both platforms.No mobile call site:
rpc.getContractDataerrors andrpc.Servertimeout validation (getSorobanRpcServer,services/stellar.ts:112, has no callers),getEvents, and the newrpc.queryEvents. The app reads no RPC events.signAuthEntriesandauthorizeEntry, and contract clients andgetSACBalancerejecting muxed addresses. The app signs auth entries from the preimage (helpers/stellar.ts:423-450).Asset.compare(LiquidityPoolAssetis only a type,KeyVal.tsx:8and:477), offer-id validation, and 17.4'stoXdrPricezero-numerator check. Offer operations are display-only (Operations.tsx:326,:362,:403,:894,:1110), and the app builds none.resourceFeebounds and thecloneFromandassembleTransactioninclusion fee. The backend prepares the transaction, andservices/backend.ts:1405only parses it._linkstemplating guard, WebAuth, Node-only redirect and header rules, per-request RPC timeouts, fetch params with a fragment, theAssembledTransactionfee,Spec.parseEvent, delegate expirations, the Wasm spec-section rule, thetoJsonmuxed range,stream()parse errors, record link names, templated relative hrefs and the XDR.valuedoc change../axiossubpaths, Metro has package exports off inmetro.config.js:47, and the app has its own axios 1.13.2), and theHorizonApi.Predicatetype (tscpasses).Hermes-only risks checked:
TextDecoder: the SDK passes onlyfatalandignoreBOM.fatalis covered by the polyfill, andignoreBOM: trueon a non-fatal decoder is what the base already does.TextEncoderis native in Hermes. The SDK usesReadableStreamonly throughresponse.body.getReader(), which the existingresponseBodypolyfill covers.WeakSet,2n ** 50n(terser keeps it),Object.hasOwnand Errorcause. The URL getters come fromreact-native-url-polyfill.Verification
At
9e5e0d2a:yarn testpasses atb80ca267: 334 suites passed (1 skipped), 4,831 tests passed (6 skipped). This PR adds 140 tests: 103 for the polyfill, 7 for theWhelpers, 12 for scrubbing, 11 for memo folding, 4 in the Protocol 30 block and 3 for Horizon submission errors.fast-text-encoding, then the polyfill) and run the real SDK'sStellarToml.Resolver,Federation.ServerandscValToNativeagainst it, withfetchmocked. A scratch harness on thehermesVM that ships with React Native 0.81.4 (not part of this PR) also passes at8b84ab04: 46 of 46 checks, 9 of them for4ffbfdd1's view-window change.yarn lint:tspasses.yarn checkexits 1, because of lint debt that is already onmain:yarn lint:checkreports 111 eslint errors in 18 files, andyarn format:check, whichyarn checknever reaches after that failure, flags 9 tracked files. This PR adds none. None of those files is touched here, the per-file error counts and the flagged files are identical tomain's, and eslint and prettier are clean on every file this PR changes.yarn lint:translationsreports zero translation violations. It exits 1 only because of 9 existingimport/orderandno-continueerrors in 4 files this PR does not touch.Device smoke tests, against testnet on an iPhone 17 Pro simulator (iOS 26.5) and a Pixel 10 Pro emulator (Android API 37):
stellar_signXDRfrom the mock dApp, signature verified on the hostTextDecoder,Bufferor polyfill errors46930ea3, and the device runs were not repeated for the later commits:4ffbfdd1and8b84ab04are covered by the unit suite and the Hermes VM harness;9e5e0d2aonly touches tests.IS_E2E_TEST=true, which skips WalletConnect's request-origin check. That does not matter for an SDK-bump smoke test. They used debug (Metro) bundles only.Follow-ups and out of scope
@stellar/typescript-wallet-sdk-km@5.0.0still pins 17.0.1.W(feat(send): support muxed contract (W) destinations #1036). The repo squash-merges, so rebase each sibling ontomainafter this PR lands.Whelpers in any screen (the sibling PRs do that), and acceptingWinisValidStellarAddress, which still rejects it here.Closes stellar/wallet-eng-monorepo#100
🤖 Generated with Claude Code