Sample audits · Workstation audits, October 2026

Solana Mobile SDKs: mobile-wallet-adapter and seed-vault-sdk

The Mobile Wallet Adapter libraries and the Seed Vault SDK.

Auditedsolana-mobile/mobile-wallet-adapter at commit 100e023048b7ffd3a67af2bf334f4bda3b9d8d7c
solana-mobile/seed-vault-sdk at commit 8f8ba289fed8c53f2deb3194fbbd73f4ab6fe028
Date11 October 2026
How it ranlocal run on our workstation, full audit, Standard review
Verdict after reviewFail (rule: Fail if a High finding remains after review, otherwise Pass with notes)

Related: Solana Mobile documentation and SDKs (June 2026)

1
High after review
8
Medium after review
137
Low after review
16
Info after review
0
excluded on review
How to read this page

Each finding keeps the number it has in the audit report. The rating shown first is the one after review; the first automated rating is listed with it. 90 findings were first rated Medium or High; 9 of them are Medium or High after review. Text marked "From the report" is quoted from the audit report; fixes are suggestions and were not tested. Code locations link to the file and line at the audited commit.

High after review (1)

119. High sendTransaction returns a base64 signature
First rating: High · Reviewed rating: High · Review: confirmed
From the report
Evidence

Evidence: .map((output) => { return base64FromUint8Array(output.signature); });

Why it matters

Solana transaction signatures are base58. The returned value is not a valid signature string, so confirmTransaction and explorer lookups fail for every transaction sent through this path. The package's own test expects base58. Apps that retry after a failed confirmation can send a payment twice.

Suggested fix, not tested

Encode with base58FromUint8Array.

Review: The sign-and-send branch returns base64FromUint8Array(output.signature). The local wallet always exposes sign-and-send, so wallet-adapter-mobile's sendTransaction takes this branch and returns a base64 string where a base58 transaction signature is required; confirming it fails. The sibling web3.js client correctly maps to base58, so this is a bug, not a design choice. One claim in the report is wrong: the package's own test compares against a base64 encoder, so it locks the bug in. The duplicate-payment consequence is speculative: the transaction is sent once, only the returned id is wrong. Kept at HIGH because the default send path of a published npm package returns an unusable signature for every user. Upstream: PR #1662 "fix: return base58 signatures from sendTransaction" was closed unmerged on 2026-10-07.

Upstream: No exact upstream report; related items: #1662.

Medium after review (8)

72. Medium Rejected auth token is kept and reused
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: ProtocolContract.ERROR_AUTHORIZATION_FAILED -> "Auth token invalid"

Why it matters

Every later call fails with the same dead token until the app disconnects explicitly.

Suggested fix, not tested

Clear the token on this error, or retry once with a fresh authorize.

Review: On ERROR_AUTHORIZATION_FAILED the client builds a Failure message ("Auth token invalid") and never clears authToken. The next transact sends the same token again. Ordinary token expiry or revocation on the wallet side therefore makes every later connect fail the same way until the app clears the token or calls disconnect. The failure is loud, which is why this is borderline. It is in the shipped clientlib-ktx and the fix is one line.

77. Medium Remote wallet session is cached after the wallet disconnects
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: if (this.#session) { return callback(this.#session.wallet); }

Why it matters

Later calls reuse a dead session, and connected stays true.

Suggested fix, not tested

Clear the session on close or error, emit a change event, and retry once.

Review: #transact reuses #session whenever it is set; it is cleared only on explicit disconnect or a fresh authorization. A request made after the remote socket closed is sent on a closed socket and its promise never settles; no modal is shown, so the user has no cancel path, and connected stays true. The trigger is a remote wallet's phone sleeping or backgrounding. Remote transport only.

79. Medium React Native authorization cache returns public keys in the wrong shape
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: (JSON.parse((await asyncStorage.getItem(CACHE_KEY)) as string) as AuthorizationResult)

Why it matters

Uint8Array keys come back as plain objects, unlike in the web caches.

Suggested fix, not tested

Rebuild the byte arrays on read, as the other caches do.

Review: The cached accounts carry publicKey: Uint8Array, which JSON.stringify turns into a plain object. The web cache rebuilds the byte array on read; the React Native fork returns the parsed object as is, and its test uses accounts without a publicKey. On a silent reconnect, adapter.publicKey is built from a plain object, which should fail. The exact failure mode was inferred from the library constructor rather than executed.

118. Medium Malformed chain value crashes the wallet app
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence

Evidence: mIoHandler.post(() -> mCallbacks.onAuthorizeRequest(new AuthorizeRequest(future, ..., chain, ...)));

Why it matters

The chain value comes straight from the dApp's authorize request. The AuthorizeRequest constructor throws IllegalArgumentException for any value that is not namespace:reference (AuthorizeRequest.java:63). The throw happens inside a runnable on the wallet's I/O looper thread, so it is uncaught and the wallet process crashes. Any connecting dApp can crash the wallet with one request.

Suggested fix, not tested

Validate chain with Identifier.isValidIdentifier in handleAuthorize, and answer with an invalid-params or cluster-not-supported error. Never throw inside posted runnables.

Review: A chain value such as "foo" passes the server's checks untouched; the AuthorizeRequest constructor then throws inside a runnable posted to the wallet's I/O looper, which has no handler, so the wallet process dies. Confirmed, but the impact is availability only: one dApp request after a session is up, no state corruption, no signing impact. The fix is a one-line validation.

120. Medium React Native client module throws from bridge methods
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence

Evidence: } else { throw e } … } ?: throw NullPointerException("Tried to invoke $method without an active session")

Why it matters

A transport or protocol failure that is neither a remote JSON-RPC error nor a timeout is rethrown from the @ReactMethod. The sibling catch (Throwable) does not catch a throw from inside another catch block, so the host app process crashes. Calling invoke or endSession (line 223) without a session crashes the app the same way.

Suggested fix, not tested

Reject the promise on every path, and never throw from a bridge method.

Review: The rethrow inside a catch block is real and escapes the bridge method, which crashes the host app. The reachable cause is narrow: it needs a wallet response with neither result nor error, or a malformed error. The no-session paths are not reachable through the shipped JavaScript. Fair MEDIUM: shipped package, but the trigger needs a misbehaving wallet.

