Sample audits · Earlier documentation audits

Solana Mobile documentation and SDKs, June 2026

In June 2026 we audited the Solana Mobile developer documentation and SDKs and shared the full report with Solana Mobile. In the months that followed, ten of the issues it identified were corrected on the very pages it cited (eight in full, two in part), in the public commits listed below. We did not receive a reply, and the changes do not reference the report. We publish the complete June findings here, together with a fresh audit of the current documentation, so readers can follow what has changed and what remains open.

AuditedSolana Mobile developer documentation, the Mobile Wallet Adapter libraries and specification, the publishing CLI and the tutorial applications
Date9 June 2026
How it ranearlier documentation audit; severities are as rated in that report and were not re-rated
Re-check, 11 October 20268 fixed and 2 partly fixed on the pages the report cited; the other findings were not re-checked on that date

Public commits to the Solana Mobile documentation that corrected the cited pages:

Related: Solana Mobile developer documentation (October 2026) · Solana Mobile SDKs: mobile-wallet-adapter and seed-vault-sdk (October 2026) · Solana Mobile stack, third audit (September 2026)

21
Critical or High as first rated
25
Medium as first rated
25
Low as first rated
6
Advisory as first rated
2
withdrawn by us

Severities as published in our original report; these findings were not re-reviewed by hand the way the sample audits were. The 11 October 2026 re-check status is shown under each finding where it maps.

All June findings

B1. Medium MWA-protocol spec version-header inconsistency
Location: mobile-wallet-adapter/spec/spec.md

mobile-wallet-adapter/spec/spec.md line 19 declares **Version: 2.0.0** but the changelog table (lines 23-27) lists three releases: 1.0.0, 2.0.0, and 2.1.0 — Add optional wallet icon parameter to authorize response. A 2.1.0 release row exists in the changelog without bumping the header version field. Reader cannot tell which version the spec body describes.

Suggested fix, not tested

align header **Version:** field with most-recent changelog entry; OR mark un-released changelog rows explicitly (e.g. "2.1.0 — DRAFT").

B2. High chain vs cluster parameter — three-way inconsistency at canonicity layer
Location: recipes/, get-started/react-native/

Fixed on the page our report cited; still in place on 11 October 2026

Docs site uses FOUR forms across four files: (a) get-started/react-native/setup.md prop table line 38 — chain: string (canonical). (b) get-started/react-native/invoke-mwa-sessions-directly.md lines 96, 258, 448 — cluster: 'solana:devnet' (deprecated form ×3); line 146 — chain: 'solana:devnet' (canonical ×1). (c) recipes/mobile-wallet-adapter/caching-wallet-authorization.md line 193 — cluster: "devnet" (legacy pre-1.0 form, bare cluster name without solana: prefix). (d) recipes/solana-development/anchor-integration.md lines 83, 96, 193 — cluster: RPC_ENDPOINT (variable contains an RPC URL like https://api.devnet.solana.com, not a chain identifier — WRONG VALUE TYPE regardless of key name).

Suggested fix, not tested

Standardize on chain: 'solana:<network>' per current spec; mark cluster: examples as deprecated form with migration pointer; correct cluster: RPC_ENDPOINT to chain: 'solana:devnet' separately from passing RPC_ENDPOINT to new Connection(...).

B5. Critical Code-sample syntax errors — })); over-close in Anchor recipe
Location: recipes/solana-development/anchor-integration.md

Fixed on the page our report cited; still in place on 11 October 2026

recipes/solana-development/anchor-integration.md has three locations with extra ) closing in wallet.authorize(...) invocation: lines 85 (}));), 98 (}));), 195 (}));). The authorize call opens with await wallet.authorize({ (one {, one () and should close with }) — the snippets close with })). Copy-paste reproduction fails to compile.

Suggested fix, not tested

Remove the extra ) at each location.

B6. High Code-sample bugs — missing comma in APP_IDENTITY object literal
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Fixed on the page our report cited; still in place on 11 October 2026

get-started/react-native/invoke-mwa-sessions-directly.md defines APP_IDENTITY twice (lines 88-92, 134-138), both with uri: 'https://yourdapp.com' immediately followed (without a comma) by icon: "favicon.ico". JavaScript object literal requires comma between properties; this is a syntax error. Two occurrences.

Suggested fix, not tested

Add trailing comma after uri: value at both locations.

B7. Critical Non-async transact callback contains await
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Fixed on the page our report cited; still in place on 11 October 2026

invoke-mwa-sessions-directly.md Versioned Transactions tab (line 255) and Legacy Transactions tab (line 321) both invoke await transact((wallet) => { — the callback is NOT declared async but the body uses await wallet.authorize(...). JavaScript syntax error at module evaluation.

Suggested fix, not tested

Mark callbacks async (wallet) => at both locations.

B8. High Wrong imports from @solana/web3.js
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Fixed on the page our report cited; still in place on 11 October 2026

invoke-mwa-sessions-directly.md lines 247-252 (Versioned) and 313-318 (Legacy) import sendTransaction and confirmTransaction as top-level exports of @solana/web3.js. Both are methods on the Connection class, not top-level exports. Build-time error "Module has no exported member 'sendTransaction'".

Suggested fix, not tested

Remove these imports; call connection.sendTransaction(...) / connection.confirmTransaction(...) instead.

B9. High Missing await and ,→. typo in signMessages call
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Partly fixed on the page our report cited; re-checked 11 October 2026

invoke-mwa-sessions-directly.md lines 453-456: const signedMessages = wallet.signMessages({ lacks await; AND addresses: [authorizationResult.accounts[0].address]. ends with a DOT instead of a COMMA before payloads: [...]. Two bugs in 4 lines. Code parses to access a property .payloads on an array, then () invocation — runtime error.

Suggested fix, not tested

Prepend await; replace ]. with ],.

B10. High Unclosed transact() parenthesis in SIWS sample
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Fixed on the page our report cited; still in place on 11 October 2026

invoke-mwa-sessions-directly.md SIWS example (lines 184-197): const signInResult = await transact(async (wallet: Web3MobileWallet) => { ... }); — but the snippet ends at line 197 with } only; the closing ); is missing. Causes syntax error.

Suggested fix, not tested

Add ); after the closing }.

B11. High Wrong field access authorizationResult.publicKey
Location: recipes/solana-development/anchor-integration.md

Fixed on the page our report cited; still in place on 11 October 2026

recipes/solana-development/anchor-integration.md line 210 sets feePayer: authorizationResult.publicKey,. Per MWA spec + reference docs, AuthorizationResult exposes accounts (array) with accounts[0].address (base64-encoded). .publicKey is not a defined field — yields undefined, transaction construction silently broken.

Suggested fix, not tested

Replace with feePayer: new PublicKey(toByteArray(authorizationResult.accounts[0].address)).

B12. High Groovy single-quote literal in Kotlin DSL
Location: recipes/solana-development/using-anchor-programs.md

recipes/solana-development/using-anchor-programs.md line 23 inside a build.gradle.kts block: implementation('io.github.funkatronics:kborsh:${versions.KOTLIN_KBORSH_VERSION}'). Single-quote string literal is Groovy DSL syntax; Kotlin DSL (.kts) requires double quotes. Other two lines in same block correctly use "...". Build fails on copy.

Suggested fix, not tested

Replace '...' with "...".

B13. Medium Hanging continuation backslash in npm install block
Location: get-started/react-native/invoke-mwa-sessions-directly.md

Fixed on the page our report cited; still in place on 11 October 2026

invoke-mwa-sessions-directly.md lines 36-39 (npm tab inside <CodeGroup>): npm install <pkg1> \ newline <pkg2> \ newline — a TRAILING backslash after the second package name with no further line continuation. Shell waits for input on npm install.

Suggested fix, not tested

Remove trailing backslash from final package line.

B14. Medium Kotlin null-safety / non-Boolean truthiness in send-transaction example
Location: recipes/solana-development/using-anchor-programs.md

using-anchor-programs.md lines 199-203 use if (response.result) {...} else if (response.error) {...}. response.result is a value (likely a String or nullable String), not Boolean — Kotlin if requires Boolean; should be if (response.result != null). Same issue for response.error. Additionally response.error.message accesses a field without !! or ?.. Reader copy-pastes; compiler rejects.

Suggested fix, not tested

Rewrite as val result = response.result; if (result != null) { ... } else { response.error?.let { println("Failed: ${it.message}") } } or analogous.

B15. Medium Undocumented placeholder identifiers in Kotlin dependency block
Location: recipes/solana-development/using-anchor-programs.md

using-anchor-programs.md build.gradle.kts uses ${versions.KOTLIN_WEB3_SOLANA_VERSION}, ${versions.KOTLIN_RPC_CORE_VERSION}, ${versions.KOTLIN_KBORSH_VERSION} without narrating where versions comes from. Reader copy-pastes; gradle fails to resolve property. Convention is gradle/libs.versions.toml + libs.kotlin.web3.solana.get() or similar.

Suggested fix, not tested

Either inline concrete versions (consistent with kotlin/installation.md line 20-23 which uses literals like :2.0.3) OR add a gradle.properties snippet showing versions.KOTLIN_*=….

B16. Medium Plaintext signing-key passwords in build.gradle example
Location: dapp-store/build-and-sign-an-apk.md

dapp-store/build-and-sign-an-apk.md Native Android tab (lines 109-114) recommends inline storePassword "your_keystore_password" + keyPassword "your_key_password" in app/build.gradle. Even with placeholder strings, this is an explicit anti-pattern (keystore passwords committed alongside source). Vendor's own publisher policy at dapp-store/publisher-policy.md does not enforce this, but consumer-side outcome is leaked credentials.

Suggested fix, not tested

Demonstrate the gradle.properties (gitignored) + signingConfigs { dappStore { storePassword providers.gradleProperty('KEYSTORE_PASS').get(); ... } } pattern; add note "never commit keystore passwords".

