Repository navigation
feat(signing): render W addresses and fail closed on unreadable dApp requests - #1037
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>
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 Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 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>
@gorhom/bottom-sheet 5.2.6's handlePortalOnUnmount returns early while a modal is still opening, because currentIndexRef only updates once an animation completes. A details sheet whose request fails closed as it opens was left on screen with a dead close button, and the request sheet it minimized was never restored. Patch it to unmount such a modal, and pin both details sheets with the real gorhom modal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 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>
A sign-auth-entry request that can't be displayed now reads "Unable to read this authorization" / "The authorization entry could not be decoded.", on its Cancel-only sheet and in Authorization Entry Details. Transaction requests and the send and swap Transaction details keep the transaction wording. UnreadableTransactionNotice takes an optional variant, which DappRequestUnreadableContent passes through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-9a1c072efd73b33be9ce |
|
iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-8ce16b4121340beaf686 |
A render error in Transaction details or Authorization Entry Details switched the request sheet to the Cancel-only notice. That unmounted the details button, which owned the nested sheet, so the sheet had to close itself mid-view, and that needed a patch to @gorhom/bottom-sheet: gorhom left a sheet that was still opening on screen when its owner unmounted. Now the details sheet stays open, showing its own notice, until the user closes it, and the request underneath already offers only Cancel. Each details component is split into its button and its sheet, and the request sheets keep the sheet in the same keyed slot in both layouts, so the switch no longer unmounts it. The gorhom patch is dropped. Send's review sheet keeps the combined component, now built from the two pieces. The details sheets' close buttons get testIDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
🔵 Needs a closer look
Defer approval because this security-sensitive change spans WalletConnect signing guards and nested native sheet/error-boundary lifecycles, requiring independent iOS and Android validation of fail-closed behavior, rejection and cancellation handling, and teardown without signing bypasses.
No open issues 🎉
1 file not reviewed
mock-dapp/package-lock.json: Generated file
What changed in this PR
This pull request hardens dApp signing flows by preflighting transaction and authorization display data, failing closed when requests are unreadable, and adding full muxed contract (W…) address coverage.
Changes:
- Adds displayability checks, error boundaries, localized unreadable notices, and Cancel-only states for unsafe dApp requests.
- Preserves nested details sheets after render failures and prevents signing when display data is unavailable.
- Adds
Waddress fixtures/tests and mock dApp support for SAC transfers.
Review note: the swap review still renders its “Transaction details” button when useSignTransactionDetails returns null; the button and sheet should be gated together.
This pull request requires changes.
| File | Summary |
|---|---|
src/providers/WalletKitProvider.tsx |
Integrates displayability state, render-error handling, and approval blocking. |
src/i18n/locales/pt/translations.json |
Adds Portuguese unreadable transaction and authorization copy. |
src/i18n/locales/en/translations.json |
Adds English unreadable transaction and authorization copy. |
src/helpers/walletKitUtil.ts |
Defines method-specific fail-closed request logic. |
src/helpers/signingDisplay.ts |
Preflights transaction and authorization display conversions. |
src/components/screens/WalletKit/DappSignTransactionBottomSheetContent.tsx |
Adds Cancel-only transaction requests and persistent details-sheet handling. |
src/components/screens/WalletKit/DappSignAuthEntryBottomSheetContent.tsx |
Adds Cancel-only authorization requests and details-sheet handling. |
src/components/screens/WalletKit/DappRequestUnreadableContent.tsx |
Renders unreadable notices with a Cancel action. |
src/components/screens/WalletKit/DappRequestBottomSheetContent.tsx |
Propagates unreadable-request and render-error state. |
src/components/screens/WalletKit/DappAuthEntryDisplay.tsx |
Adds error boundaries around authorization invocation bodies. |
src/components/screens/WalletKit/DappAuthEntryDetailsBottomSheet.tsx |
Propagates authorization detail render errors. |
src/components/screens/WalletKit/DappAuthEntryDetails.tsx |
Separates the authorization details trigger from its persistent sheet. |
src/components/screens/SignTransactionDetails/types/index.ts |
Adds empty, ready, and undisplayable detail states. |
src/components/screens/SignTransactionDetails/SignTransactionDetails.tsx |
Separates the transaction details trigger from its sheet. |
src/components/screens/SignTransactionDetails/index.ts |
Exports the new transaction detail components. |
src/components/screens/SignTransactionDetails/hooks/useSignTransactionDetails.ts |
Builds display state and runs transaction preflight checks. |
src/components/screens/SignTransactionDetails/helpers/detailsState.ts |
Extracts displayable details from the state model. |
src/components/screens/SignTransactionDetails/components/UnreadableTransactionNotice.tsx |
Provides transaction and authorization unreadable notices. |
src/components/screens/SignTransactionDetails/components/SignTransactionDetailsBottomSheet.tsx |
Adds render-error handling and fallback notices. |
src/components/ErrorBoundary.tsx |
Adds reusable reporting and resettable render-error handling. |
mock-dapp/src/routes.ts |
Supports generated SAC transfer signing requests. |
mock-dapp/src/fixtures/sacTransferToMuxedContract.ts |
Builds native SAC transfers to muxed contract addresses. |
mock-dapp/README.md |
Documents the SAC transfer fixture and Node requirement. |
mock-dapp/public/index.html |
Adds the SAC transfer-to-W UI action. |
mock-dapp/package.json |
Adds Stellar SDK 17.4.0 and updates the Node engine. |
mock-dapp/package-lock.json |
Locks the new SDK and dependencies. |
__tests__/support/sacTransferFixtures.ts |
Provides reusable SAC transaction and authorization fixtures. |
__tests__/providers/WalletKitProvider.test.tsx |
Tests fail-closed approval behavior and reset handling. |
__tests__/helpers/walletKitUtil.test.ts |
Tests method-specific unreadable-request decisions. |
__tests__/helpers/soroban.test.ts |
Verifies W conversion and display-token behavior. |
__tests__/helpers/signingDisplay.test.ts |
Tests display preflight coverage and reporting. |
__tests__/helpers/signingDisplay.render.test.tsx |
Compares preflight results with actual rendering. |
__tests__/components/screens/WalletKit/DappSignTransactionBottomSheetContent.test.tsx |
Tests transaction Cancel-only states and details wiring. |
__tests__/components/screens/WalletKit/DappSignAuthEntryBottomSheetContent.test.tsx |
Tests authorization Cancel-only states and wording. |
__tests__/components/screens/WalletKit/DappRequestNestedDetailsSheet.test.tsx |
Verifies nested details-sheet persistence after render errors. |
__tests__/components/screens/WalletKit/DappAuthEntryDisplay.test.tsx |
Tests authorization rendering, W values, and boundary behavior. |
__tests__/components/screens/SignTransactionDetails/hooks/useSignTransactionDetails.test.ts |
Tests detail states, preflight failures, memoization, and fee bumps. |
__tests__/components/screens/SignTransactionDetails/components/SignTransactionDetailsBottomSheet.test.tsx |
Tests detail fallbacks, reporting, reset behavior, and callbacks. |
__tests__/components/screens/SignTransactionDetails/components/SignTransactionAuthorizations.test.tsx |
Verifies full W rendering in authorization arguments. |
__tests__/components/screens/SignTransactionDetails/components/Operations.test.tsx |
Verifies full W rendering under operation parameters. |
__tests__/components/screens/SignTransactionDetails/components/KeyVal.test.tsx |
Verifies display, expansion, and copying of W values. |
__tests__/components/ErrorBoundary.test.tsx |
Covers fallback rendering, reporting, callbacks, and reset semantics. |
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>
The boundary sat inside each card's expanded body. Collapsing and re-expanding a broken card remounted it, so the same error was reported again, and a throw outside the card bodies escaped it: the signer, a card title, or renderDetailContent itself, which runs while the display renders. One boundary around the display reports a broken entry once and catches all of those. When it trips, the notice replaces the whole entry instead of one card's body, as in the transaction details sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TL;DR
Signing screens now show muxed contract addresses (
W…) in full, in transaction details and in auth entries. On SDK 17.4.0 this needed no display change, like extension stellar/freighter#3045; new tests pin it on every signing surface.A dApp request (transaction or auth entry) whose details the app can't display can now only be rejected. The app builds the display data before it offers Confirm. If that fails, the request sheet shows "Unable to read this transaction", or "Unable to read this authorization" for an auth entry, with only Cancel, and the app refuses to sign the request whichever button asks it to.
A new error boundary wraps the transaction details and the whole auth-entry display, so a render error there shows the same notice instead of closing the app. On a dApp request a trip also switches the request underneath to the Cancel-only state. The details sheet stays open with its notice until the user closes it, so nothing closes on its own. On send and swap, which the app builds itself, it only shows the notice, and Confirm stays.
Closes stellar/wallet-eng-monorepo#108
Changed lines
Source code is the app code under
src/, translations included. Tests are the unit tests and their fixtures, and the mock dApp's code. Other is the mock dApp's lockfile,package.jsonand README.Screenshots and Videos
These are on testnet, with requests from the mock dApp.
Fail closed
The auth-entry wording
Waddresses render in full (no display change was needed on SDK 17.4.0)Implementation details (for agents)
Decisions (from the Protocol 30 design spec)
addressToStringkeeps throwing.validateAuthEntryAddressand history rely on that throw, and making it return a placeholder would weaken auth-entry validation, which the spec lists as an anti-goal. No render path needed a display-safe wrapper.Wrendering: on 17.4.0,Address.fromScAddresshandlesW, soaddressToString,scValToDisplayTokensandScValDisplayalready render it. Task 1 only adds tests, as in extension feat(signing): render W addresses and survive undecodable dApp XDR freighter#3045.What changed
signingDisplay.ts(new) dry-runs every conversion the details make that can throw: operation arguments, contract and deployer addresses, constructor arguments, auth invocations and their sub-invocations, and the CAP-71 signer address. Conversions that can't throw on decoded XDR aren't repeated; the module doc lists them, including the twotoXdr("base64")React keys. It exportsassertOperationsDisplayable,assertInvocationDisplayableandisAuthEntryDisplayable. A parse failure only leaves a breadcrumb; a decodable but undisplayable value is reported to Sentry.SignTransactionDetailsStateisempty | ready | undisplayable. It's built bybuildSignTransactionDetailsStateanduseSignTransactionDetailsState, andgetDisplayableDetailsunwraps it.useSignTransactionDetailskeeps its signature and still returns details ornull. The send and swap callers are unchanged: onlyWalletKitProvideruses the new state hook.nullcase:useSignTransactionDetailsnow also returnsnullwhen the post-parse dry-run fails, and it logs that witherrorinstead ofwarn, so it reaches Sentry. That's practically unreachable for transactions the app builds itself. When it happens it hides the "Transaction details" button, and Confirm is unaffected.WalletKitProvider.tsxL270-L315 derivesisRequestUndisplayablefrom three things: the transaction state,isAuthEntryDisplayablefor auth entries, and ahasDetailsRenderErrorflag that the boundary sets.isDappRequestUndisplayableapplies the rule per method:emptyfails closed for XDR methods, andsignMessagenever does.handleDappRequestrefuses to sign an undisplayable request and logs it. Every approval goes through it, including the Blockaid sheet's "Confirm anyway" (handleProceedAnyway), so this one check covers both.handleClearDappRequest's deferred reset, so the next request starts clean.DappRequestUnreadableContent(new) renders the header,UnreadableTransactionNoticeand one Cancel. Cancel is disabled while signing, matching the request buttons, so a rejection can't go out for a request that may still be signed. There are no banners, because the Blockaid banner opens a sheet that offers "Confirm anyway".DappSignTransactionBottomSheetContentandDappSignAuthEntryBottomSheetContent.variant. Transaction requests, send and swap keep the transaction wording.ErrorBoundary(new) reports to Sentry throughlogger.errorand shows a fallback. It resets whenresetKeychanges, throughgetDerivedStateFromProps.componentDidCatchcallsonErrorin afinally, so a throwing logger can't skip the fail-closed flag.SignTransactionDetailsBottomSheet), which covers send, swap and dApp requests, and the whole auth-entry display (DappAuthEntryDisplay). One boundary above the cards reports a broken entry once, also catches a throw outside a card's body, and its notice replaces the whole entry (8d296c61, from review).onErrorsetshasDetailsRenderError. The send flow'sSendReviewBottomSheet, which feat(send): support muxed contract (W) destinations #1036 changes, is untouched here.SignTransactionDetails.tsxandDappAuthEntryDetails.tsxare each split into a button and a sheet. The request sheets (transaction, auth entry) own the sheet's ref and render the sheet in the same keyed slot,details-sheet, in both layouts, so switching toDappRequestUnreadableContentdoesn't unmount it.SignTransactionDetails(default export), now built from the two pieces.sign-transaction-details-close-buttonanddapp-auth-entry-details-close-button.@gorhom/bottom-sheet'shandlePortalOnUnmount, which leaves a sheet on screen when its owner unmounts while the sheet is still opening. With the owner kept mounted, this PR no longer reaches that path.DappRequestNestedDetailsSheet.test.tsx(new) pins this for both sheets (see Tests).POST /session/:id/request/signXDRaccepts{ "fixture": "sacTransferToMuxedContract", "network", "to"? }.transferfrom the session's account to the SEP-23 vectorWA7Q…AWWC, or toto, with a source-account auth entry. It uses@stellar/stellar-sdk17.4.0, which is pinned next to the existing stellar-base and loaded only when the fixture is requested.tothat isn't an address gets a 400, since it's bad input. Anxdrmust be a non-empty string, as before.engines.nodebecomes>=22.12.0, which CI's Node 22 satisfies.signAuthEntrypreimage generator stays on stellar-base 14.1.0, which CI's e2e flows use.Tests: +9 suites and +114 tests over the base (
b80ca267, 334 suites and 4,831 tests). The full suite is 343 suites and 4,945 tests, all passing. Counts are Jest's, so eachit.eachcase counts.Wdisplay (nosrc/change):soroban.test.ts(+4): the W destination,addressToString→W…, the throw kept for unknown arms, and onescvAddresstoken.KeyVal.test.tsx(+1): renders, reveals and copies a W argument.Operations.test.tsx(+1): a W under thetolabel.SignTransactionAuthorizations.test.tsx(+1) andDappAuthEntryDisplay.test.tsx(+7): a W in auth entries, and the auth-entry boundary, whose notice in Authorization Entry Details uses the authorization wording, catches a throw in the display's own render, and reports a broken entry once.__tests__/support/sacTransferFixtures.ts.signingDisplay.test.ts(22): the dry-run covers every throwing conversion the render makes, and Sentry vs breadcrumb is right for each failure kind.signingDisplay.render.test.tsx(18, new): the pre-flight and the real render agree. For a SAC transfer to each fixture recipient (threeWs, aC, aGand anM), the transaction, a fee bump of it and its auth entry pass the pre-flight and render without tripping a boundary. A render-only throw fails all 18.useSignTransactionDetails.test.ts(18):empty,readyandundisplayablecases, including a fee bump whose inner transaction can't be displayed, state stability, and the unchangeduseSignTransactionDetailscontract.walletKitUtil.test.ts(+6):isDappRequestUndisplayableper method, and the render-error flag.ErrorBoundary.test.tsx(7): fallback, a single report,onErroreven when reporting throws, reset onresetKey, and no remount or report whenresetKeychanges without an error.SignTransactionDetailsBottomSheet.test.tsx(8): the notice, the Sentry report,onRenderError, notice-only on send and swap, and reset per transaction.DappSignTransactionBottomSheetContent.test.tsx(6) andDappSignAuthEntryBottomSheetContent.test.tsx(6):DappRequestNestedDetailsSheet.test.tsx(3, new): when a render error in the details fails the request closed, the details sheet stays mounted and open with its notice, its close button still closes it, and the request underneath offers only Cancel, for Transaction details and for Authorization Entry Details. A request that can't be displayed from the start mounts no details sheet. A stub ofcomponents/BottomSheetrecords mounts and unmounts; both sheet cases fail on the previous layout, which unmounted the sheet.WalletKitProvider.test.tsx(6), a provider-level harness:Verification
yarn testpasses.yarn lint:tspasses.maintoo:mainhas 111, and the branch removed 5 of them inKeyVal.test.tsx.mock-dapp/public/index.html, which already failed onmain; its 60 new lines follow the file's existing style.yarn lint:translationsreports no missing translations. Its 9 errors areimport/orderandno-continuein 4 untouched files, the same as onmain.IS_E2E_TEST=true. That setting is needed because the WalletConnect origin check only accepts https dApps. (d) and (f) were re-run at7ce3ac83, without the gorhom patch, on the iPhone 17 Pro and the Pixel_10_Pro; the second (a) ran on9b78fa38.W: Confirm offered;toshows the fullWA7Q…AWWC, tapping revealsscvAddress; same W under Authorizations → transfer; approve signsScValDisplayfor addresses, in Transaction details and in Authorization Entry Details: Confirm still offered before; after the trip the details sheet stays open with its notice; closing it shows the request with the notice and only Cancel; Cancel rejects8d296c61, on the iPhone SE (3rd generation) simulator, iOS 27.2, with a throw forced locally in the auth-entry display's own render (reverted): Authorization Entry Details shows only "Unable to read this authorization", in place of the whole entry, and closing it leaves the request with the notice and only Cancel.f654834a. After (d)'s injected trip in Authorization Entry Details, the auth-entry sheet reads "Unable to read this authorization", where it read "Unable to read this transaction" before, and a malformedsignXDRstill reads "Unable to read this transaction". The screenshots are under "Screenshots and Videos" at the top./responseprints it aserror, notrejected, because it only recognises thrownErrorobjects. That mapping predates this PR, and the same happens for a displayable request.Follow-ups / out of scope
@gorhom/bottom-sheet'shandlePortalOnUnmountearly return upstream: a modal whose owner unmounts while it is still opening is left on screen. This PR no longer reaches it, but other screens could.ducks/walletKit↔helpers/walletKitUtil. The provider test un-mocks the duck because of it.withAddress(CAP-71) auth-preimage variant still throws, because stellar-base 14.1.0 lacks that arm. Nothing in CI uses it. Porting the generator to SDK 17 would fix it./responsereports wallet rejections aserror(pre-existing).🤖 Generated with Claude Code