121. Medium Session counter can go negative
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: public void onSessionError() { ... if (mClientCount.decrementAndGet() == 0) {

Why it matters

Close and error callbacks also fire for sessions that never finished establishing, and so were never counted. Any local process can connect and drop, pushing the count to -1. A real session then never starts the auth repository, or an active one is shut down mid-session.

Suggested fix, not tested

Decrement only for sessions that were counted.

Review: Close and error callbacks decrement the counter even for connections that never completed the handshake. A local process that connects and drops can take a live session's count to zero, which clears the active authorization and stops serving, or push it negative so the next real session is never announced. Impact: a local denial of service of a signing session.

130. Medium Every chain and feature entry copies the first one
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: for (int c = 0; c < chainsArr.length(); c++) { chains[c] = chainsArr.getString(0); }

Why it matters

dApps receive wrong per-account chain and feature lists. Line 321 has the same bug, and line 316 also reads the wrong key.

Suggested fix, not tested

Use getString(c), and read the correct key.

Review: chains[c] = chainsArr.getString(0) (and the same for features) gives silently wrong data in the public AuthorizedAccount.chains and .features of the shipped clientlib. Trivial fix; few dApps read these fields yet.

147. Medium Content-change events never reach the hook
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence

Evidence: addListener(SEED_VAULT_EVENT_BRIDGE_NAME, ...) while native code emits on "SeedVaultContentChangeEventBridge"

Why it matters

handleContentChange is never called.

Suggested fix, not tested

Subscribe to both channels, or emit on one shared constant.

Review: The native module emits content changes on one event channel and the useSeedVault hook listens on another, with no other subscriber in the repository, so the documented content-change callback can never fire.

Low after review (137)

3. Low Base58 decoder bound check is off by one
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: bytes[i] = (c <= BASE58_ALPHABET_ASCII_LOOKUP.length ? BASE58_ALPHABET_ASCII_LOOKUP[c] : -1);

Why it matters

The lookup table has 128 entries, so character U+0080 throws ArrayIndexOutOfBoundsException instead of the documented IllegalArgumentException. Callers that parse dApp-supplied sign-in addresses catch only the documented type.

Suggested fix, not tested

Use c < BASE58_ALPHABET_ASCII_LOOKUP.length, or replace the hand-written codec with a maintained Base58 library.

4. Low Seed Vault results are delivered twice to JavaScript
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: handleActivityResult(requestCode, resultCode, data) (module-wide listener) and if (receivedRequestCode == requestCode) { ... callback(resultCode, data) (per-call listener)

Why it matters

Each vault result goes to both the event API and the promise API. Request codes are small integers (0–6) that can collide with other modules. JavaScript code can act on one signing result twice.

Suggested fix, not tested

Route each request to exactly one consumer, keyed by a unique high request code. Remove the per-call listener once it fires. Reject the promise when no activity is available.

6. Low Seed Vault IDs are typed as numbers in TypeScript but sent as strings
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: type AuthToken = number; while native code does putString("authToken", "$authToken")

Why it matters

The documented types do not match what crosses the bridge. Passing a number where a native String parameter is expected fails at runtime. 64-bit values can also lose precision as JavaScript numbers.

Suggested fix, not tested

Type tokens and IDs as decimal strings end to end. Add one native parse helper that validates the value and rejects instead of throwing NumberFormatException.

7. Low Null JSON values reach JavaScript as the string "null"
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: else -> map.putString(key, value.toString())

Why it matters

A null field in a wallet response (for example wallet_uri_base) arrives as the text "null", so it looks present and then fails validation. Long values arrive as strings, and nulls are silently dropped from arrays, which shifts indexes.

Suggested fix, not tested

Map JSONObject.NULL to putNull and pushNull, and handle Long explicitly.

8. Low Remote session never fails when the socket drops before the wallet connects
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: socket.addEventListener('close', () => { rejectPendingRequests(

Why it matters

If the reflector connection closes before the session is established, the wallet promise is never rejected. The dApp and its modal wait forever on "waiting for scan".

Suggested fix, not tested

Reject the wallet promise on close or error while the session is not yet established. Remove the stale first-phase error handler that can re-dial.

9. Low connect() and autoConnect() return before the connection finishes
First rating: Medium · Reviewed rating: Low · Review: rated too high
Location: mobile-wallet-adapter/js/packages/wallet-adapter-mobile/src/adapter.ts:204 (line corrected on review; the report cites :203)
From the report
Evidence

Evidence: async connect(): Promise<void> { this.#connect(); }

Why it matters

Callers see success immediately. Connection failures and cancellations become unhandled promise rejections instead of reaching the app.

Suggested fix, not tested

return await this.#connect() in both connect() and autoConnect() (line 199).

10. Low Transaction payloads and auth tokens written to the device log by the React Native client module
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: Log.d(TAG, "invoke $method with params $params")

Why it matters

params holds auth tokens, transactions and sign-in payloads. They end up in logcat, bug reports and crash tooling for every app that embeds the library. Lines 82 and 195 log the same kind of data.

Suggested fix, not tested

Log only the method name and a request ID.

11. Low Seed Vault auth tokens written to the device log by the React Native module
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: Log.d(TAG, "Seed authorized, AuthToken=$authToken")

Why it matters

The auth token is the credential for signing and key derivation on a seed. It is logged on every authorize, create, import, deauthorize and event path (also lines 200, 228, 245, 575, 584, 593). Signatures and public keys are logged too.

Suggested fix, not tested

Remove the values from the log messages.

12. Low Wallet library logs account keys and app identity on every authorization operation
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: Log.v(TAG, "Returning AuthRecord from auth token: " + authRecord);

Why it matters

The record's text form includes the dApp identity and the authorized public keys. Every issue, reissue, revoke and lookup therefore logs which address is linked to which app (also lines 270, 433, 443, 456).

Suggested fix, not tested

Log record IDs only.

18. Low Missing limits are reported as 0
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: jo.optInt(ProtocolContract.RESULT_MAX_TRANSACTIONS_PER_REQUEST, 0)

Why it matters

An absent limit cannot be told apart from an explicit 0, which means "no limit".

Suggested fix, not tested

Use has() with a nullable value.

19. Low No wallet availability check before starting an association
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: val scenario = scenarioProvider.provideAssociationScenario(timeout)

Why it matters

Keys and a scenario are created before the code learns that no wallet is installed.

Suggested fix, not tested

Call LocalAssociationIntentCreator.isWalletEndpointAvailable first and return "no wallet found".

20. Low Raw exception text returned as the user-facing message
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: TransactionResult.Failure(e.message.toString(), e)

Why it matters

Users can see internal text or the literal word null. The wallet server also echoes internal exception messages to dApps.

Suggested fix, not tested

Return a fixed, readable message and keep the exception for logs.

21. Low Error text inserted into the page as HTML
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: errorMessageElement.innerHTML = An unexpected error occurred: ${error.message};

Why it matters

The current callers pass fixed errors, but this is a latent HTML-injection sink inside the dApp's page.

Suggested fix, not tested

Use textContent.

22. Low Wrong validation message for reflector IDs
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Association port number must be between 49152 and 65535.

Why it matters

The message describes a different constraint, which misleads anyone debugging.

Suggested fix, not tested

State the real reflector-ID range.

23. Low Wallet session start failures only reach the console
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: } catch (e) { console.error(e);

Why it matters

The wallet UI never learns that session start failed.

Suggested fix, not tested

Surface the failure through a callback or event.

24. Low In-flight wallet request state is lost on process death
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: private var callback: ((Int) -> Unit)? = null

Why it matters

If the app is killed while the wallet is in front, the outcome of a sign-and-send request is unknown. The Seed Vault React Native module has the same gap: its KEY_PENDING_EVENT constant is declared but never used.

Suggested fix, not tested

Persist a minimal phase record and reconcile it on resume.

25. Low Wire messages logged in the Nostr relay transport
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Log.d(TAG, "WALLET SEND MESSAGE: " + new String(message));

Why it matters

Protocol traffic is exposed to anyone who can read the device log.

Suggested fix, not tested

Remove the line, or gate it behind a debug flag.

27. Low Sample wallet re-runs association on every activity creation
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: val result = viewModel.processLaunch(intent, callingPackage)

Why it matters

A rotation starts a second scenario on the same port. The singleTask activity has no onNewIntent, so a second association intent is ignored.

Suggested fix, not tested

Process the launch only when savedInstanceState == null, and handle onNewIntent.

31. Low Vault activities are launched without verifying the vault package
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: if (heldPermission.equals(ri.activityInfo.permission)) { intent.setClassName(...)

Why it matters

A look-alike app that declares the same filter and permission string could receive the user's recovery-phrase entry.

Suggested fix, not tested

Verify the resolved package's signature before launching, as isAvailable already does.

32. Low Wallet presence guessed from a 3-second blur timeout
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: const timeoutId = setTimeout(() => { cleanup(); reject();

Why it matters

A slow wallet cold start is reported as "wallet not found", and users who have a wallet are shown the install prompt.

Suggested fix, not tested

Lengthen or remove the heuristic.

33. Low Auth tokens stored unencrypted on Android
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: await asyncStorage.setItem(CACHE_KEY, JSON.stringify(authorizationResult));

Why it matters

The auth token is a credential. The Kotlin sample's PersistanceUseCase.kt:56 stores it in plain SharedPreferences with backup enabled.

Suggested fix, not tested

Use Keystore-backed encrypted storage with a pinned library version.

34. Low React Native transact skips error mapping
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: try { return SolanaMobileWalletAdapter.invoke(method, params); } catch (e) {

Why it matters

Without await, rejections bypass handleError. endSession in finally can also mask the original error.

Suggested fix, not tested

Use return await, and wrap endSession in its own try/catch.

35. Low Example wallet reauthorization check never times out
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Promise.race([ clientTrustUseCase!!.verifyReauthorizationSource(...), async () => { setTimeout(() => { throw new Error(

Why it matters

The race is given a function rather than a promise, so it wins immediately and the request is never resolved.

Suggested fix, not tested

Race against a real rejecting timeout promise.

36. Low Sample wallets mark web identities as verified without checking
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: return new VerificationSucceeded(AssociationType.LocalFromBrowser, ...)

Why it matters

Any web page can claim any identity and be treated as verified. android/fakewallet/.../ClientTrustUseCase.kt:53 does the same.

Suggested fix, not tested

Return "not verifiable" until real verification exists.

37. Low Example stores a missing wallet URI as the text "null"
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: putString(WALLET_URI_BASE, walletUriBase.toString())

Why it matters

The stored string is read back as a bogus relative URI.

Suggested fix, not tested

Store the value only when it is non-null, and remove the key otherwise.

38. Low Example release builds signed with the debug keystore
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: release { ... signingConfig signingConfigs.debug

Why it matters

Developers copy this configuration into production apps.

Suggested fix, not tested

Supply release signing from outside the repository.

39. Low Example web app swallows every wallet error
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: } catch {}

Why it matters

Users get no feedback when connecting or signing fails.

Suggested fix, not tested

Show a readable message and log the details.

43. Low Rotation replaces the recovery phrase on the create-seed screen
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (intent.action == WalletContractV1.ACTION_CREATE_SEED) { viewModel.createNewSeed(authorize) }

Why it matters

This runs on every onCreate. A rotation or theme change silently generates a new phrase after the user has written down the old one, or wipes the words typed so far.

Suggested fix, not tested

Initialise only when savedInstanceState == null, or when the view model is not yet initialised.

44. Low Authorization screen resets PIN attempt counters on recreation
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: authorizeCommonViewModel.setRequest(callingActivity, uid, intent)

Why it matters

The request is resubmitted on every recreation, which resets the PIN and biometric failure counters and discards the user's seed selection.

Suggested fix, not tested

Set the request once, and keep the counters across recreation.

45. Low React Native client module build file out of step with current Android tooling
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: classpath 'com.android.tools.build:gradle:9.2.1'

Why it matters

The default Kotlin version is 1.9.0, namespace is missing, and the build still uses the lintOptions DSL. These do not line up with the pinned plugin version.

Suggested fix, not tested

Use one compatible AGP/Gradle/Kotlin set, add namespace, and replace lintOptions with lint {}.

46. Low React Native packages pin different Android Gradle Plugin versions
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: classpath 'com.android.tools.build:gradle:8.2.2'

Why it matters

A host app that uses both packages resolves a single plugin version. The resulting tool errors look like project errors.

Suggested fix, not tested

Align the packages, or stop pinning the plugin in library build scripts.

47. Low Seed Vault React Native module pins an old Gradle plugin and an open React Native version
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: classpath 'com.android.tools.build:gradle:7.4.2' … implementation "com.facebook.react:react-native:+"

Why it matters

The module is out of step with the repository root, and + makes builds non-reproducible.

Suggested fix, not tested

Inherit the plugin version from the host app, and pin the React Native dependency.

48. Low Sample wallet loses the pending request code on recreation
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: private var requestCode: Int? = null

Why it matters

A result that arrives after recreation matches nothing and is dropped.

Suggested fix, not tested

Save and restore the request code.

49. Low Sample wallet crashes when a second request arrives
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: check(pendingEvent == null) { "Received a request while another is pending" }

Why it matters

A double tap crashes the activity.

Suggested fix, not tested

Ignore or queue the second request, or disable the button while one is pending.

50. Low Permission result read without checking for an empty array
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: when (grantResults[0]) {

Why it matters

An interrupted permission dialog delivers an empty array, which throws here.

Suggested fix, not tested

Use grantResults.firstOrNull(), and request the permission only on first creation.

51. Low Snackbar opens Settings on any dismissal
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {

Why it matters

A timeout opens system Settings without the user asking.

Suggested fix, not tested

Act only on DISMISS_EVENT_ACTION.

52. Low Sample screens cannot scroll
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: <androidx.constraintlayout.widget.ConstraintLayout ... android:layout_height="match_parent">

Why it matters

Lower controls are unreachable on small screens and in landscape. The same applies to the fakewallet authorize and sign fragments.

Suggested fix, not tested

Wrap the content in a ScrollView.

53. Low Scanner layout depends on a fixed 400dp margin
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: android:layout_marginTop="400dp"

Why it matters

On shorter screens the title and camera preview are pushed off-screen.

Suggested fix, not tested

Use guideline- or percentage-based constraints.

54. Low Heading sizes cancel the user's font-size setting
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: val currScale = LocalDensity.current.fontScale.times(displayScaleFactor())

Why it matters

Users with large-text settings get fixed-size headings while body text grows.

Suggested fix, not tested

Do not invert the user's font scale, or limit the compensation to a documented device class.

55. Low Hard-coded icon tint in the Kotlin sample
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: tint = Color.Black,

Why it matters

The icon does not follow the theme and disappears on dark backgrounds.

Suggested fix, not tested

Use the theme's content colour.

56. Low Hard-coded white tile background in the simulator
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: .background(Color.White)

Why it matters

The literal colour bypasses the theme. SelectSeedContents.kt:120 repeats it.

Suggested fix, not tested

Add a named theme token and use it in both places.

57. Low Seed list items have no stable keys
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: items(count = seedDetails.seeds.size) { index ->

Why it matters

After a delete or reload, scroll position and item state follow the index instead of the seed.

Suggested fix, not tested

Use items(seeds, key = { it.id }).

58. Low Bottom sheets disagree on who draws the scrim
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: ModalBottomSheet( sheetState = sheetState,

Why it matters

The dimming doubles when moving between sheets.

Suggested fix, not tested

Use a transparent scrim here too, as the authorize sheet does.

59. Low Main screens start with a transparent window background
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: <item name="android:windowBackground">@android:color/transparent</item>

Why it matters

The first frame does not match the Compose theme.

Suggested fix, not tested

Set the window background from the theme's background colour, with a night variant.

60. Low Two competing colour palettes in the simulator
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: <item name="colorPrimary">@color/purple_500</item>

Why it matters

The XML theme and the Compose theme define different palettes.

Suggested fix, not tested

Derive the XML theme from the Compose tokens, or strip it to a bare parent.

61. Low Type scale is inverted
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: titleSmall = TextStyle( ... fontSize = 18.sp, (titleMedium is 16sp)

Why it matters

Visual hierarchy is reversed.

Suggested fix, not tested

Make the title sizes decrease from large to small.

62. Low Shimmer colours bypass the theme
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: return if (isSystemInDarkTheme()) Color(0xFF2F2A35) else Color(0xFFE8E0EB)

Why it matters

The shimmer ignores the theme's dark-mode override.

Suggested fix, not tested

Define named tokens in both palettes.

63. Low Dialog size fixed with magic numbers
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: .height(197.dp) .width(178.dp)

Why it matters

Text clips under larger font or display sizes.

Suggested fix, not tested

Size the dialog from its content.

64. Low Test-suite column headers do not line up with the grid
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Row(... .padding(horizontal = Sizes.dp32), horizontalArrangement = Arrangement.SpaceBetween

Why it matters

The labels do not sit over their columns.

Suggested fix, not tested

Render the headers as the grid's first row.

65. Low Sample React Native screen uses a percentage height
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: height: '90%',

Why it matters

The parent has no defined height, so the layout is unpredictable, and safe areas are ignored.

Suggested fix, not tested

Use flex: 1 and safe-area insets.

66. Low Deauthorize does not clear the active session's authorization
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (mActiveAuthorization == authRecord) {

Why it matters

The freshly loaded record is a new object, so the reference comparison never matches. The live session keeps signing rights after its token is revoked.

Suggested fix, not tested

Compare by record ID, or re-check revocation in the signing guards.

68. Low Display address is dropped from stored accounts
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: mAccountsDao.insert(authRecordId, account.publicKey, account.accountLabel, account.accountIcon, account.chains, account.features);

Why it matters

Display address and format are accepted at issue time but never persisted, so reauthorization returns different accounts.

Suggested fix, not tested

Add the columns with a migration and carry them through the record.

69. Low Authorize response omits per-account chains and features
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (aa.accountLabel != null) { account.put(ProtocolContract.RESULT_ACCOUNTS_LABEL, aa.accountLabel); }

Why it matters

The wallet supplies per-account chains and features, but dApps never receive them.

Suggested fix, not tested

Serialise both fields when they are present.

70. Low Each scenario leaks a looper thread
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: final LooperThread t = new LooperThread(); t.start(); (quit only in finalize(), line 124)

Why it matters

Threads accumulate until a garbage collection happens to run the finaliser.

Suggested fix, not tested

Quit the looper in close() and remove finalize().

71. Low Kotlin client auth token updated from several threads without coordination
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: authToken = it.authToken

Why it matters

Overlapping calls or a cluster change can overwrite a fresh token with a stale one.

Suggested fix, not tested

Guard the auth state with a Mutex and store the chain with the token.

73. Low Cached authorization reused without checking chain or sign-in request
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: const cachedAuthorizationResult = await this.#authorizationCache.get(); if (cachedAuthorizationResult) {

Why it matters

A cluster switch keeps targeting the old chain. A sign-in request is answered from the cache with no fresh nonce-bound signature.

Suggested fix, not tested

Accept a cache hit only when the chain and identity match and no sign-in payload is present.

74. Low Cache writes and clears can run out of order
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: this.#authorizationCache.clear(); // TODO ... not awaiting

Why it matters

A late write after disconnect brings the token back into storage, and the next page load reconnects silently.

Suggested fix, not tested

Await the cache operations, or serialise them through one queue.

75. Low Disconnect does not revoke the authorization with the wallet
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: #disconnect = async () => { this.#authorizationCache.clear();

Why it matters

The wallet-side token stays valid while the dApp believes it has disconnected.

Suggested fix, not tested

Call deauthorize, on a best-effort basis, before clearing local state.

76. Low Wallet session is not closed when the callback fails
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: const result = await callback(await wallet); loadingSpinner.close(); close();

Why it matters

When the callback throws, the session and socket stay open.

Suggested fix, not tested

Close in a finally block.

78. Low Adapter keeps the old account after the wallet reports none
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (properties.accounts && properties.accounts.length > 0) {

Why it matters

The selected account and public key stay set after a wallet-side disconnect.

Suggested fix, not tested

On an empty list, clear the state and emit disconnect.

80. Low Simulator serves queries before its data has loaded
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: seedCollection.first() (a state flow seeded with SeedCollection.getDefaultInstance())

Why it matters

The "wait until valid" step returns immediately with the empty default. Queries see no data, and the ID counters can reuse IDs.

Suggested fix, not tested

Wait for the first real emission from the store.

81. Low Expiry check is never used on active sessions
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: if (authRecord == null || authRecord.isRevoked()) {

Why it matters

Expiry is enforced at token lookup, but the guard on an already-active session checks revocation only.

Suggested fix, not tested

Use one shared "is usable" predicate everywhere.

82. Low Authorization tables have no foreign keys
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: COLUMN_ACCOUNTS_PARENT_ID + " INTEGER NOT NULL,"

Why it matters

Orphan accounts and authorizations without accounts are possible, and the code reads accounts[0].

Suggested fix, not tested

Add foreign keys with cascade, and enable them in onConfigure.

83. Low Timeout executor is never shut down
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Executors.newSingleThreadScheduledExecutor();

Why it matters

Each scenario leaves a worker thread behind.

Suggested fix, not tested

Call shutdownNow() on close.

84. Low Account detail changes are not announced
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: this.#authorization.accounts.some((account, ii) => account.address !== authorization.accounts[ii].address);

Why it matters

Label, icon, chain and feature updates are stored but never emitted.

Suggested fix, not tested

Compare whole account objects.

85. Low One storage key shared by different owners
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: const CACHE_KEY = 'SolanaMobileWalletAdapterDefaultAuthorizationCache';

Why it matters

Two instances overwrite or clear each other's data.

Suggested fix, not tested

Prefix the key with its owner, and store a kind and version.

86. Low Dead and broken account query
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: COLUMN_ACCOUNTS_PARENT_ID + "=?" + parentId

Why it matters

The placeholder is never bound, so the query can never match. The method is unused.

Suggested fix, not tested

Delete it, or bind the argument.

87. Low Empty chain or feature lists come back as [""]
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: return content.split(ARRAY_DIVIDER);

Why it matters

Empty lists round-trip as one empty string, and values containing the divider split wrongly.

Suggested fix, not tested

Store the lists as JSON.

88. Low Base58 decoding has no input size limit
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: for (int i = start; i < bytes.length; i++) { mod = mod * 58 + bytes[i];

Why it matters

Decoding is quadratic, and the sign-in address passed to it is chosen by the dApp. A long string can tie up the wallet's protocol thread.

Suggested fix, not tested

Reject inputs longer than an encoded address or signature (44 or 88 characters) before decoding.

89. Low Local WebSocket server accepts frames of any size
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: new Draft_6455(Collections.emptyList(), Collections.singletonList(new Protocol(...)))

Why it matters

Any local process can send huge frames before any association check and exhaust wallet memory.

Suggested fix, not tested

Configure a maximum frame and message size.

90. Low JSON-RPC messages are parsed with no size or depth limit
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: o = new JSONObject(jsonStr); } catch (JSONException e) {

Why it matters

Deep nesting causes a StackOverflowError, which bypasses the handler. The client parser (JsonRpc20Client.java:160) is the same.

Suggested fix, not tested

Cap the size and nesting depth before parsing.

91. Low Nostr events are signature-checked before the sender is checked
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (!NostrCrypto.verifyEvent(event)) { ... if (!senderPubkey.equals(mDappNostrPubkey)) {

Why it matters

Anyone publishing events with the session tag can force expensive signature work. The JavaScript client (transact.ts:924) has the same order.

Suggested fix, not tested

Compare the sender key first, cap the message length, then verify.

92. Low Wallet server accepts unlimited parallel requests
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: dispatchRpc(id, method, params);

Why it matters

A dApp can flood the wallet with prompts. A late authorize can overwrite another request's active authorization.

Suggested fix, not tested

Track the outstanding request, and reject or queue additional ones.

93. Low Concurrent sessions share one authorization slot
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: protected AuthRecord mActiveAuthorization = null;

Why it matters

An authorization granted on one connection is visible to signing requests on another.

Suggested fix, not tested

Scope the active authorization to each session, and limit the number of live sessions.

94. Low Remote wallet opens a new session per concurrent call
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: this.#session = { close, wallet: await wallet };

Why it matters

Concurrent calls open several sessions, and earlier ones are dropped without being closed.

Suggested fix, not tested

Reuse the in-flight session promise, and close sessions before discarding them.

95. Low One boolean flag guards several overlapping wallet flows
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: #performSignIn = async (input?: SolanaSignInInput) => { this.#connecting = true;

Why it matters

Overlapping flows clear each other's flag, and a double tap starts two associations.

Suggested fix, not tested

Keep one in-flight promise per operation.

96. Low Wallet-side scenario creation is not atomic
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (uri == scenarioUri && scenario != null) {

Why it matters

Concurrent intents create two scenarios. The replaced one is never closed and keeps its port.

Suggested fix, not tested

Guard creation with a mutex, and close the previous scenario.

97. Low Session lock released by a cleanup that does not own it
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (mutex.isLocked) { mutex.unlock() }

Why it matters

A late cleanup can release the lock held by the next session.

Suggested fix, not tested

Use withLock, or lock and unlock with an owner token.

98. Low Authorization database writes are unchecked and not transactional
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: final int authRecordId = (int) mAuthorizationsDao.insert(identityRecord.getId(), now, cluster, walletUriBaseId, scope);

Why it matters

A failed insert (-1) still produces a token that can never be redeemed. A crash midway leaves an authorization with no accounts.

Suggested fix, not tested

Wrap issue and reissue in a transaction, and fail on -1.

99. Low Unknown protocol version breaks session setup
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: throw new IllegalArgumentException("Unknown/unsupported version: " + versionString);

Why it matters

A wallet that speaks a newer version breaks session establishment instead of negotiating down.

Suggested fix, not tested

Fall back to the highest mutually supported version.

100. Low Bridged strings change type or gain extra quotes
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: value.booleanOrNull != null -> map.putBoolean(key, value.boolean) … array.pushString(value.toString()) (line 91)

Why it matters

Strings such as "123" or "true" become numbers or booleans. Array strings keep their JSON quotes, so feature IDs never match. Seed Vault's Extensions.kt has the same code.

Suggested fix, not tested

Check isString first, and use value.content.

101. Low Sign-in message parser can backtrack heavily
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: "(?:(?:[^:?#]+):)?(?:[^?#\\n]*)?(?:[^?#\\n]*)..."

Why it matters

Adjacent unbounded quantifiers and a repeated group make this public parser slow on long input.

Suggested fix, not tested

Cap the input length, or parse line by line.

102. Low Identifier check uses a backtracking regex with no length cap
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: Pattern.compile("^\\S+:\\S+$")

Why it matters

Chain strings from dApps reach this pattern unbounded.

Suggested fix, not tested

Check the length first, then scan for the colon.

103. Low Derivation-path depth checked after the loop
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: for (int i = 1; i < path.size(); i++) {

Why it matters

The loop walks every segment before the depth limit applies, and the overflow uses an undocumented exception type.

Suggested fix, not tested

Check the size first, and throw the documented exception.

104. Low Three transport state machines duplicated in one file
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: export async function startNostrScenario(config: NostrWalletAssociationConfig)

Why it matters

The local, remote and Nostr copies have already drifted; only the local path handles ping and close properly.

Suggested fix, not tested

Extract the shared handshake and request table.

105. Low Wallet server file mixes parsing, encoding and types
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: public class MobileWalletAdapterServer extends JsonRpc20Server { (about 1,100 lines)

Why it matters

The size and mixing make validation gaps like those in this report easy to miss.

Suggested fix, not tested

Split out the request and result types, and centralise parameter validation.

106. Low Session-counting code copied into three scenarios
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: private final AtomicInteger mClientCount = new AtomicInteger();

Why it matters

The three copies already behave differently.

Suggested fix, not tested

Move the counting into the base class once.

107. Low Local and remote wallet classes duplicate authorization logic
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: export class RemoteSolanaMobileWalletAdapterWallet

Why it matters

The copies already differ in how they detect capabilities.

Suggested fix, not tested

Extract a shared base or helper.

108. Low Library published before its device tests run
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository

Why it matters

The release reaches Maven Central inside the build job. The integration test job depends on that job, so it runs too late to block the release.

Suggested fix, not tested

Publish from a job that needs both build and test.

109. Low React Native wallet library keeps only the first authorized account
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: response.accounts.first().let { account ->

Why it matters

A multi-account authorization silently shrinks to one account, unlike in the native library. An empty list throws inside an unguarded coroutine and crashes the wallet.

Suggested fix, not tested

Map every account and call the array overload. Reject an empty list.

110. Low npm packages published without tests
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: publish: pnpm publish-packages

Why it matters

Publishing builds, then publishes. Tests and type checks run in another workflow that does not gate it. The Seed Vault workflow has the same shape.

Suggested fix, not tested

Run tests and type checks before publishing.

111. Low Build tool downloaded and run without a checksum
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: gh release download -R plantuml/plantuml -p 'plantuml-[0-9]*[0-9].jar'

Why it matters

The latest release runs unverified.

Suggested fix, not tested

Pin a version and verify its SHA-256.

112. Low JSON bridge helpers copied across three modules
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: internal fun ReadableMap.toJson(): JsonObject = buildJsonObject {

Why it matters

The copies are identical, so the string-handling defect in finding 100 exists in both.

Suggested fix, not tested

Extract one shared helper library.

113. Low Base58 re-implemented in several modules
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: object Base58EncodeUseCase { ... private val BASE58_ALPHABET = byteArrayOf(

Why it matters

The common module already ships Base58; the samples carry their own copies.

Suggested fix, not tested

Reuse the shared codec.

114. Low Kit and web3.js clients duplicate the wrapper logic
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: export function augmentWalletAPI(wallet: MobileWallet): KitMobileWallet { return new Proxy<KitMobileWallet>(

Why it matters

A fix applied to one client can be missed in the other.

Suggested fix, not tested

Share the wrapper, and keep only the transaction codecs separate.

115. Low Loading spinner duplicates the modal base
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: export default class EmbeddedLoadingSpinner { #root ...

Why it matters

The root lookup, listeners, open/close and injection logic are copied.

Suggested fix, not tested

Extend the shared modal base.

116. Low Examples re-implement base64 helpers
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: return btoa(String.fromCharCode.call(null, ...byteArray));

Why it matters

The package already exports this helper, and the spread throws on large inputs.

Suggested fix, not tested

Import the shared encoder.

117. Low Return from nested lambdas relies on implicit labels
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence

Evidence: return@with it.with(SignInResult(

Why it matters

A later edit can silently change which block the return exits.

Suggested fix, not tested

Use explicit labels, or a named function.

122. Low authorize lets parse errors escape
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: signInPayload = signInJson != null ? SignInWithSolana.Payload.fromJson(signInJson) : null; } catch (JSONException e) {

Why it matters

A bad nonce, date or address in the sign-in payload, or an unknown cluster (line 171), throws an unchecked exception. The connection drops with no reply. The error message on line 200 also names the wrong parameter.

Suggested fix, not tested

Also catch IllegalArgumentException, and return invalid-params with a correct message.

123. Low sign_messages throws instead of returning an error
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: throw new IllegalArgumentException("request must contain an array of addresses with which to sign messages");

Why it matters

The dispatcher catches only IOException. A malformed request kills the connection, and the same applies at lines 773 and 780.

Suggested fix, not tested

Respond with an invalid-params error and return.

124. Low get_capabilities answers twice
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (o.keys().hasNext()) { handleRpcError(id, ERROR_INVALID_PARAMS, "params expected to be empty", null); }

Why it matters

Execution falls through, so the client receives an error and a result for the same request ID.

Suggested fix, not tested

Add return; after the error response.

125. Low Malformed auth token raises an unchecked exception
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: final byte[] payload = Base64.decode(authToken, Base64.DEFAULT);

Why it matters

The dApp chooses the token. Invalid base64 aborts the request instead of producing "authorization failed".

Suggested fix, not tested

Catch the decode error and return null.

126. Low Reauthorization shows the requester's identity, not the stored one
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: new ReauthorizeRequest(future, request.identityName, request.identityUri, request.iconUri, authRecord.chain, authRecord.scope)

Why it matters

Auth tokens are bearer tokens. An app holding another app's token can present its own identity, and wallet-side source checks see that identity instead of the one stored with the token.

Suggested fix, not tested

Pass the stored identity, and reject a mismatch.

127. Low Short encrypted frames raise unchecked exceptions
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: final int seqNum = ByteBuffer.wrap(payload, 0, SEQ_NUM_LENGTH_BYTES).getInt();

Why it matters

Frames shorter than the header, IV and tag throw exceptions the receive handler does not catch. The sequence number also advances before authentication succeeds.

Suggested fix, not tested

Check the minimum length first, and commit the sequence number only after the tag verifies.

128. Low Bad wallet public key escapes the handshake handler
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: otherPublicKey = ECDSAKeys.decodeP256PublicKey(message); } catch (UnsupportedOperationException e) {

Why it matters

The decoder throws IllegalArgumentException for short or wrongly prefixed keys (ECDSAKeys.java:46). Any process answering on the loopback port can crash the dApp's session thread.

Suggested fix, not tested

Catch both exception types, and validate that the point is on the curve.

129. Low Malformed hex in Nostr events raises unchecked exceptions
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: return schnorrVerify(hexToBytes(id), hexToBytes(sig), hexToBytes(pubkey)); } catch (JSONException e) {

Why it matters

Odd-length hex from an untrusted relay throws StringIndexOutOfBoundsException, which tears down the session.

Suggested fix, not tested

Validate length and alphabet before decoding, and return false on any failure.

131. Low Malformed authorize results from the wallet are not handled
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: publicKey = JsonPack.unpackBase64PayloadToByteArray(b64EncodedAddress); … Uri.parse(walletIconStr)

Why it matters

Invalid base64 throws, an empty accounts array is indexed at 0, and the wallet icon accepts any URI scheme.

Suggested fix, not tested

Map decode errors to an invalid-response error, require at least one account, and allow only data: icons.

132. Low Reflector ID length decoded incorrectly
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: b = bytes[offset++]; value |= (b & 0x7f) << (7 * offset);

Why it matters

The shift uses the already-incremented offset, so a one-byte length is multiplied by 128. The code works only because slice clamps to the buffer end.

Suggested fix, not tested

Shift by 7 * (offset - 1), cap the byte count, and bounds-check the length.

133. Low Nostr transport trusts the first sender on the session tag
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: const walletNostrPubkey = event.pubkey;

Why it matters

The session tag is derived from the public association key. Anyone who sees it can race the real wallet, become the pinned peer and receive the handshake.

Suggested fix, not tested

Filter the subscription on events addressed to the dApp key, and verify the address before pinning.

134. Low Handshake failures leave transact() hanging
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey,

Why it matters

Parse failures become unhandled rejections and the wallet promise never settles. A clean close before the handshake also never rejects.

Suggested fix, not tested

Catch in the handshake branch, reject, and close the socket.

135. Low Timed-out association can still run the signing callback
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: const result = await Promise.race([ (async () => { ... callback(await wallet)

Why it matters

The app reports a timeout while the wallet later connects and prompts the user to sign. Retrying risks a duplicate transaction.

Suggested fix, not tested

Close the scenario on timeout, and check a cancelled flag before running the callback.

136. Low Wallet-side React Native scenario creation crashes on bad input
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: val kotlinConfig = json.decodeFromString(MobileWalletAdapterConfigSerializer, config)

Why it matters

This runs inside launch on a scope with no exception handler. A decode or start failure kills the process and never settles the promise.

Suggested fix, not tested

Catch and reject, and add a CoroutineExceptionHandler.

137. Low Digital asset link checks crash on unknown packages or bad URIs
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: verifier.verify(packageName, URI.create(clientIdentityUri)) … packageManager.getPackageUid(packageName, 0) (line 55)

Why it matters

The dApp chooses these values. A bad URI or unknown package crashes the wallet during an unauthenticated authorize request.

Suggested fix, not tested

Catch the exceptions, and return false or reject.

138. Low Pending requests map is unsynchronized and cancel does nothing
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: private val pendingRequests = mutableMapOf<String, MobileWalletAdapterRemoteRequest>()

Why it matters

Several threads mutate the map. cancelRequest (line 204) casts entries to a type they never have, so cancellation is a no-op.

Suggested fix, not tested

Use a ConcurrentHashMap, cancel through .request, and remove resolved entries.

139. Low Wallet requests block the React Native module thread
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: .methodCall(method, convertMapToJson(params), CLIENT_TIMEOUT_MS).get() as JSONObject

Why it matters

The call can block the shared native-modules thread for up to 90 seconds.

Suggested fix, not tested

Run the call in the module's I/O scope and settle the promise from there.

140. Low React Native modules never tear down
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: init { reactContext.addActivityEventListener(mActivityEventListener) }

Why it matters

Listeners, scopes and sockets survive a React reload. The wallet-side module (SolanaMobileWalletAdapterWalletLibModule.kt:185) leaks its running scenario the same way.

Suggested fix, not tested

Implement invalidate() to remove listeners, cancel scopes and close scenarios.

141. Low Simulator provider crashes when all accounts are removed
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: throw AssertionError("Accounts are not expected to be deleted")

Why it matters

The repository does emit account-delete notifications (SeedRepository.kt:465), so "remove all accounts" crashes the vault process.

Suggested fix, not tested

Handle the delete notification, and never throw inside the collector.

142. Low Double-checked locking that does not lock
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: val didInitialization = synchronized(this::seedRepository) {

Why it matters

A property reference is a new object on each evaluation, so binder threads do not exclude each other and initialisation can run twice.

Suggested fix, not tested

Lock on a private lock object, or use by lazy.

143. Low Simulator signs any bytes as a "message"
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: // TODO: validate message is a Solana-compatible message before signing

Why it matters

A requester can get a transaction signed while the user sees a harmless message prompt.

Suggested fix, not tested

Reject message payloads that parse as transactions.

144. Low Public-key result throws an undocumented unchecked exception
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: throw new UnsupportedOperationException("requestPublicKeys did not return a result");

Why it matters

The method's contract says ActionFailedException, so callers that follow the documented pattern crash.

Suggested fix, not tested

Throw ActionFailedException, and close cursors on the failure paths.

145. Low Seed Vault React Native module crashes without an activity
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: val application = reactContext.currentActivity?.application!!

Why it matters

When there is no current activity, or the provider returns null, this throws a null-pointer exception and the app crashes. Cursors are never closed.

Suggested fix, not tested

Use the application context, reject on null, and close cursors with use {}.

146. Low Seed Vault React Native module crashes on bad input
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: Wallet.getAccounts(application, authToken.toLong(), ...)

Why it matters

Number parsing, base64 and JSON decoding, and vault API errors are uncaught in bridge methods and result callbacks (also line 498).

Suggested fix, not tested

Wrap each method, reject the promise, and skip invalid entries.

148. Low Hook runs checks on every render and accepts a simulated vault
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: checkIsSeedVaultAvailable(true); checkSeedVaultPermission();

Why it matters

Bridge calls repeat on every render, their rejections are unhandled, and production wallets accept the insecure simulator.

Suggested fix, not tested

Run the checks in useEffect with error handling, and default allowSimulated to false.

149. Low Content observer is never unregistered
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: reactContext.contentResolver.registerContentObserver(WalletContractV1.WALLET_PROVIDER_CONTENT_URI_BASE, true, object : ContentObserver(

Why it matters

After a reload the observer fires into a destroyed context and throws on the main looper.

Suggested fix, not tested

Unregister it in invalidate().

150. Low showSeedSettings never settles on cancel
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if (resultCode != Activity.RESULT_CANCELED) { ... callback(null)

Why it matters

The JavaScript caller waits forever when the user cancels.

Suggested fix, not tested

Settle the promise on cancel.

151. Low Token issuance runs without the repository lock
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: public AuthRecord issue(@NonNull String name, ... (not synchronized)

Why it matters

Every other public method takes the lock. Here, multi-step inserts and purges race with revoke and reissue.

Suggested fix, not tested

Make both issue overloads synchronized.

152. Low Unusable keystore entry crashes the wallet
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: throw new RuntimeException("Android keystore error; aborting", e);

Why it matters

Only a missing key is recovered. A corrupt key crashes the wallet on its I/O thread.

Suggested fix, not tested

Recreate the key and reset the database, or surface a recoverable error.

153. Low Session send can race with close
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: mMessageSender.send(encryptedPayload);

Why it matters

The field is read outside the lock while doClose() sets it to null, which causes a null-pointer exception.

Suggested fix, not tested

Copy the field to a local inside the lock and null-check it.

154. Low Reconnect attempts run without the scenario lock
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: mConnectionBackoffExecutor.schedule(this::doTryConnect, delay, TimeUnit.MILLISECONDS);

Why it matters

doTryConnect is documented as requiring the lock but runs unlocked, so it can open a socket after close(). NostrRelayScenario.java:209 and LocalAssociationScenario.java:94 and :213 do the same.

Suggested fix, not tested

Schedule a lambda that takes the lock and checks the state first.

155. Low Sign-in fallback dereferences a null address
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: prepareMessage(addressRaw ?: it.accounts.first().publicKey) … arrayOf(addressRaw!!)

Why it matters

For wallets without native sign-in, signing in without an explicit address always fails.

Suggested fix, not tested

Compute the address once and use it for both the message and the signing call.

156. Low Custom cache objects lose their this binding
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: set: config.authorizationResultCache.set, ... clear: config.authorizationResultCache.clear,

Why it matters

A class-based cache supplied by the caller breaks, and authorization persistence fails silently.

Suggested fix, not tested

Wrap the methods in arrow functions.

157. Low Default wallet icon generation can throw
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: Bitmap iconBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

Why it matters

The intrinsic size can be -1 or 0, which throws. Base64.DEFAULT inserts line breaks into the data URI.

Suggested fix, not tested

Use a bounded fallback size and Base64.NO_WRAP.

158. Low Test library shipped as a runtime dependency
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: implementation libs.androidx.junit.ktx

Why it matters

JUnit and androidx.test are pulled into every consuming app.

Suggested fix, not tested

Move it to a test configuration.

159. Low Wallet modal loads fonts from a third-party server
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:..." rel="stylesheet">

Why it matters

Every dApp's users contact Google's font servers without the dApp's consent, and the modal breaks under a strict content security policy.

Suggested fix, not tested

Bundle the font, or use a system font stack.

160. Low AI implementation workflow has broad write power over untrusted issue text
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: permissions: contents: write ... --allowedTools "...,Bash(git:*),...,Bash(gh api:*)"

Why it matters

The agent reads issue bodies and comments that anyone can write, while holding a write token and unrestricted git and API tools. A prompt injection could push to any branch or tag. The agent action is also pinned to a movable tag, here and in claude-investigate.yml.

Suggested fix, not tested

Allow only exact commands, push only to claude/* branches, drop the generic API tool, and pin the action by commit.

161. Low AI triage workflow acts on untrusted issue text with write tools
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: --allowedTools "Write,Bash(gh issue edit:*),Bash(gh issue close:*),...,Bash(gh api:*)"

Why it matters

The workflow runs on every new issue with no maintainer gate. Injected text could close or relabel other issues, or call any write endpoint.

Suggested fix, not tested

Restrict the commands to the triggering issue, and remove the generic API tool.

162. Low Documentation publish job can never run
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence

Evidence: if: ${{ github.event_name == 'push' && github.event.push.base_ref == 'main' }}

Why it matters

Push events have no push object, so the condition is always false and the docs are never published.

Suggested fix, not tested

Use github.ref == 'refs/heads/main'.

Info after review (16)

1. Info Seed phrase generated with a non-cryptographic random generator
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: Bip39PhraseUseCase.bip39EnglishWordlist[Random.nextInt(Bip39PhraseUseCase.bip39EnglishWordlist.size)]

Why it matters

kotlin.random.Random is not a cryptographic generator, so new recovery phrases are predictable. Every key derived from them is predictable too. Developers do load real funds into simulator-created seeds, and the code is a template for real vaults.

Suggested fix, not tested

Generate entropy with SecureRandom (or libsodium) and derive the mnemonic from it with a vetted BIP39 library.

2. Info Recovery phrases are not checked against the BIP39 checksum
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: fun toSeed(seedPhraseWordIndices: List<Int>): ByteArray { … require(size == 12 || size == 24)

Why it matters

Only the word count is validated. Randomly picked words rarely form a valid checksummed mnemonic, so a seed created here may not restore in any other wallet.

Suggested fix, not tested

Use a proven BIP39 implementation for both generation (entropy to mnemonic) and validation (checksum word).

5. Info Test-only reset method wipes all seeds and is reachable by ordinary vault clients
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: RESET_SEED_VAULT_SIMULATOR_METHOD -> callResetSeedVaultSimulator()

Why it matters

Any app with the normal, user-grantable vault permission can call this method and delete every stored seed. An implementation copied from the simulator inherits a remote wipe.

Suggested fix, not tested

Compile the method only into test builds, or require a signature-level permission.

13. Info Simulator logs each recovery phrase word and the PIN
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: Log.d(TAG, "setSeedPhraseWord($index, $w)") … Log.d(TAG, "setPIN($p)")

Why it matters

Anyone with adb access or a bug report can rebuild the full recovery phrase and PIN. The seed details object is also logged whole (line 152, and SeedRepository.kt lines 149 and 185).

Suggested fix, not tested

Delete these log lines and override the seed-details toString() to redact secrets.

14. Info Hand-written HKDF and signature encoding
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: public static byte[] hkdfSHA256L16(@NonNull byte[] ikm, @NonNull byte[] salt) {

Why it matters

Hand-rolled cryptographic building blocks are easy to get subtly wrong. The same applies to the DER/P1363 converters in ECDSASignatures.java.

Suggested fix, not tested

Use a maintained implementation, for example BouncyCastle's HKDF.

15. Info Hand-written Schnorr signing for the Nostr transport
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: public static byte[] schnorrSign(...) { // hardcoded zero randomness here

Why it matters

BigInteger point arithmetic is not constant-time, and the auxiliary randomness is fixed.

Suggested fix, not tested

Use a vetted BIP-340 implementation and fresh auxiliary randomness.

16. Info Unfinished BIP32-Ed25519 code with TODO stubs
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: TODO("wrong. This is the expanded private key, not the Sodium secret key.")

Why it matters

Custom curve code with runtime TODO() throws is a trap for anyone who copies it.

Suggested fix, not tested

Delete it, or replace it with a vetted library.

17. Info Association port chosen with Math.random
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: 49152 + Math.floor(Math.random() * (65535 - 49152 + 1))

Why it matters

The port forms part of the association, so it should not be predictable. reflectorId.ts also scales 32 random bits over a 2^53 range.

Suggested fix, not tested

Use crypto.getRandomValues with bias-free reduction.

26. Info No local-network permission handling for newer Android versions
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: <uses-permission android:name="android.permission.INTERNET" />

Why it matters

Both sides use loopback sockets. Newer Android versions are introducing local-network permission gating.

Suggested fix, not tested

Declare the permission where it applies and fall back gracefully when it is denied.

28. Info Simulator biometric approval is a plain button
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: onClick = onBiometricAuthorizationSuccess

Why it matters

Tapping the icon counts as biometric approval, and this ships in release builds.

Suggested fix, not tested

Use BiometricPrompt in real flows, and keep the simulation in test variants only.

29. Info Release builds trust and are signed with the published test key
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: <!-- <Root>/testkeystore/PrivilegedKey --> plus the release build using signingConfigs["simulator"] (build.gradle:55)

Why it matters

The privileged known-signer certificate is a public test key whose password is in the repository. Anyone can sign an app with it and obtain privileged vault access.

Suggested fix, not tested

Keep test certificates in debug/test flavors only, and sign releases with a private key.

30. Info Content provider selection parser supports only one clause
First rating: Low · Reviewed rating: Info · Review: rated too high
From the report
Evidence

Evidence: require(selectionArgs.size == 1) { "Expected only 1 selectionArg; got $selectionArgs.size" }

Why it matters

Filtering on byte-array columns throws, string comparison trims only one side, and the error message interpolates the wrong value. The parser is safe against SQL injection.

Suggested fix, not tested

Document the limit or implement it properly, and fix the message.

40. Info Sign-in results are not verified by the library or examples
First rating: Info · Reviewed rating: Info · Review: location checked, kept as rated
From the report
Evidence

Evidence: result.authResult.signInResult?.run { TransactionResult.Success(this, result.authResult) }

Why it matters

Sign-in is returned as success without checking the signature, nonce or domain. The examples do not use a server nonce.

Suggested fix, not tested

Document clearly that verification is the caller's job, or provide a verification helper.

41. Info Example wallet keeps its signing key in plain storage
First rating: Info · Reviewed rating: Info · Review: location checked, kept as rated
From the report
Evidence

Evidence: await AsyncStorage.setItem(ASYNC_STORAGE_KEY, JSON.stringify(encodeKeypair(nextKeypair)));

Why it matters

The code is marked test-only, but it is easy to copy into a real wallet.

Suggested fix, not tested

Keep it out of production copies, or use Keystore-backed storage.

42. Info Test keystore passwords in build scripts
First rating: Info · Reviewed rating: Info · Review: location checked, kept as rated
From the report
Evidence

Evidence: storePassword = 'gene...' (truncated)

Why it matters

This is acceptable for throwaway test keys, provided those keys never sign a distributed build. The same pattern appears in fakewallet/build.gradle and fakewalletreact/android/app/build.gradle.

Suggested fix, not tested

Keep these keys out of every release pipeline.

67. Info Stopping the auth repository leaves it half-closed
First rating: Medium · Reviewed rating: Info · Review: could not be settled
From the report
Evidence

Evidence: mAuthDb.close();

Why it matters

The initialised flag and the DAOs still point at the closed database, so the next session fails. Calling stop() without a prior start throws a null-pointer exception.

Suggested fix, not tested

Guard stop(), reset the state, and reopen in start().

Want this for your code?

Upload a ZIP or link a public GitHub repo, pick the areas and the depth, and get findings by severity with fixes.