B17. Medium Deprecated signTransactions documented as primary API without deprecation marker
Location: get-started/react-native/invoke-mwa-sessions-directly.md, mobile-wallet-adapter/spec/spec.md

mobile-wallet-adapter/spec/spec.md line 149 explicitly lists solana:signTransactions under Deprecated Features: "deprecated, but can be supported by wallet endpoints to maintain backwards compatibility". Reader treats both as live current API.

B18. Low Sentence fragment / hanging comma in migration recipe
Location: recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md

Partly fixed on the page our report cited; re-checked 11 October 2026

recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md line 34: *"Ensure registerMwa is invoked in a non-SSR context. if you're using a framework with Server Side Rendering (e.g Next.js),"* — sentence ends with a trailing comma. Reader unsure if instruction is complete.

Suggested fix, not tested

Complete the sentence ("...wrap the registration in a useEffect hook or invoke from a client-only entry point.")

B19. Low Typo assocication
Location: get-started/react-native/invoke-mwa-sessions-directly.md

invoke-mwa-sessions-directly.md line 30 — *"Calling transact dispatches an assocication intent..."*. Should be "association".

Suggested fix, not tested

Spelling fix.

B20. Low Typo ProgramDerivedAddres in narrative
Location: recipes/solana-development/using-anchor-programs.md

using-anchor-programs.md line 54 references ProgramDerivedAddres (missing one s) in narrative text. The code block below correctly uses ProgramDerivedAddress. Reader scanning the narrative for the class name may search for the wrong identifier.

Suggested fix, not tested

Spelling fix.

B21. Low JSX inflation at index.md
Location: index.md, sample-apps/sample_app_overview.md

index.md is ~95% JSX (<HeroCard>, <CardGroup>, <Card>, <Columns>, inline <div> with inline CSS) — out of 125 lines, only ~5 carry semantic content. Mirror snapshot serving any plain-markdown reader (LLM ingestion, offline browser, this audit) loses navigational structure. sample_app_overview.md similarly duplicates content across 4 Tab variants (All / RN / Android / Testing) with identical Card bodies — ~600 lines of mostly-JSX vs ~20 unique items.

Suggested fix, not tested

If vendor maintains LLM-readable / plain-markdown surface (which Mintlify llms.txt + .md append trick suggests they do), strip JSX wrappers OR provide markdown fallback bullets. Consumer-side: token-budget-tradeoff acceptable for ingestion.

B22. Info Mirror coverage at split substrate (known limitation, mitigated)
Location: mirror root

Mock MWA Wallet, Kotlin MobileWalletAdapter class implementation, and full TypeScript SDK source all live in repos.

B23. High Origin attestation flow (attest_origin / ERROR_ATTEST_ORIGIN_ANDROID) is undocumented at docs.solanamobile.com
Location: mobile-wallet-adapter/spec/spec.md lines 1077–1161 vs SolanaMobileDocs_full/ (zero coverage)

MWA spec lines 1056–1161 specify a sophisticated Web-dapp identity verification flow involving (a) Trusted Web Activities + Digital Asset Links to bring browser security model into wallet trust, (b) wallet-hosted HTML attestation script with keypair, (c) ERROR_ATTEST_ORIGIN_ANDROID error with context/challenge/attest_origin_uri data, (d) dApp loads attestation script in invisible iframe, (e) postMessage origin-attestation handshake, (f) signed-challenge return + retry of authorize. Mirror grep for attest_origin|ERROR_ATTEST_ORIGIN|origin.attestation over SolanaMobileDocs_full/ returns ZERO matches across all 57 docs pages. This is the SOLE mechanism by which web dApps can be identity-verified by wallets per the spec. Without consumer-facing documentation, web dApp integrators cannot implement origin attestation and wallets must either reject all web dApps as unverified OR extend trust without verification.

Suggested fix, not tested

author a dedicated documentation page covering origin attestation flow with example wallet-side attestation script + dApp-side iframe loader. Reference from recipes/mobile-wallet-adapter/ and from recipes/mobile-wallet-adapter/migrating-to-wallet-standard.md (since web dApps using Wallet Standard registration must implement origin attestation if calling wallets enforce it).

B24. High Nostr transport (MWA 2.1 addition) is undocumented at docs.solanamobile.com
Location: mobile-wallet-adapter/spec/spec.md lines 142–350 vs SolanaMobileDocs_full/ (zero coverage)

MWA spec lines 142–350 define an entire new transport mechanism: Nostr relay events of kind 20012 (provisional NIP), session_identifier derivation via SHA-256 of association_token, CONNECT event flow, SESSION_END signal, P-256 ECDSA Nostr keypair generation. Mirror grep for Nostr|nostr over SolanaMobileDocs_full/ returns ZERO matches. The Nostr URI scheme solana-wallet:/v1/associate/{local|remote}/nostr has no consumer-facing reference. NEW protocol feature with no migration / quickstart / recipe documentation. Wallet endpoints implementing only the legacy WebSocket transport will silently fail association attempts from MWA 2.1 dApps using Nostr transport.

Suggested fix, not tested

author a dedicated documentation page covering Nostr transport (when to choose; how to configure; reflector vs Nostr comparison; relay selection criteria; security model). Update solana-mobile-stack/mobile-wallet-adapter.md to enumerate transports including Nostr.

B25. High Spec JSON examples use curly quotes throughout — copy-paste fails JSON.parse()
Location: mobile-wallet-adapter/spec/spec.md (5+ ranges enumerated)

mobile-wallet-adapter/spec/spec.md lines 591–603 (authorize Params), 622–646 (authorize Result), 716–723 (deauthorize Params), 1099–1108 (authorize+clone_authorization Params modifications), 1140–1147 (ERROR_ATTEST_ORIGIN_ANDROID error data) — all use curly quotes (Unicode U+201C " and U+201D ") instead of straight ASCII quotes (U+0022 "). Multiple JSON examples affected. A reader extracting these as JSON literals via copy-paste gets parse errors. Pervasive across the spec body. Examples elsewhere in spec (lines 230, 261, 293, 322, 357, 412–425, 449–462) DO use straight quotes — inconsistency suggests JSON examples were composed/edited in a tool that auto-curlied quotes for some sections.

Suggested fix, not tested

bulk replace " → " and " → " within fenced JSON code blocks (lines 591–603, 622–646, 716–723, 1099–1108, 1140–1147 at minimum; full grep recommended). Add CI gate forbidding curly quotes inside fenced code blocks.

B26. Low Typo qd signature verification at spec line 499
Location: mobile-wallet-adapter/spec/spec.md line 499

— lowercase qd should be Qd to match the X9.62-encoded public keypoint identifier used everywhere else in the spec.

Suggested fix, not tested

Spelling fix.

B27. Medium chain accepted-values enumeration mixes canonical + legacy without deprecation markers
Location: mobile-wallet-adapter/spec/spec.md line 612

Spec line 612: *"chain: (optional) if set, the chain identifier... supported values include solana:mainnet, solana:testnet, solana:devnet, mainnet-beta, testnet, devnet."* The canonical CAIP-2 values (solana:*) and the legacy bare values (mainnet-beta/testnet/devnet) appear coequal. The cluster PARAMETER is labeled as alias-deprecated at line 617, but the legacy bare VALUES of chain have no (deprecated) annotation within the accepted-values list. The deprecation lives at the parameter-name axis (cluster is alias) and at the value-format axis (bare values should be migrated to CAIP-2) but the latter is undeclared in the spec body.

Suggested fix, not tested

Reformat spec line 612 as "supported values: canonical (CAIP-2) solana:mainnet, solana:testnet, solana:devnet; legacy mainnet-beta, testnet, devnet (will be removed in MWA 3.0; migrate to CAIP-2 form)" — separate the two value families with an explicit deprecation timeline.

B28. Low wait_for_commitment_to_send_next_transaction option has no version annotation
Location: mobile-wallet-adapter/spec/spec.md line 819

Spec line 819 introduces this option for sign_and_send_transactions without indicating whether it's an MWA 2.0 or 2.1 addition. Wallet implementations need to determine support; absence of version annotation forces consumer-side capability probing via get_capabilities or empirical testing.

Suggested fix, not tested

Add changelog row with version-introduction; annotate at parameter site (wait_for_commitment_to_send_next_transaction: (since MWA 2.x)).

B29. Medium Minty-fresh ships a documented workaround for solana.core.Message.from(byteArray) deserialization bug
Location: Minty-fresh/libs/mintycore/src/main/java/com/solanamobile/mintyfresh/mintycore/usecase/PerformMintUseCase.kt lines 217–225

Minty-fresh/libs/mintycore/src/main/java/com/solanamobile/mintyfresh/mintycore/usecase/PerformMintUseCase.kt lines 217–225: code comment "there is a deserialization bug in solana.core.Message.from(byteArray) so have to build up the Message (and Transaction) object manually (for now)". The workaround manually rebuilds the Transaction object from recentBlockhash + feePayer + instructions + signature instead of using the deserializer. The vendor's own reference app shipping with a documented library-bug workaround is non-trivial; the bug appears to be long-standing ((for now) suggests acknowledged but unfixed). Consumer-side integrators following this reference app will adopt the workaround without surface awareness of the underlying bug.

Suggested fix, not tested

fix the underlying solana.core.Message.from(byteArray) deserialization bug at the SDK level; OR document the workaround as a permanent integration recipe in recipes/solana-development/; OR mark Message.from(byteArray) as @Deprecated with replacement pointer.

B30. Low Hardcoded 600ms delay in Minty-fresh confirmation polling
Location: Minty-fresh/libs/mintycore/.../PerformMintUseCase.kt line 246

Minty-fresh/libs/mintycore/.../PerformMintUseCase.kt line 246: delay(600) before first confirmation poll attempt. Inline-comment line 245: // https://www.validators.app/ping-thing.

Suggested fix, not tested

Extract const val INITIAL_CONFIRMATION_POLL_DELAY_MS = 600 with KDoc citing the rationale + external reference.

B31. Medium Kotlin SDK default blockchain is Solana.Devnet
Location: mobile-wallet-adapter/android/clientlib-ktx/src/main/java/com/solana/mobilewalletadapter/clientlib/MobileWalletAdapter.kt line 38

MobileWalletAdapter.kt line 38: var blockchain: Blockchain = Solana.Devnet. Production apps integrating the SDK must explicitly set Solana.Mainnet; forgetting silently directs the dApp to devnet. The DEFAULT for a public SDK is the devnet form (development environment), not the mainnet form (production environment). This is fail-open behavior for new integrators.

Suggested fix, not tested

change default to Solana.Mainnet; OR add a require() check on first transact() invocation if blockchain is unset; OR document the default prominently in get-started/kotlin/setup.md with a "you MUST set this for production" callout.

B32. Low Subprotocol enumeration mismatches between docs and spec
Location: docs vs spec

The docs site does not enumerate either subprotocol name (mirror grep). Consumer code attempting to manually upgrade WebSocket connections lacks this reference.

Suggested fix, not tested

add subprotocol reference in solana-mobile-stack/mobile-wallet-adapter.md (low cost; informational).

B33. Low Spec line 332 walet typo
Location: mobile-wallet-adapter/spec/spec.md line 332

— "This allows the walet endpoint to cater its UX accordingly..." (line 332). Single character missing.

Suggested fix, not tested

Spelling fix (walet → wallet).

B34. High Wallet-side origin-attestation enforcement is unimplemented in walletlib
Location: mobile-wallet-adapter/android/walletlib/src/main/java/.../protocol/MobileWalletAdapterServer.java (zero matches for attest_origin|ERROR_ATTEST_ORIGIN|originAttestation across walletlib body) + .../scenario/AuthorizeRequest.java lines 135–142

MWA spec lines 1056–1161 define ERROR_ATTEST_ORIGIN_ANDROID (code -100) as the wallet endpoint's mechanism to require web-dApp challenge-response identity verification. MobileWalletAdapterServer.java enumerates exactly 5 error codes (ERROR_AUTHORIZATION_FAILED, ERROR_NOT_SIGNED, ERROR_INVALID_PAYLOADS, ERROR_NOT_SUBMITTED, ERROR_TOO_MANY_PAYLOADS, ERROR_CLUSTER_NOT_SUPPORTED per lines 220–222, 629–637, 996–1010) — **zero handling of ERROR_ATTEST_ORIGIN_ANDROID**. Scenario layer AuthorizeRequest.java exposes completeWithDecline() (line 135) and completeWithClusterNotSupported() (line 139) but no completeWithAttestOriginAndroid(context, challenge, attest_origin_uri). Wallet apps building on walletlib cannot implement spec lines 1056–1161 origin-attestation enforcement without forking walletlib. This is the SOLE web-dApp identity verification mechanism per spec.

Suggested fix, not tested

add MobileWalletAdapterServer.ERROR_ATTEST_ORIGIN_ANDROID = -100 constant; add AuthorizationResult overload or new AuthorizeRequest.completeWithAttestOriginAndroid(context: ByteArray, challenge: ByteArray, attestOriginUri: Uri) callback signature; wire scenario → MobileWalletAdapterServer to emit {code: -100, message, data: {context, challenge, attest_origin_uri}} per spec lines 1140–1149. Document wallet-side reference attestation script + Digital Asset Links flow at recipes/mobile-wallet-adapter/.

B35. High Walletlib transport has zero Nostr support despite spec defining Nostr as MWA 2.1 transport
Location: walletlib association/RemoteAssociationUri.java lines 33–39 (validate only REMOTE_PATH_SUFFIX); transport/websockets/ReflectorWebSocket.java lines 63–66 (WEBSOCKETS_PROTOCOL + WEBSOCKETS_BASE64_PROTOCOL only)

Spec lines 142–350 define Nostr URI form solana-wallet:/v1/associate/{local|remote}/nostr + kind-20012 relay events + session_identifier derivation via SHA-256 of association_token + P-256 ECDSA Nostr keypair. RemoteAssociationUri.java (78 lines) handles only /v1/associate/remote/ reflector form with parseReflectorHostAuthority + parseReflectorId; **no RemoteNostrAssociationUri analog exists** in association/. AssociationUri.parse (lines 82–92) tries Local + RemoteWebSocket variants only and silently returns null on parse failure. Wallet apps based on walletlib are silently incompatible with MWA 2.1 Nostr-originated association URIs — the URI doesn't parse, returns null, and connection fails opaquely.

Suggested fix, not tested

implement RemoteNostrAssociationUri parsing /v1/associate/remote/nostr URIs + Nostr-transport scenario class; OR explicitly document at walletlib README + spec that walletlib is a WebSocket-only reference implementation and Nostr transport requires a separate library.

B36. Medium MobileWalletAdapterServer.AuthorizationResult constructor throws when walletIcon is null despite @Nullable field annotation
Location: walletlib protocol/MobileWalletAdapterServer.java lines 355–356 + 385–411

Lines 355–356 declare @Nullable public final Uri walletIcon;. Lines 405–410 constructor body: if (walletIcon != null && walletIcon.getScheme() != null && walletIcon.getScheme().equals("data")) { this.walletIcon = walletIcon; } else { throw new IllegalArgumentException("wallet icon URI must be a data URI"); }. **If walletIcon == null, the if is false → else branch throws**. The deprecated constructor chain at lines 385–391 calls this(..., null, signInResult) passing null walletIcon → runtime IAE. Any caller using the deprecated overload to preserve backward compatibility instead hits unconditional IAE. Annotation lie.

Suggested fix, not tested

either (a) accept null walletIcon (allow this.walletIcon = walletIcon when null), or (b) remove @Nullable annotation, or (c) update deprecated constructor chain to pass a sentinel default walletIcon. Add unit test verifying deprecated-constructor call path.

B37. Medium BaseScenario.finalize() (lines 125–127) uses Java's deprecated finalize() for Looper cleanup
Location: walletlib scenario/BaseScenario.java lines 124–127 + 172

finalize() is deprecated since Java 9 (JEP 421 removal in Java 18+), runs unpredictably under memory pressure on Android, and may not run at all in low-memory conditions. Using it to call mIoLooper.quitSafely() is fragile — the Looper may stay alive indefinitely (resource leak). Modern Android pattern is explicit close() (already declared abstract at line 172) or AutoCloseable. Coverage of close() callers is operator-deferred — the abstract method exists but is not enforced via try-with-resources.

Suggested fix, not tested

remove finalize(); document that callers MUST invoke close() explicitly; OR implement AutoCloseable to enable try-with-resources idiom. Consider adding a logging-only finalizer warning if close() was not called (current Android idiom).

B38. Medium Walletlib uses assert(...) for runtime safety checks throughout — disabled by default in production
Location: walletlib (multiple files; enumerated above)

Java assertions require -ea JVM flag to evaluate; on Android production builds assertions are NEVER evaluated. Walletlib uses assert(...) for non-trivial preconditions: BaseScenario.java lines 132 (mSessionEstablishedFuture == null), 140 (mActiveSessionId == null && mSessionEstablishedFuture != null), 148 (mSessionEstablishedFuture != null); MobileWalletAdapterServer.java lines 235, 649, 650, 1021, 1022 (result-non-null + signed-payload-length checks); ReflectorWebSocket.java lines 71 (mState == CONNECTING || mState == CLOSED), 87–88 (mState == CONNECTED || ... || ... CLOSING), 106–107, 126 (mState != NOT_CONNECTED); AuthRepositoryImpl.java lines 234 (!authRecord.isRevoked()), 266 (payloadHmac.length == HMAC_LENGTH_BYTES), 422 (!authRecord.isRevoked()). These checks fail-open in production — preconditions are silently violated rather than raising IllegalStateException. **Library code SHOULD use defensive checks (Preconditions.checkState, Objects.requireNonNull, explicit IAE throws)** that survive production builds.

Suggested fix, not tested

replace assert(...) with if (!cond) throw new IllegalStateException(...) for hard invariants; OR if (BuildConfig.DEBUG) check(cond) for debug-only checks; OR document at module README that assertions are advisory only. Lint rule (UseObjectsRequireNonNull / Error Prone AssertEqualsArgumentOrderChecker family) could enforce.

B39. Low Long-standing TODO (#44): support multiple addresses at BaseScenario.java line 369 — signMessages rejects multi-address requests
Location: walletlib scenario/BaseScenario.java lines 361–370

Spec defines sign_messages addresses as a JSONArray (server line 767–778 unpacks array). BaseScenario catches IllegalArgumentException from the SignMessagesRequest constructor and completes with RequestDeclinedException("Unexpected address; not signing message"). Comment indicates the issue tracker bug #44 — multi-year limitation (file Copyright 2024 but TODO predates per typical lifecycle). Consumer-side dApps requesting multi-address sign_messages against walletlib-based wallets are silently rejected.

Suggested fix, not tested

track and close #44; OR document at spec line 765 that single-address-per-request is the current implementation limit (positive-form documentation if intentional).

B40. Low Auth-token format is HMAC-prefixed JSON, NOT JWT — but code calls it "JWT"
Location: walletlib authorization/AuthRepositoryImpl.java line 186 (comment)

AuthRepositoryImpl.java line 186 comment: "Look up the identity secret key for the key specified in this JWT". Actual format (lines 241–272): JSON object {typ, iid, tid} UTF-8-encoded + 32-byte HMAC-SHA256 appended + Base64. Not JWT (no header.payload.signature dot-separated structure; not Base64URL; no alg field; no header at all). Naming gap may mislead reviewers expecting JWT semantics — e.g., assuming JWT signature verification interop with standard jose/jjwt libraries.

Suggested fix, not tested

rename comment to "Look up the identity secret key for the key specified in this auth token"; OR migrate format to actual JWT (JWS Compact Serialization) for interop. Code review hygiene fix.

B41. Low revokeNonReissuableAuthRecord treats authRecordAgeMs < 0 (clock-backwards) as "issued in the future" and revokes the auth record
Location: walletlib authorization/AuthRepositoryImpl.java lines 282–300

AuthRepositoryImpl.java lines 287–289: if (authRecordAgeMs < 0) { Log.w(TAG, "AuthRecord issued in the future; revoking"); revoke = true; }. Routine system events that move clock backwards (NTP sync after offline period, daylight savings end, manual user clock change, OS timezone correction) silently revoke ALL auth records whose issued timestamp now exceeds System.currentTimeMillis(). User experience: wallet app loses all dApp authorizations without explanation; all dApps must re-authorize. Zero clock-skew tolerance.

Suggested fix, not tested

introduce a CLOCK_SKEW_TOLERANCE_MS (e.g., 5 minutes) and only revoke if authRecordAgeMs < -CLOCK_SKEW_TOLERANCE_MS; OR use monotonic clock for age computation (Android SystemClock.elapsedRealtime() paired with a calibration offset). Document the time-source dependency.

B42. Medium Kotlin Compose scaffold disconnect() does NOT call walletAdapter.deauthorize() — cross-scaffold inconsistency with RN scaffold
Location: solana-kotlin-compose-scaffold/app/src/main/java/.../viewmodel/MainViewModel.kt lines 259–270 vs solana-mobile-dapp-scaffold/template/components/providers/AuthorizationProvider.tsx lines 128–137

solana-kotlin-compose-scaffold/.../MainViewModel.kt lines 259–270 disconnect() only invokes persistenceUseCase.clearConnection() (clears local SharedPreferences). The wallet endpoint retains the auth token until natural expiry or purge. By contrast, solana-mobile-dapp-scaffold/template/components/providers/AuthorizationProvider.tsx lines 128–137 deauthorizeSession correctly calls await wallet.deauthorize({auth_token: authorization.authToken}). Same vendor publishes two scaffolds with divergent deauthorization discipline: consumer apps copying the Kotlin scaffold inherit the gap (wallet-side auth_token persists indefinitely; privacy + auth-lifetime concern); consumer apps copying the RN scaffold do not.

Suggested fix, not tested

update Kotlin scaffold disconnect() to invoke walletAdapter.transact(sender) { deauthorize(authResult.authToken) } (or equivalent clientlib idiom) before clearing local persistence; verify cross-scaffold parity at next vendor release; add scaffold-parity CI check.

B43. Medium Kotlin Compose scaffold DI module silently inherits SDK Devnet default
Location: solana-kotlin-compose-scaffold/app/src/main/java/.../di/SolanaKotlinComposeScaffoldModule.kt lines 30–36

solana-kotlin-compose-scaffold/.../di/SolanaKotlinComposeScaffoldModule.kt lines 30–36 providesMobileWalletAdapter constructs MobileWalletAdapter(connectionIdentity = ConnectionIdentity(...)) without specifying blockchain. Scaffold pattern propagates the fail-open default: consumer integrators copying the scaffold for production deploys silently ship a Devnet-pointed wallet adapter unless they remember to override. No inline comment / TODO warns the consumer.

Suggested fix, not tested

either (a) set explicit blockchain = Solana.Mainnet in the scaffold DI module (since scaffolds usually point users at the production-target default) with a comment noting "switch to Solana.Devnet for development"; (b) add a prominent block comment // REPLACE-ME: set blockchain explicitly for production deploys; (c) parameterize via BuildConfig field like the existing RPC_URI pattern (consistent with line 44 of MainViewModel.kt).

B44. Medium Both scaffolds use placeholder identity defaults without REPLACE-ME markers
Location: solana-kotlin-compose-scaffold/.../di/SolanaKotlinComposeScaffoldModule.kt lines 14–16 + solana-mobile-dapp-scaffold/template/components/providers/AuthorizationProvider.tsx lines 66–70

Kotlin: solanaUri = "https://solana.com", identityName = "Solana", iconUri = "favicon.ico" (DI module lines 14–16). RN: name: 'React Native dApp', uri: 'https://solanamobile.com', icon: 'favicon.ico' (AuthorizationProvider.tsx lines 66–70). Consumer apps shipping with these defaults misrepresent their identity to wallet apps: the MWA UI displays "Solana" or "React Native dApp" instead of the consumer's actual app name; the wallet's identity-record DB associates the consumer app's auth tokens with the wrong vendor identity URI. No inline REPLACE-ME marker or build-time warning.

Suggested fix, not tested

insert prominent // REPLACE-ME: identity values reach the wallet UI; update before production ship comment at each site; OR use placeholders like "YOUR_APP_NAME" / "https://YOUR_APP_DOMAIN/" that fail-loud at first authorize attempt (wallet endpoint validates per server lines 142–145 identity.uri must be an absolute, hierarchical URI); OR introduce build-time validation against a deny-list of scaffold defaults.

B45. Low RN dApp scaffold uses deprecated cluster parameter on authorize
Location: solana-mobile-dapp-scaffold/template/components/providers/AuthorizationProvider.tsx line 120

solana-mobile-dapp-scaffold/template/components/providers/AuthorizationProvider.tsx line 120: wallet.authorize({ cluster: RPC_ENDPOINT, identity: APP_IDENTITY }). The RN scaffold transmits the deprecated alias on every fresh authorize call, training consumer integrators to follow the legacy parameter shape.

Suggested fix, not tested

update scaffold to use chain: RPC_ENDPOINT (or CAIP-2 form like solana:devnet/solana:mainnet); add comment indicating cluster is deprecated alias kept for back-compat at wallet endpoint.

B46. High AuthorizationFuture.processResult populates per-account chains[] and features[] arrays with the FIRST element repeated N times instead of element-N
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterClient.java lines 308–323

Lines 312–314 for (int c = 0; c < chainsArr.length(); c++) { chains[c] = chainsArr.getString(0); } — note getString(0), not getString(c). Same bug at lines 319–322 for features[c] = featuresArr.getString(0). Result: an account with chains: ["solana:devnet", "solana:mainnet"] parses into String[] chains = ["solana:devnet", "solana:devnet"]. Consumer dApps that key request routing by account.chains or feature dispatch by account.features get silently wrong data — every chain entry mirrors index 0, every feature entry mirrors index 0. Defect-class: classic copy-paste loop-index typo. The arrays still have correct length — so simple chains.length checks pass; only per-index inspection surfaces the gap.

Suggested fix, not tested

Replace chainsArr.getString(0) with chainsArr.getString(c) at line 313; replace featuresArr.getString(0) with featuresArr.getString(c) at line 321. Add unit test for AuthorizationFuture parsing JSON with multi-entry chains + multi-entry features; assert per-index equality with the source array.

B47. High createRequestUniqueId for dApp Store publication attestation uses Math.random() instead of cryptographically secure RNG
Location: dapp-publishing/packages/core/src/portal/attestation.ts lines 20–28

Lines 20–28 generate a 32-char numeric ID from charset 0123456789 via Math.random() — V8's Math.random is XorShift128+ (not CSPRNG). The function name + usage context (request_unique_id field of a signed attestation {slot_number, blockhash, request_unique_id}) indicate replay-resistance dependency: the attestation's freshness depends jointly on blockhash recency and per-request uniqueness. With Math.random(), an attacker observing past request_unique_id values can fit the XorShift128+ state in seconds (well-known attack on Math.random); subsequent IDs become predictable. Pre-generated signed attestations could be replayed at validator-acceptable blockhash windows. Cryptographic-axis weakness; security-class.

Suggested fix, not tested

Replace Math.random()-based generation with crypto.randomUUID() (built-in; 122 bits CSPRNG entropy) or crypto.randomBytes(16).toString('hex'). Both produce non-predictable IDs without external dependencies. Adjust attestation server-side validator if it requires the all-digit shape; otherwise update to base64url/hex shape.

B48. Medium assert(...) recurrence at clientlib — same fail-open-in-production pattern as V3 B38 walletlib (cross-axis same-vendor sibling SDK)
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterClient.java (2 sites) + .../scenario/LocalAssociationScenario.java (10 sites)

Java assert(...) requires -ea JVM flag and is NEVER enabled in Android production builds. Clientlib uses assert(...) for non-trivial preconditions: MobileWalletAdapterClient.java:695 (numExpectedPayloads > 0 at unpackResponsePayloadArray), MobileWalletAdapterClient.java:727 (numExpectedBooleans > 0 at unpackResponseBooleanArray), LocalAssociationScenario.java:156, 165, 174, 185, 194, 204, 226, 241, 250, 264 (state-machine invariants + mSessionEstablishedFuture null-state guards). Production wallet apps using clientlib never evaluate these preconditions — state-machine invariants silently violated rather than raising IllegalStateException.

Suggested fix, not tested

Cross-SDK consistency (apply at both walletlib and clientlib in same release).

B49. Medium Undocumented spec-deviation workaround silently masks non-compliant wallet responses at signMessagesDetached
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterClient.java lines 1018–1027

Lines 1018–1026 contain inline comment: "Workaround: some wallets have been observed to only reply with the message signature. This is non-compliant with the spec, but in the interest of maximizing compatibility, detect this case and reuse the original message." Behavior: if a wallet returns only the 64-byte signature payload (no signed message prefix), signMessagesDetached substitutes the original mMessages[i] as the "signed message" in the result. The substitution is invisible to the dApp caller — result.messages[i].message is whatever the dApp passed in, not necessarily what the wallet endpoint actually saw/signed. Privacy axis: if a non-compliant wallet returns the signature but signed a slightly-different message (UTF-8 normalization, trimming whitespace, etc.), the dApp's result claims the original-message was signed when it was not. Dapps that audit signedMessage for tampering get false-positive PASS.

B50. High parseKeypair swallows all error types and returns undefined; caller's downstream usage is undefined-as-Keypair TypeError
Location: dapp-publishing/packages/cli/src/cli/signer.ts lines 12–24 + downstream CliSetup.ts:373–379

cli/signer.ts lines 12–24: try-block reads the keypair file + JSON-parses + constructs Keypair.fromSecretKey(...). Catch-block ignores the error entirely and displays generic message "Something went wrong when attempting to retrieve the keypair at <path>". Function then implicitly returns undefined. Caller path at CliSetup.ts:373–379 loadSignerKeypair checks if (!keypair) throw new Error('Failed to load the signer keypair') — recovers from undefined but the original error context is irrecoverable (file-not-found vs malformed JSON vs wrong secret-key length vs permission denied all yield the same generic message). Consumer debugging requires re-running with strace or manually inspecting the file.

Suggested fix, not tested

Surface the original error: try { ... } catch (error) { const detail = error instanceof Error ? error.message : String(error); showMessage('KeyPair Error', \Failed to load keypair from \${pathToKeypairFile}: \${detail}\, 'error'); throw error; }. Either rethrow OR document that parseKeypair returning undefined indicates a multi-cause failure. Eliminate the if (!keypair) rescue at loadSignerKeypair — if parseKeypair throws, the calling stack carries diagnostic information.

B51. Medium checkForSelfUpdate hard-blocks CLI on minor version mismatch
Location: dapp-publishing/packages/cli/src/cli/selfUpdate.ts lines 51–58

cli/selfUpdate.ts lines 51–58: if (latest.major > current.major || latest.minor > current.minor) throw new Error('Please update to the latest version of the dApp Store CLI before proceeding...'). The error throw kills CLI execution entirely. Behavior: a publisher running CLI vN.5.0 who hasn't upgraded to vN.6.0 (released yesterday) gets blocked at the very first invocation in a CI/CD pipeline. No grace period for minor versions; no operator opt-out flag at this gate (the gate fires before enforceSelfUpdatePolicy reads --skip-self-update). Ecosystem norms: minor versions are non-breaking; blocking on minor is overly strict.

Suggested fix, not tested

Restrict hard-block to MAJOR version mismatch only (if (latest.major > current.major)); emit WARNING for minor version mismatch (if (latest.minor > current.minor) console.warn('A newer minor version is available: ...')); allow user to suppress warning via --skip-self-update-warnings (independent of --local-dev). Network-error path (registry down) currently kills CLI; add try-catch around notifier.fetchInfo() so registry outages don't block publishers.

B52. Low attestation.ts returns payload AND attestationPayload fields pointing at the same signedMessageBuffer
Location: dapp-publishing/packages/core/src/portal/attestation.ts lines 62–65 + dapp-publishing/packages/cli/src/portal/workflowClient.ts lines 537–555

Lines 62–65: return { payload: Buffer.from(signedMessageBuffer).toString('base64'), attestationPayload: Buffer.from(signedMessageBuffer).toString('base64'), requestUniqueId, attestation }. Both fields hold identical base64-encoded data; one is redundant. Cross-axis with workflowClient.ts:537–555 submitToStore which probes attestation.payload OR attestation.attestationPayload OR input.attestationPayload as a three-step fallback — the dual-field surfaces churn between API contract revisions. Receiver-side code must understand whichever field is populated.

Suggested fix, not tested

Pick one field name (recommend payload per simpler shape); deprecate the other with @deprecated JSDoc comment + migration path documented; coordinate with portal server-side to migrate at next contract version. The three-step fallback at workflowClient should be reducible to a single-field-read after migration.

B53. Medium ensurePublicationSignerBalance continues publication with a WARNING when balance check fails, allowing wasted upload + signing-time failure
Location: dapp-publishing/packages/cli/src/publication/fundingPreflight.ts lines 113–122

publication/fundingPreflight.ts lines 113–122: if balance fetch throws (RPC timeout, RPC error response, RPC-side rate limit), the function returns buildRpcWarningMessage(...) instead of throwing — caller at CliSetup.ts:159–161 surfaces the warning and continues into APK upload + workflow. Publisher uploads multi-MB APK + waits for ingestion + reaches the signing step → signing fails with "Account has insufficient funds" → publisher must restart from scratch having spent bandwidth + minutes. The funding preflight's purpose is explicitly to "fail fast before uploading the APK" (lines 11–12 inline comment) — continuing on RPC failure inverts this design goal.

Suggested fix, not tested

Distinguish balance-confirmed-low (throw) vs balance-unknown-due-to-RPC-error (operator decision). Add --require-balance-check flag that promotes RPC errors to fatal; default to current warning-then-continue behavior for backward compatibility. Alternatively: retry RPC fetch 2-3 times with backoff before degrading to warning (network blips shouldn't immediately degrade).

B54. Low MobileWalletAdapterClient.signAndSendTransactions error message is the EXACT INVERSE of sibling signTransactions
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterClient.java line 1082 vs line 853

Line 1082: throw new IllegalArgumentException("transactions must be null or empty") — message says transactions MUST be null/empty. The check (lines 1080–1084) is if (t == null || t.length == 0) throw ... — fires when t IS null or empty. The correct message is "transactions must NOT be null or empty" (note "NOT"). Sibling at line 853 signTransactions has identical check with correct message: "transactions must not be null or empty". Asymmetric error-message correctness between sibling-methods that share the same precondition. Consumer dApp seeing the message at line 1082 is misled about which condition caused the throw.

Suggested fix, not tested

Fix line 1082 to "transactions must not be null or empty" (insert "not"). Add lint rule scoped to sibling-method pairs sharing the same precondition: error messages should match.

B55. Low Long-standing TODO at clientlib MobileWalletAdapterClient.java:40–42 assumes Solana-only signature length; cross-axis recurrence with V3 B39 walletlib (#44): support multiple addresses TODO
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterClient.java lines 40–42

Line 40–42: // TODO: this assumes Solana-length signatures. Revisit this assumption when adding support for alternative chains. plus private static final int OFFCHAIN_MESSAGE_SIGNATURE_LENGTH = 64; at line 42. Vendor's reference SDKs (both clientlib + walletlib) carry chain-specific assumption-TODOs at production stable releases.

Suggested fix, not tested

Either (a) document at MWA spec that current reference impl is Solana-only (positive-form); (b) close the TODOs with explicit migration plan + tracking issue; (c) refactor signature handling into chain-pluggable abstraction (matches the spec's multi-chain framing). Cross-SDK coordination required.

B56. Info JsonRpc20Client error messages for reserved-method-name detection lack closing parenthesis
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/JsonRpc20Client.java lines 49–50 + 97–98

Lines 49–50: "reserved method name (starts with 'rpc.'" — open-paren after "name", no close-paren. Same defect at line 98 in notification(): "reserved notification name (starts with 'rpc.'". Java string literal is technically valid; the message displayed to consumer is grammatically malformed. Pedantic; low impact.

Suggested fix, not tested

Append ) to both messages: "reserved method name (starts with 'rpc.')" and "reserved notification name (starts with 'rpc.')".

B57. Low MobileWalletAdapterSession.parseHelloReq throws UnsupportedOperationException for crypto-class failures
Location: mobile-wallet-adapter/android/clientlib/src/main/java/.../protocol/MobileWalletAdapterSession.java lines 101–110

Lines 111–112: try { ... signature crypto ... } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { throw new UnsupportedOperationException("Failed signing HELLO_REQ public key payload"); } and similarly at line 109 for the DER→P1363 conversion. UnsupportedOperationException is semantically incorrect for these failure modes: NoSuchAlgorithmException is environment-misconfiguration (algorithm provider missing), SignatureException is runtime crypto failure (key tampering / algorithm-state corruption), InvalidKeyException is key-shape failure. None of these are "operation not supported" — the operation IS supported but failed for diverse reasons. Generic UnsupportedOperationException flattens the diagnostic.

Suggested fix, not tested

Throw more specific exception: IllegalStateException for missing algorithm provider; SecurityException for runtime crypto failure; or wrap in SessionMessageException (the class is already defined at the parent + used at parseHelloRsp line 148). Preserves the original throwable as cause so consumer debugging can trace.

B58. High publish/Publish* legacy surface (PublishCoreSubmit / PublishCoreUpdate / PublishCoreRemove / PublishCoreSupport) are SILENT no-ops with Promise<never> return type but actually return undefined as never
Location: dapp-publishing/packages/core/src/publish/{PublishCoreSubmit,PublishCoreUpdate,PublishCoreRemove,PublishCoreSupport}.ts (4 sites) + dapp-publishing/packages/core/src/portal/compat.ts:1-6

Each function takes typed PublishXxxInput, emits a single console.warn via deprecateLegacyPublishSurface, and returns undefined as never. The TypeScript signature Promise<never> semantically means the promise NEVER resolves successfully (typically only rejects or never settles); returning undefined breaks the type contract. Consumer impact: a publisher invoking await publishSubmit(network, input, dryRun) sees a console.warn (typically not surfaced in CI / automation / silent-runner contexts) but the call resolves successfully with undefined. The publication does NOT happen, the publisher's downstream code "succeeds" silently, and no error is raised. The console.warn channel is unreliable for behavior-changing deprecations — warnings get suppressed in many runners and IDEs. The deprecateLegacyPublishSurface helper at portal/compat.ts:1-6 is the only signal.

Suggested fix, not tested

Two-step: (a) immediate fix — change Promise<never> return to Promise<void> or Promise<{ deprecated: true, replacement: string }> and have the function throw new Error("publishSubmit is deprecated; use createPublicationWorkflow(...).startPublication(...) from @solana-mobile/dapp-store-publishing-tools"). Throwing converts silent-failure to fail-loud. (b) longer-term: remove these surfaces entirely in next major version; the typed input objects keep deprecation TODOs visible to consumers via TypeScript ts(2769) errors when calling the removed function. NEVER ship a function whose runtime behavior is "do nothing" while the type signature claims Promise<never>.

B59. High AnchorCounterDapp tutorial uses AnchorProvider commitment: "processed" for wallet-signed transactions — documented "Blockhash not found" preflight reject trap
Location: tutorial-apps/AnchorCounterDapp/src/components/counter/counter-data-access.tsx lines 35-38 + ConnectionProvider.tsx:18 default config

tutorial-apps/AnchorCounterDapp/src/components/counter/counter-data-access.tsx:35-38 constructs new AnchorProvider(connection, anchorWallet, { preflightCommitment: "confirmed", commitment: "processed" }). The commitment: "processed" setting governs connection.getLatestBlockhash calls inside Anchor's tx-build path — Anchor will fetch a blockhash from a processed-commitment slot. The Trap: dApp uses one RPC (e.g. its own connection endpoint) and the wallet uses a DIFFERENT RPC (e.g. api.mainnet-beta.solana.com). Processed-commitment blockhashes have very short propagation latency across the Solana validator network; a processed-but-not-finalized blockhash from dApp RPC is virtually guaranteed to NOT be visible at wallet RPC at sign time, leading to BlockhashNotFound preflight reject. The tutorial teaches consumers the wrong pattern — wallet-signed transaction blockhashes MUST use commitment: "finalized" against an RPC that wallets also trust (api.mainnet-beta.solana.com). New developers copy-pasting this tutorial inherit the trap; production apps surface intermittent "Transaction simulation failed: BlockhashNotFound" errors that defy easy reproduction.

Suggested fix, not tested

(a) Tutorial-side fix: change AnchorProvider config to { preflightCommitment: "finalized", commitment: "finalized" } for wallet-signed-tx flows; document that the choice is mandatory in inline comment naming the BlockhashNotFound failure-mode. (b) Add tutorial-level README section: "Why commitment: 'finalized' for wallet-signed transactions" — explain dApp-RPC vs wallet-RPC blockhash-visibility lag. (c) Vendor-wide: audit other tutorial-apps + react-native-samples for the same defective default; fix as cluster.

B60. Medium AnchorCounterDapp tutorial useCallback hooks have wrong useMemo/useCallback deps arrays at multiple sites
Location: tutorial-apps/AnchorCounterDapp/src/components/sign-in/sign-in-ui.tsx lines 27, 59 + src/utils/useMobileWallet.tsx lines 28, 92-103

Example sites: sign-in-ui.tsx:27 useCallback(handleConnectPress, [authorizationInProgress, authorizeSession]) — the callback at line 12 uses connect (line 18) and authorizationInProgress, but the deps array names authorizeSession (destructured at line 9 but unused in callback body) instead of connect. Identical defect at sign-in-ui.tsx:59 (signIn button — closure references signIn but deps name authorizeSession). Same class at useMobileWallet.tsx:28 (signIn callback uses authorizeSessionWithSignIn but deps array [authorizeSession]) and useMobileWallet.tsx:92-103 (final useMemo deps [signAndSendTransaction, signMessage, signTransactions] missing connect, connectAnd, signIn, disconnect). react-hooks/exhaustive-deps ESLint rule would flag every site. Tutorial-scaffold-fidelity axis: tutorial code teaches consumers via copy-paste; stale-closure defects propagate into production codebases.

Suggested fix, not tested

(a) Fix the deps arrays in tutorial code; ensure react-hooks/exhaustive-deps ESLint rule is in the tutorial's eslint config + passes in CI. (b) Cross-tutorial sweep: audit all other tutorial-apps + react-native-samples for useCallback / useMemo deps-array correctness. (c) Add tutorial-level lint pass before merging tutorial contributions.

B61. Medium Inconsistent dual-casing handling for portal status field across sibling functions in same package — ingestion.ts checks both casings, session.ts checks only one
Location: dapp-publishing/packages/core/src/portal/workflow/ingestion.ts lines 14, 34-38, 103 vs state/session.ts lines 38, 42

portal/workflow/ingestion.ts:14, 34-38, 103 exhaustively check both PascalCase + lowercase casings of "Ready" / "ready" / "Failed" / "failed". By contrast, portal/workflow/state/session.ts:38 checks only session.status === "failed" (lowercase only); :42 checks only session.status === "completed" (lowercase only). Defect class: if the portal returns a session-status with PascalCase casing at the session endpoint but lowercase at the ingestion endpoint (or vice versa as the portal's contract evolves), resolvePublicationSessionStage will silently fall through to default "PreparedForMint" instead of recognizing the actual stage. Inconsistent defensive-handling across sibling consumer-side files of the same API. Root-cause axis: the portal-side API contract is non-canonical at status field casing; the consumer's defense is partial.

Suggested fix, not tested

Either (a) normalize casing at a single deserialization boundary (e.g. constructor / single normalizer) so the consumer code can rely on canonical casing throughout; OR (b) check both casings at every call site consistently. Preferred: (a) — single normalizer at client.getPublicationSession / client.getIngestionSession deserialization layer that lowercases all status-class fields. Long-term: coordinate with portal-side to canonicalize at API contract level.

B62. Low client.getIngestionSession({ sessionId, ingestionSessionId }) 2-instance dual-shape redundancy at API contract — same value passed as two different parameter names
Location: dapp-publishing/packages/core/src/portal/workflow/lifecycle.ts lines 353-356 + ingestion.ts lines 99-102 (2 sites)

Both lifecycle.ts:353-356 and ingestion.ts:99-102 call client.getIngestionSession with { sessionId: createdIngestionSessionId, ingestionSessionId: createdIngestionSessionId } — passing the same string twice under different keys. The dual-key shape suggests the contract evolved between sessionId and ingestionSessionId and the consumer hedges. Defect-class: API contract has two fields meaning the same thing; consumer code keeps both populated; receiver-side code must handle whichever is populated. Wire-shape sprawl.

Suggested fix, not tested

Coordinate with portal-side to pick one canonical field name; deprecate the other with @deprecated TypeScript marker + migration path; consolidate consumer calls to single-field after migration.

B63. Low resolvePublicationSignerAddress 3-level fallback chain silently conflates publisher with collectionAuthority when earlier fields are undefined
Location: dapp-publishing/packages/core/src/portal/workflow/state/bundle.ts lines 118-126

portal/workflow/state/bundle.ts:118-126 returns first-defined of dappWalletAddress ?? requiredSigner ?? collectionAuthority. collectionAuthority is typically a DIFFERENT wallet from the publisher (it's the original NFT collection authority that grants verify-collection-PDA write permission). If dappWalletAddress + requiredSigner are both undefined in a particular signerAuthority shape, the function returns the collection authority as if it were the publisher. Subsequent code at execution.ts:331-337 validates signer.publicKey !== requiredSignerAddress and throws — so the runtime path is loud — but the FALLBACK SEMANTICS are still suspect: a developer reading the code learns that a missing publisher field is "OK because we'll use the collection authority" which is a category error.

Suggested fix, not tested

(a) Replace fallback chain with explicit null-checks + throw with diagnostic message: if (!bundle.signerAuthority.dappWalletAddress) throw new Error("Publication bundle is missing dappWalletAddress at signerAuthority") — fail-fast and loud rather than silent-cascade-then-late-validate. (b) Document the fallback semantics inline if intentional — clarify when requiredSigner and collectionAuthority are valid publisher candidates (operations like admin-deployment-of-collection where authorities coincide).

B64. Medium uploadLocalApkToPortal does NOT validate the uploadUrl returned by portal createUploadTarget is HTTPS — partial HTTPS-enforcement gap inherited from V4 §A32 / §B.Q
Location: dapp-publishing/packages/core/src/portal/workflow/source/preparation.ts lines 102-110 (uploadTarget.uploadUrl unchecked)

HOWEVER preparation.ts:102-110 performs the actual APK upload via await fetch(uploadTarget.uploadUrl, ...) with NO scheme validation. The uploadUrl is portal-issued (typically a presigned S3 / GCS URL with a TTL-limited token in the query string). If a misconfigured portal returns http:// instead of https:// (dev environment leaked into production, regional endpoint typo, etc.), the APK + content-length + sha256 hash get sent over plaintext HTTP. Risk class: the presigned URL itself becomes capturable by a network observer within its TTL window, enabling MITM to PUT arbitrary bytes to that exact storage path. The APK build artifact is not particularly secret in itself, but the presigned-URL leak allows substitution attacks. Partial-enforcement axis: HTTPS enforced at primary portal endpoint, NOT at portal-issued secondary URLs.

Suggested fix, not tested

Add scheme-check inline at line 102: if (!uploadTarget.uploadUrl.startsWith("https://")) throw new Error("Portal upload URL must use HTTPS scheme; received: " + uploadTarget.uploadUrl). Apply same gate to any other portal-issued secondary URLs (e.g. uploadTarget.publicUrl used as releaseFileUrl at line 130 — gets sent back to portal as the canonical file URL; if portal returns http there, downstream consumers reference http-URL forever). Cross-axis enforcement gate: "every portal-issued URL passed to fetch / stored to portal contract MUST be HTTPS unless dev-mode flag explicitly opts out".

B65. Low publicationStageToCheckpoint case "Failed" falls through to default: return "created" — semantically misleading + likely dead branch
Location: dapp-publishing/packages/core/src/portal/workflow/state/checkpoints.ts lines 47-49

portal/workflow/state/checkpoints.ts:47-49: case "Failed": default: return "created". The semantic interpretation is "a Failed stage = no checkpoint progress at all" — but in execution.ts:339-345 the Failed stage throws BEFORE reaching checkpoint resolution, so this branch never fires at the runtime path. Either the branch is dead code OR the semantic mapping is wrong (a Failed publication did make some progress — likely past created and possibly past mint-submitted — mapping to created discards diagnostic information).

Suggested fix, not tested

(a) If branch is dead: delete the case "Failed": line so falling-through to default is implicit; document that Failed stage is caught upstream. (b) If branch is intended: return a more meaningful default (e.g. preserve session.checkpoint if present; only fall back to "created" if no checkpoint exists). Stack-trace-friendly diagnostic preservation.

B66. Low getTokenMetadataCreateCollectionAddress deserialize-error catch-throws generic message — diagnostic loss at portal-tx validation
Location: dapp-publishing/packages/core/src/portal/signer.ts lines 151-163

portal/signer.ts:151-163: try { const [decodedInstruction] = CreateStruct.deserialize(...) } catch (error) { throw new Error("Portal transaction contains an invalid token metadata create instruction."); }. The original error (Borsh deserialization detail — offset, field name, expected vs actual type) is dropped. Diagnostic loss class.

Suggested fix, not tested

Preserve the original error as cause: throw new Error("Portal transaction contains an invalid token metadata create instruction.", { cause: error }). Modern Node Error.cause chains support — error stacks show the underlying Borsh failure.

B67. Info resolvePublicationSessionStage Failed-state fallback chain breaks on empty-string lastError
Location: dapp-publishing/packages/core/src/portal/workflow/execution.ts lines 339-345

execution.ts:339-345: throw new Error(normalizedSession.lastError || normalizedSession.error || "Publication session failed"). If lastError is the empty string "" (a legitimate "no specific error message" value the portal might emit), || falls through to error; if error is also empty string, falls through to the generic default. Defect class: distinguishes "no error" from "empty error" only at the truthiness layer, which collapses both to the same fallback path. If lastError IS set to empty string deliberately (e.g. portal signals "failed but no message"), the consumer sees the generic "Publication session failed" message rather than realizing the portal returned an empty diagnostic. Inconsistent fallback-discipline within same package.

Suggested fix, not tested

Use nullish-coalescing ?? instead of logical-or || at fields where empty-string is a meaningful value: normalizedSession.lastError ?? normalizedSession.error ?? "Publication session failed". Audit all fallback chains in package for || vs ?? consistency (some sites correctly use ?? per bundle.ts; the execution.ts site is the exception).

B68. High config={{commitment: 'processed'}} at app-level ConnectionProvider recurs across 4 OFFICIAL vendor tutorials — V5 B59 blockhash trap pattern cross-tutorial 5-instance
Location: tutorial-apps/{first-mobile-dapp/App.tsx:16, SimpleStorageDapp/App.tsx:16, MobileNFTMinter/App.tsx:19, SolanaReactNativeTutorial/SolanaReactNativeTutorialComplete/App.tsx:14} (4 sites) + V5 AnchorCounterDapp (5th site)

The shared connection object then propagates the processed commitment to every connection.getLatestBlockhash() call at downstream transaction-building sites (e.g., SimpleStorageDapp/components/SignTransactionButton.tsx:25, SolanaReactNativeTutorial/.../RecordMessageButton.tsx:35 + :58). 5-tutorial-instance recurrence at SAME vendor within their officially published learning corpus — consumers copy-pasting any of these tutorials inherit the trap. Same vendor's ConnectionProvider.tsx SDK-level default is commitment: 'confirmed' (correct) — tutorials OVERRIDE the correct default with the trap.

Suggested fix, not tested

Vendor-wide cross-tutorial sweep: change all config={{commitment: 'processed'}} defaults to 'confirmed' at app-level ConnectionProvider. Update inline comments naming the BlockhashNotFound failure-mode. Add an in-line block at each tutorial README explaining "Why 'confirmed' for wallet-signed transaction flows." Cross-tutorial coherence: vendor MAY add a lint rule / pre-merge gate in the tutorial-repo CI that flags commitment: 'processed' at any Connection/ConnectionProvider site adjacent to wallet.signTransactions paths.

B69. High Authorization-credential payload (including authToken Bearer-equivalent) console-logged in 3 official tutorials' AuthorizationProvider scaffold + AsyncStorage-persistence in 2 of them creates dual-surface credential leak
Location: tutorial-apps/{first-mobile-dapp/components/AuthorizationProvider.tsx:121, SimpleStorageDapp/components/providers/AuthorizationProvider.tsx:118-120 + :135, FarmingIdleGame/hooks/AuthorizationProvider.tsx:138} (4 auth-log sites) + tutorial-apps/{first-mobile-dapp,SimpleStorageDapp}/components/SignTransactionButton.tsx (2 tx-blob-log sites)

first-mobile-dapp/components/AuthorizationProvider.tsx:121 console.log(authorizationResult) logs the full AuthorizationResult object (containing the auth token + account addresses + base64 keys). SimpleStorageDapp/components/providers/AuthorizationProvider.tsx:118-120 console.log('Retrieving auth ' + JSON.parse(cacheFetchResult, cacheReviver)) logs the FULL authorization object including authToken (Bearer-equivalent credential). SimpleStorageDapp/.../AuthorizationProvider.tsx:135 console.log('Caching auth: ' + JSON.stringify(authorization)) logs every auth-state transition with the token. FarmingIdleGame/hooks/AuthorizationProvider.tsx:138 same pattern — console.log('Caching auth: ' + JSON.stringify(auth)) (FarmingIdleGame additionally cleans address-only logging at :123-125 — partial discipline). Dual-surface leak class: logcat persists on-device + AsyncStorage stores cleartext on-device. Combined: an Android app crash report aggregator (Sentry / Firebase Crashlytics) sampling logcat could exfiltrate the authToken; a backup file dump or device-image-class attack exposes the AsyncStorage plaintext. Cross-tutorial recurrence: 3-of-5 audited tutorials at this vendor exhibit the credential-log defect. Consumers copy-pasting any tutorial AuthorizationProvider scaffold inherit the credential-leak. SimpleStorageDapp/components/SignTransactionButton.tsx:66 + first-mobile-dapp/components/SignTransactionButton.tsx:78 also console.log(fromUint8Array(signedTransaction.serialize())) — leaking the SERIALIZED SIGNED TRANSACTION (containing the user's signature on a real transaction) to console.log.

Suggested fix, not tested

(a) Immediate: replace every console.log of sensitive payload with explicit redaction (e.g., console.log('Auth received for: ' + nextAuthorization.selectedAccount.address.slice(0,8) + '…') — log only an opaque session identifier + truncated public address; NEVER the authToken or signature). (c) Vendor-wide tutorial sweep: audit other tutorial-apps + react-native-samples for parallel patterns; fix as cluster.

B70. Medium AsyncStorage.setItem / AsyncStorage.removeItem inside try/catch but NOT awaited — Promise rejection escapes try/catch (unhandled-rejection class)
Location: tutorial-apps/{SimpleStorageDapp/components/providers/AuthorizationProvider.tsx:136, FarmingIdleGame/hooks/AuthorizationProvider.tsx:139, FarmingIdleGame/hooks/AuthorizationProvider.tsx:148}

SimpleStorageDapp/components/providers/AuthorizationProvider.tsx:136 calls AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(authorization)) inside a try block but without await — the IIFE's try/catch at :131-141 wraps a synchronous statement that immediately returns the Promise; if the Promise later rejects (storage full, disk error, RN bridge crash), the rejection propagates outside the catch and becomes an unhandled-rejection warning. FarmingIdleGame/hooks/AuthorizationProvider.tsx:139 (AsyncStorage.setItem) + :148 (AsyncStorage.removeItem) exhibit the identical defect. Defect class: the try/catch surface here is dead — error handling is intended at the surface but the missing await neutralizes it.

Suggested fix, not tested

Add await before every AsyncStorage.* call inside try/catch blocks. ESLint rule @typescript-eslint/no-floating-promises would flag every site automatically; verify the rule is enabled in tutorial-repo .eslintrc.* and passes in CI. Cross-tutorial sweep at vendor level.

B71. Medium Local new Connection(clusterApiUrl('devnet'), 'confirmed') inside transaction-button callback BYPASSES app-level ConnectionProvider config — architecture-coherence break at scaffold layer
Location: tutorial-apps/first-mobile-dapp/components/SignTransactionButton.tsx:28 (and likely other Button.tsx siblings — SendMemoButton, RequestAirdropButton etc. — same scaffold class)

first-mobile-dapp/components/SignTransactionButton.tsx:28 constructs a fresh Connection inline at the button callback — hard-coding both cluster ('devnet') AND commitment ('confirmed') — while App.tsx:11 declares APP_CLUSTER = 'testnet' and App.tsx:16 passes config={{commitment: 'processed'}} to ConnectionProvider. Three layers, three different settings. Defect class: the central ConnectionProvider exists but the consumer-facing button doesn't use it — useConnection() is the prescribed access, but this site reaches around the context API to construct its own Connection with its own settings. A vendor design intent that consumers wire up centrally is silently undermined at the demonstration code. Cross-tutorial contrast: SimpleStorageDapp/SignTransactionButton.tsx:15 + :25 DO use useConnection() (correct pattern). Discipline split within the same vendor's tutorial-set.

Suggested fix, not tested

Refactor first-mobile-dapp SignTransactionButton (and sibling Button components) to consume useConnection() from app-level context, matching the SimpleStorageDapp pattern. Cross-tutorial discipline: the canonical pattern is "one ConnectionProvider per app, consumers use useConnection()" — vendor MUST converge tutorial scaffolds to this pattern. Tutorial-level lint config — eslint-no-restricted-imports blocking direct Connection constructor calls in component code (allowed only in providers/ subfolder).

B72. Medium Scaffold-constant naming inconsistency — name says one thing, value says another — 3-tutorial recurrence
Location: tutorial-apps/{SimpleStorageDapp/components/providers/ConnectionProvider.tsx:10, MobileNFTMinter/components/providers/ConnectionProvider.tsx:10, SolanaReactNativeTutorial/SolanaReactNativeTutorialComplete/App.tsx:9} (3 sites)

SimpleStorageDapp/components/providers/ConnectionProvider.tsx:10 + MobileNFTMinter/components/providers/ConnectionProvider.tsx:10 declare export const RPC_ENDPOINT = 'devnet' — the name suggests an RPC URL (e.g. https://api.devnet.solana.com) but the value is a cluster identifier. SolanaReactNativeTutorial/SolanaReactNativeTutorialComplete/App.tsx:9 declares const DEVNET_ENDPOINT = clusterApiUrl('testnet') — name says DEVNET but value is testnet. Defect class: misleading scaffold constants teach consumers wrong vocabulary; downstream confusion when consumer attempts to substitute a real RPC URL for RPC_ENDPOINT (would crash, since clusterApiUrl(url) only accepts cluster identifiers). The downstream AuthorizationProvider passes cluster: RPC_ENDPOINT to wallet.authorize() — accidentally correct because RPC_ENDPOINT is actually a cluster id, but only by coincidence of naming abuse.

Suggested fix, not tested

Rename RPC_ENDPOINT = 'devnet' → CLUSTER = 'devnet' (or CLUSTER_ID). Rename DEVNET_ENDPOINT = clusterApiUrl('testnet') → RPC_ENDPOINT = clusterApiUrl('testnet') AND fix the value to actual devnet (or rename the constant to match the testnet value). Vendor-wide tutorial scaffold sweep: naming = identity.

B73. Info await setAuthorization(nextAuthorization) — cargo-cult await on a React useState setter that returns void — 4-site recurrence
Location: tutorial-apps/{first-mobile-dapp,SimpleStorageDapp,MobileNFTMinter}/components/.../AuthorizationProvider.tsx + AnchorCounterDapp/src/utils/useAuthorization.tsx} (4 sites) | **LOW** | Remove the cargo-cult await — change to setAuthorization(nextAuthorization); plain statement. Add an ESLint rule (@typescript-eslint/await-thenable) that flags await` of non-Promise values. Cross-tutorial sweep at vendor level.

first-mobile-dapp/components/AuthorizationProvider.tsx:105, SimpleStorageDapp/.../AuthorizationProvider.tsx:152, MobileNFTMinter/.../AuthorizationProvider.tsx:107, and AnchorCounterDapp/src/utils/useAuthorization.tsx:123 all contain the identical statement await setAuthorization(nextAuthorization) inside handleAuthorizationResult. React useState setters are synchronous ((value: T) => void); awaiting undefined is a no-op runtime, but type-misleading: tutorial consumers reading this code learn that setAuthorization is async (it isn't) and develop wrong mental model about React state-management semantics. Cross-tutorial recurrence at SAME vendor — strongly suggests scaffold copy-paste origin without per-tutorial review.

B74. Low Javadoc copy-paste defect at ReflectorWebSocket.java StateCallbacks.onReflectionEstablished
Location: mobile-wallet-adapter/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/transport/websockets/ReflectorWebSocket.java:243-244

ReflectorWebSocket.java:243-244 documents onReflectionEstablished as "Invoked when this WebSocket fails attempting to connect to the server" — clearly copy-pasted from sibling onConnectionFailed Javadoc at line 241. The method actually fires on SUCCESSFUL reflection-handshake establishment per doReflectionEstablished at line 228 + the State transition at line 230 → REFLECTION_ESTABLISHED.

Suggested fix, not tested

Replace lines 243-244 Javadoc with accurate description: /** Invoked when this WebSocket successfully completes the reflection handshake with the server */. Vendor-wide doc sweep for sister-method copy-paste defects (audit all StateCallbacks-class interfaces in walletlib for parallel Javadoc errors).

B75. Low InterruptedException swallowed without restoring thread-interrupt status — Java best-practice violation at LocalWebSocketServer.java:74-75
Location: mobile-wallet-adapter/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/transport/websockets/server/LocalWebSocketServer.java:74-75

The close() method calls stop(CLOSE_TIME_MS, ...) inside a try { ... } catch (InterruptedException ignored) {} block. When an InterruptedException is caught, Java best-practice REQUIRES Thread.currentThread().interrupt() to preserve the interrupt status for upstream code that needs to detect cancellation. The current code silently consumes the interrupt — downstream code calling LocalWebSocketServer.close() from an interruptible thread will lose its interrupt-state and continue running blindly.

Suggested fix, not tested

Restore interrupt status in the catch block: } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.w(TAG, "Interrupted while stopping WebSocket server"); }. Vendor-wide sweep for catch (InterruptedException ignored) pattern across walletlib + clientlib Java. (Search regex: catch\s*\(\s*InterruptedException\s+\w+\s*\)\s*\{\s*\} — IntelliJ inspection "Interrupted exception is swallowed" catches this automatically).

B76. Low FarmingIdleGame/utils/programUtils.tsx burner-wallet transaction path skips preflight + confirms only to processed-commitment — fragile-default tutorial pattern
Location: tutorial-apps/FarmingIdleGame/utils/programUtils.tsx:331-333 + :335-342 (2 sites in same function sendAndConfirmSignedTransaction)

programUtils.tsx:331-333 connection.sendRawTransaction(rawTransaction, {skipPreflight: true}) — skipping preflight on a real-money-class transaction (the path that signs with both owner wallet + burner keypair). Skipping preflight saves a few hundred ms of latency but means client-side errors (insufficient funds, account-not-found, program-error) aren't caught before submission → wasted SOL fees on failures. programUtils.tsx:335-342 connection.confirmTransaction({...}, 'processed') — confirming only to processed-commitment. A processed-commitment confirmation can ROLL BACK at the same slot if the validator network reorgs the leader slot. For an idle game with low blast radius this is OK; for a tutorial scaffold consumers might generalize to value-bearing apps, this is fragile.

Suggested fix, not tested

(a) Default { skipPreflight: false } (omit the option) — let preflight catch client-side errors. (b) Default confirmation commitment to 'confirmed' (or 'finalized' for value-bearing tx). (c) Tutorial-level annotation: clearly mark fragile defaults as such — // skipPreflight: true OK only for low-stakes idle-game tx; PRODUCTION value-bearing tx MUST set skipPreflight: false. (d) Cross-tutorial sweep at vendor level.

B77. Low FarmingIdleGame/utils/programUtils.tsx:156 getWithdrawIx transfers lamports: playerBalance — rent-exempt reserve protection breaks the transfer at runtime
Location: tutorial-apps/FarmingIdleGame/utils/programUtils.tsx:150-162

programUtils.tsx:150-162 (getWithdrawIx) computes const playerBalance = await program.provider.connection.getBalance(player) then attempts SystemProgram.transfer({fromPubkey: player, toPubkey: owner, lamports: playerBalance}) — transferring the ENTIRE account balance including the rent-exempt reserve. Solana runtime rejects any transfer that would drop a non-system-owned account below its rent-exempt minimum. The transaction WILL fail at runtime. Stale-code clue: the inline comment at :160 says // 5000 for fee — suggesting the author intended lamports: playerBalance - 5000 (subtract tx fee) but forgot to update the actual code. Stale-comment + stale-code combination.

Suggested fix, not tested

Subtract both the tx fee AND the rent-exempt minimum: const rentExempt = await connection.getMinimumBalanceForRentExemption(0); const fee = 5000; SystemProgram.transfer({fromPubkey: player, toPubkey: owner, lamports: playerBalance - rentExempt - fee}); Update the comment to match. Vendor-level reviewer-side: tutorial code that demonstrates value-transfer mechanics MUST be runtime-verified before shipping; the broken withdraw teaches consumers wrong patterns.

B78. Info assert(mState != State.NOT_CONNECTED) at ReflectorWebSocket.java:146 — 3rd Java-site in same vendor for the V4 §A30 / V5 §A37 PRODUCTION-ASSERTION-AVOIDANCE pattern
Location: mobile-wallet-adapter/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/transport/websockets/ReflectorWebSocket.java:146

The onError callback contains assert(mState != State.NOT_CONNECTED); inside the synchronized block. Java assert statements are stripped at runtime when not run with -ea (assertion-enabled JVM flag — typically OFF on Android production builds).

Suggested fix, not tested

Replace assert(mState != State.NOT_CONNECTED); with explicit runtime check: if (mState == State.NOT_CONNECTED) { Log.e(TAG, "onError fired in NOT_CONNECTED state — likely race"); return; }. Vendor-wide Java sweep for assert( pattern; replace with explicit runtime guards.

B79. Info MobileWalletAdapterWebSocket.onClose NPE risk — ws.messageReceiver.receiverDisconnected() called without null-guard; could fire double-close or post-error-close path
Location: mobile-wallet-adapter/android/walletlib/src/main/java/com/solana/mobilewalletadapter/walletlib/transport/websockets/server/LocalWebSocketServer.java:97-99

LocalWebSocketServer.java:97-99 calls ws.messageReceiver.receiverDisconnected() then sets ws.messageReceiver = null. If the underlying WebSocket library fires onClose twice (some library implementations do at error paths) OR onClose fires AFTER onError already nulled the receiver, the second call NPEs at ws.messageReceiver.receiverDisconnected().

Suggested fix, not tested

Add null-guard: if (ws.messageReceiver != null) { ws.messageReceiver.receiverDisconnected(); ws.messageReceiver = null; }. Or use a synchronized (ws) block to ensure idempotence at double-close paths. Audit other WebSocket-callback methods (onOpen, onMessage × 2 overloads, onError) for parallel null-guard gaps.

Withdrawn by us (2)